为了防止自己编写的二进制程序被逆向分析,许多软件会采取各种手段,为程序加上重重壁垒
二进制代码的保护手段种类繁多,并且运用及其灵活
Ag:对汇编指令进行一定程度的混淆变换,可以干扰静态分析中的反汇编过程
在程序中穿插各种反调试技术,能有效抵御动态分析
对程序中的关键算法进行虚拟化保护,可以给逆向工作者带来极大的阻力
抵御静态分析
在常用的IDA Pro等工具,还是如Ghidra之类的新兴工具,在载入二进制程序后,它们首先进行的工作是对程序进行反汇编:将机器码转换为汇编指令,在反汇编结果的基础上开展进一步的分析
显然如果反汇编受到干扰,那么静态分析就会变得非常困难
此外反汇编结果正确与否将直接影响到诸如Hex-Rays Decompiler等反编译工具反编译的正确性
因此许多开发者会选择对汇编指令本身做一些处理,使反编译器无法生成逻辑清晰的伪代码,从而增加逆向选手的难度
干扰反汇编最简单的方法就是在代码中增加花指令
花指令没有固定的形式,泛指用于干扰逆向工作者的无用指令
示例:
push ebp
mov ebp,esp
sub esp,0x100
这是常见的函数头,反汇编经常依次作为判断函数起始地址的依据,也以此进行栈指针分配的计算
如果在其中加入一些互相抵消的操作
push ebp
pushfd
add esp,0xd
nop
sub esp,0xd
popfd
mov ebp,esp
sub esp,0x100
可以看到代码复杂度提高了,但实际进行的操作效果并没有变化
此外pushfd和popfd等指令会让一些解析栈指针的逆向工具产生错误
另一种常见的干扰静态分析的方式是在正常的指令中插入一个特定的字节,并在该字节前加入向该字节后跳转语句,以保证实际执行的指令效果不变
对于这一特定的字节,要求其是一条较长指令的首字节(如0xE8为call指令的首字节),插入的这个字节被称为脏字节
由于x86是不定长指令集,如果反汇编器没有正确地从每条指令得到起始位置开始解析,就会出现解析错误乃至完全无法展开后续分析的情况
之前介绍线性扫描和递归下降这两种最具代表行的反汇编算法
对于以OllyDBG和WinDBG为代表的线性扫描反汇编工具,由于它们只是从起始地址开始一条一条的线性向下解析,我们可以简单地使用一条无条件跳转指令实现脏字节的插入
对于前文的代码片段我们在第一条和第二条指令之间插入一个跳转指令,并且加入0xE8字节
push ebp
jmp addr1
db 0xE8
addr1:
mov ebp,esp
sub esp,0x100
根据线性扫描反汇编算法,当反汇编解析完jmp addr1指令后,会紧接着从下一个0xE8开始进行解析,而0xE8call指令的起始字节,就会导致反汇编认为从0xE8开始的5字节为一条call指令,从而让后续的指令全部被错误解析
而对于IDA为代表的递归下降反汇编器,由于递归下降反汇编算法在遇到无条件跳转时,会转向跳转的目标地址递归地解析指令,就会导致插入0xE8字节直接被跳过
然而,递归下降反汇编器尽管部分模拟了程序执行得到控制流过程,但它并不是真正运行,所以不能获取到所有的信息
可以利用这一点将上面代码改为
push ebp
jz addr1
jnz addr1
db 0xE8
addr1:
mov ebp,esp
sub esp,0x100
即将一条无条件跳转语句改成两条成功条件相反的条件转跳语句
由于递归下降反汇编算法不能获取到程序运行中的上下文信息,遇到条件跳转语句时,它会递归地将跳转地分支与不跳转的分支进行反汇编
显然,在反汇编完jnz语句后,它不跳转的分支就是下一个地址,从而使0xE8开头的“指令”被解析
在实际操作中,为了达到更好的效果,往往会将这些跳转目标代码的顺序打乱,即“乱序”,从而达到类似控制流混淆得到效果
push ebp
jz addr2
jnz addr2
db 0XE8
addr3:
sub esp,0x100
...
addr2:
mov ebp,esp
jmp addr3
还有一种常见的静态混淆方式是指令替换,又称变形
在汇编语言中,大量的指令都可以设法使用其他指令来实现相同或类似的功能
Ag:函数调用指令call可以使用其他指令替换
call addr
可替换为
push addr
ret
而函数返回指令ret,也可以替换为
push ecx
mov ecx,[esp+4]
add esp,8
jmp ecx
注意该替换破坏了ecx寄存器,因此我们需要保证此时ecx没有正在被程序使用,在实际操作中,可以根据上下文调整
在CTF中,出题人通常选择替换涉及函数调用与返回的指令,如call、ret,这样可以导致IDA等工具解析出的函数地址范围与调用关系出现错误,从而干扰静态分析