第50章 混淆技术

代码混淆技术是一种用于阻碍逆向工程分析人员解析程序代码(或功能)的指令处理技术。

50.1 字符串变换

我们在第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.2 可执行代码

50.2.1 插入垃圾代码

在正常执行指令序列中插入一些虽然可被执行但是没有任何作用的指令,本身就是一种代码混淆技术。

我们可以看一个简单的例子。

指令清单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寄存器。混淆代码利用了源程序的中间之后,大幅度地增加了反编译的难度,何乐不为呢?

50.2.2 用多个指令组合代替原来的一个指令
50.2.3 始终执行或者从来不会执行的代码

在下面的代码中,假定此处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.
50.2.4 把指令序列搞乱
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:
50.2.5 使用间接指针
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的调用信息。

全局变量或者函数也可以这样混淆。

50.3 虚拟机以及伪代码

编程人员可以构建其自身的PL或者ISA解释器(类似VB.NET或者Java)。这样的话,反编译者就得花很多时间来理解这些解释器指令的意义以及细节。当然,他们基本上必须开发一种专用的反汇编或者反编译工具了。

50.4 一些其他的事情

笔者对Tiny C编译器做了一些修改,然后用它编译了一个小程序(参见url: http://go.yurichev.com/ 17220

)。请分析该程序的具体功能(参见G.1.13)。

50.5 练习题

50.5.1 练习1

这是一个很短的程序,采用打了补丁的Tiny C编译器编译。看看它能做什么?

答案请参见G.1.15。