Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2015-0057 — Übersetzter Artikel: Die Ausnutzung der CVE-2015-0057-Sicherheitslücke auf 32-Bit- und 64-Bit-Systemen. Ausnutzung des Use-after-Free-Fehlers in win32k!xxxEnableWndSBArrows (CVE 2015-0057) sowohl auf 32-Bit- als auch auf 64-Bit-Systemen(Aaron Adams von NCC) | Kitploit
Tools/GitHubGitHub/highandhigh/cve-2015-0057
SpeicherforensikSchwachstellenanalyseExploitationReverse EngineeringPapers & ForschungLernen & BildungBinary-Exploitation
GitHubhighandhigh/cve-2015-0057

CVE-2015-0057

Übersetzter Artikel: Die Ausnutzung der CVE-2015-0057-Sicherheitslücke auf 32-Bit- und 64-Bit-Systemen. Ausnutzung des Use-after-Free-Fehlers in win32k!xxxEnableWndSBArrows (CVE 2015-0057) sowohl auf 32-Bit- als auch auf 64-Bit-Systemen(Aaron Adams von NCC)

Repository anzeigen
8vor 9 JahrenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

Ausnutzung von CVE-2015-0057 auf 32-Bit- und 64-Bit-Systemen

Autor: Aaron Adams

Übersetzung: 55-AA

Anmerkung des Übersetzers: Teile dieses Artikels wurden sinngemäß übersetzt; bei Fragen siehe Original.

Begriffe:

  • Operationsprimitive: Ähnlich einer Funktion, eine Sammlung von Korruptionsoperationen, um eine vollständige, wiederverwendbare Funktionalität zu erreichen, z. B. Speicher lesen, beliebige Daten schreiben usw.

Geleitwort

Anfang dieses Jahres stieß ich auf eine interessante Schwachstelle in win32k.sys (CVE-2015-0057) und realisierte eine stabile Ausnutzung auf 32-Bit- und 64-Bit-Systemen, die von XP bis Windows 8.1 reicht (mit einigen Ausnahmen). Dieser Artikel beschreibt ausführlich, wie ich die Ausnutzung auf beiden Plattformen umgesetzt habe; am Ende sind auch einige zusätzliche Dinge angehängt. Außerdem wird beschrieben, wie die Ausnutzung mit niedrigen Integritätsrechten unter Windows 8.1 mit aktiviertem SMEP gelingt.

Dieser Artikel ist lang; ich habe mich bemüht, so viele Details wie möglich zu liefern, um die Komplexität der Ausnutzung dieser Schwachstelle zu zeigen, statt diese Details zu verbergen. Natürlich habe ich auch einige Details ausgelassen. Ich hoffe, diese Details sind für euch hilfreich.

Einleitung

Am 10. Februar 2015 veröffentlichte Microsoft Details zu MS15-010. Dieser Bug wurde zuerst von Udi Yavo von enSilo entdeckt. Udi gab eine hervorragende Analyse auf dem Breaking-Malware-Blog: „one bit rule-bypassing windows 10 protections using single bit“. Ich empfehle, diesen Artikel gründlich zu lesen, um den Bug tiefgehend zu verstehen, auch wenn ich hier so viele Details wie möglich nennen werde – Details zu einigen Hürden, die beim Auslösen der Schwachstelle überwunden werden müssen. Die Ausnutzung dieser Schwachstelle ist sehr interessant; viele Details stammen aus Udids Blog. Hier seine Aussage:

Angemessene Offenlegung: Obwohl dieser Blog technisch ist, werden wir keinen Code und keine vollständigen Details preisgeben, um zu verhindern, dass irgendein Techniker diesen Exploit nachbauen kann.

Als Bonus für die Ausnutzung dieser Schwachstelle erhielten wir eine Pokémon-Entwicklung: Tech-Elf. Ich muss Udi wohl Anerkennung zollen, denn er hat den Bug entdeckt und im Blog die Zusammenhänge sowie Details zur Ausnutzung bereitgestellt – das war äußerst nützlich.

Zuvor hatte ich niemals eine Schwachstelle in win32k.sys ausgenutzt und war mit User-Mode-Callbacks und vielen zugehörigen APIs nicht vertraut. Deshalb danke ich auch einigen bekannten Sicherheitsforschern für ihre im Internet verfügbaren hervorragenden Ressourcen, etwa Skywing, Tarjei Mandt, Alex Ionescu und j00ru. Diese Leute haben so viel technisches Wissen öffentlich bereitgestellt; sie alle verdienen Anerkennung. Eine Arbeit, auf die ich mich stark gestützt habe, ist Tarjei Mandts Win32k.sys exploitation paper.

Während ich diesen Exploit schrieb, realisierte ein hervorragender Reverse Engineer eine stabile Ausnutzung von CVE-2015-1701; der dortige Beispielcode zu User-Mode-Callbacks war sehr nützlich. Dank an den Autor.

Erwähnenswert ist, dass meine folgende Analyse unter Windows 7 durchgeführt wurde, da es offenbar die einzige Version ist, in der alle Strukturen in win32k.sys über entsprechende Symbole verfügen. Die meisten dieser Symbole lassen sich auf Strukturen in anderen Versionen von win32k.sys anwenden. Aus unbekannten Gründen entfernte Microsoft diese Symbole ab Windows 8.

Schließlich möchte ich sagen: Mein Ansatz zur Ausnutzung ist ziemlich komplex. Es ist durchaus möglich, dass es einen einfacheren Weg gibt, den ich nur nicht gefunden habe. Ich würde gern hören, wenn jemand einen anderen Ansatz verwendet hat. Wie auch immer, ich hoffe, all das ist hilfreich für die Erforschung von win32k.sys-Schwachstellen.

Bug

Im Folgenden werfen wir einen Blick auf den Bug im Disassembler-Code von win32k!xxxEnableWndSBArrows. Es ist ein ziemlich eleganter Bug:

Ungepatchter Zustand:

root@kitploit:~
.text:FFFFF97FFF1B157D mov r8d, r13d 
.text:FFFFF97FFF1B1580 mov rdx, r14 
.text:FFFFF97FFF1B1583 call xxxDrawScrollBar    ; 触发usermode callback 
.text:FFFFF97FFF1B1588 jmp short loc_FFFFF97FFF1B1519
 [...] 
.text:FFFFF97FFF1B1519 mov eax, [rbx]           ; 引用 tagSBINFO 指针而没有经过检查 
.text:FFFFF97FFF1B151B mov ebp, 0FFFFFFFBh 
.text:FFFFF97FFF1B1520 xor eax, esi

Im obigen Code kann Win32K!xxxDrawScrollBar unter bestimmten Umständen in den Benutzermodus zurückrufen. Im Benutzermodus kann der Zeiger auf tagSBINFO vom Angreifer freigegeben werden. Wenn die Ausführung zum obigen Code zurückkehrt, referenziert der Code an der Adresse 0xFFFFF97FFF1B1519 einen ungültigen Zeiger.

Gepatchter Zustand:

root@kitploit:~
    .text:FFFFF97FFF1D69C3 xor r8d, r8d 
    .text:FFFFF97FFF1D69C6 mov rdx, rbp 
    .text:FFFFF97FFF1D69C9 call xxxDrawScrollBar        ; 触发usermode callback
    .text:FFFFF97FFF1D69CE cmp rbx, [rdi+0B0h]          ; 检查 tagSBINFO 指针是否正确 
 ---.text:FFFFF97FFF1D69D5 jz short loc_FFFFF97FFF1D69E4; 如果正确则继续原来的流程 
 |  .text:FFFFF97FFF1D69D7 mov rcx, rbp 
 |  .text:FFFFF97FFF1D69DA call _ReleaseDC 
 |  .text:FFFFF97FFF1D69DF jmp loc_FFFFF97FFF1D6958     ; 跳转到函数退出 
 |->.text:FFFFF97FFF1D69E4 mov eax, [rbx]               ; 放心地使用正确的 tagSBINFO 指针 
    .text:FFFFF97FFF1D69E6 xor eax, r14d

In der gepatchten Version sehen wir eine Prüfung, bevor der tagSBINFO-Zeiger verwendet wird. Die zugehörigen Strukturinformationen werden später angegeben.

Grundlagen – Speicherkorruption Stufe 1

Bei der Umsetzung dieses Exploits führten wir mehrere Korruptionen durch. In einer dieser Korruptionen wird die Schwachstelle ausgelöst.

Die technische Ursache dieses Bugs ist ein UAF (Use-after-Free) im Desktop-Heap. Anfangs war das verwirrend für mich, da ich die User-Mode-Callback-Mechanismen in win32k.sys nicht kannte und nicht verstand, wie sie funktionieren. Deshalb nahm ich an, es handle sich um eine Race Condition bei einer Sperre, die zu UAF führt. Tatsächlich wird die Sperre jener Struktur korrekt verwendet, und der Ablauf entspricht den Erwartungen. Kurz gesagt, die eigentliche Ursache ist:

  1. Die Funktion win32k!xxxEnableWndSBArrows besitzt einen Zeiger auf ein tagSBINFO im Desktop-Heap, der aus der fensterbezogenen tagWND-Struktur gelesen wird und eine Bildlaufleiste beschreibt.

  2. win32k!xxxEnableWndSBArrows ruft eine Funktion auf, die einen User-Mode-Callback auslöst (diese Funktion kann im Benutzermodus per Hook abgefangen werden).

  3. Sobald der Code im Benutzermodus ausgeführt wird, kann die Struktur im Desktop-Heap über andere win32k-Systemaufrufe verändert werden, einschließlich des Freigebens der tagSBINFO-Struktur aus dem Desktop-Heap.

  4. Nach der Rückkehr in den Kernelmodus referenziert win32k!xxxEnableWndSBArrows tagSBINFO nicht erneut aus tagWND und prüft auch nicht, ob der ursprünglich referenzierte Zeiger gültig ist, sondern verwendet direkt den ursprünglich referenzierten Zeiger – der tatsächlich bereits freigegeben wurde.

Das ist schon alles; abgesehen von User-Mode-Callbacks ist diese Phase ziemlich klar.

Verstehen, wie wir den Ablauf steuern

Aber wie führen wir die Korruption durch und warum? Wie in Udids Blog erwähnt, kannst du an einer bestimmten Stelle 2 Bits setzen oder löschen – an einer Position, die der Systemcode als WSBflags-Feld in der tagSBINFO-Struktur ansieht. Das ist kein üblicher UAF-Ausnutzungsansatz, aber der Artikel gibt einen Hinweis darauf, wie es gemacht wird; ich erläutere das in den folgenden Abschnitten. Zuerst wollen wir verstehen, wie wir diese Bits manipulieren können.

tagSBINFO-Struktur (auf 32-Bit und 64-Bit identisch):

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Diese UAF-Schwachstelle befindet sich in der Funktion win32k!xxxEnableWndSBArrows(), die dazu dient, die Pfeile eines oder beider (horizontaler oder vertikaler) Bildlaufleisten-Steuerelemente zu aktivieren oder zu deaktivieren. Ein Bildlaufleisten-Steuerelement ist ein spezielles Fenster zur Steuerung einer Bildlaufleiste. Es kann mit der Funktion CreateWindow() unter Verwendung der eingebauten Fensterklasse "SCROLLBAR" erstellt werden.

Der Funktionsprototyp von win32k!xxxEnableWndSBArrows():

root@kitploit:~
BOOL xxxEnableWndSBArrows(PWND wnd, UINT WSBflags, UINT wArrows);

Die Bedeutung des Parameters WSBflags entspricht den Definitionen in WinUser.h und gibt an, welche Bildlaufleisten betroffen sind:

root@kitploit:~
#define SB_HORZ 0 
#define SB_VERT 1 
#define SB_CTL  2 
#define SB_BOTH 3

Der Parameter wArrows gibt an, ob die Pfeile aktiviert oder deaktiviert sind. Ein gesetztes Bit bedeutet, dass die Pfeile deaktiviert sind; andernfalls sind sie aktiviert. Die beiden niederwertigsten Bits von wArrows betreffen die horizontale Bildlaufleiste, die nächsten beiden Bits die vertikale Bildlaufleiste; die übrigen Bits sind für den Zweck dieses Exploits irrelevant.

Der folgende Code stammt aus win32k!xxxEnableWndSBArrows(); wenn SB_HORZ oder SB_BOTH gesetzt ist, werden die zugehörigen Bits für die horizontalen Pfeile gesetzt oder gelöscht:

Dieser Bug liegt beim Setzen der Flags für horizontale und vertikale Bildlaufleisten. Nach dem Aktualisieren der horizontalen Bildlaufleiste ruft win32k!xxxEnableWndSBArrows() die Funktion win32k!xxxDrawScrollBar() auf, sobald das zugehörige Fenster der Bildlaufleiste auf dem Desktop sichtbar ist; wie im früheren Artikel erwähnt, kann dies einen potenziellen User-Mode-Callback auslösen.

Bevor wir die User-Mode-Callbacks besprechen, fahren wir damit fort, was nach dem Aufruf von win32k!xxxDrawScrollBar() geschieht. Das ist im Wesentlichen dieselbe Logik wie bei der horizontalen Bildlaufleiste, mit nur wenigen Bit-Unterschieden. Wenn wir uns dafür entscheiden, die vertikale Bildlaufleiste zu deaktivieren, und annehmen, dass wir die UAF auslösen, werden 2 Bits an eine bestimmte Stelle im tagSBINFO-Heapblock geschrieben. Wenn der ursprüngliche Wert also 0x2 war, wird er jetzt 0xe. Wie in der folgenden Abbildung dargestellt.

Diese eine Bitänderung reicht aus, um letztlich Codeausführung zu erreichen. Ich habe nicht untersucht, wie man durch Löschen von Bits zur Ausnutzung gelangt, aber es ist möglich.

Der Kern des oben Gesagten ist: Um sowohl horizontale als auch vertikale Bildlaufleisten manipulieren zu können, muss ein Bildlaufleisten-Steuerelement erstellt werden, das beide Elemente besitzt. Dies erreicht man durch einen Aufruf von CreateWindow() mit den Flags WS_HSCROLL und WS_VSCROLL. Der Code lautet:

root@kitploit:~
g_hSBCtl = CreateWindowEx( 
    0,                  // No extended style 
    "SCROLLBAR",        // class 
    NULL,               // name 
    SBS_HORZ | WS_HSCROLL | WS_VSCROLL, // 垂直+水平 
    10,                 // x 
    10,                 // y 
    100,                // width 
    100,                // height 
    g_hSpray[UAFWND],   // 无模式父窗口 
    (HMENU)NULL,
    NULL,               // window owner 
    NULL                // extra params 
    );

Mit dem folgenden Code kann die Sichtbarkeit sichergestellt werden (normalerweise ist sie standardmäßig gegeben, hier wird sie explizit aufgerufen):

root@kitploit:~
result = ShowWindow(g_hSBCtl, SW_SHOW);

Die Bildlaufleiste ist standardmäßig aktiviert. Wenn wir bereit sind, die verwundbaren Codepfade auszulösen, können wir die Bildlaufleiste deaktivieren, um die gewünschten Bits zu korrumpieren:

root@kitploit:~
result = EnableScrollBar(g_hSBCtl, SB_CTL | SB_BOTH, ESB_DISABLE_BOTH);

Auslösen der Schwachstelle

Obwohl wir oben die Details des Bugs und die Auslösung des relevanten Codes beschrieben haben, haben wir den wichtigsten Schritt übersehen: das Abfangen des User-Mode-Callbacks, der von win32k!xxxDrawScrollBar() ausgelöst wird, damit wir den Heap-Inhalt ändern können, bevor win32k!xxxEnableWndSBArrows() die Ausführung fortsetzt. Wir müssen den Bug tatsächlich auslösen, aber ohne Kenntnis der win32k.sys-Mechanismen und der zugehörigen APIs – so wie ich am Anfang – ist das ein Abenteuer für sich.

Der frühere Artikel enthält eine gute Darstellung des Aufrufstapels, die diesen Prozess vertieft: Die Auslösung erfolgt über win32k!xxxDrawScrollBar(), dann wird ClientLoadLibrary() aufgerufen und über KeUserModeCallback() weitergeleitet. Wir müssen genau verstehen, wie KeUserModeCallback() aufgerufen wird, um es in unserem eigenen Prozess Hooken zu können.

Ich habe einige gute Artikel gefunden, die User-Mode-Callbacks mehr oder weniger behandeln. Die Teile, die win32k betreffen, waren sehr nützlich:

  • https://media.blackhat.com/bh-us-11/Mandt/BH_US_11_Mandt_win32k_WP.pdf(Tarjei的论文)
  • http://azimuthsecurity.com/resources/recon2012_mandt.pptx (Tarjei的PPT包含许多扩展信息)
  • http://www.nynaeve.net/?p=204
  • http://www.cprogramdevelop.com/3825874/
  • http://www.zer0mem.sk/?p=410
  • https://www.reactos.org/wiki/Techwiki:RegisterUserApiHook
  • http://pasotech.altervista.org/windows_internals/Win32KSYS.pdf
  • http://j00ru.vexillium.org/?p=614
  • http://uninformed.org/index.cgi?v=10&a=2#SECTION00042000000000000000

Normalerweise besitzt jeder Prozess eine Tabelle mit Zeigern auf User-Mode-Callback-Funktionen; PEB->KernelCallBackTable verweist auf diese Tabelle. Wenn der Kernel eine Benutzermodus-Funktion aufrufen möchte, übergibt er den Funktionsindex an KeUserModeCallBack(). Im obigen Beispiel verweist der Index auf die Benutzermodus-Funktion __ClientLoadLibrary().

KeUserModeCallBack() sucht in PEB->KernelCallBackTable anhand des Index die entsprechende Funktion und führt sie aus, was schließlich im Benutzermodus KiUserModeCallbackDispatch() aufruft.

Um den gewünschten Einstiegspunkt zu Hooken, muss der Index von __ClientLoadLibrary() in PEB->KernelCallBackTable nachgeschlagen und durch unsere eigene Funktion ersetzt werden. Erwähnenswert ist, dass dieser Index je nach Betriebssystemversion und Hardwareplattform variiert.

Wenn wir PEB->KernelCallBackTable betrachten wollen, können wir die Adresse dieser Tabelle mithilfe von WinDbg ermitteln. Beim Vergleich von 32-Bit- und 64-Bit-Plattformen habe ich keine großen Unterschiede festgestellt.

root@kitploit:~
kd> dt !_PEB @$peb 
ntdll!_PEB 
    +0x000 InheritedAddressSpace    : 0 '' 
    +0x001 ReadImageFileExecOptions : 0 '' 
    +0x002 BeingDebugged            : 0 '' 
    +0x003 BitField                 : 0x8 '' 
    +0x003 ImageUsesLargePages      : 0y0 
[...] 
    +0x02c KernelCallbackTable      : 0x76daf620 Void 
    
kd> dds 0x76daf620 
76daf620 76d96443 user32!__fnCOPYDATA 
76daf624 76ddf0e4 user32!__fnCOPYGLOBALDATA 
76daf628 76da736b user32!__fnDWORD 
76daf62c 76d9d603 user32!__fnNCDESTROY 
76daf630 76dc50f9 user32!__fnDWORDOPTINLPMSG 
76daf634 76ddf1be user32!__fnINOUTDRAG 
76daf638 76dc6cd0 user32!__fnGETTEXTLENGTHS 
76daf63c 76ddf412 user32!__fnINCNTOUTSTRING
76daf640 76d9ce49 user32!__fnINCNTOUTSTRINGNULL 
[...] 
76daf724 76da3962 user32!__ClientLoadLibrary 

kd> ?? (0x76daf724-0x76daf620)/4 int 0n65

Im obigen Beispiel wissen wir, dass der Index von __ClientLoadLibrary 65 ist; genau dort wollen wir Hooken. Nach dem Hooken stellte ich fest, dass __ClientLoadLibrary vom win32k-Code mehrfach aufgerufen wird! Zuerst mussten wir unsere Hook-Funktion darüber informieren, wann der interessierende Aufruf unmittelbar bevorsteht, damit wir sicher sein können, genau an der Stelle zu Hooken, die geändert werden muss. Daher verwendet der Hook-Code eine globale Variable als Kennzeichen; nur wenn dieses Kennzeichen gesetzt ist, werden die entsprechenden Aktionen ausgeführt.

Nun gibt es zwei Hindernisse:

  1. Wenn die ursprüngliche __ClientLoadLibrary normal ausgeführt wird, stellte ich fest, dass die Ausführung bei Auslösung der Schwachstelle im win32k nicht in den Benutzermodus gelangt. Dem bin ich nicht weiter nachgegangen, sondern habe nur vermutet, dass die zu ladende DLL bereits geladen ist, sodass die Ladefunktion nicht erneut aufgerufen werden muss. Damit der Ladevorgang stattfinden kann, ließ ich meine Hook-Funktion von __ClientLoadLibrary absichtlich kein Ergebnis zurückgeben, sodass sie fortlaufend erneut zu laden versucht. Meine Aufgabe bestand lediglich darin, NULL in der Parameterstruktur zurückzugeben, was durch Reverse Engineering der Funktion __ClientLoadLibrary() in user32.dll erreicht wurde.

  2. Die Ausführung von EnableScrollBar() löst letztlich __ClientLoadLibrary aus und gelangt dann zu der Stelle, die wir über win32k!xxxDrawScrollBar() ausnutzen wollen. Deshalb musste ich wissen, wie oft dieser Aufruf erfolgt, bevor das für uns interessante Ereignis eintritt. Mithilfe eines Zählers konnte ich genau feststellen, wann der Bug ausgelöst und der Hook-Code erreicht wurde. Glücklicherweise ist dieser Zähler über Plattformen und Betriebssystemversionen hinweg konstant.

Daher sieht die Hook-Funktion wie folgt aus:

root@kitploit:~
void ClientLoadLibraryHook(void * p) 
{ 
    CHAR Buf[PGSZ]; 
    memset(Buf, 0, sizeof(Buf)); 
    if (g_PwnFlag) 
    { 
        dprintf("[+] __ClientLoadLibrary hook called\n"); 
        if (++g_HookCount == 2) 
        {
            g_PwnFlag = 0;      // 仅仅执行一次.. 
            ReplaceScrollBarChunk(NULL); 
        }
    } 
    fpClientLoadLibrary(&Buf); // 调用原始函数
}

Sobald wir sicher sind, dass der aktuelle Aufruf von win32k!xxxDrawScrollBar() stammt, können wir versuchen, den Bug auszulösen. Für die reine Auslösung genügt der Aufruf DestroyWindow(g_hSBCtl). Dies führt dazu, dass die tagSBINFO-Struktur des Fensters freigegeben wird, während die Fensterstruktur selbst nicht sofort freigegeben wird, da ihre Referenzzählung noch vom ursprünglichen Aufruf genutzt wird. tagSBINFO besitzt jedoch keinen solchen Referenzzählmechanismus und wird daher sofort freigegeben.

An diesem Punkt haben wir den Bug ausgelöst. Obwohl wir den Heapblock, der tagSBINFO enthält, nicht neu zugewiesen haben, können wir die beiden Bits, die für „deaktivieren“ stehen, in den bereits freigegebenen Heap schreiben. Im nächsten Schritt ersetzen wir diesen freigegebenen Heapblock durch etwas, das wir kontrollieren, damit wir etwas Interessanteres tun können, als nur ein paar Bits zu setzen. Dazu benötigen wir etwas Hintergrundwissen über den Desktop-Heap.

Desktop-Heap

win32k.sys speichert GUI-Objekte, die mit einem bestimmten Desktop verbunden sind, im Desktop-Heap. Dazu gehören Fensterobjekte und deren zugehörige Strukturen wie Eigenschaftenlisten, Fenstertext und Bildlaufleisten. Tarjeis Artikel erwähnt das; besonders wichtig ist, dass der Desktop-Heap tatsächlich nur eine vereinfachte Version eines User-Mode-Backend-Allokators ist und ebenfalls RtlAllocateHeap() und RtlHeapFree() verwendet. Der Desktop-Heap wird durch eine _HEAP-Struktur verwaltet; da es keinen Frontend-Allokator gibt, existieren auch keine Low-Fragmentation-Heap (LFH) oder Lookaside-Listen.

Für jeden erstellten Desktop gibt es einen zugehörigen Desktop-Heap, der ihn bedient. Das bedeutet, wir können einen neuen Desktop anlegen, um einen „sauberen“ Desktop-Heap zu erhalten, auf dem unsere Operationen besser vorhersagbar sind. Für Prozesse mit niedrigen Integritätsrechten ist das jedoch nicht von Bedeutung, da solche Prozesse keinen neuen Desktop erstellen dürfen.

Das Hauptproblem besteht nun darin, die Allokationen zu verfolgen (später werden weitere Details zu Metadaten usw. behandelt).

Überwachung der Desktop-Heap-Allokationen

Um Allokationen und Freigaben des Desktop-Heaps zu verfolgen, verwende ich gerne WinDbg-Skripte:

64-Bit-Heap-Überwachung

root@kitploit:~
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", @rcx, @edx, @r8; .echo ; gc";
ba e 1 nt!RtlAllocateHeap "r @$t2 = @r8; r @$t3 = @rcx; gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @rax; gc";

32-Bit-Heap-Überwachung

root@kitploit:~
ba e 1 nt!RtlAllocateHeap "r @$t2 = poi(@esp+c); r @$t3 = poi(@esp+4); gu; .printf \"RtlAllocateHeap(%p, 0x%x):\", @$t3, @$t2; r @eax; gc"; 
ba e 1 nt!RtlFreeHeap ".printf\"RtlFreeHeap(%p, 0x%x, %p)\", poi(@esp+4), poi(@esp+8), poi(@esp+c); .echo ; gc"

Neben diesen Debug-Skripten können wir, da der Desktop-Heap lediglich eine vereinfachte Form eines User-Mode-Backend-Allokators ist, durchaus auch den in WinDbg integrierten Befehl !heap verwenden.

Lücken im Heap füllen

Um diesen Bug auszunutzen, müssen wir den zuletzt freigegebenen tagSBINFO-Heapblock ersetzen; außerdem wissen wir, wie diese typischen Bugs ausgenutzt werden: durch Korrumpieren benachbarter Daten. Das bringt die grundlegende Anforderung mit sich, Heapblöcke in der Nähe der zu korrumpierenden Struktur vorab zu allozieren. Um vorhersagen zu können, wo ein Heapblock zugewiesen wird, müssen wir das gesamte Heap-Layout kontrollieren (oder so weit wie möglich). Dazu ist es praktikabel, möglichst viele Heapblöcke zu allozieren, um bereits freigegebene Blöcke aufzufüllen, sodass neu zugewiesene Blöcke zusammenhängend sind. Wenn wir eine Lücke brauchen, können wir an vorhersehbarer Stelle eine schaffen (durch Freigeben bereits zugewiesener Blöcke).

Dieser Abschnitt ist ein einfacher Einblick in einige Faktoren, die die Allokation beeinflussen; die obigen WinDbg-Skripte helfen uns dabei. Tarjei nannte in seiner win32k-Präsentation die wichtigsten Objekte, die im Desktop-Heap allokiert werden; sie stimmen genau mit dem überein, was ich beobachtet habe. Diese sind:

  • Window
  • Menu
  • Hook
  • CallProcData
  • Input Context

Der Desktop-Heap ist ziemlich interessant. Die meisten Allokationen sind direkt mit Fensterobjekten verknüpft und werden über die tagWND-Struktur verwaltet. Das bedeutet: Wenn wir einen Heapblock beliebiger Größe allozieren möchten (sozusagen kleine Blöcke für kleine Lücken), müssen wir zuerst ein zugehöriges Fenster allozieren. Man kann sich die Fensterstruktur als Schnittstelle für Heap-Allokationen vorstellen. Ein weiterer interessanter Punkt ist, dass viele über Fensteroperationen allozierte Heapblöcke nicht sofort freigegeben werden können, es sei denn, das Fenster selbst wird zerstört – das wirkt sich natürlich auf den Heap aus. Stellen wir uns schließlich vor, wir allozieren über ein Fenster einen Heapblock der Größe N, wie im obigen Beispiel – müssen wir viele Heapblöcke der Größe N allozieren? Sicher ist, dass die allozierte Fensterstruktur unabhängig von ihrer Größe nicht in einer verknüpften Liste gespeichert wird. Daher kontrolliert jedes Fenster eine Heap-Allokation der Größe N. Das heißt: Wenn du viele Heapblöcke der Größe N allozieren musst, musst du zuerst viele Fenster erzeugen und die Fenster als Helfer für die Heap-Allokation nutzen.

Außerdem gibt es drei wichtige Datentypen, die im Desktop-Heap allokiert werden und die wir über das Fensterobjekt indirekt nutzen können, um Daten im Heap zu kontrollieren. Diese Datentypen setzen wir intensiv für die Ausnutzung und das Heap-Feng-Shui ein. Die drei Datentypen sind:

  1. tagPROPLIST-Struktur: Wenn sie klein genug allokiert wird, kann sie kleine Lücken füllen. Ein Fensterobjekt enthält eine tagPROPLIST; auf 32-Bit-Systemen ist sie 0x10 Bytes groß, auf 64-Bit-Systemen 0x18 Bytes.

  2. Fenstertext: Dies ist eine im Desktop-Heap allozierte UNICODE-Zeichenfolge beliebiger Größe, die über die _LARGE_UNICODE_STRING-Struktur gespeichert und in tagWND eingebettet ist. Beachte, dass das Feld strName eine Struktur und kein Zeiger ist; die Struktur enthält jedoch einen Zeiger, der mit dem Heapblock des Fenstertexts zusammenhängt.

  3. tagSBINFO-Struktur: die Ursache der Schwachstelle, enthält 4 oder alle steuerbaren Memberfelder.

Abbildung 2 zeigt die Beziehungen zwischen diesen Datentypen:

Um den Heap zu initialisieren, erstellte ich eine große Anzahl von tagWND-Strukturen (durch das Erzeugen von Fensterobjekten). Das füllt viele große Lücken im Heap und bietet uns zugleich eine Schnittstelle, um andere benötigte Heapblöcke zu allozieren. Unter Win8 und Win8.1 führt das Allozieren eines neuen Fensters dazu, dass automatisch eine tagPROPLIST-Struktur allokiert wird (das ist mit den zuvor erwähnten WinDbg-Skripten beobachtbar). Unter Win7 und früheren Versionen allozieren wir selbst eine neue tagPROPLIST, um kleine Lücken zu füllen.

Hier haben alle von uns gesprayten Fensterobjekte keinen Fenstertext; falls nötig, können wir ihn jedoch weiterhin nutzen, um Heapblöcke beliebiger Größe zu allozieren oder freizugeben. Sobald eine Eigenschaftenliste (property list) erstellt wurde, kann man sie nicht mehr entfernen, außer das Fenster wird zerstört. Wir können jedoch die Neuallokation dieser Liste steuern, um neue Eigenschaften aufzunehmen; dieser Mechanismus kann genutzt werden, um an früherer Stelle Lücken zu schaffen. Alles, was du tun musst, ist, eine neue Eigenschaft zu setzen, die in der ursprünglichen Liste nicht vorhanden ist (unterschieden über atomKey).

Verifizieren des Feng-Shui-Layouts

Interessanterweise ist der Desktop-Heap in den Benutzerraum abgebildet, wenn auch schreibgeschützt. Das bedeutet, wir können das von uns aufgebaute Feng-Shui-Layout verifizieren und sicherstellen, dass es funktioniert. Zuerst müssen wir herausfinden, wohin der Desktop-Heap im Benutzermodus abgebildet ist. Tarjei erwähnte das in seiner Arbeit über win32k. Im TEB gibt es eine undokumentierte Struktur namens Win32ClientInfo, die damit zusammenhängt; ihre ungefähre Definition lautet wie folgt:

root@kitploit:~
typedef struct _CLIENTINFO { 
    ULONG_PTR CI_flags; 
    ULONG_PTR cSpins; 
    DWORD dwExpWinVer; 
    DWORD dwCompatFlags; 
    DWORD dwCompatFlags2; 
    DWORD dwTIFlags; 
    PDESKTOPINFO pDeskInfo; 
    ULONG_PTR ulClientDelta; // 不完整. 参见 reactos 
} CLIENTINFO, *PCLIENTINFO;

Die Struktur PDESKTOPINFO ist wie folgt definiert:

root@kitploit:~
typedef struct _DESKTOPINFO { 
    PVOID pvDesktopBase; 
    PVOID pvDesktopLimit;   // 不完整. 参见 reactos  
} DESKTOPINFO, *PDESKTOPINFO;

Das erste Feld pvDesktopBase zeigt auf die Kernelmodus-Adresse des Desktop-Heaps; das merken wir uns. Das Feld ulClientDelta in Win32ClientInfo ist die Differenz zwischen einer Kernelmodus-Adresse und einer Benutzermodus-Adresse. Mit diesen Informationen erhalten wir, was wir brauchen.

Wir wollen die Heap-Struktur jedoch nicht selbst analysieren, sondern einen user32-Handle – etwa einen HWND-Wert – in die Benutzermodus-Mapping-Adresse umwandeln, um festzustellen, ob er mit anderen Heap-Allokationen in Zusammenhang steht. Um diesen Handle zu finden, müssen wir eine Struktur namens gShared suchen, die sich üblicherweise in uer32.dll befindet und ab Windows 7 exportiert wird; daher ist sie leicht zu finden.

Auf den meisten Systemen ist diese Struktur wie folgt definiert:

root@kitploit:~
kd> dt !tagSHAREDINFO 
win32k!tagSHAREDINFO 
    +0x000 psi                  : Ptr32 tagSERVERINFO 
    +0x004 aheList              : Ptr32 _HANDLEENTRY 
    +0x008 HeEntrySize          : Uint4B 
    +0x00c pDispInfo            : Ptr32 tagDISPLAYINFO 
    +0x010 ulSharedDelta        : Uint4B 
    +0x014 awmControl           : [31] _WNDMSG 
    +0x10c DefWindowMsgs        : _WNDMSG 
    +0x114 DefWindowSpecMsgs    : _WNDMSG

In obiger Struktur zeigt aheList auf ein Array von _HANDLEENTRY; jedes _HANDLEENTRY enthält einen Handle, der auf eine Kernelmodus-Adresse verweist. Über die „Differenz zwischen Kernel- und Benutzermodus-Adresse“ erhalten wir eine nutzbare Benutzermodus-Adresse. Leider ist das auf Versionen vor Windows 7 nicht möglich, da gSharedInfo nicht exportiert wird. Tarjeis Artikel zufolge kann die undokumentierte Funktion CsrClientConnectToServer verwendet werden, um eine Kopie von gSharedInfo zu erhalten, aber ich habe kein funktionierendes Beispiel gefunden. Ärgerlicherweise benötigt diese Funktion eine Struktur, deren Größe je nach System variiert; daher kann man meiner Erfahrung nach nicht völlig dem vertrauen, was man in ReactOS sieht.

Sobald wir die Position der Abbildung berechnet haben, können wir eine Funktion bauen, die uns die Position des Fensterobjekts im Desktop-Heap mitteilt. Wenn wir dann wissen wollen, wo der Heapblock für die zugehörige Eigenschaftenliste oder den Textstring allokiert wurde, genügt es, die Benutzermodus-Struktur zu analysieren.

Ersetzen von tagSBINFO durch tagPROPLIST

Jetzt nähern wir uns endlich der Ausnutzung dieser Schwachstelle. Wir haben bereits eine Methode, Heapblöcke zu kontrollieren, eine Methode, um zu prüfen, ob die Heapblock-Position korrekt ist, und wir können den Bug auch auslösen. Jetzt können wir endlich den freigegebenen tagSBINFO-Heapblock durch eine ausgewählte tagPROPLIST-Eigenschaftenliste ersetzen. Beachte, dass tagPROPLIST lediglich der Kopf einer größeren Liste ist; wir können also die Größe der Liste an die des Bildlaufleisten-Heapblocks anpassen. Hinter tagPROPLIST folgt im Wesentlichen ein Array von tagPROP-Strukturen – oder eine Eigenschaftenliste; daher werde ich die Begriffe Array und Liste nicht unterscheiden. Die tagPROPLIST-Struktur ist auf 64-Bit-Systemen wie folgt definiert:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
    +0x000 cEntries     : Uint4B 
    +0x004 iFirstFree   : Uint4B 
    +0x008 aprop        : tagPROP 
        +0x000 hData    : Ptr64 
        +0x008 atomKey  : Uint2B 
        +0x00a fs       : Uint2B

Wie bereits erwähnt, besitzt jedes Fensterobjekt eine zugehörige Eigenschaftenliste. Diese Liste wird über die Funktion SetProp() erstellt. Sie dient dazu, vorhandene Eigenschaften über einen Abgleich mit autoKey zu suchen; wenn die Eigenschaft nicht existiert, wird ein neuer Eintrag in der Eigenschaftenliste angelegt. Wenn überhaupt keine Eigenschaftenliste existiert, wird eine erstellt und an die tagWND-Struktur angebunden.

Wenn wir bereits ein verwundbares tagWND gesprayt und den zugehörigen tagPROPLIST-Eintrag erstellt haben, sieht das endgültige Layout wie in Abbildung 3 dargestellt aus:

Sobald das eingerichtet ist, können wir das Bildlaufleisten-Steuerelement allozieren, das wir ausnutzen wollen. Das führt zu dem in Abbildung 4 gezeigten Ergebnis:

Dann lösen wir durch Manipulation der Bildlaufleiste den User-Mode-Callback-Hook aus; in der Hook-Funktion geben wir die tagSBINFO-Struktur frei, indem wir das Fenster zerstören. Dies führt zu der Situation in Abbildung 5:

Unter 64-Bit ist die tagSBINFO-Struktur 0x28 Bytes groß; ein tagPROPLIST-Array-Eintrag ist 0x18 Bytes, wobei 0x10 Bytes für das standardmäßige tagPROP verwendet werden. Eine Eigenschaftenliste mit zwei Array-Einträgen ist also 0x28 Bytes (0x8 + 0x10 + 0x10) – das passt perfekt. Angenommen, wir haben den Speicher bereits gesprayt, um Lücken zu füllen. Wir brauchen lediglich ein Fenster mit einer Eigenschaftenliste, dem wir unmittelbar nach der Freigabe der tagSBINFO-Struktur (wie in der vorigen Abbildung gezeigt) einen neuen Eigenschaften-Eintrag hinzufügen. Dabei wird zunächst der bisherige 0x18-Byte-tagPROPLIST-Heapblock freigegeben. Da der Heap bereits gesprayt wurde, gibt es weder davor noch dahinter freie Heapblöcke, also kommt es auch zu keiner Heapblock-Verschmelzung, und es gibt keinen ausreichend großen Platz für die neu allozierte Größe von 0x28 Bytes. Dadurch wird die gerade freigegebene Position von tagSBINFO verwendet (ihre Größe beträgt genau 0x28 Bytes). Diese Situation ist in Abbildung 6 dargestellt:

Nach der Rückkehr aus unserer Hook-Callback-Funktion wird die UAF ausgelöst und ein paar Bits werden in das Feld cEntries in tagPROPLIST geschrieben. Der ursprüngliche cEntries-Wert war 0x2, was bedeutet, dass wir zwei Eigenschaften-Einträge erstellt haben. Nach dem Überschreiben wird er zu 0xe; die Bits 3 und 4 (von 1 beginnend gezählt) werden auf 1 gesetzt.

Zu diesem Zeitpunkt haben wir den Heap überlaufen lassen und die Anzahl der Einträge dieser Eigenschaftenliste erhöht – auf mehr als 0xc. Als Nächstes werden wir den benachbarten Heapblock überlaufen lassen; das nennen wir Korruptionsstufe 2.

Missbrauch der Eigenschaftenliste – Korruptionsstufe 2

So viel wurde in Udids Blog erklärt. Man könnte es davor als „typischen Heap-Überlauf“ bezeichnen; meiner Erfahrung nach ist es jedoch schwierig, von diesem Punkt aus eine beliebige Lese-/Schreiboperation oder Codeausführung zu erreichen. Wir betrachten die tagPROPLIST-Struktur auf 64-Bit erneut:

root@kitploit:~
kd> dt -b !tagPROPLIST 
win32k!tagPROPLIST 
    +0x000 cEntries     : Uint4B 
    +0x004 iFirstFree   : Uint4B 
    +0x008 aprop        : tagPROP 
        +0x000 hData    : Ptr64 
        +0x008 atomKey  : Uint2B 
        +0x00a fs       : Uint2B

Die Korruptionsstufe 1 liefert uns ein korrumpiertes tagPROPLIST-Array, mit dem wir zusätzliche tagPROP-Array-Einträge hinzufügen können. tagPROPLIST hat nur zwei Felder:

  • cEntries: gibt die Anzahl der vorhandenen Listeneinträge an
  • iFirstFree: gibt den Index des ersten freien Eintrags an. Ein voller Listenzustand (d. h., ein neuer wird benötigt) bedeutet, dass iFirstFree == cEntries ist.

Wenn ein neuer Listeneintrag in die Liste eingefügt wird, wird zunächst eine Funktion aufgerufen, die jeden Eintrag scannt, bis sie einen passenden iFirstFree-Indexwert findet. Wird keiner gefunden, wird geprüft, ob der Wert von iFirstFree größer als cEntries ist. Wenn der zu atomKey gehörende Eintrag nicht in der Liste vorhanden ist, wird geprüft, ob iFirstFree != cEntries ist. Wenn sie ungleich sind, wird ein Eintrag an der Position iFirstFree eingefügt; sind sie gleich, wird eine neue Eigenschaftenliste allokiert, die den einzufügenden Eintrag aufnehmen kann, wobei die vorhandenen Einträge kopiert und der neue Eintrag eingefügt wird.

Das Feld atomKey entspricht LPCTSTR lpString. Wie in der MSDN-Dokumentation zu SetProp() beschrieben, kann der Aufrufer entweder einen String-Zeiger oder einen 16-Bit-Atomwert übergeben. Bei der Übergabe eines String-Zeigers wird dieser vor dem Speichern in der Eigenschaftenliste automatisch in einen Atomwert umgewandelt. Da wir beliebige Atomwerte an SetProp() übergeben können, haben wir die Möglichkeit, diese beiden Bytes zu kontrollieren – allerdings mit einigen Einschränkungen. Der korrumpierte Wert für atomKey darf nicht mit einem bereits vorhandenen Atomwert übereinstimmen, da sonst beim Setzen eines neuen Eintrags der vorhandene Eintrag mit demselben Atomwert ersetzt würde. Außerdem ist das Feld fs für uns nicht steuerbar: Ein Wert von 0 bedeutet, dass atomKey < 0xBFFF ist und einem ganzzahligen Atomwert entspricht. Ein Wert von 2 für fs bedeutet, dass atomKey >= 0xC000 ist.

Ein weiterer Punkt ist, dass tagPROP nur 0xc Bytes groß ist. Diese Struktur ist auf 64-Bit-Systemen an 0x10 Bytes ausgerichtet, sodass beim Einfügen eines tagPROP-Eintrags zusätzliche 4 Bytes nicht korrumpiert werden können. Ein letzter wichtiger Punkt: Die ersten 8 Bytes eines tagPROPLIST-Heapblocks definieren die Größe der Liste; das bedeutet, dass jeder neu eingefügte tagPROP-Eintrag stets an einer 8-Byte-ausgerichteten Position geschrieben wird.

Für jeden eingefügten tagPROP sieht die Situation auf 64-Bit-Systemen so aus:

  • Offset 0x0: 8 Bytes frei steuerbare Daten (hData)
  • Offset 0x8: 2 Bytes weitgehend steuerbare Daten (atomKey)
  • Offset 0xa: 2 Bytes nicht steuerbare Daten (fs)
  • Offset 0xc: 4 Bytes nicht modifizierbare Daten (Padding)

Diese Situation ist viel besser als nur zwei Bits, aber immer noch nicht perfekt. Es sei denn, wir überschreiben mit den ersten 8 Bytes etwas, das aus dem vollständig steuerbaren Feld hData stammt – ansonsten sind wir ziemlich eingeschränkt. Wenn wir tiefere Felder in einer benachbarten Struktur überschreiben müssen, lassen sich unkontrollierbare Korruptionen bestimmter Werte nicht vermeiden. Ich habe einige Zeit damit verbracht, im Desktop-Heap nach passenden Objekten zu suchen. Angesichts der obigen Korruptionsbeschränkungen war der einzige Weg, den ich mir vorstellen konnte, um beliebige Adressen lesen und schreiben zu können, das Feld strName in tagWND zu korrumpieren – eine _LARGE_UNICODE_STRING-Struktur:

root@kitploit:~
kd> dt !_LARGE_UNICODE_STRING 
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           : Uint4B 
    +0x004 MaximumLength    : Pos 0, 31 Bits 
    +0x004 bAnsi            : Pos 31, 1 Bit 
    +0x008 Buffer           : Ptr64 Uint2B

Wenn wir das Buffer-Feld dieser Struktur korrumpieren können, können wir über die Manipulation des Fenstertexts MaximumLength Bytes an einer beliebigen Adresse lesen und schreiben. Genau das habe ich getan. Du hast diese Struktur vielleicht im früheren Abschnitt bemerkt, in dem es darum ging, einen Heapblock beliebiger Größe und Werte im Desktop-Heap zu erzeugen; dieselbe Vorgehensweise lässt sich hier anwenden.

Bis jetzt wissen wir, wie wir mit tagPROPLIST-Einträgen Daten korrumpieren können, welche Teile wir steuern können und – was noch wichtiger ist – welche Einschränkungen uns erwarten; diese unterscheiden sich zwischen 32-Bit und 64-Bit. Was wir zuvor auf 64-Bit gemacht haben, funktioniert auf 32-Bit nicht. Als Nächstes werden wir von der Korruptionsstufe 2 – also dem Schreiben von Daten über die tagPROP-Struktur – zu einem anderen Korruptions-Primitiv übergehen, mit dem wir vollständig steuerbare Daten schreiben können. Das meine ich mit Korruptionsstufe 3.

Aufbau eines Lese-/Schreib-Primitivs – Korruptionsstufe 3

64-Bit

Das Ziel ist es, das Feld strName in einem benachbarten tagWND zu korrumpieren. Wir wissen bereits, dass es sich um eine _LARGE_UNICODE_STRING-Struktur handelt, aber schauen wir uns weitere Details der tagWND-Struktur an; sie sieht folgendermaßen aus:kd> dt !tagWND win32k!tagWND +0x000 head : THRDESKHEAD +0x028 state : Uint4B +0x028 bHasMeun : Pos 0, 1 Bit +0x028 bHasVerticalScrollbar : Pos 1, 1 Bit +0x028 bHasHorizontalScrollbar : Pos 2, 1 Bit [SNIPPED FLAGS] +0x028 bDestroyed : Pos 31, 1 Bit +0x02c state2 : Uint4B [SNIPPED FLAGS] +0x02c bWMCreateMsgProcessed : Pos 31, 1 Bit +0x030 ExStyle : Uint4B +0x030 bWS_EX_DLGMODALFRAME : Pos 0, 1 Bit +0x030 bUnused1 : Pos 1, 1 Bit +0x030 bWS_EX_NOPARENTNOTIFY : Pos 2, 1 Bit [SNIPPED FLAGS] +0x030 bUIStateFocusRectHidden : Pos 31, 1 Bit +0x034 style : Uint4B +0x034 bReserved1 : Pos 0, 16 Bits [SNIPPED FLAGS] +0x034 bWS_POPUP : Pos 31, 1 Bit +0x038 hModule : Ptr64 Void +0x040 hMod16 : Uint2B +0x042 fnid : Uint2B +0x048 spwndNext : Ptr64 tagWND +0x050 spwndPrev : Ptr64 tagWND +0x058 spwndParent : Ptr64 tagWND +0x060 spwndChild : Ptr64 tagWND +0x068 spwndOwner : Ptr64 tagWND +0x070 rcWindow : tagRECT +0x080 rcClient : tagRECT +0x090 lpfnWndProc : Ptr64 int64 +0x098 pcls : Ptr64 tagCLS +0x0a0 hrgnUpdate : Ptr64 HRGN_ +0x0a8 ppropList : Ptr64 tagPROPLIST +0x0b0 pSBInfo : Ptr64 tagSBINFO +0x0b8 spmenuSys : Ptr64 tagMENU +0x0c0 spmenu : Ptr64 tagMENU +0x0c8 hrgnClip : Ptr64 HRGN__ +0x0d0 hrgnNewFrame : Ptr64 HRGN__ +0x0d8 strName : LARGE_UNICODE_STRING +0x0e8 cbwndExtra : Int4B +0x0f0 spwndLastActive : Ptr64 tagWND +0x0f8 hImc : Ptr64 HIMC_ +0x100 dwUserData : Uint8B +0x108 pActCtx : Ptr64 _ACTIVATION_CONTEXT +0x110 pTransform : Ptr64 _D3DMATRIX +0x118 spwndClipboardListenerNext : Ptr64 tagWND +0x120 ExStyle2 : Uint4B +0x120 bClipboardListener : Pos 0, 1 Bit [SNIPPED FLAGS] +0x120 bChildNoActivate : Pos 11, 1 Bit

Oben ist die Struktur für 64-Bit-Systeme. Man kann sehen, dass die Verschiebung (Offset) der _LARGE_UNICODE_STRING-Struktur, die wir überschreiben wollen, 0xd8 beträgt. Man wird auch ein wichtiges Feld am Anfang dieser Struktur bemerken. Ursprünglich hatte ich gehofft, es nach Herzenslust zu malträtieren, aber in _THRDESKHEAD gibt es eine große Anzahl von Zeigern, was uns auf Trab hält; und leider können wir nicht kontrollieren, wohin wir schreiben – der Grund für diese Einschränkung wurde bereits zuvor diskutiert.

Definition der _THRDESKHEAD-Struktur:

root@kitploit:~
kd> dt !_THRDESKHEAD 
win32k!_THRDESKHEAD 
    +0x000 h        : Ptr64 Void 
    +0x008 cLockObj : Uint4B 
    +0x010 pti      : Ptr64 tagTHREADINFO 
    +0x018 rpdesk   : Ptr64 tagDESKTOP 
    +0x020 pSelf    : Ptr64 UChar

Das Problem mit _THRDESKHEAD ließ uns nicht nur zögern, sondern veranlasste uns auch, die Ausrichtungsbeschränkung erneut zu überdenken. Egal an welcher Verschiebung (Offset) sich der neue tagPROP-Listeneintrag befindet, unsere Schreiboperation überschreibt direkt die Stelle am Anfang von _LARGE_UNICODE_STRING:

root@kitploit:~
win32k!_LARGE_UNICODE_STRING 
    +0x000 Length           <-- hData (完全可控) 覆盖这里 
    +0x004 MaximumLength    <-- 和这里 
    +0x004 bAnsi            <-- 和这里 
    +0x008 Buffer           <-- atomKey and fs (部分可控) 覆盖这里

Es ist klar, dass wir den Buffer-Zeiger überschreiben wollen, um auf Speicher an beliebigen Adressen zugreifen zu können. Allerdings könnten wir, selbst wenn wir die anderen Felder dieser Struktur sicher angreifen könnten, den benötigten Zeiger nicht kontrollieren.

Wir können nicht beliebige Daten korrumpieren; die Lösung dieses Problems ist nicht länger eine Frage der Korrumpierung von tagPROPLIST, sondern wird zu einem völlig anderen Satz von Korruptionsmechanismen.

In Versionen nach Windows XP wird der Heap-Block-Header (d. h. _HEAP_ENTRY) des User-Mode-Backend-Allokators (wie des Kernel-Desktop-Heaps) auf dem Heap gespeichert und befindet sich vor dem eigentlichen Inhalt des Blocks. Der Desktop-Heap selbst wird über die _HEAP-Struktur verwaltet, was uns bei der Ausnutzung dieses Heap-Blocks eine gewisse Freiheit verschafft.

Die Definition der _HEAP_ENTRY-Struktur lautet wie folgt:

root@kitploit:~
kd> dt !_HEAP_ENTRY 
ntdll!_HEAP_ENTRY 
    +0x000 PreviousBlockPrivateData : Ptr64 Void 
    +0x008 Size             : Uint2B 
    +0x00a Flags            : UChar 
    +0x00b SmallTagIndex    : UChar 
    +0x00c PreviousSize     : Uint2B 
    +0x00e SegmentOffset    : UChar 
    +0x00f UnusedBytes      : UChar

Der Heap-Block-Header ist insgesamt 0x10 Bytes groß. Die ersten 8 Bytes sind PreviousBlockPrivateData; wenn die angeforderte Größe die normale Größe von 0x10 überschreitet (kleiner als 8 Bytes wird auf 8 Bytes ausgerichtet), dient es dazu, die tatsächlichen Daten des vorherigen Blocks aufzunehmen. Dies wird im Leviathan-Blogeintrag kurz beschrieben; auch in früheren Artikeln über den User-Mode-Heap wird es beschrieben. Size und PreviousSize geben die Größe des aktuellen Blocks bzw. des vorherigen Blocks in Einheiten von 0x10 Bytes an. Flags zeigt an, ob der Block frei ist usw. Wenn der Sicherheitsmodus von _HEAP_ENTRY im _HEAP aktiviert ist, enthält SmallTagIndex eine XOR-Prüfsumme der Daten im Block.

Obwohl die Ausrichtungsbeschränkung zu unseren Ungunsten ist, existiert sie tatsächlich. Ein tagPROPLIST ist immer mindestens 0x18 Bytes groß, zuzüglich des 0x10 Bytes großen tagPROP. Ein tagPROPLIST von 0x28 Bytes mit zwei Listeneinträgen wird in einem Heap-Block von 0x20 Bytes platziert, und die überschüssigen Bytes, die durch PreviousBlockPrivateData dargestellt werden, verwenden den benachbarten Heap-Block. Das bedeutet: Wenn wir den dritten Listeneintrag hinzufügen, wird der benachbarte Heap-Block korrumpiert; die kontrollierbaren 8 Bytes von hData überschreiben den Anfang von _HEAP_ENTRY.

Was wir tun wollen, ist dies auszunutzen, um auf irgendeine Weise beliebige Daten an die Position des Anfangs von Buffer zu schreiben. Zuerst verändern wir das Heap-Layout, um uns dem korrumpierten tagPROPLIST-Heap-Block zu nähern. Während des Steuerungsprozesses haben wir einen kleinen Heap-Block, der eine fensterbezogene Textzeichenfolge enthält; wir nennen ihn den „Überschreibungs-Heap-Block". Direkt neben diesem „Überschreibungs-Heap-Block" platzieren wir ein tagWND, damit wir dieses tagWND korrumpieren können. Abbildung 7 unten veranschaulicht diesen Prozess. Beachte, dass wir die zuvor gesprühten Heap-Blöcke zur Platzersparnis weggelassen haben; diese sollten nun als implizit angesehen werden.

Als Nächstes fügen wir ein drittes tagPROP in die Liste des tagPROPLIST ein. Es überschreibt die letzten 8 Bytes von _HEAP_ENTRY und die ersten 8 Bytes im „Überschreibungs-Heap-Block". Auf diese Weise können wir das _HEAP_ENTRY des „Überschreibungs-Heap-Blocks" so verändern, dass seine Größe die tatsächliche Größe übersteigt und groß genug ist, um die benachbarte tagWND-Struktur einzuschließen.

Nun geben wir den soeben korrumpierten „Überschreibungs-Heap-Block" frei, damit der Heap-Manager ihn in die Freiliste einordnet, deren Blockgröße (jede Freiliste entspricht einer festen Größe) größer ist als die tatsächliche Größe des „Überschreibungs-Heap-Blocks". Dann verwenden wir diesen Block wieder, indem wir den Fenstertext (den wir vollständig kontrollieren) ändern. Allerdings gibt es dabei ein kleines Problem, das wir lösen müssen. Wenn der „Überschreibungs-Heap-Block" freigegeben wird, versucht der Heap-Manager, den vorherigen benachbarten Block zu finden – abhängig vom korrumpierten Size-Feld. Der Heap-Manager prüft, ob dieser benachbarte Block frei ist, um ihn zu verschmelzen. Wir wollen ihn in jedem Fall kontrollieren und ein „in Verwendung"-Kennzeichen setzen. Eine geringfügige Änderung unseres Heap-Layouts kann dies erreichen. An dieser Stelle platzieren wir einen gefälschten Heap-Block, dessen Heap-Header das „in Verwendung"-Kennzeichen gesetzt hat und dessen PreviousSize-Wert auf den Wert des korrumpierten Size-Felds gesetzt ist. Das erreichen wir einfach durch das Zuweisen des Fenstertexts eines anderen Fensters. Das neue Heap-Layout ist unten dargestellt (Abbildung 8):

Jetzt können wir den korrumpierten „Überschreibungs-Heap-Block" freigeben, indem wir die Textzeichenfolge des zugehörigen Fensters so aktualisieren, dass ihre Länge größer ist als die ursprünglichen 0x10 Bytes. Auf diese Weise wird der korrumpierte „Überschreibungs-Heap-Block" zuerst freigegeben und in die Freiliste eingetragen; seine Größe ist jedoch die korrumpierte, seine deklarierte Größe ist also größer als die tatsächliche. Diese Größe kann je nach unserem tatsächlichen Bedarf angepasst werden. Auf diese Weise werden unsere Zeichenfolgendaten in diesen „Überschreibungs-Heap-Block" geschrieben; dann verwenden wir diesen „Überschreibungs-Heap-Block", um das benachbarte tagWND zu beliebigen Daten zu korrumpieren. Wie unten dargestellt (Abbildung 9):

Dies ist die Korrumpierung von Phase 3. Jetzt können wir den Zeiger strName.Buffer mit beliebigen gewünschten Daten überschreiben. Allerdings bereitet das Korrumpieren anderer Daten des tagWND weiterhin etwas Mühe – aber das ist kein Problem, da der Desktop-Heap in den Benutzerbereich abgebildet ist! Bevor wir also alles korrumpieren, lesen wir den gesamten Inhalt des tagWND, ändern den Inhalt der strName-Struktur nach unseren Wünschen und senden alle Daten über die Änderung des Fenstertexts.

Über strName erhalten wir nicht nur eine „Primitive" für beliebiges Lesen und Schreiben, sondern wir können strName auch wiederholt ändern, was der Änderungsmechanismus des Fenstertexts erlaubt. Solange die Länge der geschriebenen Zeichenfolge den Wert von MaximumLength nicht überschreitet, kann derselbe Heap-Block weiterverwendet werden. Jedes Mal also, wenn wir die Adresse von strName ändern wollen, um einen Wert an einer bestimmten Stelle zu lesen, aktualisieren wir den „Überschreibungs-Heap-Block" mit einer neuen Zeichenfolge und unseren Daten. Diese Wiederverwendung ist in Abbildung 10 dargestellt. Beachte, dass ich die Vogelperspektive der Abbildung erneut vergrößert habe, um jede einzelne Korrumpierung im Detail zu zeigen.

Das bedeutet, dass wir letztendlich nur zwei zusätzliche Dinge korrumpieren müssen (zusätzlich zu den ursprünglichen tagPROPLIST-Listeneinträgen):

  1. Den Kopf des „Überschreibungs-Heap-Blocks". Wir können ihn vor der Korrumpierung lesen und wissen daher, wie wir ihn nach der Verwendung wiederherstellen können. Interessanterweise können wir sogar den Heap-Block des dritten tagPROPLIST-Listeneintrags reparieren, indem wir einfach das atomKey, mit dem wir korrumpiert haben, an die ursprüngliche Position senden!
  2. Die strName-Struktur, die wir durch das Schreiben des Fenstertexts leicht ändern können. Bei der Operation muss der Rückgabewert auf null gesetzt werden.

Wenn wir nun einige Bytes von einer bestimmten Stelle im Speicher lesen wollen, fragen wir den Fenstertext über die Funktion InternalGetWindowText() ab, in der sich der Einstieg des korrumpierten strName befindet. Wir können die Anzahl der Bytes lesen, die das Length-Feld deklariert. Wenn wir ebenso an eine beliebige Stelle im Speicher schreiben wollen, aktualisieren wir den korrumpierten Fenstertext mit der Funktion NtUserDefSetText(), wobei die geschriebene Menge jedoch nicht größer sein sollte als der Wert, den das MaximumLength-Feld deklariert (auch dieser Wert ist von uns festlegbar). Auf diese Weise wird der vorhandene Puffer wiederverwendet und zeigt auf die gewünschte Speicheradresse.

Heap-Encoding unter Windows 8 und 8.1

Obwohl der Backend-Allokator im Benutzermodus seit Windows Vista Heap-Encoding (heap encoding) verwendet, hat der Desktop-Heap es bis Windows 8 nie aktiviert. Daher entsteht auf Systemen nach Windows 8 ein Hindernis, wenn wir den „Überschreibungs-Heap-Block" überschreiben. Allerdings enthält die _HEAP-Struktur, die den Heap beherbergt, dieses Cookie und verwendet es, um den gesamten Heap-Header zu kodieren. Daher können wir dieses Cookie aus dem in den Benutzerbereich abgebildeten Desktop-Heap lesen und damit den Kopf des „Überschreibungs-Heap-Blocks" kodieren; die Kodierungsmethode erhalten wir durch Reverse Engineering des Allokator-Codes. Wenn man seine Vorgehensweise nachahmt, akzeptiert der Allokator diese Operation.

32-Bit-Systeme

Zunächst ist zu beachten, dass die tagPROP-Struktur auf 32-Bit-Systemen 8 Bytes groß ist und nicht 0xc Bytes wie auf 64-Bit-Systemen; außerdem ist das von uns kontrollierte hData-Feld nur 4 Bytes groß und nicht 8 Bytes wie auf 64-Bit-Systemen. Zugleich gibt es auch keine zusätzlichen Auffüllbytes; auf 64-Bit-Systemen gibt es 8 Bytes Auffüllung (Padding), sodass die gesamte Struktur genau 8 Bytes beträgt. Das bedeutet, dass wir den benachbarten Heap-Block-Header nicht vollständig korrumpieren können, wenn wir die Daten nur teilweise kontrollieren. Auf bestimmten Windows-Versionen ist dies möglich, da wir die wichtigsten Felder kontrollieren können; auf Windows 8 und 8.1 ist der Heap-Header jedoch kodiert, und letztendlich können wir über das fs-Feld unsicher einen Teil des Heap-Headers überschreiben. Der _HEAP_ENTRY-Header auf 32-Bit-Systemen sieht ähnlich aus, aber es fehlt das Feld PreviousBlockPrivateData.

Wir können weiterhin nicht alle Teile des tagWND korrumpieren, da abgeschnittene Zeiger (truncated pointers) unvermeidbar sind. Und ich habe immer noch kein Objekt gefunden, das dies erfüllt. Da sich _LARGE_UNICODE_STRING auf 64-Bit-Systemen gut bewährt hat, möchte ich es auch auf 32-Bit-Systemen verwenden.

Meine Idee ist: Wenn wir das iFirstFree-Feld der tagPROPLIST-Struktur (den Index des ersten freigegebenen Eigenschaftseintrags in der Eigenschaftsliste) korrumpieren können, indem wir den Indexwert erhöhen, dann können wir es auf eine weiter entfernte Position im Heap zeigen lassen. Zum Beispiel können wir es auf den Anfang von tagWND.strName zeigen lassen. Abbildung 11 zeigt diese Idee:

Zur Verdeutlichung des Ablaufs verwenden wir nun zwei tagPROPLIST-Strukturen: „Eigenschaftsliste A" für den UAF und eine weitere „Eigenschaftsliste B". Wir müssen genau wissen, welche Teile der in „Eigenschaftsliste A" eingefügten tagPROPs das iFirstFree-Feld von „Eigenschaftsliste B" überschreiben werden. Wir müssen auch bedenken, dass wir jeweils nur 8 Bytes schreiben können; daher müssen wir mindestens ein zusätzliches tagPROP in „Eigenschaftsliste A" einfügen: Die erste Korrumpierung trifft den benachbarten Heap-Header, die zweite das tagPROPLIST-Feld von „Eigenschaftsliste B". Diese können je nach Betriebssystem und Heap-Blockgröße variieren und müssen in meinem Exploit an verschiedene Heap-Layouts angepasst werden. Abbildung 12 zeigt, wie wir korrumpieren. Beachte, dass das erste tagPROPLIST in der Abbildung nicht in einzelne Felder aufgeteilt ist, sodass tagPROP[0] implizit ist. Beim zweiten tagPROPLIST jedoch werden die internen Mitglieder aufgeteilt, um unseren Korrumpierungsprozess zu veranschaulichen. Deshalb wird tagPROP[0] angezeigt:

Zunächst beachten wir: Wenn wir pro tagPROP 8 Bytes schreiben, bedeutet das, dass wir die Überschreibung von iFirstFree nur teilweise kontrollieren können (da sie aus den Feldern atomKey und fs stammt) – und das ist es, was uns am meisten interessiert. Denn über den Wert von atomKey können wir mindestens zwei kritische Bytes vollständig kontrollieren; wenn dieser Wert klein genug ist, wird das fs-Feld zu 0. Daher überschreiben wir cEntries mit dem Wert von hData auf einen vernünftigen Wert und nutzen atomKey, um iFirstFree auf das tagWND zeigen zu lassen; der Zeiger strName.Buffer, den wir überschreiben wollen, befindet sich in diesem tagWND. Wenn wir die Werte von Length und MaximumLength nicht direkt überschreiben können, können wir dem Zielfenster vorab eine Zeichenfolge zuweisen, um sicherzustellen, dass seine Länge bereits auf einen bestimmten Wert gesetzt ist.

Werfen wir einen Blick auf die 32-Bit-tagWND-Struktur, um zu sehen, was wir erreichen können. Beachte, dass ich diesmal den Parameter -b verwende, damit wir die Verschiebung (Offset) von Buffer in strName bequem berechnen können.

root@kitploit:~
kd> dt -b !tagWND 
win32k!tagWND 
    +0x000 head         : _THRDESKHEAD 
        +0x000 h        : Ptr32 
        +0x004 cLockObj : Uint4B 
        +0x008 pti      : Ptr32 
        +0x00c rpdesk   : Ptr32 
        +0x010 pSelf    : Ptr32 
    +0x014 state        : Uint4B 
    +0x014 bHasMeun     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x014 bDestroyed   : Pos 31, 1 Bit 
    +0x018 state2       : Uint4B 
[SNIPPED FLAGS] 
    +0x018 bWMCreateMsgProcessed    : Pos 31, 1 Bit 
    +0x01c ExStyle                  : Uint4B 
    +0x01c bWS_EX_DLGMODALFRAME     : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x01c bUIStateFocusRectHidden  : Pos 31, 1 Bit 
    +0x020 style        : Uint4B 
    +0x020 bReserved1   : Pos 0, 16 Bits 
[SNIPPED FLAGS] 
    +0x020 bWS_POPUP    : Pos 31, 1 Bit 
    +0x024 hModule      : Ptr32 
    +0x028 hMod16       : Uint2B 
    +0x02a fnid         : Uint2B 
    +0x02c spwndNext    : Ptr32 
    +0x030 spwndPrev    : Ptr32 
    +0x034 spwndParent  : Ptr32 
    +0x038 spwndChild   : Ptr32 
    +0x03c spwndOwner   : Ptr32 
    +0x040 rcWindow     : tagRECT 
        +0x000 left     : Int4B 
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x050 rcClient     : tagRECT 
        +0x000 left     : Int4B
        +0x004 top      : Int4B 
        +0x008 right    : Int4B 
        +0x00c bottom   : Int4B 
    +0x060 lpfnWndProc  : Ptr32 
    +0x064 pcls         : Ptr32 
    +0x068 hrgnUpdate   : Ptr32 
    +0x06c ppropList    : Ptr32 
    +0x070 pSBInfo      : Ptr32 
    +0x074 spmenuSys    : Ptr32 
    +0x078 spmenu       : Ptr32 
    +0x07c hrgnClip     : Ptr32 
    +0x080 hrgnNewFrame : Ptr32 
    +0x084 strName      : _LARGE_UNICODE_STRING 
        +0x000 Length   : Uint4B 
        +0x004 MaximumLength : Pos 0, 31 Bits 
        +0x004 bAnsi    : Pos 31, 1 Bit 
        +0x008 Buffer   : Ptr32 
    +0x090 cbwndExtra   : Int4B 
    +0x094 spwndLastActive  : Ptr32 
    +0x098 hImc         : Ptr32 
    +0x09c dwUserData   : Uint4B 
    +0x0a0 pActCtx      : Ptr32 
    +0x0a4 pTransform   : Ptr32 
    +0x0a8 spwndClipboardListenerNext   : Ptr32 
    +0x0ac ExStyle2                     : Uint4B 
    +0x0ac bClipboardListener           : Pos 0, 1 Bit 
[SNIPPED FLAGS] 
    +0x0ac bChildNoActivate             : Pos 11, 1 Bit

Die Verschiebung (Offset) von strName ist 0x84, die von Buffer ist 0x8c. Beachte, dass wir den Index des tagPROP-Listeneintrags haben und 8 Bytes schreiben können. Daher können wir leicht feststellen, ob iFirstFree auf die Offset-Adresse 0x88 des Fensters indiziert, an der sich MaximumLength befindet. Da nur zwei Bytes von Buffer kontrolliert werden können, ist die Schreiboperation nicht durchführbar; da unser Ziel darin besteht, dies als „Primitive" für beliebiges Lesen und Schreiben zu nutzen, ist dieses Ergebnis nicht akzeptabel. Wenn wir einen Index schreiben, der auf 0x90 zeigt, überschreiben wir cbwndExtra – das ist nicht das, was wir anstreben.

Erinnern wir uns an das, was wir beim früheren Heap-Feng-Shui kontrollieren konnten, und prüfen wir dann, ob es im tagWND interessante Offsets gibt, die wir kontrollieren können. Bei Offset 0x70 im tagWND befindet sich das Feld pSBInfo. Dieser Offset ist durch 8 teilbar, daher können wir diesen Zeiger mit Teilen der Daten von hData aus dem gefälschten tagPROP überschreiben.

Können wir pSBInfo so überschreiben, dass er direkt auf strName in derselben tagWND-Struktur zeigt? Vielleicht können wir mit der Scrollbar-API strName korrumpieren, um unser Ziel zu erreichen.

pSBInfo zeigt auf eine tagSBINFO-Struktur; diese Struktur wurde bereits im anfänglichen UAF-Prozess erwähnt.

root@kitploit:~
kd> dt -b !tagSBINFO 
win32k!tagSBINFO 
    +0x000 WSBflags     : Int4B 
    +0x004 Horz         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B 
    +0x014 Vert         : tagSBDATA 
        +0x000 posMin   : Int4B 
        +0x004 posMax   : Int4B 
        +0x008 page     : Int4B 
        +0x00c pos      : Int4B

Wir erinnern uns: WSBflags gibt uns nicht mehr Kontrolle, aber wir wissen zumindest, dass es auf 1 gesetzt wird, wenn die Scrollbar aktiviert ist, und auf 0, wenn sie deaktiviert wird. Dieses Flag-Feld kann nicht auf beliebige Werte gesetzt werden; durch Reverse Engineering der zugehörigen Funktionalfunktionen fanden wir heraus, dass sich dieses Feld nicht ändert, wenn der Scrollbar-Status nicht geändert wird. Die Werte in der tagSBDATA-Struktur scheinen interessanter zu sein. Wenn wir die Dokumentation von SetScrollInfo() lesen, können wir die Bedeutung dieser Werte gut verstehen. Es scheint, dass wir über die SCROLLINFO-Struktur Parameter an SetScrollInfo() übergeben können. Solange sich in der Nähe des zu korrumpierenden Fensters ein Scrollbar-Steuerelement befindet, können wir den Zeiger pSBInfo direkt manipulieren (er sendet eine spezielle Fensternachricht an das zugehörige Fenstersteuerelement). Offensichtlich können wir die Werte von posMin und posMax bedingungslos kontrollieren. Die Felder page und pos sind etwas problematisch, da sie auf einen bestimmten Bereich beschränkt sind; wir versuchen derzeit, diese zu vermeiden. Setze das SIF_RANGE-Flag in der SCROLLINFO-Struktur, um zu deklarieren, wo wir den Maximal- und Minimalwert setzen wollen.

Wir wollen Buffer mit beliebigen Daten überschreiben – das bedeutet, wir wollen, dass posMin ihn überschreibt. Daher können wir pSBInfo so überschreiben, dass er auf strName.MaximumLength zeigt. Solange wir die Scrollbar nicht aktivieren oder deaktivieren, wird das Feld WSBflags nicht überschrieben, was die Integrität von strName.MaximumLength gewährleistet. Das bedeutet: Unabhängig davon, wie wir posMin setzen (über nMin in SCROLLINFO), wird Buffer überschrieben, und posMax wird auf cbwndExtra geschrieben. Das ist kein großes Problem; auf 64-Bit-Systemen können wir diesen Wert vorab lesen und später wiederherstellen. Die allgemeine Idee des Überlaufs ist in Abbildung 13 dargestellt:

Nun veranschaulichen wir mit Abbildung 14 den Angriffsprozess auf 32-Bit-Systemen. Bevor wir irgendeine Adresse korrumpieren, die weit vom UAF entfernt ist, treten wir einen Schritt zurück und betrachten die relevanten Heap-Blöcke und das Heap-Layout in der Abbildung. Da wir nun mehr Details kennen, ist das, was als Nächstes zu tun ist, offensichtlich.

Als Nächstes fügen wir zwei Eigenschaftseinträge in „Eigenschaftsliste A" ein. Dies korrumpiert die Daten in der Nähe von „Eigenschaftsliste A", dank der früheren UAF-Korrumpierung, und lässt gleichzeitig iFirstFree von „Eigenschaftsliste A" auf pSBInfo zeigen. Beachte, dass dies auch den nahe gelegenen pSBInfo-Wert korrumpiert; aber wir können ihn vorab lesen, um ihn nach der Korrumpierung wiederherzustellen.

Wir fügen in „Eigenschaftsliste B" ein neues tagPROP ein, dessen atom-Bezeichner sich von den bereits in der Liste vorhandenen unterscheidet, sodass dieses tagPROP am nächsten freien Index eingefügt wird. Dadurch wird pSBInfo korrumpiert, sodass er auf strName.MaximumLength im selben tagWND zeigt.

Schließlich aktualisieren wir die Scrollbar, um das Feld strName.Buffer zu korrumpieren (wie in Abbildung 17):

Beachte: Anders als im 64-Bit-Fall können wir die Längenwerte von strName nicht korrumpieren. Wir können dem Fenster vorab eine Textzeichenfolge geeigneter Länge zuweisen, sodass ihr Wert bereits verwendet wird. Danach müssen wir – ob wir nun Daten aus einer Kernel-Adresse lesen oder dorthin schreiben wollen – nur SetScrollInfo() aufrufen, um den Buffer-Wert des Zielfensters zu aktualisieren, und dann die Fenstertext-API verwenden.

Jetzt haben wir auf 32-Bit-Systemen eine wiederverwendbare „Primitive" für beliebiges Lesen und Schreiben an beliebigen Adressen!

Code-Ausführung

Von nun an wird angenommen, dass wir eine „Primitive" für beliebiges Lesen/Schreiben haben. Wenn ich also sage, einen Wert leaken/lesen oder überschreiben, dann meine ich die Ausführung dieser „Primitive", die in der vorherigen Korrumpierungsphase aufgebaut wurde. Diese „Primitive" ist auf beiden Plattformen weitgehend identisch. Was übrig bleibt, ist lediglich das Überschreiben eines Funktionszeigers, sodass er auf eine Shellcode-Payload an einer bestimmten Stelle zeigt. Die übliche Methode besteht darin, den zweiten Eintrag von nt!HalDispatchTable zu überschreiben, der der Funktion HalQuerySystemInformation() entspricht. Anschließend wird sie im Benutzermodus durch den Aufruf von NtQueryInternalProfile() ausgelöst.

Wir müssen die Ladebasis des Kernel-Moduls kennen, um die Kernel-Adresse von nt!HalDispatchTable zu berechnen. Dazu können wir im Benutzermodus NtQuerySystemInformation() aufrufen, um Modulinformationen zu erhalten; diese Informationen enthalten auch die Modulbasis.

root@kitploit:~
// 枚举值 11 代表 SystemModuleInformation , 这是未文档化的... 
rc = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)11, pModuleInfo, 0x100000, NULL);

Danach laden wir ntoskrnl.exe im Benutzermodus, um die Verschiebung (Offset) von nt!HalDispatchTable zu finden; so erhalten wir seine Adresse im Kernel-Adressraum. Dann verwenden wir die „Leseprimitive", um die Kernel-Adresse von HaliQuerySystemInformation() (diese Funktion ist nicht exportiert) zu lesen, um sie zu modifizieren; anschließend korrumpieren wir mit der „Schreibprimitive" den Zeiger dieser Funktion, sodass er auf die Adresse des Shellcodes zeigt (möglicherweise im Kernel-Adressraum oder im Benutzer-Adressraum; Details später). Die Anzahl der gelesenen bzw. geschriebenen Bytes ist auf 32-Bit- und 64-Bit-Systemen gleich.

SMEP umgehen

Windows 8 und 8.1 führen Unterstützung für SMEP ein, und einige Sicherheitsprodukte können es auch unter Windows 7 aktivieren; daher nehmen wir an, dass es definitiv vorhanden ist. SMEP verhindert, dass wir Code im Benutzerbereich mit Kernel-Berechtigungen ausführen, wodurch es unbrauchbar wird, den Eintrag von nt!HalDispatchTable so zu ändern, dass er auf eine Benutzerbereichsadresse zeigt. Daher wollen wir, dass er auf eine kontrollierbare Stelle im Kernel-Adressraum zeigt; der Code an dieser Stelle kann den Wert des cr4-Registers ändern, um SMEP zu deaktivieren, sodass wir in den Benutzer-Adressraum springen können. Der MWR-Artikel stellt einen interessanten Trick für 64-Bit vor: Durch Selbstabbildung von Seitentabelleneinträgen (Page Table Entries) erhält man für eine beliebige virtuelle Adresse eine entsprechende gültige Kernel-Adresse. Anschließend wird der Seitentabelleneintrag direkt mit der „Schreibprimitive" modifiziert und seine Maskenbits geändert. Ich habe diesen Trick auf 32-Bit-Systeme portiert, aber es gibt einige Unterschiede zwischen Systemen mit aktiviertem PAE und solchen ohne PAE.

Um dies zu erreichen, besteht der naheliegende Weg darin, eine Benutzeradresse in den Kernel-Adressraum abzubilden und dann mit der „Schreibprimitive" zu bewirken, dass der Seitentabelleneintrag Systemberechtigung statt Benutzerberechtigung hat. Das ist unser erster Schritt. Als ich dies unter Windows 8 implementierte, stieß ich auf ein interessantes Problem. Der Desktop-Manager (dwm.exe) unter Windows 8 und später scannt regelmäßig die Fenster auf dem Desktop und fragt ihre Namen ab – aus welchem Grund genau, habe ich nicht untersucht. Diese Operation sendet auch keine Nachricht an das Fenster, aber es gibt eine entsprechende Fensterbehandlungsfunktion, die GetInternalWindowText() aufruft. Das Problem ist daher: Wenn wir die Felder der strName-Struktur des Fensters nutzen, um den Seitentabelleneintrag der Speicherseite zu überschreiben, die den Shellcode enthält, so gehört diese Speicherseite zur Seitentabelle unseres eigenen Prozessraums. Wenn dwm.exe den Fensternamen aus dem Kernel abruft, führt der modifizierte Seitentabelleneintrag dazu, dass der Kernel prüft, ob strName.Buffer null ist, und diese Adresse indirekt referenziert; wenn die Adresse ungültig ist, stürzt das System ab.

Um die Abfrage von dwm.exe zu erfüllen, habe ich eine Kernel-Adresse als Payload verwendet. Auf diese Weise ist der mit dieser Adresse verbundene Seitentabelleneintrag immer gültig, unabhängig davon, was der aktuelle Prozess lädt. Ich habe sie auf dem Desktop-Heap platziert, da wir ihre Kernel-Adresse mit der zuvor erwähnten Methode berechnen können. Wir verwenden weiterhin die Methode der Selbstabbildung von Seitentabelleneinträgen; zu diesem Zeitpunkt ist die Seitentabelle bereits als hoch privilegiert gekennzeichnet, aber nicht als ausführbar. Wir müssen also nur das Ausführungsbit setzen.

Die Schritte sind wie folgt:

  1. Einen Fenstertext-Puffer erstellen, der die Payload aus Phase 1 enthält, und seine Kernel-Adresse berechnen.
  2. Mit dem Trick der „Selbstabbildung von Seitentabelleneinträgen" den Seitentabelleneintrag für die in Phase 1 erhaltene Kernel-Basisadresse berechnen.
  3. Mit der „Schreibprimitive" das Ausführungs-Maskenbit des Seitentabelleneintrags setzen.
  4. nt!HalDispatchTable so überschreiben, dass es auf die in Phase 1 gefundene Kernel-Adresse zeigt.
  5. NtQueryInternalProfile() aufrufen, um zur Payload zu springen.
  6. Das SMEP-Maskenbit in cr4 deaktivieren und zur Benutzerbereichs-Payload in Phase 2 springen.
  7. Die Benutzerbereichs-Payload in Phase 2 ausführen, um Rechte zu erweitern, und zurückkehren.
  8. Das SMEP-Maskenbit in cr4 wiederherstellen, um eine Erkennung durch PatchGuard zu verhindern, nach der zugehörigen Bereinigung zurückkehren.

Umgehung der Low-Integrity-Sandbox

Unter Windows 8.1 gibt es ein weiteres Problem: NtQuerySystemInformation() prüft den SID-Wert der niedrigen Integritätsstufe (Low Integrity). Das bedeutet, dass nur mittlere Integrität oder höher die Kernel-Basisadresse erhalten kann. Dies lässt sich leicht mit dem bekannten sidt-Trick erreichen. Wir speichern die Adresse der IDT in den Benutzerbereich (dies erfordert keine Berechtigungsprüfung) und lesen dann mit der „Leseprimitive" den IDT-Eintrag, den wir benötigen; dieser zeigt meist in den Kernel-Adressraum, sodass wir die Kernel-Adresse des Interrupt-Handlers leaken können. Anschließend suchen wir die Verschiebung (Offset) in der zum Kernel-Modul gehörenden PE-Datei.

Sobald wir die Kernel-Ladebasis erhalten haben, können wir die Adresse von nt!HalDispatchTable berechnen.

Die übliche Methode besteht darin, die Datei ntoskrnl.exe zu laden, die Offsets ihrer Symbole aufzulösen und zur geleakten Kernel-Ladebasis zu addieren. Für eine Sandbox im Enhanced Mode funktioniert dies jedoch nicht, da es eine Einschränkung des Dateisystems selbst gibt: Man kann C:\windows\system32\ntoskrnl.exe nicht lesen. Um diese Einschränkung zu umgehen, nutzen wir unsere „Leak-Primitive", um die benötigten Symboladressen aus dem Kernel-PE-Abbild im Speicher zu parsen.

Fazit

Das ist das gesamte Material. Danke fürs Lesen. Mit den in diesem Dokument vorgestellten Techniken konnte ich auf 32-Bit- und 64-Bit-Systemen stabile Exploits für XP, Vista, 7, 8, 8.1 und Server 2012 implementieren. Unter Windows 2003 und 2008 funktioniert es standardmäßig nicht, da User-Mode-Callbacks nicht gehookt werden können; daher können diese beiden Systeme nicht angegriffen werden, es sei denn, die von uns geforderten Bedingungen sind erfüllt. Der Exploit-Prozess ist recht komplex und es gibt viele Hürden zu überwinden, aber das hat mir auch viel Spaß gemacht und viel Lehrreiches geboten. Viele der hier verwendeten gangbaren Methoden und Forschungsergebnisse wurden bereits in Artikeln anderer Forscher erwähnt. Meines Wissens gibt es nur eine Gegenmaßnahme, die die Ausnutzung von win32k.sys vermeidet, nämlich die die Google-Chrome-Sandbox verwendet; sie blockiert die win32k-Kernel-Systemaufrufe zur Laufzeit effektiv. Ich freue mich über jede Verbesserung und jedes Feedback; wenn einige der von mir vorgeschlagenen Techniken Mängel aufweisen, sagt es mir, und ich werde dieses Dokument aktualisieren. Ihr könnt mich über Twitter @fidgetingbits oder per E-Mail unter [email protected] kontaktieren.

Tool herunterladen