现在的位置: 首页 > 综合 > 正文

关于自修改代码的一点浅析

2013年01月16日 ⁄ 综合 ⁄ 共 7003字 ⁄ 字号 评论关闭

相信大家都在程序调试或者分析中碰到过自修改代码的情况吧。所谓自修改代码,就是程序自我保护的一种机制。它使我们的反汇编调试器看起来相当地无助。因为我们看到的所谓的反汇编代码并非执行过程中的代码,它表面上看起来不合逻辑甚至一塌糊涂,但是运行起来却井井有条。因此,这项技术被广泛用在那些反破解的商业软件中,在试图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语言写成的完整代码如下:(待续) 

抱歉!评论已关闭.