相信大家都在程序调试或者分析中碰到过自修改代码的情况吧。所谓自修改代码,就是程序自我保护的一种机制。它使我们的反汇编调试器看起来相当地无助。因为我们看到的所谓的反汇编代码并非执行过程中的代码,它表面上看起来不合逻辑甚至一塌糊涂,但是运行起来却井井有条。因此,这项技术被广泛用在那些反破解的商业软件中,在试图bypass杀毒软件的黑客软件中也颇有涉及。在另一方面,cracker初学者们对这类程序大伤脑筋,他们一边诅咒反汇编调试器一边对着一大堆非法指令符莫名其妙。他们必须不停地设置断点,慢慢地让程序在运行过程中将真正的代码暴露出来。
没错,这就是很多壳的基本运行原理。然而我并不打算在这里教大家怎么脱壳,大部分程序员都已经为我们做好了现成的脱壳程序,剩下的都是一些繁杂的充斥着各种SEH等保护机制的超级壳,也许强大到作者也没有办法写出脱壳脚本。我写这篇文章的目的就是对代码自修改技术做一点点分析而已。
为了更好地诠释代码自修改,我引用下面段代码。
FE 0D xxxxxx DEC byte ptr DS:[The_fake_jmp] ;将装有The_fake_jmp入口地址的DS进行修改,即将The_fake_jmp的第一个字节内容减一。
...... ;其他的代码
B8 01 000000 MOV eax,1
The_fake_jmp:
75 xx JNZ another_place ;eax非零则跳转
......
如果没有看到前面的代码,也许很多程序员会想当然地认为一定会跳转到another_place,其实程序运行时已经将the_fake_jmp改成了74 xx,即改成了jz。这样程序运行的结果将与程序员所预料的完全相反。
通过这个例子大家也许会发现,存在自修改代码的程序调试起来确实要小心.难道我们必须要一步一步单步调试才能发现隐藏的陷阱吗?当然不是.前面已经提到了,不吝惜自己的断点可以很方便地识破这些小小的破绽。
下面我将通过调试著名木马Bifrost 1.2.1的脱壳服务端(该木马可以在chasenet.org下载到)来观察它的一些自修改代码片断。
传说中Bifrost的原始服务端是加壳的。有人已经做出了所谓的脱壳版本。实际上,我发现两者的区别不是很大。oep都没有经过修改。这里选用的调试器是网络上比较流行的应用层调试器Ollydbg。
载入服务端后我们可以看到OD的提示。说明该服务端存在大量自修改程序。我们随便就能找到一些调试器无法识别的指令。这里我选用00406111处.这里的代码如下:(这些代码也许在其他版本的服务端上是不同的)
00406111 F4 hlt
00406112 A4 movs byte ptr es:[edi], byte ptr [esi>
00406113 64:9A 325427F8 >call far D779:F8275432
0040611B 9B wait
0040611C F1 int1
0040611D 65:338D 355EF3B>xor ecx, dword ptr gs:[ebp+BFF35E35]
00406124 DEBF D977A7B3 fidivr word ptr [edi+B3A777D9]
0040612A A8 62 test al, 62
0040612C 9B wait
0040612D 002A add byte ptr [edx], ch
不仅仅是这些代码,它的一大段上下文都让人莫名其妙。到这里我们假定它是自修改代码的一部分。我们在00406111点右键-断点-内存写入,然后F9运行,我们会到达尝试对这段代码进行修改的程序段。仔细看看,呵呵,不错,我们似乎到达了对代码进行修改的函数里。其附近代码如下,我在这里对它们加了一点简短的注释。
004073C0 55 push ebp
004073C1 8BEC mov ebp, esp
004073C3 56 push esi
004073C4 33F6 xor esi, esi ;清空esi
004073C6 3975 0C cmp dword ptr [ebp+C], esi ;esi在这里做计数器
004073C9 7E 1B jle short 004073E6 ;若解密完毕则跳转
004073CB 8B45 08 mov eax, dword ptr [ebp+8]
004073CE 33D2 xor edx, edx
004073D0 8D0C06 lea ecx, dword ptr [esi+eax] ;解密段指针
004073D3 8BC6 mov eax, esi
004073D5 F775 14 div dword ptr [ebp+14]
004073D8 8B45 10 mov eax, dword ptr [ebp+10]
004073DB 8A0402 mov al, byte ptr [edx+eax] ;我们的密钥
004073DE 3001 xor byte ptr [ecx], al ;xor加密解密方式
004073E0 46 inc esi
004073E1 3B75 0C cmp esi, dword ptr [ebp+C]
004073E4 ^ 7C E5 jl short 004073CB
004073E6 5E pop esi
004073E7 5D pop ebp
004073E8 C3 retn
有点像某些shellcode的编码器,不是吗?值得一提的是,xor的密钥并不是一个常数,它在加密解密过程中会发生变化。我们需要更多的调试来看清楚它是怎么工作的。我们在004073DE处设置一个断点,然后Ctrl+F2重载,F9运行,呵呵,我们可以看到它停在解密阶段。我们看各个寄存器的值:EAX:00407320 ECX:00407028 ESI:000000,其余的跟我们要观察的没有太多关系,我们就不看了,我们所知道的就是它从00407028处开始加密。不妨看看00407028这里是什么:
00407028 /75 5D jnz short 00407087
0040702A |CC int3
0040702B |55 push ebp
0040702C |CC int3
0040702D |FA cli
0040702E ^|73 80 jnb short 00406FB0
00407030 |77 3E ja short 00407070
00407032 |20D6 and dh, dl
00407034 |20D6 and dh, dl
00407036 |79 5F jns short 00407097
00407038 |6D ins dword ptr es:[edi], dx
又是一堆无聊的代码。我们再F9运行,它又停在了断点处,我们再看:EAX:004073D6 ECX:00407029 ESI:000001看,密钥变了,再运行一次,我们又看到EAX又变回了00407328。说明这是两个密钥轮流对代码进行xor修改。密钥看起来像个指针,但是我们没必要太关心它,根据修改前后数值对比我们也能找出密钥,不是吗?我们再来看它到底修改了多长的代码。我们将原来的断点取消,再在004073E6处设置断点,直接看看它自修改的结果。运行,我们发现ecx停在了00407380处,这是加密的结尾,esi的值为359(十六进制),我们知道它这次修改了0x359字节的东西 。我们来看看它修改过的,也就是真实的代码。
00407028 55 push ebp
00407029 8BEC mov ebp, esp
0040702B 83EC 2C sub esp, 2C
0040702E 53 push ebx
0040702F 56 push esi
00407030 57 push edi
00407031 E8 00000000 call 00407036
00407036 59 pop ecx
00407037 894D E4 mov dword ptr [ebp-1C], ecx
0040703A EB 42 jmp short 0040707E
0040703C 56 push esi
0040703D 6972 74 75616C4>imul esi, dword ptr [edx+74], 416C617>
00407044 6C ins byte ptr es:[edi], dx
呵呵,这回好看多了。但是我们刚才定的地址是00406111,它似乎不在这个区段里。所以我们还得看看这区段的解密过程。因此,再次在004073DE处设置断点,运行看它是否还继续有活干。果然,它又停下来了,此时各寄存器的值为:EAX:004073F6 ECX:00401028,取消004073DE的断点,再运行,看:EAX:004073F6 ECX:00407027,如果没有取消断点,我们会发现,这轮解密用的是同一个密钥:F6。我们来看看00406111处变成什么了。
00406111 0252 92 add dl, byte ptr [edx-6E]
00406114 6C ins byte ptr es:[edi], dx
00406115 C4A2 D10E8F21 les esp, fword ptr [edx+218F0ED1]
0040611B 6D ins dword ptr es:[edi], dx
0040611C 07 pop es
0040611D 93 xchg eax, ebx
0040611E C57B C3 lds edi, fword ptr [ebx-3D]
00406121 A8 05 test al, 5
00406123 49 dec ecx
呵呵,看起来比我想像的要麻烦,因为解密后的代码还是一团糟。我们继续设置内存写入断点看看,跑飞了。可能运行过程中没有碰到该函数段吧。我们换个地方试试,设在00405B48,结果来到了另外一处解码器处。代码如下:
00407122 85C9 test ecx, ecx
00407124 8945 F4 mov dword ptr [ebp-C], eax
00407127 7E 30 jle short 00407159
00407129 894D F4 mov dword ptr [ebp-C], ecx
0040712C 8B48 10 mov ecx, dword ptr [eax+10]
0040712F 8B78 0C mov edi, dword ptr [eax+C]
00407132 8B70 14 mov esi, dword ptr [eax+14]
00407135 037D 08 add edi, dword ptr [ebp+8]
00407138 0333 add esi, dword ptr [ebx]
0040713A 894D DC mov dword ptr [ebp-24], ecx
0040713D 8BD1 mov edx, ecx
0040713F 897D D8 mov dword ptr [ebp-28], edi
00407142 C1E9 02 shr ecx, 2
00407145 F3:A5 rep movs dword ptr es:[edi], dword ptr [esi]
00407147 8BCA mov ecx, edx
00407149 83C0 28 add eax, 28
0040714C 83E1 03 and ecx, 3
0040714F FF4D F4 dec dword ptr [ebp-C]
00407152 F3:A4 rep movs byte ptr es:[edi], byte ptr [esi]
00407154 ^ 75 D6 jnz short 0040712C
00407156 8945 F4 mov dword ptr [ebp-C], eax
00407159 8B7D F0 mov edi, dword ptr [ebp-10]
0040715C 837F 74 10 cmp dword ptr [edi+74], 10
00407160 0F8C F2010000 jl 00407358
00407166 83BF 84000000 0>cmp dword ptr [edi+84], 0
天啊,这是用VM保护过的代码,而且我们也会发现,这些内容是经第一个解码器解码过的内容。真的是相当复杂。由于有过多的vm保护指令,这里就不对该解码器进行具体分析了。
说到这里,也许有人要问,这样的保护技术是怎样实现的。Kris Kaspersky在他的著作中给出了几种实现的方法,其中堆栈自修改的方式比较复杂,而且修改的代码必须完全可重定位,即在内存中的任何位置都可以独立执行。。。这需要相当高的汇编技巧,这里不介绍了。我要介绍的是另外一种不需要太多技巧的方法。
顺便提一下很多人对自修改代码的误区。他们认为自修改技术只能在反汇编调试下得到完整分析,因此认为它的编写也只有汇编才能支持。事实上,包括现有的汇编编译器,没有任何一个程序语言编译器声明支持自修改代码。只要有这样的技巧,不仅是汇编,C语言也支持。
假若我们是商业软件的开发商,我们希望自己的软件不被破解,于是我们现在要对软件注册段的函数进行自修改以及加密。我们假设自己的检测注册码的代码如下:
int iRegister(char csEnterkey[24])
{
//char csMykey[] = "ABCD-EFGH-IJKL-MNOP-QRST";
if(!strcmp( "ABCD-EFGH-IJKL-MNOP-QRST",csEnterkey))
{
printf("Thank you for register!/n");
return 0;
}
else printf("Invalid key,sorry./n");
return -1;
}
这里我们使用类似Bifrost服务端的初级自修改加密方式,即xor加密这段函数,我用C语言写成的完整代码如下:(待续)