概念
花指令又名垃圾代码、脏字节,英文名是junk code。花指令就是在不影响程序运行的情况下,往真实代码中插入一些垃圾代码,从而影响反汇编器的正常运行;或是起到干扰逆向分析人员的静态分析,增加分析难度和分析时间。总结就是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法, 在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行, 而程序无法很好地反编译, 难以理解程序内容, 达到混淆视听的效果。
花指令的分类
可执行花指令
顾名思义,可以执行的花指令,这部分垃圾代码会在程序运行的时候执行,但是执行这些指令没有任何意义,并不会改变寄存器的值,同时反汇编器也可以正常的反汇编这些指令。目的是为了增加静态分析的难度,加大逆向分析人员的工作量。
不可执行花指令
不可以执行的花指令,这类花指令会使反编译器在反编译的时候出错,反汇编器可能错误的反汇编这些指令。根据反汇编的工作原理,只有花指令同正常指令的前几个字节被反汇编器识别成一组无用字节时,才能破坏反汇编的结果。因此,插入的花指令应当是一些不完整的指令,被插入的不完整指令可以是随机选择的。
为了能够有效迷惑反汇编器,同时又确保代码的正确运行,花指令必须满足两个基本特征,即:
垃圾数据必须是某个合法指令的一部分。
程序运行时,花指令必须位于实际不可执行的代码路径。
原理
反编译器的线性反编译(理解花指令的重点)
反编译器的工作原理是,从exe的入口AddressOfEntryPoint处开始,依序扫描字节码,并转换为汇编,比如第一个16进制字节码是0xE8,一般0xE8代表汇编里的CALL指令,且后面跟着的4个字节数据跟地址有关,那么反编译器就读取这一共5个字节,反编译为CALL 0x地址
。
对应的,有些字节码只需要一个字节就可以反编译为一条指令,例如0x55对应的是push ebp
,这条语句每个函数开始都会有。同样,有些字节码又需要两个、三个、四个字节来反编译为一条指令。
也就是说,如果中间只要一个地方反编译出错,例如两条汇编指令中间突然多了一个字节0xE8,那反编译器就会将其跟着的4个字节处理为CALL指令地址相关数据给反编译成一条CALL 0x地址
指令。但实际上0xE8后面的四个字节是单独的字节码指令。这大概就是线性反编译。
线性扫描和递归下降
1.线性扫描算法
线性扫描算法p1从程序的入口点开始反汇编,然后对整个代码段进行扫描,反汇编扫描过程中所遇到的每条指令。线性扫描算法的缺点在于在冯诺依曼体系结构下,无法区分数据与代码,从而导致将代码段中嵌入的数据误解释为指令的操作码,以致最后得到错误的反汇编结果。
特点:从入口开始,一次解析每一条指令,遇到分支指令不会递归进入分支。
2.递归下降算法
递归下降采取另外一种不同的方法来定位指令。递归下降算法强调控制流的概念。控制流根据一条指令是否被另一条指令引用来决定是否对其进行反汇编,遇到非控制转移指令时顺序进行反汇编,而遇到控制转移指令时则从转移地址处开始进行反汇编。通过构造必然条件或者互补条件,使得反汇编出错。
特点:递归下降分析当遇到分支指令时,会递归进入分支进行反汇编。
花指令的构造
jx+jnx
__asm {
jz s;
jnz s;
_emit 0xC7;
s:
jnz实际上是fake的,因为jz这个指令,让ida认为jz下面的是另外一个分支,所以这里去除,就是将jnz下面包括jnz 全c了,然后把loc_402669+1的字节码全给nop了
这种混淆去除方式也很简单,特征也很明显,因为是近跳转,所以ida分析的时候会分析出jz或者jnz会跳转几个字节,这个时候我们就可得到垃圾数据的长度,将该长度字节的数据全部nop掉即可解混淆。
call pop / add esp
这里call指令,其实本质就是jmp&push 下一条指令的地址,但是这里其实就是一个jmp指令
所以 push这条指令是多余的,需要add esp,4 调整堆栈,但是ida会默认把call 后面的那个地址
当成一个函数。
stx/jx
clc是清除EFlags寄存器的carry位的标志,而jnb是根据cf==0时跳转的,然而jnb这个分支指令,ida
又将后面的部分认作成了另外的分支
jmp xxx红色
看下花指令源代码, 实际是e9在搞鬼,ida会默认将e9后面的4个字节当成地址,只要nop掉就行
多重跳转的
利用idapython去除
def nop(addr,endaddr):
while(addr<endaddr):
PatchByte(addr,0x90)
addr+=1
def undefine(addr,endaddr):
while addr<endaddr:
MakeUnkn(addr,0)
addr+=1
def dejunkcode(addr,endaddr):
while addr<endaddr:
MakeCode(addr)
# 匹配模版
if GetMnem(addr)=='jmp' and GetOperandValue(addr,0)==addr+5 and Byte(addr+2)==0x12:
next=addr+10
nop(addr,next)
addr=next
continue
addr+=ItemSize(addr)
永恒跳转
最简单的jmp指令
jmp s
db junk_code;
s:
这种jmp单次跳转只能骗过线性扫描算法,会被IDA识别(递归下降)
多层跳转
__asm {
jmp s1;
_emit 68h;
s1:
jmp s2;
_emit 0CDh;
_emit 20h;
s2:
jmp s3;
_emit 0E8h;
s3:
}
和单次跳转一样,这种也会被IDA识别。将花指令改写一下骗过IDA
__asm {
_emit 0xE8
_emit 0xFF
//_emit 立即数:代表在这个位置插入一个数据,这里插入的是0xe8
}
可以看到IDA错误的识别loc_411877处的代码,成功的实现了花指令的目的
常用指令含义
push ebp ----把基址指针寄存器压入堆栈
pop ebp ----把基址指针寄存器弹出堆栈
push eax ----把数据寄存器压入堆栈
pop eax ----把数据寄存器弹出堆栈
nop -----不执行
add esp,1-----指针寄存器加1
sub esp,-1-----指针寄存器加1
add esp,-1--------指针寄存器减1
sub esp,1-----指针寄存器减1
inc ecx -----计数器加1
dec ecx -----计数器减1
sub esp,1 ----指针寄存器-1
sub esp,-1----指针寄存器加1
jmp 入口地址----跳到程序入口地址
push 入口地址---把入口地址压入堆栈
retn ------ 反回到入口地址,效果与jmp 入口地址一样
mov eax,入口地址 ------把入口地址转送到数据寄存器中.
jmp eax ----- 跳到程序入口地址
jb 入口地址
jnb 入口地址 ------效果和jmp 入口地址一样,直接跳到程序入口地址
xor eax,eax 寄存器EAX清0
CALL 空白命令的地址 无效call
栈指针平衡
这里借用大佬的图来看一个栈针平衡的示例,总结就是先进后出,每一次pop和push要改变esp让栈平衡
例题:
[HNCTF 2022 WEEK2]e@sy_flower
当使用IDA分析伪代码时,有花指令会发生
无法查看伪代码,需要去给出的地址查看具体发生的问题
要设置一下IDA,让它显示出栈指针(Options-General-Disassembly-"Stack pointer")
可以把stack pointer打开,然后再把number of opcode bytes 设为5
发现jz和jnz互补跳转了,后面地址+1已经提示了跳转的字节大小。
选中这一行,Edit-Patch program-Change bytes,把第一个e9改为90
对main函数用P重新定义下,再F5反编译
逻辑就是输入的flag先互换位置,再与0x30异或
enc = list('c~scvdzKCEoDEZ[^roDICUMC')
flag = []
for i in range(len(enc)):
flag.append(chr(ord(enc[i])^0x30))
for i in range(int(len(flag)/2)):
tmp = flag[2*i]
flag[2 * i] = flag[2 * i + 1]
flag[2 * i + 1] = tmp
for i in flag:
print(i,end="")
[MoeCTF 2022]chicken_soup
查壳32,打开分析后发现401000和401080对v4加密了,点击进去,就发现401000和401080被加花了
jz和jnz互补跳转。打开opcode bytes的显示,然后分别对E9改为90nop掉,然后用P再F5
进去main,可以看到sub_401000和sub_401080分别对v4进行加密
思路:输入字符串---->flag长度检验为38个—>经过2个函数指针的运算后----->与加密过后的数据进行比较
第一层加密,即flag[i] = flag[i] + flag[i+1]
第二层加密,即每个字符自身左移4位与自身右移4位进行按位与运算
即flag[i] >> 4 | flag[i] << 4
直接逆回去
enc =[0xcd,0x4d,0x8c,0x7d,0xad,0x1e,0xbe,0x4a,0x8a,0x7d,0xbc,0x7c,0xfc,0x2e,0x2a,0x79,0x9d,0x6a,0x1a,0xcc,0x3d,0x4a,0xf8,0x3c,0x79,0x69,0x39,0xd9,0xdd,0x9d,0xa9,0x69,0x4c,0x8c,0xdd,0x59,0xe9,0xd7]
for i in range(len(enc)):
enc[i] = ((enc[i]//16 | enc[i]<<4)) & 0xff
print((enc[i]))
for i in range(len(enc)-1, 0, -1):
enc[i-1] -= enc[i]
print(bytes(enc))
参考:
手脱花指令及IDA脚本编写 - 『脱壳破解区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn
【逆向学习】花指令的去除_去除花指令-CSDN博客
花指令的模式识别以及处理 - YenKoc - 博客园 (cnblogs.com)
[原创][花指令]由易到难全面解析CTF中的花指令-软件逆向-看雪-安全社区|安全招聘|kanxue.com
CTF逆向Reverse 花指令介绍 and NSSCTF靶场入门题目复现_Sciurdae的博客-CSDN博客
汇编跳转指令: JMP、JECXZ、JA、JB、JG、JL、JE、JZ、JS、JC、JO、JP 等_jg指令-CSDN博客