花指令攻克

简介

花指令(junk code)是一种专门用来迷惑反编译器的指令片段,这些指令片段不会影响程序的原有功能,但会使得反汇编器的结果出现偏差,从而使破解者分析失败。比较经典的花指令技巧有利用 jmpcallret 指令改变执行流,从而使得反汇编器解析出与运行时不相符的错误代码

铺垫

反汇编算法

线性扫描

从程序入口点开始遍历一整个代码端,逐行对命令进行反汇编

缺点:在冯诺依曼体系结构下,无法区分数据与代码,从而导致将数据解释为指令操作码

递归下降

从程序入口点开始读取机器码进行反汇编,通过程序控制流来确定汇编的下一条指令,遇到无条件跳转直接跳转,遇到条件跳转则从两个命令执行分支处进行解析(优先解析顺序执行分支)

常用花指令硬编码

硬编码:汇编指令对应的机器码,通常以16进制表示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
addr: 汇编指令跳转的目标地址
immedn: 目的地址与当前指令下一条指令地址的距离, 是一个偏移值
nop               ----> 90         //1字节指令,空指令
call addr         ----> E8 immed32 //5字节指令,immed32为4字节
call far ptr addr       ----> 9A immed48 //7字节指令,immed48为6字节
jmp short near ptr addr ----> EB immed8  //2字节指令,immed8为1字节
jmp near ptr addr       ----> E9 immed32 //5字节指令
jmp far ptr addr        ----> EA immed48 //7字节指令
loop near ptr addr      ----> E2 immed8  //2字节指令
ret               ----> C2 n16     //3字节指令 等价于pop n和pop eip
retn              ----> C3         //1字节指令 等价于pop eip

两大分类

可执行花指令

可执行花指令指的是花指令代码在程序的正常执行过程中会被执行,但执行这些代码没有任何意义,执行前后不改变任何寄存器的值,同时这部分代码也会被反汇编器正常识别,其目的依然是加大静态分析的难度,让你难以识别代码的真正意图,有时这种花指令可以破坏反编译的分析,使得栈指针在反编译引擎中出现异常

压栈后恢复栈地址

1
2
3
4
_asm {
    push eax;
    add esp, 4;
}

call&ret构造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
_asm {
	call label
	label:
		add dword ptr ss : [esp], 7;//注意变长指令
	ret
}
real_code
//特点: 可执行花指令,最终会跳转至real_code标签处执行
//call指令本质是push nextcode和jmp desAddr,ret指令本质是jmp [esp]和sub esp,4
//在ret和real_code中间可以任意插入花指令,但要根据插入字节数添加对应add值

所以也可以通过push和jmp配合实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
_asm {
    push eax
    push ecx
    jmp label_1
    label_2:
    mov eax,[esp]
    add [esp],8
    jmp eax
    label_1:
    call label_2
    pop eax
}
printf("Hello world!\n");

通过压栈、退栈和跳转混淆代码,IDA将退栈指令识别为破坏堆栈平衡而报错,识别这类花指令后,将跳转和无效压栈等操作指令全部NOP掉即可

混淆特征码

如下一些指令功能可以用替代指令完成,目的即增加反汇编分析的复杂程度:

1
2
3
4
mov op1,op2     ---->    push op2 / pop op1
jmp label       ---->    push label / ret
call label      ---->    push label_next_instruction / push label / ret
push op         ---->    sub esp,4 / mov [esp],op

永真条件跳转

可见后文

不可执行花指令

不可执行花指令就是在程序执行过程中不会被执行,但利用静态分析算法的缺陷使反汇编分析执行会出错的垃圾数据,就会导致解析错误,这时我们需要跳过这些花指令才能保证程序正常运行

简单e8跳转

简单的花指令0xe8是跳转指令,可以对线性扫描算法进行干扰,但是递归扫描算法可以正常分析

1
2
3
4
5
_asm {
    jmp label1
    _emit 0xe8
    label1:
}

OD还是能被骗过去,但是因为ida采用的是递归扫描的办法所以能够正常识别

jz和jnz条件跳转

利用jz和jnz的互补条件跳转指令来代替jmp,两个跳转一个指向无效数据,一个指向正常数据来干扰递归扫描算法即ida不能正常识别

1
2
3
4
5
6
7
8
_asm {
	jz lable2
	jnz lable2
	_emit 0xe8
	lable2:
}
//特点: 不可执行花指令,无论是jz还是jnz指令,最终都跳转至label
//在jnz label和label之间可以插入任意花指令

将这类花指令NOP即可,也可以将条件跳转指令换成无条件跳转jmp,此时ida反编译就不会报错,原理可分析递归下降算法

永真条件跳转

通过设置永真或者永假的,导致程序一定会执行,由于ida反汇编会优先反汇编接下去的部分(false分支),也可以调用某些函数会返回确定值,来达到构造永真或永假条件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
_asm{
	push ebx
	xor ebx,ebx
	test ebx,ebx
	jnz label1
	jz label2
	label1:
	_emit junkcode
	label2:
	pop ebx    //需要恢复ebx寄存器
}
//特点: 可执行花指令,有2条跳转分支,但只有一条分支为永真会被执行,另一条永假不执行
//在虚假分支处可以插入任意花指令

破坏堆栈平衡

汇编中函数如果有参数或局部变量,在调用前会对堆栈进行保护 ,在返回前要还原函数调用前的堆栈,这一过程程序在编译时会自动加上,如果反编译器检测到指令破坏了堆栈平衡,即函数返回时与调用时堆栈状态发生了变化,就会报错,可以利用这一点构造破坏堆栈平衡的花指令

1
2
3
4
5
6
7
8
_asm {
    test eax,0    // 构造必然条件实现跳转,绕过破坏堆栈平衡的指令
    jz label
    add esp,0x1
    label:
}
printf("Hello World!\n");
//不可执行花指令

这类破环堆栈平衡的指令实际不会执行,但是由于IDA在反汇编分析时会分别从两个条件跳转处开始分析,因此判定堆栈不平衡导致反汇编失败,解决方法是NOP掉破坏堆栈平衡的指令

参考

花指令总结

逆向基础花指令

恶意代码分析

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
人生若只如初见
使用 Hugo 构建
主题 StackJimmy 设计