代码混淆技术是一种用于阻碍逆向工程分析人员解析程序代码(或功能)的指令处理技术。
我们在第57章可看到,在逆向工程的过程中字符串经常起到路标的作用。注意到这个问题的编程人员就会着手解决这个问题。他们会采用一些变换的手法,让他人不能直接通过IDA或者16进制编辑器直接搜索到字符串原文。
这里我们举一个简单的例子。
比方说,我们可以这样构造一个字符串:
Mov byte ptr [ebx], 'h'
mov byte ptr [ebx+1], 'e'
mov byte ptr [ebx+2], 'l'
mov byte ptr [ebx+3], 'l'
mov byte ptr [ebx+4], 'o'
mov byte ptr [ebx+5], ' '
mov byte ptr [ebx+6], 'w'
mov byte ptr [ebx+7], 'o'
mov byte ptr [ebx+8], 'r'
mov byte ptr [ebx+9], 'l'
mov byte ptr [ebx+10], 'd'
当然还有更为复杂的构造方法:
mov ebx, offset username
cmp byte ptr [ebx], 'j'
jnz fail
cmp byte ptr [ebx+1], 'o'
jnz fail
cmp byte ptr [ebx+2], 'h'
jnz fail
cmp byte ptr [ebx+3], 'n'
jnz fail
jz it_is_john
不管是以上的哪种情况,我们用十六进制的文本编译器都不能直接搜索到字符串原文。
实际上这两种方法适用于那些无法利用数据段构造数据的情景。因为它们可以在文本段直接构造数据,所以也常见于各种PIC和shellcode。
另外,笔者还见过这样使用sprintf()函数的:
sprintf(buf, "%s%c%s%c%s", "hel",'l',"o w",'o',"rld");
代码看起来很诡异,但是作为一个简单的反编译技巧来说,也不失为一个好办法。
加密存储字符串是另一种常见的处理方法。只是这样一来,就要在每次使用前对字符串解密。相关的例子可以参看第78章第2节。
在正常执行指令序列中插入一些虽然可被执行但是没有任何作用的指令,本身就是一种代码混淆技术。
我们可以看一个简单的例子。
指令清单50.1 源代码
add eax, ebx
mul ecx
指令清单50.2 采用混淆技术后的代码
xor esi, 011223344h ; garbage
add esi, eax ; garbage
add eax, ebx
mov edx, eax ; garbage
shl edx, 4 ; garbage
mul ecx
xor esi, ecx ; garbage
在程序代码中插入的混淆指令,调用了源程序不会使用的ESI和EDX寄存器。混淆代码利用了源程序的中间之后,大幅度地增加了反编译的难度,何乐不为呢?
MOV op1,op2这条指令,可以使用组合指令代替:PUSH op2, POP op1。
JMP label指令可以用PUSH label, RET这个指令对代替。反编译工具IDA不能识别出这种label标签的调用结构。
CALL label指令则可以用以下三个指令代替:PUSH {call指令后面的那个label}、PUSH label和RET指令。
PUSH op可以用以下的指令代替。SUB ESP,4或8; MOV [ESP],操作符。
在下面的代码中,假定此处ESI的值肯定是0,那么我们可以在fake luggage处插入任意长度和复杂度的指令,以达到混淆的目的。这种混淆技术称为不透明谓词(opaque predicate)。
mov esi, 1
... ; some code not touching ESI
dec esi
... ; some code not touching ESI
cmp esi, 0
jz real_code
; fake luggage
real_code:
我们还可以看看其他的例子(同样,我们假定ESI始终会是零)。
add eax, ebx ; real code
mul ecx ; real code
add eax, esi ; opaque predicate. XOR, AND or SHL, etc, can be here instead of ADD.
instruction 1
instruction 2
instruction 3
上面的3行正常执行的指令序列可以用如下所示的复杂结构代替:
begin: jmp ins1_label
ins2_label: instruction 2
jmp ins3_label
ins3_label: instruction 3
jmp exit:
ins1_label: instruction 1
jmp ins2_label
exit:
dummy_data1 db 100h dup (0)
message1 db 'hello world',0
dummy_data2 db 200h dup (0)
message2 db 'another message',0
func proc
...
mov eax, offset dummy_data1 ; PE or ELF reloc here
add eax, 100h
push eax
call dump_string
...
mov eax, offset dummy_data2 ; PE or ELF reloc here
add eax, 200h
push eax
call dump_string
...
func endp
这个程序执行时,我们只能在IDA编译工具中看到dummy_data1和dummy_data2的reference(调用信息)。它不能正常显示字符串正体message1和message2的调用信息。
全局变量或者函数也可以这样混淆。
编程人员可以构建其自身的PL或者ISA解释器(类似VB.NET或者Java)。这样的话,反编译者就得花很多时间来理解这些解释器指令的意义以及细节。当然,他们基本上必须开发一种专用的反汇编或者反编译工具了。
笔者对Tiny C编译器做了一些修改,然后用它编译了一个小程序(参见url: http://go.yurichev.com/ 17220
)。请分析该程序的具体功能(参见G.1.13)。
这是一个很短的程序,采用打了补丁的Tiny C编译器编译。看看它能做什么?
答案请参见G.1.15。