作者:小安
博客地址:我的博客
哎,想了好长时间这个知识点到底什么名字,还是最简单的就叫做“花指令”。本文参照网上众多大佬博客和自己的理解进行编写的。
背景
为什么会出现花指令?
划重点:
- 线性扫描算法:逐行反汇编(无法将数据和内容进行区分)
- 递归行进算法:按照代码可能的执行顺序进行反汇编程序。 正是因为这两种反汇编的规格和缺陷机制,所以才导致了会有花指令的诞生。
概念
花指令是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法,在真实代码中插入一些垃圾代码的同时还保证原有程序的正确执行,而程序无法很好地反编译, 难以理解程序内容,达到混淆视听的效果。
简单的说就是在代码中混入一些垃圾数据阻碍你的静态分析。
分类
可执行花指令
- 可执行花指令指的是这部分花指令代码在程序的正常执行过程中会被执行,但执行这些代码没有任何意义,执行前后不改变任何寄存器的值(当然eip这种除外),同时这部分代码也会被反汇编器正常识别。
- 首先,花指令的首要目的依然是加大静态分析的难度,让你难以识别代码的真正意图;然后,这种花指令可以破坏反编译的分析,使得栈指针在反编译引擎中出现异常。(当然我们知道栈指针实际上是没有问题的,只不过反编译引擎还有待完善的空间)
不可执行式花指令
- 花指令虽然被插入到了正常代码的中间,但是并不意味着它一定会得到执行,这类花指令通常形式为在代码中出现了类似数据的代码,或者IDA反汇编后为jmupout(xxxxx)。
- 这类花指令一般不属于CPU可以识别的操作码,那么就需要在上面用跳转跳过这些花指令才能保证程序的正常运行。
实现
- 简单的花指令 简单的花指令 0xe8是跳转指令,可以对线性扫描算法进行干扰,但是递归扫描算法可以正常分析。 两个跳转一个指向无效数据,一个指向正常数据来干扰递归扫描算法。
- 简单的jmp OD能被骗过去,但是因为ida采用的是递归扫描的办法所以能够正常识别。
#include<stdio.h>
int main() {
__asm{
jmp label1;
__emit 0xe8;
label1:
}
printf("Hello World!");
return 0;
}
3. 多级跳转
本质上和简单跳转是一样的,只是加了几层跳转。显然无法干扰ida
#include<stdio.h>
int main() {
__asm{
start://花指令开始
jmp label1;
__emit 0xe8;
label1:
jmp label2;
__emit 0xe8;
label2:
jmp label3;
__emit 0xe8;
label3:
}
printf("Hello World!");
return 0;
}
4. jnx和jx条件跳转
利用jz和jnz的互补条件跳转指令来代替jmp。竟然没有骗过OD(是因为吾爱的这个有插件吗)。但是ida竟然没有正常识别。
5. 永真条件跳转
通过设置永真或者永假的,导致程序一定会执行,由于ida反汇编会优先反汇编接下去的部分(false分支)。也可以调用某些函数会返回确定值,来达到构造永真或永假条件。ida和OD都被骗过去了。
__asm{
push ebx
xor ebx,ebx
test ebx,ebx
jnz label1
jz label2
label1:
_emit junkcode
label2:
pop ebx//需要恢复ebx寄存器
}
__asm{
clc
jnz label1:
_emit junkcode
label1:
}
6. call&ret构造花指令
__asm{
call label1
_emit junkcode
label1:
add dword ptr ss:[esp],8//具体增加多少根据调试来
ret
_emit junkcode
}
call指令:将下一条指令地址压入栈,再跳转执行
ret指令:将保存的地址取出,跳转执行
7. 汇编指令共用opcode
jmp的条指令是inc eax的第一个字节,inc eax和dec eax抵消影响。这种共用opcode确实比较麻烦。
创意花指令
前面几种花指令都是比较老套的,入门花指令还能勉勉强强骗过反编译器,不过有经验的逆向者一眼就能识破,以下几种花指令形式,可以任由自己构造。
- 替换ret指令
_asm
{
call LABEL9;
_emit 0xE8;
_emit 0x01;
_emit 0x00;
_emit 0x00;
_emit 0x00;
LABEL9:
push eax;
push ebx;
lea eax, dword ptr ds : [ebp - 0x0];
#将ebp的地址存放于eax
add dword ptr ss : [eax-0x50] , 26;
#该地址存放的值正好是函数返回值,
#不过该地址并不固定,根据调试所得。
#加26正好可以跳到下面的mov指令,该值也是调试计算所得
pop eax;
pop ebx;
pop eax;
jmp eax;
_emit 0xE8;
_emit 0x03;
_emit 0x00;
_emit 0x00;
_emit 0x00;
mov eax,dword ptr ss:[esp-8];
#将原本的eax值返回eax寄存器
}
由于:
call指令的本质:push 函数返回地址然后jmp 函数地址
ret指令的本质: pop eip
两者都是对寄存器eip中存放的地址的操作。
所以我们可以在call指令之后,清楚的明白函数返回地址存放于esp,可以将值取出,用跳转指令跳转到该地址,即可代替ret指令。
当然,这种构造跳转指令可以变化多样。
- 控制标志寄存器跳转 这一部分需要精通标志寄存器,每一个操作码都会对相应的标志寄存器产生相应的影响,如果我们对标志寄存器足够熟练,就可以使用对应的跳转指令构造永恒跳转!
- 利用函数返回确定值 有些函数返回值是确定的,比如我们自己写的函数,返回值可以是任意非零整数,就可以自己构造永恒跳转。 还有些api函数也是如此:方面可以传入一些错误的参数,如LoadLibraryA。
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
如果我们故意传入一个不存在的模块名称,那么他就会返回一个确定的值NULL,我们就可以通过这个构造永恒跳转。
另一方面,某些api函数,我们既然使用他,肯定就是一定要调用成功的,而这些api函数基本上只要调用成功就就会返回一个确定的零或者非零值,如MessageBox:
int MessageBox(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
该api只有在其调用失败的时候才能返回零,那么我们也可以通过这一点构造永恒跳转。
3. 花指令原理另类利用
当我们理解了花指令的原理后,我们可以在将花指令中的垃圾数据替换为一些特定的特征码,可以对应的
“定位功能”
“定位功能”
“定位功能”,尤其在SMC自解码这个反调试技术中可以运用。例如:
asm
{
Jz Label
Jnz Label
_emit 'h'
_emit 'E'
_emit 'l'
_emit 'L'
_emit 'e'
_emit 'w'
_emit 'o'
_emit 'R'
_emit 'l'
_emit 'D'
Label:
}
将这串特征码hElLowoRlD嵌入到代码中,那我们只需要在当前进程中搜索hElLowoRlD字符串,就可以定位到当前代码位置,然后对下面的代码进行SMC自解密。
清除
- 手动清除 找到所有的花指令,重新设置数据和代码地址。或者将花指令设置为nop(0x90) 在0x401051设置为数据类型(快捷键D),在0x401052设置为代码类型(快捷键C) 这里用一个ida python脚本添加ALT+N快捷键来将指令的第一个字节设置为NOP
from idaapi import *
from idc import *
def nopIt():
start = get_screen_ea()
patch_byte(start,0x90)
refresh_idaview_anyway()
add_hotkey("alt-N",nopIt)
- 自动清楚花指令 面有3个类别ida无法正常识别
- 互补条件跳转(比较好处理)
- 永真条件跳转 (各种永真条件比较难匹配)
- call&ret跳转(比较难处理) 所以就只对第一种jnx和jx的花指令进行自动化处理。 所有的跳转指令,互补跳转指令只有最后一个bit位不同。
70 <–> JO(O标志位为1跳转)
71 <–> JNO
72 <–> JB/JNAE/JC
73 <–> JNB/JAE/JNC
74 <–> JZ/JE
75 <–> JNZ/JNE
76 <–> JBE/JNA
77 <–> JNBE/JA
78 <–> JS
79 <–> JNS
7A <–> JP/JPE
7B <–> JNP/JPO
7C <–> JL/JNGE
7D <–> JNL/JGE
7E <–> JLE/JNG
7F <–> JNLE/JG
第一条指令跳转距离=第二条跳转距离+2。简单一点可以是\x03和\x01
抄的代码
from ida_bytes import get_bytes,patch_bytes
start= 0x401000#start addr
end = 0x422000
buf = get_bytes(start,end-start)
def patch_at(p,ln):
global buf
buf = buf[:p]+b"\x90"*ln+buf[p+ln:]
fake_jcc=[]
for opcode in range(0x70,0x7f,2):
pattern = chr(opcode)+"\x03"+chr(opcode|1)+"\x01"
fake_jcc.append(pattern.encode())
pattern = chr(opcode|1)+"\x03"+chr(opcode)+"\x01"
fake_jcc.append(pattern.encode())
print(fake_jcc)
for pattern in fake_jcc:
p = buf.find(pattern)
while p != -1:
patch_at(p,5)
p = buf.find(pattern,p+1)
patch_bytes(start,buf)
print("Done")
总结
重点:构造永恒跳转,添加垃圾数据!
重一方面构造一个永恒的跳转,一方面又比较隐蔽,不仅骗过反编译器,更让破解者找不到花指令。
参考文章
https://blog.csdn.net/abel_big_xu/article/details/117927674
https://www.anquanke.com/post/id/236490#h3-12**加粗样式**
版权归原作者 茉莉小安 所有, 如有侵权,请联系我们删除。