背景

CVE-2021-40449是卡巴斯基实验室在2021年8月下旬到9月上旬在Windows服务器上捕获的恶意样本利用的提权漏洞,该漏洞存在于win32kfull.sys驱动内,利用该漏洞可以在windows中完成从users到system的权限提升。

基本概念

内核对象:内核对象即在内核空间存在的对象,只能由内核分配,内核访问。

内核对象的引用计数:在操作系统中,可能有多个进程访问同一个内核对象,如果没有进程需要使用该对象内核就应该释放该对象,所以为了准确的释放该对象就有了引用计数。当内核对象被创建时,引用计数被标记为1,调用CloseHandle()时内核对象的引用计数就-1,这可以类比Java GC的引用计数法:

在对象中添加一个引用计数器,每当一个地方引用它时,计数器就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

句柄:由于内核对象只能由内核分配、访问、修改,当ring 3层的应用程序想要操作这些内核对象的时候,并不能直接操控内核对象。当内核对象创建好后,操作系统会使用一个句柄来标识该对象并返回给应用程序,应用程序通过操作系统提供的ring 3层API来操作句柄,ring3层API经过系统调用进入内核。在内核处句柄对应着具体的内核对象,这样ring3层的应用程序就可以通过操作句柄来间接操作内核对象。

句柄表:当一个进程初始化的时候,系统会给该进程分配一个句柄表,当进程创建内核对象的时候,内核创建对应内核对象,并遍历该进程的句柄表,在句柄表的空闲位置设置内核对象、对象指针等,并获取该位置的索引,作为进程创建对象的函数的返回值,即为句柄。

https://www.cnblogs.com/MisterXu/p/10846918.html

DC:是一个内核对象,全称device context,设备上下文对象

HDC:DC对象的句柄。

释放后重用:指一个内存空间被操作系统释放后,内存空间变为空闲状态,如果用户在这一刻申请内存,操作系统会优先分配刚释放的内存,则用户大概率可以申请到刚刚释放的内存并修改该内存空间的内容。如果在释放空间之前有指针指向该空间,在释放空间之后指针并未按照理想状态置为NULL,由于释放后可以重新申请该内存并修改内存内容,后续如果继续使用该指针,但内存内内容并不是预期的释放之前的内容,则会产生非预期行为。

eg:

#include <stdio.h>
#include <stdlib.h>

void method();
void badMethod();
// 定义函数指针
typedef void (*function)();
class test {
public:
    function p;
    test() {
    }
};
int main() {
    // new test对象
    test *t = new test();
    test *p = t;
    t->p = method;
    p->p();
    // 释放t指向的test对象的空间
    delete t;
    test *pt;
    for (size_t i = 0; i < 10000; i++) {
        // 占用刚释放的对象的内存空间
        pt = (test *)malloc(sizeof(test));
        // 将申请的空间当作test对象,并将对象的函数指针设置为恶意函数地址
        pt->p = badMethod;
    }

    // 这里原意想要调用method函数,但是实际调用了badMethod函数
    printf("第二次调用\n");
    p->p();
    return 0;
}

void method() {
    printf("method\n");
}
void badMethod() {
    printf("bad method\n");
}

漏洞形成分析

该漏洞产生于win32kfull!GreResetDCInternal函数中,该函数内会获取DC对象内的函数指针,并执行该函数指针指向的函数,但并未检查DC对象是否异常。所以如果可以在调用函数指针之前释放DC对象,并重新申请该对象的内存空间,通过构造内存布局,修改原DC对象的函数指针指向其他任意内核函数,就可以在win32kfull!GreResetDCInternal内实现任意内核函数调用

根据代码,我们可以算出DCO对象和DC对象的函数指针的关系:function pointer= ( (DCO +0x30)+0xad0),其中DCO +0x30即指向DC对象的指针

v10 = (_QWORD )(v8 + 48);

v15 = (void (_fastcall )(QWORD, _QWORD))(*v10 + 2768);

__int64 __usercall GreResetDCInternal@<rax>(HDC a1@<rcx>, __int64 a2@<rdx>, int *a3@<r8>)
{
  __int64 v24; // [rsp+50h] [rbp-20h]
  __int64 v25; // [rsp+60h] [rbp-10h]
  DCOBJ::DCOBJ((DCOBJ *)&v25, a1);              // 利用构造函数从HDC创建DCOBJ对象
  v8 = v25;
        ··········
  v10 = *(_QWORD *)(v8 + 48);                   // 赋值
  *(_QWORD *)(v10 + 1736) = 0i64;
  v24 = v11;
        ·······
  v9 = *(_QWORD *)(v25 + 512) != 0i64;
  v12 = *(_DWORD *)(v25 + 120) > 0;
        ·······
      v13 = (HDC)hdcOpenDCW(&qword_1C0141EB0, v26, 0i64, 0i64, *(_QWORD *)(v10 + 2584));// 创建新的DC对象,返回对应的HDC句柄
      if ( v13 )
      {
        *(_QWORD *)(v10 + 2584) = 0i64;
        DCOBJ::DCOBJ((DCOBJ *)&v24, v13);
        v14 = (_QWORD *)v24;
        if ( v24 )
        {
          if ( v12 )
            *(_DWORD *)(v24 + 120) = *(_DWORD *)(v24 + 116);
          v14[308] = *(_QWORD *)(v25 + 2464);
          *(_QWORD *)(v25 + 2464) = 0i64;
          v14[309] = *(_QWORD *)(v25 + 2472);
          *(_QWORD *)(v25 + 2472) = 0i64;
          v15 = *(void (__fastcall **)(_QWORD, _QWORD))(v10 + 2768);
          if ( v15 )
            v15(*(_QWORD *)(v10 + 1824), *(_QWORD *)(v14[6] + 1824i64));// 调用函数指针指向的函数,传入参数为用户传入的HDC对应的DC对象内的值
            ·······
          HmgSwapLockedHandleContents(v3, 0i64, v6, 0i64, v23);// 交换旧的和新的HDC对象
          GreReleaseHmgrSemaphore();
            ······
    bDeleteDCInternal(v6, 1i64, 0i64); // 传入了hdcOpenDCW返回的HDC,但HmgSwapLockedHandleContents交换了新旧句柄对应的DC对象,此时v6句柄对应旧DC对象。
            ······

调用该函数指针的时候,所用的两个参数也是源于用户传入的HDC句柄对应的DC对象。

v10 = (_QWORD )(v8 + 48); _

_v14[308] = (_QWORD )(v25 + 2464);

v14[309] = (_QWORD )(v25 + 2472);

v15((_QWORD )(v10 + 1824), (_QWORD )(v14[6] + 1824i64));

在win32kfull!GreResetDCInternal函数的后半段会调用win32kbase!DeleteDCInternal函数释放传入该函数的HDC句柄所对应的DC对象,到这里就达成了use-after-free的free步骤

HDC v3; 
v3=a1;     
      v13 = (HDC)hdcOpenDCW(&qword_1C0141EB0, v26, 0i64, 0i64, *(_QWORD *)(v10 + 2584));// 创建新的HDC
      v6 = v13;
      if ( v13 )
      {
        *(_QWORD *)(v10 + 2584) = 0i64;
        DCOBJ::DCOBJ((DCOBJ *)&v24, v13);
        v14 = (_QWORD *)v24;
        if ( v24 )
        {
          if ( v12 )
            *(_DWORD *)(v24 + 120) = *(_DWORD *)(v24 + 116);
          v14[308] = *(_QWORD *)(v25 + 2464);
          *(_QWORD *)(v25 + 2464) = 0i64;
          v14[309] = *(_QWORD *)(v25 + 2472);
          *(_QWORD *)(v25 + 2472) = 0i64;
          v15 = *(void (__fastcall **)(_QWORD, _QWORD))(v10 + 2768);
          if ( v15 )
            v15(*(_QWORD *)(v10 + 1824), *(_QWORD *)(v14[6] + 1824i64));
          GreAcquireHmgrSemaphore();
          LOBYTE(v23) = 1;
          HmgSwapLockedHandleContents(v3, 0i64, v6, 0i64, v23);// 交换旧的和新的HDC对象
          GreReleaseHmgrSemaphore();
                ·······
    // 删除HDC句柄对应的DC对象。
    bDeleteDCInternal(v6, 1i64, 0i64);

如果在释放DC对象之后,重新申请DC对象空间,修改里面的函数指针内容,并通过某些步骤,让内核执行DC对象内的函数指针,即可达到use步骤让内核执行任意内核函数。

漏洞利用分析

POC:https://github.com/KaLendsi/CVE-2021-40449-Exploit

POC代码分析:https://github.com/CppXL/cve-2021-40449-poc/blob/master/main.cpp

要利用该漏洞,难点在于free DC对象之后怎么使得内核再次调用DC对象的函数指针,在正常GreResetDCInternal函数流程中,是先调用DC对象的函数指针再删除这个对象,即按照正常流程即不会有use-after-free的条件。

在ring 3层调用ResetDC函数会通过系统调用进入内核调用函数NtGdiResetDC,在NtGdiResetDC会调用漏洞函数GreResetDCInternal,在GreResetDCInternal中会调用DC对象里面的函数指针。要利用该漏洞即要在调用函数指针之前完成三步动作:1、释放DC对象2、重新申请原DC对象的内存空间3、完成内存空间的布局

在函数GreResetDCInternal调用DC对象的函数指针之前会调用win32kbase!hdcOpenDCW函数。win32kbase!hdcOpenDCW函数会执行打印机驱动的用户态回调函数表里面的函数,该表里面存放了函数指针,该函数指针原先指向的是预定义的回调函数。在POC中覆盖这个函数指针,使其执行POC定义的回调函数。

在自定义回调函数中再次执行ResetDC函数并传入同一HDC句柄,则会再次执行NtGdiResetDC和GreResetDCInternal函数,而在GreResetDCInternal的后半段,会释放传入的HDC对应的DC对象并创建新的DC对象。此时达到了free步骤

在第二次ResetDC调用完成后,原DC对象已被释放,此时可以重新申请原DC对象的内存空间并完成内存布局,将原DC对象的函数指针和函数指针的参数的位置设置为想要执行的内核函数的地址及参数。在执行完第一次回调之后,GreResetDCInternal 将调用原DC对象内的函数指针,即完成了任意内核函数调用,此时达到了use步骤

完整调用链如下图:

其中漏洞相关的类定义如下,参考https://github.com/ZoloZiak/WinNT4/blob/master/private/ntos/w32/ntgdi/gre/dcobj.hxx#L97

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;

类之间的关系可以简化为下图:

调试

free部分

在free部分需要把我们想要释放的内存空间释放,并让后面的use部分成功申请到这块内存空间。

调试环境:虚拟机windows 10 1607、物理机windows 10 2004

POC:https://github.com/KaLendsi/CVE-2021-40449-Exploit

断点:

bp win32kfull!NtGdiResetDC
bp win32kfull!NtGdiResetDC+0xc1       "调用GreResetDCInternal函数"
bp win32kfull!GreResetDCInternal+0x3a "调用DCOBJ构造函数"
bp win32kfull!GreResetDCInternal+0x116 "调用_imp_hdcOpenDCW函数"
bp win32kfull!GreResetDCInternal+0x136 "第二次DCOBJ"
bp win32kfull!GreResetDCInternal+0x1b5 "调用DC对象函数指针"
bp win32kfull!GreResetDCInternal+0x1d1 "调用HmgSwapLockedHandle函数"
bp win32kfull!GreResetDCInternal+0x20d "调用_imp_bDeleteDCInternal函数"
bp cve_2021_40449!hook_DrvEnablePDEV+0x12a "循环调用"
bp win32kbase!PALMEMOBJ::bCreatePalette "调用win32kbase!PALMEMOBJ::bCreatePalette"

运行POC,断点bp win32kfull!NtGdiResetDC触发此时传入的句柄为rcx=00000000092105f1

第一次调用win32kfull!GreResetDCInternal 时传入各个参数为rcx=00000000092105f1 rdx=0000000000000000 r8=ffffb101aadf2a44 即第一个句柄值为00000000092105f1

第一次调用构造函数,利用DC对象创建DCO对象,此时rbx存放DCO对象的地址,

根据漏洞形成分析的计算公式,可以很方便的得到DC对象内的函数指针指向的函数的地址为:ffffd548a1f10c30

1: kd> dq rax
ffffb101`aadf29c0  ffffd50e`041fd010 00000000`00000001
ffffb101`aadf29d0  00000268`6e766b20 000000d7`97aff680
ffffb101`aadf29e0  00000000`00000000 00000000`092105f1
ffffb101`aadf29f0  00000000`00000000 ffffd50e`041fb030
ffffb101`aadf2a00  ffffb101`aadf2b80 ffffd548`a1f18fe6
ffffb101`aadf2a10  00000000`00000001 00000000`00000000
ffffb101`aadf2a20  ffffb101`aadf2a44 ffffd50e`041fb030
ffffb101`aadf2a30  000000d7`97aff5d0 00000000`00000000
// rbx存放了构造函数产生的DCO对象地址
1: kd> dq rbx
ffffd50e`041fd010  00000000`092105f1 80000001`00000000
ffffd50e`041fd020  ffffd800`b45ad780 00000268`6e75ea10
ffffd50e`041fd030  00100010`00000000 00000000`00000000
ffffd50e`041fd040  ffffd50e`00052030 00000000`00000000
ffffd50e`041fd050  ffffd800`b56f1260 00000009`1000a01f
ffffd50e`041fd060  ffffd50e`041fd3d0 00000000`0088000b
ffffd50e`041fd070  ffffd50e`000004f0 ffffd50e`00005d90
ffffd50e`041fd080  00000001`00000000 00000000`00000000
// ffffd50e`041fd010为rbx的值,此处ffffd50e`041fd010+0x30为PDC的地址,PDC指向DC对象即DC对象地址为ffffd50e`00052030
// 计算公式 *(dco地址+0x30)=dc地址
1: kd> dq ffffd50e`041fd010+0x30
ffffd50e`041fd040  ffffd50e`00052030 00000000`00000000
ffffd50e`041fd050  ffffd800`b56f1260 00000009`1000a01f
ffffd50e`041fd060  ffffd50e`041fd3d0 00000000`0088000b
ffffd50e`041fd070  ffffd50e`000004f0 ffffd50e`00005d90
ffffd50e`041fd080  00000001`00000000 00000000`00000000
ffffd50e`041fd090  00000000`00000000 00000000`00000000
ffffd50e`041fd0a0  ffffd50e`00001a10 ffffd50e`00004cb0
ffffd50e`041fd0b0  ffffd50e`000105f0 00000000`00000000
// ffffd50e`00052030+0xad0处为DC对象的函数指针,该指针指向了一个函数 
// 计算公式 *(dc地址 +0xad0)=函数地址
1: kd> dq ffffd50e`00052030+0xad0
ffffd50e`00052b00  ffffd548`a1f10c30 ffffd548`a1db18c0
ffffd50e`00052b10  00000000`00000000 00000000`00000000
ffffd50e`00052b20  00000000`00000000 ffffd548`a1f10930
ffffd50e`00052b30  00000000`00000000 ffffd548`a1f11dc0
ffffd50e`00052b40  ffffd548`a1f0e6b0 ffffd548`a1f11b00
ffffd50e`00052b50  00000000`00000000 ffffd548`a1f0cd70
ffffd50e`00052b60  ffffd548`a1f0d1f0 ffffd548`a1f112f0
ffffd50e`00052b70  00000000`00000000 00000000`00000000
// 以下为函数的汇编
1: kd> u ffffd548`a1f10c30
win32kfull!UMPDDrvResetPDEV:
ffffd548`a1f10c30 48895c2418      mov     qword ptr [rsp+18h],rbx
ffffd548`a1f10c35 4889742420      mov     qword ptr [rsp+20h],rsi
ffffd548`a1f10c3a 57              push    rdi
ffffd548`a1f10c3b 4883ec70        sub     rsp,70h
ffffd548`a1f10c3f 488b05ba440800  mov     rax,qword ptr [win32kfull!_security_cookie (ffffd548`a1f95100)]
ffffd548`a1f10c46 4833c4          xor     rax,rsp
ffffd548`a1f10c49 4889442468      mov     qword ptr [rsp+68h],rax
ffffd548`a1f10c4e 488bf9          mov     rdi,rcx

之后通过hdcOpenDCW函数调用用户模式的回调函数,在回调函数中再次调用ResetDC函数,此时传入的HDC和第一次调用ResetDC的是同一个句柄。

第二次调用win32kfull!GreResetDCInternal 时,传入同一个HDC句柄,即对应同一个DC对象。

0: kd> t
win32kfull!GreResetDCInternal:
ffffd548`a1f03e58 488bc4          mov     rax,rsp
1: kd> rrcx
rcx=00000000092105f1

第二次调用DCOBJ构造函数时,由于传入的是同一个HDC句柄,所以HDC句柄引用次数+1,同时两次调用构造函数构造的对象关联到同一个DC对象。

之后第二次调用win32kfull!_imp_hdcOpenDCW函数,在该函数内执行政策回调函数,win32kfull!imp_hdcOpenDCW返回一个HDC句柄值为0000000003210041,即创建了一个新的DC对象。之后通过新创建的DC对象创建DCO对象。

在win32kfull!GreResetDCInternal后半段会调用win32kfull!_imp_HmgSwapLockedHandleContents交换第一个HDC句柄和第二次调用win32kfull!imp_hdcOpenDCW创建的HDC句柄。

调用win32kfull!_imp_HmgSwapLockedHandleContents之后两个句柄对应的DC内容为已经发生了交换

// 以下内容为旧DC对象,但是句柄为新句柄
1: kd> dq ffffd50e041fd010
ffffd50e`041fd010  00000000`03210041 80000001`00000000
......
1: kd> dq ffffd50e03fee010
// 以下内容为新DC对象,但句柄为旧句柄
ffffd50e`03fee010  00000000`092105f1 80000002`00000000
......

之后调用win32kfull!_imp_bDeleteDCInternal传入HDC句柄,该函数会释放HDC句柄对应的DC对象,而此时传入该函数的HDC句柄为第二次调用hdcOpenDCW函数返回的句柄,但之前交换过新旧句柄,所以实际上释放的是旧HDC句柄对应的DC对象。


之前计算函数指针的时候,我们知道DCO +0x30是指向DC对象的指针,所以在调用win32kfull!_imp_bDeleteDCInternal函数之后,原DC对象的内存空间已经被释放,达成了use-after-free的第一步free。

function pointer= ( (DCO +0x30)+0xad0),其中DCO +0x30即指向DC对象的指针

0: kd> dq ffffd50e041fd010+0x30 // 取DC对象地址
ffffd50e`041fd040  ffffd50e`00052030 00000000`00000000
......
0: kd> !pool ffffd50e`00052030 // DC对象的内存已被释放,大小为e30
Pool page ffffd50e00052030 region is Paged session pool
*ffffd50e00052000 size:  e30 previous size:    0  (Free ) *GDev
        Pooltag GDev : Gdi pdev
 ffffd50e00052e30 size:   10 previous size:  e30  (Free)       Free
 ffffd50e00052e40 size:  1c0 previous size:   10  (Allocated)  Usqu

之后只需要申请这块内存空间并构造,刚删除的时候,虽然DC对象已经被释放,但函数指针还是指向正确的函数地址,接下来就要申请空间,覆盖这块内存空间的函数指针的值即可。

0: kd> dq ffffd50e041fd010+0x30     // 取DC对象地址
ffffd50e`041fd040  ffffd50e`00052030 00000000`00000000
0: kd> dq ffffd50e`00052030+0xad0   // 取DC对象内的函数指针
ffffd50e`00052b00  ffffd548`a1f10c30 ffffd548`a1db18c0
0: kd> u ffffd548`a1f10c30
win32kfull!UMPDDrvResetPDEV:
ffffd548`a1f10c30 48895c2418      mov     qword ptr [rsp+18h],rbx
ffffd548`a1f10c35 4889742420      mov     qword ptr [rsp+20h],rsi
ffffd548`a1f10c3a 57              push    rdi
ffffd548`a1f10c3b 4883ec70        sub     rsp,70h
ffffd548`a1f10c3f 488b05ba440800  mov     rax,qword ptr [win32kfull!_security_cookie (ffffd548`a1f95100)]
ffffd548`a1f10c46 4833c4          xor     rax,rsp
ffffd548`a1f10c49 4889442468      mov     qword ptr [rsp+68h],rax
ffffd548`a1f10c4e 488bf9          mov     rdi,rcx

use 部分

注:此部分为第二次调试,所以句柄、内存地址和前部分不一样。

在poc里面会调用CreatePalette函数,该此函数会申请内核堆,

第一个句柄rcx=0000000015213372

// 第一个DCO对象
0: kd> dq rbx
DBGHELP: SharedUserData - virtual symbol module
ffff885e`847d2620  00000000`15213372 80000001`00000000
......
// 第一个PDC 指向DC对象
0: kd> dq ffff885e`847d2620+0x30
ffff885e`847d2650  ffff885e`80063030 00000000`00000000
......
// 第一个DC对象
0: kd> dq ffff885e`80063030
ffff885e`80063030  00000000`00000000 00000000`00000000
ffff885e`80063040  00000000`00000000 ffff885e`80046010
ffff885e`80063050  00000001`00000001 ffff885e`80063030
ffff885e`80063060  00000000`00000000 00000000`00008180
ffff885e`80063070  ffffb48d`a36b4e50 00000000`00000000
ffff885e`80063080  00000000`00000000 00000000`00000000
ffff885e`80063090  00000000`00000000 00000000`00000000
ffff885e`800630a0  00000000`00000000 00000000`00000000

第二个句柄rax=0000000001211b60

1: kd> dq rdx
DBGHELP: SharedUserData - virtual symbol module
ffff885e`84121620  00000000`01211b60 80000001`00000000
......
1: kd> dq rdx+0x30
ffff885e`84121650  ffff885e`8006b030 00000000`00000000
......
1: kd> dq ffff885e`8006b030
ffff885e`8006b030  00000000`00000000 00000000`00000000
ffff885e`8006b040  00000000`00000000 ffff885e`80063030
ffff885e`8006b050  00000001`00000001 ffff885e`8006b030
ffff885e`8006b060  00000000`00000000 00000000`00008180
ffff885e`8006b070  ffffb48d`a317b8b0 00000000`00000000
ffff885e`8006b080  00000000`00000000 00000000`00000000
ffff885e`8006b090  00000000`00000000 00000000`00000000
ffff885e`8006b0a0  00000000`00000000 00000000`00000000

在DeleteDCInternel调用之后第一个DC对象的内存空间已经被释放

0: kd> !pool ffff885e`80063030
// 注意,此时DC对象地址距离堆头地址为0x30大小
Pool page ffff885e80063030 region is Paged session pool
*ffff885e80063000 size:  e30 previous size:    0  (Free ) *GDev
        Pooltag GDev : Gdi pdev
 ffff885e80063e30 size:   70 previous size:  e30  (Free)       Free
 ffff885e80063ea0 size:   b0 previous size:   70  (Free )  Usqm
 ffff885e80063f50 size:   b0 previous size:   b0  (Allocated)  Usqm

根据调试,可以得知释放的DC对象内存大小为0xe30,所以要覆盖函数指针时,所申请的内存也要刚刚好或者接近这块内存大小才有可能申请到。在poc里面,使用CreatePalette申请这块内核堆。这个函数会通过系统调用进入内核函数win32kfull!NtGdiCreatePaletteInternal,该函数调用win32kbase!PALMEMOBJ::bCreatePalette创造Palette对象,win32kbase!PALMEMOBJ::bCreatePalette会调用AllocateObject为新对象申请空间,最终通过调用ExAllocatePoolWithTag函数分配堆空间,整个调用栈如下:

0: kd> kb
 # RetAddr               :  Call Site
00 ffff880c`b95d39f4     :  win32kbase!Win32AllocPool
01 ffff880c`b95d0042     :  win32kbase!AllocateObject+0xc4
02 ffff880c`b9309ecc     :  win32kbase!PALMEMOBJ::bCreatePalette+0xb2
03 fffff800`b175a193     :  win32kfull!NtGdiCreatePaletteInternal+0xcc
04 00007ffe`a2cb2604     :  nt!KiSystemServiceCopyEnd+0x13
05 00007ff7`e44c2fe1     :  win32u!NtGdiCreatePaletteInternal+0x14
06 00000000`00000d94     :  cve_2021_40449!createPaletteofSize1+0xd1 [C:\Users\mimi\source\repos\test\cve-2021-40449\main.cpp @ 71] 
.......
2e 00007ffe`a2e9b26f     :  0x000000d1`a374ef69
2f 00007ffe`a39e1a4a     :  gdi32full!GdiPrinterThunk+0x21f
30 00007ffe`a61889e4     :  USER32!__ClientPrinterThunk+0x3a
31 00007ffe`a2cb6dc4     :  ntdll!KiUserCallbackDispatcherContinue
32 00007ffe`a2e7edda     :  win32u!NtGdiResetDC+0x14
33 00007ffe`a3682371     :  gdi32full!ResetDCWInternal+0x17a
34 00007ff7`e44c3296     :  GDI32!ResetDCW+0x31
35 00000000`00000000     :  cve_2021_40449!main+0x146 [C:\Users\mimi\source\repos\test\cve-2021-40449\main.cpp @ 685]


win32kbase!Win32AllocPool代码如下,最终是通过调用ExAllocatePoolWithTag申请堆,win32kbase!Win32AllocPool的a1参数为要申请的堆内存大小,调试过程中可以得知其要申请0xe20大小的堆,加上堆头,刚好接近刚释放的0xe3大小的堆空间大小。

__int64 __fastcall Win32AllocPool(__int64 a1, unsigned int a2)
{
  unsigned int v2; // ebx
  __int64 v3; // rdi
  __int64 result; // rax

  v2 = a2;
  v3 = a1;
  if ( (signed int)IsWin32AllocPoolImplSupported_0() < 0 )
    result = 0i64;
  else
    result = Win32AllocPoolImpl_0(33i64, v3, v2);
  return result;
}

同时在Poc代码分析里面分析了DC对象函数指针和堆头之间的位置关系,所以通过构造传入CreatePalette的LOGPALETTE结构可以刚刚好覆盖原DC对象内的函数指针以及该函数指针要调用的参数,内存分布具体见https://github.com/CppXL/cve-2021-40449-poc/blob/master/main.cpp 里面的注释。

通过函数指针调用RtlSetAllBits函数并传入RtklBitMap型指针,其中RtlBitMap的buffer指向POC进程自身的权限位,如下图:

typedef struct _RTL_BITMAP {
    ULONG  SizeOfBitMap;
    ULONG *Buffer;
} RTL_BITMAP, *PRTL_BITMAP;

0: kd> dq ffff885e80063000+0x750                        // 此处为RtlBitMap地址
ffff885e`80063750  ffffb48d`a3839010 ffffffff`ffffffff
ffff885e`80063760  ffffffff`ffffffff ffffffff`ffffffff
ffff885e`80063770  ffffffff`ffffffff ffffffff`ffffffff
ffff885e`80063780  ffffffff`ffffffff ffffffff`ffffffff
ffff885e`80063790  ffffffff`ffffffff ffffffff`ffffffff
ffff885e`800637a0  ffffffff`ffffffff ffffffff`ffffffff
ffff885e`800637b0  ffffffff`ffffffff ffffffff`ffffffff
ffff885e`800637c0  ffffffff`ffffffff ffffffff`ffffffff
0: kd> dq ffffb48d`a3839010                             // 此处存放了RtlBitMap结构,0x00-0x08为size,0x08-0x10为buffer指针,指向了自身的权限位
ffffb48d`a3839010  00000000`00000080 ffffde8f`1fb2e9d0
ffffb48d`a3839020  41414141`41414141 41414141`41414141
ffffb48d`a3839030  00000000`00000000 00000000`00000000
ffffb48d`a3839040  00000000`00000000 00000000`00000000
ffffb48d`a3839050  00000000`00000000 00000000`00000000
ffffb48d`a3839060  00000000`00000000 00000000`00000000
ffffb48d`a3839070  00000000`00000000 00000000`00000000
ffffb48d`a3839080  00000000`00000000 00000000`00000000
0: kd> dq ffffde8f`1fb2e9d0
ffffde8f`1fb2e9d0  00000006`02880000 00000000`00800000
ffffde8f`1fb2e9e0  00000000`00800000 00000000`00000000
ffffde8f`1fb2e9f0  00000000`00000000 00000000`00000000
ffffde8f`1fb2ea00  20010000`00000000 0000000f`00000001
ffffde8f`1fb2ea10  000001e0`00000000 00000000`00001000
ffffde8f`1fb2ea20  00000000`00000000 ffffde8f`1fb2ee18
ffffde8f`1fb2ea30  00000000`00000000 ffffde8f`1f1007f0
ffffde8f`1fb2ea40  ffffde8f`1f1007f0 ffffde8f`1f10080c

调用DC里面的函数指针之前,自身权限位为正常权限。

调用函数指针之后,可以看到权限位全部置为了1

补丁分析

漏洞利用分析里面分析过漏洞形成原因是因为在调用GreResetDCInternal函数时,使用DC对象指针的时候没有检查DC对象是否异常。而利用该漏洞是通过在调用回调函数时调用ResetDC实现的。

我们再次回顾一下漏洞函数,在调用hdcOpenDCW也就是在调用回调函数之前会通过DCO的构造函数从DC构造DCO对象,在基本概念中知道,内核对象每被引用一次则对象引用计数器值会加一。调用构造函数时,DC对象引用加一,正常情况下此时DC对象引用次数要为1。如果在回调函数中再次调用ResetDC,则会第二次调用GreResetDCInternal,再次调用DCO的构造函数,DC对象引用再次加一,此时引用次数为2。

所以判断DC对象异常可以通过判断DC对象的引用次数实现。

__int64 __usercall GreResetDCInternal@<rax>(HDC a1@<rcx>, __int64 a2@<rdx>, int *a3@<r8>)
{
  __int64 v24; // [rsp+50h] [rbp-20h]
  __int64 v25; // [rsp+60h] [rbp-10h]
  DCOBJ::DCOBJ((DCOBJ *)&v25, a1);              // 利用构造函数从HDC创建DCOBJ对象
  v8 = v25;
        ··········
  v10 = *(_QWORD *)(v8 + 48);                   // 赋值
  *(_QWORD *)(v10 + 1736) = 0i64;
  v24 = v11;
        ·······
  v9 = *(_QWORD *)(v25 + 512) != 0i64;
  v12 = *(_DWORD *)(v25 + 120) > 0;
        ·······
      v13 = (HDC)hdcOpenDCW(&qword_1C0141EB0, v26, 0i64, 0i64, *(_QWORD *)(v10 + 2584));// 创建新的DC对象,返回对应的HDC句柄
      if ( v13 )
      {
        *(_QWORD *)(v10 + 2584) = 0i64;
        DCOBJ::DCOBJ((DCOBJ *)&v24, v13);
        v14 = (_QWORD *)v24;
        if ( v24 )
        {
          if ( v12 )
            *(_DWORD *)(v24 + 120) = *(_DWORD *)(v24 + 116);
          v14[308] = *(_QWORD *)(v25 + 2464);
          *(_QWORD *)(v25 + 2464) = 0i64;
          v14[309] = *(_QWORD *)(v25 + 2472);
          *(_QWORD *)(v25 + 2472) = 0i64;
          v15 = *(void (__fastcall **)(_QWORD, _QWORD))(v10 + 2768);
          if ( v15 )
            v15(*(_QWORD *)(v10 + 1824), *(_QWORD *)(v14[6] + 1824i64));// 调用函数指针指向的函数,传入参数为用户传入的HDC对应的DC对象内的值
            ·······
          HmgSwapLockedHandleContents(v3, 0i64, v6, 0i64, v23);// 交换旧的和新的HDC对象
          GreReleaseHmgrSemaphore();
            ······
    bDeleteDCInternal(v6, 1i64, 0i64);  // 删除了hdcOpenDCW分配的HDC,但前面经过HmgSwapLockedHandleContents交换了句柄,实际删除的是旧的HDC
            ······

在补丁中,增加了对DC对象引用次数进行判断的逻辑,如果在GreResetDCInternal函数中DC对象引用次数大于1则表明已经发生异常,进入异常逻辑抛出错误(因为按正常流程此处DC对象引用次数应为不应该大于1)。

__int64 __fastcall sub_1C014CB0C(__int64 a1, __int64 a2, int *a3)
{
......
  int *v30; // [rsp+30h] [rbp-1h]
 .....
  v9 = (__int64)v30;
  if ( !v30 )
  {
LABEL_6:
    EngSetLastError(6i64);
LABEL_7:
    v13 = (__int64)v30;
    goto LABEL_8;
  }
  if ( *((_WORD *)v30 + 6) > 1u )
  {
    if ( *(_DWORD *)&stru_1C032C3F8.Length > 5u && (unsigned __int8)sub_1C00B5068(&stru_1C032C3F8, 0x400000000000i64) )
    {
      v31 = &v25;
      v30 = &v26;
      v29 = &v28;
      v28 = 0x1000000i64;
      SysEntryGetDispatchTableValues(v10, (__int64)&unk_1C02F466B, v11, v12);
    }
    goto LABEL_6;
  }

参考链接:

https://www.secrss.com/articles/35266

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

[https://bbs.pediy.com/thread-269930.htm](

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖