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-2020-1206 — Technische Analyse von CVE-2020-1206 (SMBleed), einer Kernel-Informationsoffenlegungsschwachstelle in Windows SMBv3, einschließlich eines nicht authentifizierten Speicherleck-Orakels und Ausnutzungstechniken in Kombination mit SMBGhost für RCE. | Kitploit
Tools/GitHubGitHub/datntsec/cve-2020-1206
SpeicherforensikSchwachstellenanalyseExploitationInformationsbeschaffungPenetrationstestsBinary-Exploitation
GitHubdatntsec/cve-2020-1206

CVE-2020-1206

Technische Analyse von CVE-2020-1206 (SMBleed), einer Kernel-Informationsoffenlegungsschwachstelle in Windows SMBv3, einschließlich eines nicht authentifizierten Speicherleck-Orakels und Ausnutzungstechniken in Kombination mit SMBGhost für RCE.

Repository anzeigen
vor 5 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

Bei der SMBGhost-Schwachstelle (CVE-2020-0796) habe ich über eine Write-What-Where-Primitive-Technik gesprochen, die durch die Ausnutzung eines Integer-Overflow-Bugs den Zeiger Alloc.Userbuffer so ändert, dass er auf eine von uns gewünschte Adresse zeigt und wir dort beliebige Daten schreiben können. Ähnlich wie SMBGhost existiert diese Schwachstelle auch in der Funktion Srv2DecompressData in srv2.sys. Schauen wir uns noch einmal die Funktion Srv2DecompressData an, die mit der SMBGhost-Schwachstelle (CVE-2020-0796) zusammenhängt und von Zecops vereinfacht wurde.``` c typedef struct _COMPRESSION_TRANSFORM_HEADER { ULONG ProtocolId; ULONG OriginalCompressedSegmentSize; USHORT CompressionAlgorithm; USHORT Flags; ULONG Offset; } COMPRESSION_TRANSFORM_HEADER, *PCOMPRESSION_TRANSFORM_HEADER;

typedef struct _ALLOCATION_HEADER { // ... PVOID UserBuffer; // ... } ALLOCATION_HEADER, *PALLOCATION_HEADER;

NTSTATUS Srv2DecompressData(PCOMPRESSION_TRANSFORM_HEADER Header, SIZE_T TotalSize) { PALLOCATION_HEADER Alloc = SrvNetAllocateBuffer( (ULONG)(Header->OriginalCompressedSegmentSize + Header->Offset), NULL); If (!Alloc) { return STATUS_INSUFFICIENT_RESOURCES; }

root@kitploit:~
ULONG FinalCompressedSize = 0;


NTSTATUS Status = SmbCompressionDecompress(
    Header->CompressionAlgorithm,
    (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER) + Header->Offset,
    (ULONG)(TotalSize - sizeof(COMPRESSION_TRANSFORM_HEADER) - Header->Offset),
    (PUCHAR)Alloc->UserBuffer + Header->Offset,
    Header->OriginalCompressedSegmentSize,
    &FinalCompressedSize);
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}


if (Header->Offset > 0) {
    memcpy(
        Alloc->UserBuffer,
        (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
        Header->Offset);
}


Srv2ReplaceReceiveBuffer(some_session_handle, Alloc);
return STATUS_SUCCESS;

}

root@kitploit:~
Die Funktion Srv2DecompressData empfängt eine vom Client gesendete komprimierte Nachricht und reserviert einen benötigten Speicherbereich, um die Nachricht darin zu dekomprimieren. Anschließend, wenn das Offset-Feld ungleich Null ist, kopiert sie die Daten (RawData) vor den komprimierten Daten in den Anfang des reservierten Speicherbereichs.

![](https://assets.kitploit.com/production/public/readmes/24502/a5bf8b5059336fb3677162343bace0d0b45085f2eb89e42d85f81e032fe233dc.png)

Der SMBGhost-Fehler liegt darin, dass die Funktion keinen Integer-Überlauf prüft, was zu einer falschen Speicherzuweisung und einem Pufferüberlauf führt. Drei Monate nachdem Microsoft SMBGhost gepatcht hatte, wurde der Fehler CVE-2020-1206 (SMBleed – laut [Zecops Blog](https://blog.zecops.com/)) entdeckt. Dieser Fehler ermöglicht es uns, die Adresse eines anderen Rechners zu leaken, und in Kombination mit SMBGhost könnten wir RCE erlangen. Um einen einfacheren Blick auf die Funktion Srv2DecompressData zu werfen, werden wir diese Funktion in ihrem ungepatchten Zustand vor dem SMBGhost-Patch verwenden und annehmen, dass sie bereits gepatcht ist.

# Manipulation von OriginalCompressedSegmentSize
Wie bei SMBGhost werden wir auch diesmal das OriginalCompressedSegmentSize mit einer etwas größeren Zahl als die von uns gesendeten dekomprimierten Daten fälschen. Wenn wir beispielsweise Daten der Größe x Byte komprimieren, setzen wir anstelle von x in das Feld OriginalCompressedSegmentSize den Wert x + 0x1000, wie das folgende Bild verdeutlicht:

![](https://assets.kitploit.com/production/public/readmes/24502/687d3bdc67e4b2f5bb3cecbe41ea52be98a5c904a8688b325a69acb0a854ca75.png)

Nicht initialisierte Kernel-Daten werden als Teil der Nachricht betrachtet.

Wie ich in der Analyse von [CVE-2020-0796](https://github.com/datntsec/CVE-2020-0796) erwähnt habe, wird Srv2DecompressData die Prüfphase nach der Funktion SmbCompressionDecompress überspringen, wenn die Dekomprimierung erfolgreich ist:``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) {
    SrvNetFreeBuffer(Alloc);
    return STATUS_BAD_DATA;
}

Obwohl das Feld OriginalCompressedSegmentSize auf x + 0x1000 statt auf x gesetzt ist, enthält die Variable FinalCompressedSize nach erfolgreicher Dekomprimierung nicht den Wert x, sondern den Wert x + 0x1000:```c NTSTATUS SmbCompressionDecompress( USHORT CompressionAlgorithm, PUCHAR UncompressedBuffer, ULONG UncompressedBufferSize, PUCHAR CompressedBuffer, ULONG CompressedBufferSize, PULONG FinalCompressedSize) { // ...

root@kitploit:~
NTSTATUS Status = RtlDecompressBufferEx2(
    ...,
    FinalUncompressedSize,
    ...);
if (status >= 0) {
    *FinalCompressedSize = CompressedBufferSize;
}

// ...

return Status;

}

root@kitploit:~
Bởi vì sau khi giải nén thành công, FinalCompressedSize được cập nhật để giữ giá trị CompressedBufferSize (tương ứng với OriginalCompressedSegmentSize được truyền vào hàm SmbCompressionDecompress). Việc cập nhật và kiểm tra sau đó là gần như không cần thiết, có thể dẫn đến một số lỗi không mong muốn.

# Grundlegende Exploitation
Die von Zecops zum Nachweis des Fehlers verwendete Nachrichtenstruktur ist die [SMB2 WRITE-Nachricht](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/e7046961-3318-4350-be2a-a8d69bb59ce8). Diese Struktur enthält Felder wie die Anzahl der beschreibbaren Bytes, Flags usw., gefolgt von einem Puffer mit beliebiger Länge. Dies ist ziemlich perfekt für die Ausnutzung des Fehlers, da wir eine Nachricht erstellen und einen Header mit einem Puffer angeben können, der nicht initialisierte Daten enthält.

Basierend auf dem POC von [Zecops](https://blog.zecops.com/) im Repository der WindowsProtocolTestSuites von Microsoft werden wir diesen kleinen Zusatz zur Kompressionsfunktion hinzufügen, um einen klareren Einblick zu erhalten:``` c
// HACK: fake size
if (((Smb2SinglePacket)packet).Header.Command == Smb2Command.WRITE)
{
    ((Smb2WriteRequestPacket)packet).PayLoad.Length += 0x1000;
    compressedPacket.Header.OriginalCompressedSegmentSize += 0x1000;
}

Beachten Sie, dass dieser POC Authentifizierungsinformationen und Schreibberechtigungen erfordert, die in vielen Fällen verfügbar sind. Der zurückgegebene Fehler gilt jedoch für jede Nachricht (einschließlich Nachrichten mit oder ohne Authentifizierungsinformationen), sodass wir möglicherweise ohne Authentifizierung ausnutzen können.

Ein weiterer Punkt ist, dass der Speicher, den wir leaken werden, von vorherigen Zuweisungen im NonPagedPoolNx stammt, und da wir die Zuweisungsgröße kontrollieren können, werden wir die Daten, die wir leaken, bis zu einem gewissen Grad kontrollieren können.

SMBleed POC Source Code

Wenn also keine Authentifizierungsinformationen vorliegen, kann die Kernel-Adresse dann geleakt werden? Um diese Frage zu beantworten, lassen Sie uns tiefer in SMB eintauchen.

Tiefer in SMB eintauchen

Bei der Authentifizierung sendet der Client die folgenden Nachrichten:

SMB2 NEGOTIATE → SMB2 SESSION_SETUP → SMB2 SESSION_SETUP

Wenn die Authentifizierungsinformationen falsch sind, wird die Verbindung nach dem zweiten SMB2 SESSION_SETUP-Paket abgebrochen:

Angenommen, wir haben keine Authentifizierungsinformationen, werden wir prüfen, ob es einen Befehl gibt, der ohne Authentifizierung gesendet werden kann. Durch die Suche stellen wir fest:

  • Der erste zu sendende Befehl ist SMB2 NEGOTIATE und es ist auch der einzige SMB2 NEGOTIATE-Befehl während einer Sitzung.
  • Die folgenden Befehle müssen bis zur erfolgreichen Authentifizierung SMB2 SESSION_SETUP sein.

Dabei wird die SMB2 NEGOTIATE-Nachricht nicht komprimiert. Der Fehler liegt in der Dekomprimierungsfunktion, daher werden wir sie nicht betrachten, sondern nur die SMB2 SESSION_SETUP-Nachrichten.

SMB2 SESSION_SETUP

Wie oben erwähnt, werden in einer normalen Sitzung zwei SMB2 SESSION_SETUP-Befehle gesendet. Die zurückgegebenen Pakete enthalten keine Daten, die für die Ausnutzung erforderlich sind, und wir haben keine Möglichkeit, das zurückgegebene Paket zu beeinflussen. Das zweite zurückgegebene Paket hat jedoch einen leeren Body mit dem Status 0xC000006D (STATUS_LOGON_FAILURE) im Paketheader. Beachten Sie, dass das erste SMB2 SESSION_SETUP-Paket eine NTLM Negotiate-Nachricht und das zweite eine NTLM Authenticate-Nachricht enthält. Die NTLM Negotiate-Nachricht ist recht einfach und möglicherweise nicht interessant, daher werden wir uns mit der NTLM Authenticate-Nachricht befassen.

NTLM Authenticate message

Nach Untersuchung der NTLM Authenticate-Nachricht stellen wir fest, dass der komplexeste Teil dieser Nachricht, der für die Ausnutzung am besten geeignet ist, die NTLM2 V2 Response-Struktur ist. Diese Struktur ist ein Byte-Array variabler Größe, das hauptsächlich die NTLMv2_CLIENT_CHALLENGE-Struktur enthält. Wir sehen, dass, wenn diese Struktur die anfänglichen Prüfungen nicht besteht, der Wert 0xC000000D (STATUS_INVALID_PARAMETER) anstelle von 0xC000006D (STATUS_LOGON_FAILURE) zurückgegeben wird. Eine der anfänglichen Prüfungen ist die Prüfung des AvPairs-Feldes.

Das AvPairs-Feld ist ein Byte-Array variabler Größe, das AV_PAIR-Strukturen enthält. Jedes AV_PAIR definiert ein Attribut/Wert-Paar, wobei das Attribut durch das AvId-Feld definiert wird, das AvLen-Feld die Länge des Werts in Bytes definiert und das Value-Feld ein Byte-Array variabler Größe ist, das den Wert selbst enthält. Ein Element mit dem Attribut MsvAvEOL und einer Länge von Null markiert das Ende des Arrays.

Die Authenticate-Nachricht wird von der Funktion SsprHandleAuthenticateMessage im Modul msv1_0.dll verarbeitet. In den anfänglichen Prüfungen stellt diese Funktion sicher, dass das AvPairs-Array die folgenden Attribute enthält: 0x0001 (MsvAvNbComputerName), 0x0002 (MsvAvNbDomainName). Allerdings wird deren Wert nicht überprüft, es wird nur durch das Array iteriert und geprüft, ob das angeforderte Attribut existiert und seine Länge innerhalb der Struktur liegt. Wenn die Länge zu groß ist, wird die Übertragung gestoppt. Tatsächlich wird also nicht überprüft, ob MsvAvEOL gültig ist.

An diesem Punkt haben wir herausgefunden, dass wir eine Anfrage erstellen können, die uns hilft, die folgende Frage zu beantworten: Sind zwei Bytes an Offset x, vom Typ uint16, größer als y? x und y werden von uns kontrolliert. Betrachten Sie das folgende Paket:

Der Inhalt des Wertes 0x0001 (MsvAvNbComputerName) ist nicht wichtig, daher können wir ihn verwenden, um den Offset des zweiten Wertes anzupassen. Für den zweiten Wert setzen wir nur das Attribut auf 0x0002 (MsvAvNbDomainName), ohne len und value zu initialisieren. Gleichzeitig setzen wir die Größe des gesamten Pakets so, dass y Bytes gemäß dem Längenfeld vorhanden sind. Es gibt zwei mögliche Ergebnisse, abhängig vom nicht initialisierten Wert des Längenfeldes des zweiten Wertes:

  • length <= y: In diesem Fall wird die Prüfung bestanden, da der gültige Wert 0x0002 (MsvAvNbDomainName) gefunden wird. Der Server gibt 0xC000006D (STATUS_LOGON_FAILURE) zurück, da die Authentifizierungsinformationen nicht korrekt sind.
  • length > y: In diesem Fall schlägt die Prüfung fehl, da der zweite Wert eine ungültige Länge hat und verworfen wird. Der Server gibt in diesem Fall 0xC000000D (STATUS_INVALID_PARAMETER) zurück.

Anhand der Antwort des Servers können wir immer die Antwort auf die obige Frage ableiten.

Allerdings ist die NTLM Authenticate-Nachricht auf 0xB48 Bytes begrenzt und wird verworfen, wenn sie größer ist. Die Prüfung wird von der Funktion SspContextGetMessage im Modul msv1_0.dll durchgeführt. Angenommen, wir schreiben nur 1 Byte in len, das andere Byte enthält einen nicht initialisierten Wert, kann dies dann umgangen werden? Leider nein, da der uint16-Wert im Little-Endian-Format codiert ist.

Somit können wir in einer einzigen SMB-Sitzung nicht erreichen, was wir wollen. Wir werden weitere Faktoren betrachten.

Observation #1: Lookaside lists

Wie in der vorherigen Forschung (CVE-2020-0796) erwähnt, verwenden die SMB-Verarbeitungsmodule im Kernel (srv2.sys und srvnet.sys) eine benutzerdefinierte Zuweisungsfunktion - SrvNetAllocateBuffer -, die von srvnet.sys exportiert wird. Diese Funktion verwendet Lookaside-Listen für kleine Zuweisungen zur Optimierung. Lookaside-Listen werden verwendet, um einen Satz von Puffern mit fester Größe effizient zu speichern, die für den Treiber wiederverwendet werden können.

Lookaside-Listen werden bei der Initialisierung erstellt. Die Liste für jede Größe und jeden logischen Prozessor wird in der folgenden Tabelle beschrieben:

Jede Zelle mit dem Symbol '📝' ist eine separate Lookaside-Liste. Um die Analyse zu vereinfachen, nehmen wir an, dass unser Ziel nur einen logischen Prozessor hat. In diesem Fall wird, solange die gleiche Anzahl von Bytes zugewiesen wird und dieselbe Lookaside-Liste verwendet wird, derselbe Puffer mehrfach wiederverwendet. Wir können dies nutzen, um eine gewisse Kontrolle über nicht initialisierte Daten zu haben.

Observation #2: Failing the decompression

Lassen Sie uns noch einmal betrachten, was passiert, wenn ein komprimiertes Paket dekomprimiert wird (siehe Writeup CVE-2020-0796 für weitere Details und Pseudocode):

Im Falle ungültiger CompressedData schlägt die Dekomprimierungsphase fehl, die Kopierphase wird nicht ausgeführt und die Verbindung wird getrennt. Aber die Dekomprimierung kann fehlschlagen, nachdem nur ein Teil der gültigen CompressedData dekomprimiert wurde. Dies ermöglicht es uns, eine Anfrage zu erstellen, bei der die von uns ausgewählten Daten an den von uns gewählten Offset geschrieben werden, wie in der folgenden Abbildung:

Back to the NTLM Authenticate message

Wir können die obigen Beobachtungen verwenden, um unsere Technik in zwei Schritten zum Laufen zu bringen:

  1. Senden Sie eine Nachricht mit ungültigen komprimierten Daten, sodass nur ein einzelnes Null-Byte dekomprimiert wird. Dieses Byte wird das erste Byte des Längenfeldes des zweiten Wertes im AvPairs-Array sein.
  2. Senden Sie eine Nachricht wie zuvor, aber stellen Sie sicher, dass dieselbe Lookaside-Liste für die Zuweisung verwendet wird, sodass das Null-Byte dort vorhanden ist.

Diesmal kann diese Technik die folgende Frage beantworten: Ist ein Byte an Offset x größer als y? Wie zuvor werden x und y von uns kontrolliert.

Da wir den Puffer mehrmals wiederverwenden können, indem wir sicherstellen, dass dieselbe Lookaside-Liste verwendet wird, können wir die Schritte mehrmals wiederholen, während wir y ändern, und schließlich den Wert des Bytes an einem bestimmten Offset ableiten.

Allerdings hat diese Technik eine Einschränkung - der Offset des Bytes, das wir lesen können, ist auf Byte 0xADB vom Anfang des Paketpuffers begrenzt. Dies liegt daran, dass der Offset der NTLM Authenticate-Nachricht (AUTHENTICATE_MESSAGE) auf 0x40 Bytes nach dem Ende der SMB2 SESSION_SETUP-Header begrenzt ist (durchgeführt von der Funktion Smb2ValidateSessionSetup in srv2.sys) und die Größe der NTLM Authenticate-Nachricht (AUTHENTICATE_MESSAGE) auf 0xB48 Bytes begrenzt ist. Wir werden einen Weg finden, dies zu umgehen.

Angenommen, wir möchten ein Byte bei Offset 0x1100 lesen. Mit der obigen Technik können wir das nicht direkt tun, aber wir können eine zusätzliche Technik verwenden: Da die Puffer aus Lookaside-Listen wiederverwendet werden, können wir das Zielbyte durch die Dekomprimierungsfunktion "anheben", indem wir das Offset-Feld so setzen, dass es über dieses Byte hinausgeht. Wir müssen nur sicherstellen, dass die dort befindlichen Daten als gültige komprimierte Daten interpretiert werden können, andernfalls findet kein Kopiervorgang statt.

Der Puffer des Pakets, das die vom Client gesendeten Daten enthält, enthält zusätzliche 16 Byte Header, die während der Dekomprimierung nicht kopiert werden. Folglich werden die kopierten und dekomprimierten Daten, einschließlich des Zielbytes, an eine Position kopiert, die 16 Byte näher am Anfang des zugewiesenen Puffers liegt. Wir können dies mehrmals wiederholen, bis der Offset des Zielbytes niedrig genug ist.

Address leak POC

Sie finden ein Skript, das die obige Technik demonstriert, hier. Beachten Sie, dass wir angenommen haben, dass der Server nur einen logischen Prozessor hat, daher müssen Sie Ihre virtuelle Maschine entsprechend konfigurieren, damit das Skript funktioniert. Wenn alles gut geht, wird das Skript die Adresse des NonPagedPoolNx-Pools lesen und leaken. Tatsächlich wird es die Adresse eines der Puffer sein, die sich in derselben Lookaside-Liste befinden.

Da diese Technik ziemlich viele Einschränkungen hat, werde ich nicht tiefer darauf eingehen. Sie können jedoch das obige Skript lesen und selbst analysieren.

A different approach – decompression

Im Laufe der Forschung erkannte Zecops, dass das dekomprimierte SMB-Paket nicht die einzige komplexe Struktur ist, die auf verschiedene Weise ungültig sein kann. Noch bevor alle SMB-bezogenen Strukturen verarbeitet werden, kann der komprimierte Puffer selbst ungültig sein. Wenn die Dekomprimierung fehlschlägt, wird die Verbindung zum Server getrennt.

Microsoft stellt drei Komprimierungsalgorithmen zur Auswahl bei der Implementierung von SMB zur Verfügung: LZNT1, Plain LZ77 und LZ77 + Huffman. Wir werden nur LZNT1 betrachten, da es recht einfach ist - etwa 80 Zeilen Python für eine Dekomprimierungsfunktion. Ich werde kurz den Dekomprimierungsprozess beschreiben: Die komprimierten Daten bestehen aus einer Folge von komprimierten Blöcken, jeder Block beginnt mit einer uint16-Variable, die die Länge dieses Blocks markiert. Wenn eine Länge von Null erreicht wird, ist die Dekomprimierung abgeschlossen. Wir werden dies verwenden, um eine Folge von Null-Bytes zu schreiben, die gültige komprimierte Daten darstellen. Das Ziel ist es, die obige Frage zu beantworten: Ist ein Byte an Offset x größer als y? Natürlich werden x und y weiterhin von uns kontrolliert.

Im Folgenden finden Sie ein Beispiel für die komprimierten Daten, die wir senden werden:

Es gibt zwei mögliche Ergebnisse, abhängig vom nicht initialisierten Wert des ersten Bytes des Längenfeldes:

  • length <= y: In diesem Fall besteht der erste Block vollständig aus Null-Bytes, was vollkommen gültig ist, und die Länge des nächsten Blocks ist Null, die Dekomprimierung ist abgeschlossen. Der Server sendet eine Antwort zurück.
  • length > y: In diesem Fall enthält der erste oder zweite komprimierte Block 0xFF-Bytes, dieser Block kann nicht dekomprimiert werden. Der Server trennt die Verbindung aufgrund ungültiger komprimierter Daten.

Wie bei der vorherigen Technik können wir die Beobachtungen 1 und 2 verwenden, um eine Nachricht mit einem nicht initialisierten Byte in der Mitte der Nachricht zu erstellen, indem wir zwei Schritte ausführen:

  1. Senden Sie eine Nachricht mit ungültigen komprimierten Daten, sodass nur ein Teil der Daten dekomprimiert wird, ähnlich wie im obigen Bild.
  2. Senden Sie eine zweite Nachricht und stellen Sie sicher, dass dieselbe Lookaside-Liste wie in der ersten Nachricht verwendet wird, sodass die Bytes aus Schritt 1 dort vorhanden sind.

Beachten Sie, dass der Offset-Wert im SMB-Paketheader auf die komprimierten Daten zeigt, die je nach Wert des nicht initialisierten Bytes gültig oder ungültig sein können.

Der bemerkenswerteste Vorteil dieser Technik gegenüber der vorherigen ist, dass es keine Offset-Begrenzung mehr gibt.

Zusammenfassend haben wir also zwei Techniken, um einen nicht initialisierten Speicherbereich aus dem Pool-Puffer zu lesen, der von der Funktion SrvNetAllocateBuffer des Moduls srvnet.sys zugewiesen wurde. Die erste Technik erstellt ein spezielles SMB-Paket und leitet dann Informationen aus der Antwort des Servers ab. Die zweite Technik mit weniger Einschränkungen erstellt spezielle komprimierte Daten und sendet sie, dann leiten wir Informationen daraus ab, ob der Server die Verbindung trennt oder nicht.

Somit können wir eine der beiden Techniken zur Ausnutzung verwenden. Und wie ich bereits sagte, hat die erste Technik viele Einschränkungen, daher werden wir uns nur mit der zweiten Technik befassen.

Diese Technik wird uns helfen, in Richtung des write-what-where-Primitives auszunutzen, das Zecops zuvor in früheren Forschungen zur Erreichung einer lokalen Privilegienausweitung demonstriert hat. Wir werden diese Technik verwenden, um Adressen im Speicherlayout zu leaken, damit wir das write-what-where-Primitive verwenden können. Unglücklicherweise wird der von der Funktion SrvNetAllocateBuffer zugewiesene Speicher hauptsächlich für Netzwerkdaten wie SMB-Pakete verwendet und enthält keine Systemzeiger. Und da wir RCE erreichen müssen, ist das Leaken von nicht initialisierten Speicherbereichen aus vorherigen Zuweisungen der Funktion SrvNetAllocateBuffer nutzlos, da die Position des benötigten Zeigers nicht sicher ist. Wir müssen etwas Nützlicheres finden.

SrvNetAllocateBuffer and the allocated buffer layout

Wie ich in der Forschung zur lokalen Privilegienausweitung (CVE-2020-0796) erwähnt habe, gibt die Funktion SrvNetAllocateBuffer nicht nur einen Puffer mit der angeforderten Größe zurück. Stattdessen gibt sie einen Zeiger zurück, der auf den Bereich direkt unterhalb des Benutzerpuffers des Pool-zugewiesenen Speicherblocks zeigt, der Informationen über den zugewiesenen Puffer enthält. Das Layout des Pool-zugewiesenen Speicherblocks ist wie folgt:

Obwohl unsere Lesetechnik nur Bytes aus dem "User Buffer"-Bereich lesen kann, können wir eine andere Technik verwenden, um Teile der SRVNET_BUFFER_HDR-Struktur in den "User Buffer" eines anderen Puffers zu kopieren, um sie zu lesen. Indem wir das Offset-Feld so setzen, dass es auf die SRVNET_BUFFER_HDR-Struktur außerhalb der Daten zeigt, die wir lesen möchten. Wir müssen nur sicherstellen, dass die dort befindlichen Daten als gültige komprimierte Daten interpretiert werden können, andernfalls findet kein Kopiervorgang statt.

Hunting for pointers

Betrachten wir die Felder der SRVNET_BUFFER_HDR-Struktur und prüfen, ob es Inhalte gibt, die es wert sind, gelesen zu werden:``` c #pragma pack(push, 1) struct SRVNET_BUFFER_HDR { /00/ LIST_ENTRY ConnectionBufferList; /10/ WORD BufferFlags; // 0x01 - no transport header, 0x02 - part of a lookaside list /12/ WORD LookasideListIndex; // 0 to 8 /14/ WORD LookasideListLogicalProcessor; /16/ WORD TracingDataCount; // 0, 1 or 2, for TracingPtr1/2, TracingUnknown1/2 /18/ PBYTE UserBufferPtr; /20/ DWORD UserBufferSizeAllocated; /24/ DWORD UserBufferSizeUsed; /28/ DWORD PoolAllocationSize; /2C/ BYTE unknown1[4]; /30/ PBYTE PoolAllocationPtr; /38/ PMDL pMdl1; /40/ DWORD BytesProcessed; /44/ BYTE unknown2[4]; /48/ SIZE_T BytesReceived; /50/ PMDL pMdl2; /58/ PVOID pSrvNetWskStruct; /60/ DWORD SmbFlags; /64/ PVOID TracingPtr1; /6C/ SIZE_T TracingUnknown1; /74/ PVOID TracingPtr2; /7C/ SIZE_T TracingUnknown2; /84/ BYTE unknown3[12]; }; #pragma pack(pop)

root@kitploit:~
Các con trỏ `UserBufferPtr`, `PoolAllocationPtr`, `pMdl1`, `pMdl2` sind Zeiger, die in den Pool-allokierten Speicherblock zeigen, mit den Offsets, die im Voraus berechnet werden können, daher reicht es, einen von ihnen auszulesen. Einen Zeiger auf einen Pool-allokierten Speicherblock zu haben, wird uns definitiv beim Exploit helfen. Darüber hinaus sind die folgenden Zeiger sehr wichtig:
- **ConnectionBufferList**: Eine verknüpfte Liste aller empfangenen, aber noch nicht verarbeiteten Puffer einer Verbindung. Der Kopf dieser Liste ist ein Verbindungsobjekt, das von der Funktion SrvNetAllocateConnection in srvnet.sys erstellt wird. Ein Puffer wird durch die Funktion SrvNetWskReceiveComplete zur Liste hinzugefügt. In unserem Fall wird es nur einen einzigen Puffer in der Liste geben, daher zeigen beide Zeiger (Flink und Blink der LIST_ENTRY-Struktur) auf den Listenkopf innerhalb des Verbindungsobjekts.
- **pSrvNetWskStruct**: Ursprünglich ein Zeiger auf das oben erwähnte Verbindungsobjekt. Der Zeiger wird von der Funktion SrvNetWskReceiveEvent gesetzt, aber von der Funktion SrvNetWskReceiveComplete mit einem Zeiger auf die SRVNET_BUFFER_HDR-Struktur überschrieben. Daher ist es nicht nützlicher, ihn zu lesen, als einen der vier bereits genannten Zeiger zu lesen. Übrigens, wenn Sie nach „pSrvNetWskStruct“ suchen, werden Sie sehen, dass es eine Rolle beim Exploit von EternalBlue spielt.
- **TracingPtr1/2**: Diese Zeiger werden nur verwendet, wenn die Tracing-Funktion aktiviert ist.

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Wie Sie sehen können, ist der einzige andere nützliche Zeiger, den wir lesen können, ein Zeiger in der ConnectionBufferList-Struktur. Beide Zeiger (Blink und Flink in der LIST_ENTRY-Struktur) zeigen auf das Verbindungsobjekt. Dieses Objekt wurde vom EternalBlue-Forscher SRVNET_RECV genannt, daher werden wir diesen Namen ebenfalls verwenden.

## Abrufen einer Modulbasisadresse
Jetzt wissen wir, wie wir zwei Zeiger erhalten – einen Zeiger auf den Pool-allokierten Speicherblock und einen Zeiger auf die SRVNET_RECV-Struktur – wir können die beiden Puffer mit der Write-What-Where-Primitive frei modifizieren. Es mag viele Wege geben, RCE zu erreichen, aber das Abrufen einer Basisadresse eines Moduls wird die einfachste Wahl sein, da es viele Dinge gibt, die wir im Datenabschnitt eines Moduls ändern können. Wie wir gesehen haben, zeigt keiner der Zeiger im von SrvNetAllocateBuffer allokierten Speicherblock auf ein Modul. Es gibt jedoch einige Zeiger, die auf Module zeigen:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

Die Lesetechnik, die wir haben, erlaubt uns nur, Daten im "User Buffer"-Bereich zu lesen, während sich diese Zeiger ziemlich weit entfernt befinden und von vielen anderen Zeigern gezeigt werden. Wir benötigen einen Code-Abschnitt, der Folgendes tun kann, um den Zeigerwert in den "User Buffer"-Bereich zu kopieren:``` c
ptr1 = *(pSrvNetRecv + offset1)
value = *ptr1
ptr2 = *(pSrvNetRecv + offset2)
*ptr2 = value

Wenn wir ein solches Codestück finden könnten, würden wir es aktivieren, um den ersten Zeiger (z.B. HandlerFunctions) in den "User Buffer"-Bereich zu kopieren, ihn zu lesen, dann den zweiten Zeiger (z.B. den Srv2ConnectHandler-Funktionszeiger) in den "User Buffer" zu kopieren und zu lesen, und daraus die Modul-Basisadresse ableiten. Die Zecops Gruppe hat lange nach einem solchen Codestück gesucht, aber kein passendes gefunden. Schließlich verwendeten sie eine andere Wahl in Bezug auf die Funktion SrvNetFreeBuffer (vereinfacht wie unten), die fast die gewünschte Funktionalität hat:``` c void SrvNetFreeBuffer(PSRVNET_BUFFER_HDR Buffer) { PMDL pMdl1 = Buffer->pMdl1; PMDL pMdl2 = Buffer->pMdl2;

root@kitploit:~
if (pMdl2->MdlFlags & 0x0020) {
    // MDL_PARTIAL_HAS_BEEN_MAPPED flag is set.
    MmUnmapLockedPages(pMdl2->MappedSystemVa, pMdl2);
}

if (Buffer->BufferFlags & 0x02) {
    if (Buffer->BufferFlags & 0x01) {
        pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
        pMdl1->ByteCount -= 0x50;
        pMdl1->ByteOffset += 0x50;
        pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

        pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
        pMdl2->ByteCount = pMdl1->ByteCount;
        pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
        pMdl2->Size = /* some calculation */;
        pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL
    }

    Buffer->BufferFlags = 0;

    // ...

    pMdl1->Next = NULL;
    pMdl2->Next = NULL;

    // Return the buffer to the lookaside list.
} else {
    SrvNetUpdateMemStatistics(NonPagedPoolNx, Buffer->PoolAllocationSize, FALSE);
    ExFreePoolWithTag(Buffer->PoolAllocationPtr, '00SL');
}

}

root@kitploit:~
Wenn der Puffer freigegeben wird und die Buffer-Flags 0x02 (bedeutet, dass der Puffer Teil einer Lookaside-Liste ist) und 0x01 (bedeutet, dass der Puffer keinen Transport-Header hat) gesetzt sind, werden einige Operationen an den zwei MDL-Objekten durchgeführt, um einen Transport-Header hinzuzufügen, bevor die Flags wieder auf 0 gesetzt und der Puffer zurück in die Lookaside-Liste gegeben wird. Wenn wir die Operationen an den MDL-Objekten genauer betrachten, stellen wir fest, dass der Code einen Double-Dereference-Read gefolgt von einem Double-Dereference-Write mit zwei von uns kontrollierten Variablen (zwei MDL-Zeigern) durchführt – genau das, wonach wir suchen. Der Nachteil ist, dass der Inhalt, den wir lesen möchten, ebenfalls verändert wird – ein Nebeneffekt, den wir hoffen vermeiden zu können.

Mit dem oben Gesagten gehen wir wie folgt vor, um den AcceptSocket-Zeiger zu lesen:
1. Bereite Puffer A aus einer Lookaside-Liste vor, sodass der „User-Buffer“-Bereich mit Nullen gefüllt ist. Der User-Buffer dieses Puffers wird den Zeiger enthalten, den wir lesen werden.
2. Bereite Puffer B aus einer anderen Lookaside-Liste vor, so dass:
- Der pMdl1-Zeiger auf die Adresse des AcceptSocket-Zeigers minus 0x18 zeigt (da der Offset von MappedSystemVa in der MDL-Struktur 0x18 beträgt).
- Der pMdl2-Zeiger auf den „User-Buffer“-Bereich von Puffer A zeigt.
- Das Flags-Feld auf 0x03 gesetzt ist.

Wir können die Felder der SRVNET_BUFFER_HDR-Struktur überschreiben, indem wir sie aus einem größeren Puffer mittels der im Abschnitt [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) beschriebenen Technik entpacken.

3. Wenn Puffer B freigegeben wird, laufen folgende Aktionen ab:
- Die MDL-Flags werden vom zweiten MDL in Puffer A gelesen. Wenn das MDL_PARTIAL_HAS_BEEN_MAPPED-Flag gesetzt ist, wird MmUnmapLockedPages aufgerufen, und das System könnte abstürzen. Deshalb müssen wir den Puffer in Schritt 1 mit Nullen füllen.
- Der AcceptSocket-Zeiger und der umliegende Speicher werden wie folgt modifiziert:```
+00 |  00 00 00 00 00 00 00 00
+08 |  __ __ __|10 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  [+50..................]  <--  AcceptSocket
+20 |  __ __ __ __ __ __ __ __
+28 |  [-50......] [+50......]
  • Der AcceptSocket-Zeiger und der umgebende Speicher werden wie hier beschrieben gelesen:``` +00 | __ __ __ __ __ __ __ __ +08 | __ __ __ __ __ __ __ __ +10 | __ __ __ __ __ __ __ __ +18 | ab cd ef gh ij kl mn op <-- AcceptSocket +20 | __ __ __ __ __ __ __ __ +28 | qr st uv wx __ __ __ __
root@kitploit:~
- Der „User buffer“ von Buffer A wird wie hier beschrieben geändert: (Die orangefarbenen Bytes enthalten Zeiger, die wir lesen möchten; wir müssen sie nur richtig anordnen.)```
+00 |  00 00 00 00 00 00 00 00
+08 |  ?? ?? 04 00 __ __ __ __
+10 |  __ __ __ __ __ __ __ __
+18 |  __ __ __ __ __ __ __ __
+20 |  00 c0 ef gh ij kl mn op
+28 |  qr st uv wx ab 0d 00 00

  1. Lies den AcceptSocket-Zeiger aus dem „User buffer“-Bereich von Puffer A.

Die gute Nachricht ist, dass wir den Zeiger gelesen haben. Die schlechte Nachricht ist, dass wir dabei einige Daten in der SRVNET_RECV-Struktur beschädigt haben. Zum Glück für uns hat der Fehler keine Auswirkungen auf das System, solange nichts mit der betreffenden Verbindung passiert. Wenn etwas passiert, z. B. das Schließen der Verbindung, stürzt das System ab. Das ist kein Problem, da wir bald RCE haben und den Fehler beheben können, wenn wir wollen.

Nach dem Lesen des AcceptSocket-Zeigers verwenden wir die gleiche Technik, um den srvnet!SrvNetWskConnDispatch-Zeiger zu lesen. Der Grund, warum wir den AcceptSocket-Zeiger und nicht den HandlerFunctions-Zeiger lesen, ist, dass das HandlerFunctions-Array von allen Verbindungen gemeinsam genutzt wird, während der von AcceptSocket referenzierte Puffer nicht von anderen Verbindungen gemeinsam genutzt wird. Wenn wir also Teile von AcceptSocket beschädigen, betrifft dies nur die Stabilität einer einzigen Verbindung.

Wenn wir eine Kopie der auf dem Zielcomputer verwendeten srvnet.sys-Datei haben, können wir die Basisadresse des srvnet.sys-Moduls leicht ableiten, indem wir den Offset des geleakten SrvNetWskConnDispatch-Zeigers abziehen.

Implementing arbitrary read

Angenommen, wir haben die Basisadresse des srvnet.sys-Moduls, können wir jede beliebige Funktion des Moduls aufrufen. Aber was ist mit den Funktionsargumenten? Die Funktion srv2!Srv2ReceiveHandler wird von SrvNetCommonReceiveHandler aufgerufen und der Aufruf hat die folgende Form:``` c HandlerFunctions = *(pSrvNetRecv + 0x118); Arg1 = *(ULONG_PTR)(pSrvNetRecv + 0x128); Arg2 = *(ULONG_PTR)(pSrvNetRecv + 0x130); (HandlerFunctions[1])(Arg1, Arg2, Arg3, Arg4, Arg5, Arg6, Arg7, Arg8);

root@kitploit:~
Die ersten beiden Argumente werden aus der SRVNET_RECV-Struktur gelesen, daher können wir sie kontrollieren, jedoch nicht die restlichen Argumente. Die x86-64-Aufrufkonvention legt fest, dass der Aufrufer für die Zuweisung und Freigabe des Stack-Speichers für die Argumente verantwortlich ist. Obwohl die Funktion mit 8 Argumenten aufgerufen werden soll, können wir den Zeiger durch eine beliebige andere Funktion ersetzen.

![](https://assets.kitploit.com/production/public/readmes/24502/223827f49f26993604c20b873d75ea5b11d306a85ff5210ceb2583f0a0b9452c.png)

Im Folgenden finden Sie die Schritte, die wir verwenden, um den Funktionsaufruf auszulösen:
1. Senden Sie eine speziell erstellte Nachricht, sodass der Zeiger auf die SRVNET_RECV-Struktur der Verbindung in einen Puffer kopiert wird, den wir lesen können.
2. Senden Sie eine weitere gültige Nachricht, die dieselbe SRVNET_RECV-Struktur wiederverwendet, die Verbindung jedoch noch nicht schließt. Beachten Sie, dass die SRVNET_RECV-Struktur beim Schließen der Verbindung nicht freigegeben wird. Die Funktion SrvNetPrepareConnectionForReuse wird aufgerufen, um die Struktur zurückzusetzen, damit sie für die nächste Verbindung wiederverwendet werden kann.
3. Lesen Sie den Zeiger auf die SRVNET_RECV-Struktur, den wir in Schritt 1 kopiert haben.
4. Ersetzen Sie den Zeiger auf HandlerFunctions und die Argumente mithilfe der Write-What-Where-Primitive.
5. Senden Sie eine zusätzliche Nachricht über die Verbindung aus Schritt 2, sodass die Ersatzfunktion für srv2!Srv2ReceiveHandler aufgerufen wird.

Jetzt müssen wir nur noch eine Funktion finden, die Speicher von einer Stelle an eine andere kopiert, damit wir beliebigen Speicher in einen Pool-Puffer kopieren können, den wir dann lesen können. memcpy ist eine Option, und srvnet.sys hat eine solche Funktion (genauer gesagt memmove), aber diese Funktion erwartet ein drittes Argument, das die Anzahl der zu kopierenden Bytes angibt, das wir nicht kontrollieren können. Allerdings sind wir nicht auf Funktionen beschränkt, die in srvnet.sys implementiert sind; wir können auch Funktionen aus der Importtabelle von srvnet aufrufen, und die Funktion RtlCopyUnicodeString ist eine perfekte Wahl, um das zu erreichen, was wir wollen.

Die Funktion RtlCopyUnicodeString nimmt zwei UNICODE_STRING-Zeiger als Argumente und kopiert den Inhalt des Quellstrings in den Zielstring. Im Gegensatz zu nullterminierten C-Strings werden Strings im Kernel durch die UNICODE_STRING-Struktur definiert, die einen Zeiger auf den String und die Länge des Strings in Bytes enthält. Der String-Puffer kann beliebige binäre Daten enthalten. Wenn Sie sich den Code der Funktion RtlCopyUnicodeString ansehen, sehen Sie, dass das Kopieren mit der Funktion memmove durchgeführt wird, also reines Kopieren binärer Daten. Alles, was wir tun müssen, ist, zwei UNICODE_STRING-Strukturen vorzubereiten und RtlCopyUnicodeString aufzurufen, um die kopierten Daten zu lesen:

![](https://assets.kitploit.com/production/public/readmes/24502/8a450b86ecf834ec4905a2bb72dab3781d1dd2ad56ee197a003e282180ce51bc.png)

## Shellcode ausführen
Nachdem wir eine bequeme Arbitrary-Read-Primitive erreicht haben, wenden wir uns der nächsten Herausforderung zu, die auf das Ziel der Remote-Code-Ausführung durch Ausführen eines Shellcodes abzielt. Wir verwenden die Technik, die Morten Schenk in seinem Vortrag auf der [Black Hat USA 2017](https://www.blackhat.com/docs/us-17/wednesday/us-17-Schenk-Taking-Windows-10-Kernel-Exploitation-To-The-Next-Level%E2%80%93Leveraging-Write-What-Where-Vulnerabilities-In-Creators-Update.pdf) (Seiten 47-51) vorgestellt hat.

Die Idee ist, einen Shellcode unterhalb der KUSER_SHARED_DATA-Struktur zu schreiben, deren Adresse konstant ist – die einzige Adresse, die im Kernel-Speicherlayout neuerer Windows-Versionen nicht randomisiert ist. Anschließend ändern wir den zugehörigen Page Table Entry (PTE), um die Seite ausführbar zu machen. Die Basisadresse der Page Table Entries im Kernel ist zwar randomisiert, kann aber über die Funktion MiGetPteAddress in ntoskrnl.exe abgerufen werden. Im Folgenden sind die Schritte aufgeführt, die wir zur Ausführung unseres Shellcodes verwenden:
1. Verwenden Sie die Arbitrary-Read-Primitive, um die Basisadresse von ntoskrnl.exe aus der Importtabelle von srvnet zu lesen.
2. Lesen Sie die Basisadresse des Page Table Entry aus der Funktion MiGetPteAddress, wie in Mortens Folien beschrieben.
3. Schreiben Sie den Shellcode an die Adresse KUSER_SHARED_DATA + 0x800 (0xFFFFF78000000800). Beachten Sie, dass wir auch einen der Pool-Puffer zum Speichern des Shellcodes verwenden könnten; die Verwendung von KUSER_SHARED_DATA dient der Vereinfachung.
4. Berechnen Sie die zugehörige Page-Table-Entry-Adresse und löschen Sie das NX-Bit, um die Ausführung zu ermöglichen, wie in Mortens Folien beschrieben.
5. Rufen Sie den Shellcode mithilfe der zuvor beschriebenen Technik zum Aufrufen einer beliebigen Funktion auf.

Der Shellcode, den Zecops für die Reverse Shell verwendet, ist [sleepya's Shellcode](https://github.com/worawit/MS17-010/tree/master/shellcode), der für den EternalBlue-Exploit geschrieben wurde. Sie haben den Shellcode modifiziert, um ihn auf neueren Windows-Versionen ausführbar zu machen.

# Debug

![](https://assets.kitploit.com/production/public/readmes/24502/916c7b643fdf7c8967b2b80189a686858d4cceaf35ddb1f38b83a7ae92f7a14f.png)

Die Informationen eines SMB-Pakets haben eine ähnliche Struktur wie oben. Angenommen, wir müssen die Adresse des User Buffers leaken, müssen wir den Zeiger UserBufferPtr lesen. Um diesen Zeiger zu lesen, nutzen wir die [Technik](https://github.com/datntsec/CVE-2020-1206#srvnetallocatebuffer-and-the-allocated-buffer-layout), bei der das Offset-Feld so positioniert wird, dass es über den Zeiger hinausragt und in den User Buffer eines anderen Buffers kopiert wird.

Beispiel für ein vom Client gesendetes SMB-Paket mit folgendem Inhalt:```c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x0
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x2116
Data = ‘A’ * 0x1101.

Dieses Paket wird bei Ankunft auf dem Server in einem Buffer gespeichert, der von der Funktion SrvNetAllocateBuffer erstellt wird. Da die Gesamtgröße des Pakets zwischen 0x1100 und 0x2100 liegt, gibt diese Funktion einen Alloc mit einem User-Buffer der Größe 0x2100 zurück (nennen wir ihn Alloc A), und speichert dann die vom Client gesendeten Informationen wie unten dargestellt:

Man kann sehen, dass der Bereich von Adresse 0xffffd38439044050 bis 0xffffd38439045160 die vom Client gesendeten Daten enthält, der Bereich von 0xffffd38439045160 bis 0xffffd38439046150 sind nicht initialisierte Daten auf der Serverseite, und der Bereich von 0xffffd38439046150 bis 0xffffd38439046240 sind die Daten des SRVNET_BUFFER_HDR von Alloc A. Somit liegt der Zeiger, den wir lesen möchten, bei 0xffffd38439046150 + 0x18 = 0xffffd38439046168.

Um diesen Zeiger zu lesen, habe ich die Technik verwendet, die ich oben erwähnt habe, und das Offset-Feld so gesetzt, dass es über den zu lesenden Zeiger hinausgeht. Deshalb wurde das Offset des obigen Pakets auf 0x2116 gesetzt, obwohl seine Größe kleiner als 0x2100 ist.

Als nächstes ruft der SMB-Server die Funktion SrvNetAllocateBuffer auf, um einen Speicherbereich basierend auf der Summe von OriginalCompressedSegmentSize und Offset (0x2116) zuzuweisen. Daraufhin wird ein Alloc mit einem User-Buffer der Größe 0x4100 zugewiesen (nennen wir ihn Alloc B). Die zugewiesenen Daten haben folgende Form:

Um unerwartete Fehler zu vermeiden, habe ich zuvor mehrfach Buffer in derselben Lookaside-Liste wie Alloc B erstellt und mit 0x0-Bytes gefüllt.

Als nächstes wird der SMB-Server die komprimierten Daten dekomprimieren und die unkomprimierten Daten, die vom Client gesendet wurden, in den User-Buffer von Alloc B kopieren:

Wie man sehen kann, gibt es keine komprimierten Daten, da OriginalCompressedSegmentSize = 0 ist. Das Programm kopiert Daten von 0xffffd38439044060 bis 0xffffd38439044060 + 0x2116 = 0xffffd38439046176 von Alloc A in den User-Buffer von Alloc B. Somit wurde ein Teil der SRVNET_BUFFER_HDR-Informationen von Alloc A in den User-Buffer von Alloc B kopiert.

Jetzt verwenden wir die Technik, die wir zuvor besprochen haben, um die Adresse des Allocation-Pools (die Adresse des User-Buffers) zu leaken.

Angenommen, wir möchten wissen, ob ein Byte an der Adresse 0xffffd3843636f15e größer als 0x7f ist. Wir werden ein SMB-Paket mit den folgenden Informationen erstellen:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0x7f+3) + ‘\xff’(0xff - 0x7f)) + ‘\xff’*0x1fe9
root@kitploit:~
Warum es notwendig ist, einen solchen SMB zu erstellen, werden wir Schritt für Schritt analysieren. Zunächst beträgt die Summe von OriginalCompressedSegmentSize und Offset 0x4100, sodass ein Alloc mit einem ähnlichen User-Buffer wiederverwendet wird, nämlich der zuvor verwendete Alloc B. Da wir erraten wollen, ob ein Byte an der Adresse 0xffffd3843636f15e größer als 0x7f ist, und diese Adresse 0x210e von der User-Buffer-Adresse entfernt liegt, werden die unkomprimierten Daten 0x210e Bytes (‘B’ * 0x210e) haben. Als nächstes folgt ein Bereich mit gültigen komprimierten Daten (komprimiert durch die Funktion compress()), gefolgt von ungültigen komprimierten Daten (‘\xff’* 0x1fe9). Damit beim Dekomprimieren nur der gültige komprimierte Datenbereich in einen anderen Alloc dekomprimiert wird, dann die Verbindung aufgrund der nachfolgenden ungültigen komprimierten Daten unterbrochen wird und die unkomprimierten Daten nicht in diesen Alloc kopiert werden, sodass die zuvor kopierten Daten erhalten bleiben.

![](https://assets.kitploit.com/production/public/readmes/24502/3e1ffd768b120645917e7f1757987f80c79514b52f3bc18e46fdfb0a2d2ca05f.png)

Oben sehen Sie einen Alloc, der die oben genannten Informationen enthält und vom SMB-Server erstellt wurde. Als nächstes ruft der SMB-Server die Funktion SrvNetAllocateBuffer auf, um einen entsprechenden Alloc zu erstellen. Da die Summe von OriginalCompressedSegmentSize und Offset 0x4100 beträgt, wird Alloc B wiederverwendet:

![](https://assets.kitploit.com/production/public/readmes/24502/5b218cbd142d58da175ee6a58bfc505646a560467db6db28ab70e0db34ee9ce3.png)

Danach dekomprimiert der SMB-Server die vom Client gesendeten Informationen in den User-Buffer von Alloc B am entsprechenden Offset.

![](https://assets.kitploit.com/production/public/readmes/24502/f3b545366e8f3bcff6a69d184cc383bfcf70643c23ff0ac01ece1a95cc0fd88d.png)

Die roten Daten sind die dekomprimierten Daten, der Rest bleibt unverändert. Wie im obigen Bild zu sehen ist, bleibt das Byte, das wir wissen müssen, erhalten, direkt danach folgen die gerade dekomprimierten Daten.

Um herauszufinden, ob dieses Byte größer als 0x7f ist, gehen wir wie folgt vor:

Wir erstellen weiterhin ein SMB-Paket mit folgendem Inhalt:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0x2004
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x20fd
Data = ‘B’ * 0x20f1

Obwohl die Summe von OriginalCompressedSegmentSize und Offset größer als 0x4100 ist, reserviert der SMB-Server beim Allozieren eines Bereichs zum Speichern des vom Client gesendeten Pakets (mit einer Gesamtgröße unter 0x4100) trotzdem nur einen Alloc mit einem User-Buffer von 0x4100 Byte, wie unten dargestellt:

Die grün markierten Daten oben sind die Daten des zuvor zugewiesenen Alloc B, die aufgrund derselben Lookaside-Liste wiederverwendet werden.

Als Nächstes ruft der SMB-Server die Funktion SrvNetAllocateBuffer auf, um einen Alloc für die dekomprimierten Daten zuzuweisen:

Durch die Dekomprimierung entnimmt der SMB-Server die Daten von der User-Buffer-Adresse + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. Die entnommenen Daten haben folgende Form:

Mit dem oben beschriebenen Dekomprimierungsalgorithmus werden die ersten 2 Bytes gelesen und als Länge des Blocks verwendet. Basierend auf dieser Länge wird der folgende Teil nach der Länge extrahiert und dekomprimiert.

Oben ist die Länge 0xB0D3, aber gemäß dem Algorithmus ergibt sich die tatsächliche Länge nach der Formel: length = length & 0xFFF + 1 → length = 0xD4. Es wird die nächsten D4 Bytes extrahiert und normal dekomprimiert, bis das Byte FF erreicht wird (da die D4 Bytes sowohl alle 00-Bytes als auch einen Teil des FF-Bytes enthalten). Dann werden die komprimierten Daten als ungültig betrachtet, die Dekomprimierung wird beendet und die Verbindung getrennt.

Aufgrund der Trennung durch den Server können wir schließen, dass das gesuchte Byte größer als 0x7f ist.

Was passiert, wenn das zu erratende Byte kleiner ist? Wir setzen die obige Analyse fort, aber diesmal verwenden wir das Vergleichsbyte D7, sodass D3 kleiner als D7 ist. Sehen wir uns an, was passiert:

Zuerst senden wir folgendes Paket an den SMB-Server:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0x1ff2
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x210e Data = ‘B’ * 0x210e + compress(‘\xb0’ + ‘\x00’(0xd7+3) + ‘\xff’(0xff - 0xd7)) + ‘\xff’*0x1fe9
root@kitploit:~
Der SMB-Server erstellt einen Alloc-Speicher wie folgt:

![](https://assets.kitploit.com/production/public/readmes/24502/3df2c6c8e5de90714455663bbb922efec7ae6888a970a3d65e389a28aea16837.png)

Als Nächstes ruft er die Funktion SrvNetAllocateBuffer auf, um einen Alloc wie folgt zuzuweisen:

![](https://assets.kitploit.com/production/public/readmes/24502/51affc73361727907ada6fb3f439831d0e95cee531a65106fa79ce4519ceaab8.png)

Natürlich wird dieser Alloc aus einem Alloc mit derselben Lookaside-Liste (Alloc B) wiederverwendet. Anschließend führt das Programm die normale Dekomprimierung mit gültigen komprimierten Daten durch und trennt die Verbindung bei ungültigen komprimierten Daten:

![](https://assets.kitploit.com/production/public/readmes/24502/01a9cc43acbc6b7cc283f40a0dbe348608a3566bfc34ce24aa2cc694315e67a3.png)

Im nächsten Schritt senden wir erneut ähnliche Daten wie zuvor, und der SMB-Server weist einen entsprechenden Alloc zu:

![](https://assets.kitploit.com/production/public/readmes/24502/ed438d4d0049b2188573e0fb3171cadf23701c7b0cef2de43a52434c07381341.png)

Die oben grün markierten Daten stammen vom zuvor zugewiesenen Alloc B, der aufgrund derselben Lookaside-Liste wiederverwendet wird.

Als Nächstes ruft der SMB-Server die Funktion SrvNetAllocateBuffer auf, um einen Alloc zuzuweisen, der die dekomprimierten Daten enthält:

![](https://assets.kitploit.com/production/public/readmes/24502/7ed3dca4594bd1772c44689190d4df3b597d43daf7c87edb281b3e82a834884e.png)

Durch die Dekomprimierung holt der SMB-Server die Daten von der Adresse User buffer + Offset = 0xffffd3843636d060 + 20fd = 0xffffd3843636f15d. Die entnommenen Daten haben folgende Form:

![](https://assets.kitploit.com/production/public/readmes/24502/68f677093ef93e02c5df9d70dce560e4d06e74446970f1f14f1ad37f08ce898e.png)

Analog zum vorherigen Mal beträgt die anfängliche Länge 0xB0D3, die nach der Berechnung zu 0xD4 wird. Es werden die nächsten D4 Bytes entnommen und normal dekomprimiert. Da jedoch D4 < D7 ist, enthalten die nun zur Dekomprimierung entnommenen komprimierten Daten nur Nullen. Es dekomprimiert normal bis zum Ende dieses Blocks. Als Nächstes wird die Länge des nächsten Blocks anhand der Länge des vorherigen Blocks ermittelt. Die nächsten beiden Bytes (D5 und D6) sind 0x0, daher ist die Länge jetzt 0x0, also beträgt die entnommene Länge 0x0000 → Ende des Dekomprimierungsvorgangs → Dekomprimierung erfolgreich → der SMB-Server gibt eine Antwort zurück → wir wissen, dass das gesuchte Byte kleiner oder gleich 0xD7 ist.

Auf diese Weise verfahren wir so lange, bis wir alle 6 Bytes einer Adresse offengelegt haben. Wir erhalten dann die Allocation-Pool-Adresse.

Sobald die Allocation-Pool-Adresse bekannt ist, ermitteln wir die Basisadresse von srvnet, indem wir den Zeiger auf die SRVNET_RECV-Struktur auf ähnliche Weise wie beim Offenlegen der Allocation-Pool-Adresse auslesen.

Nachdem wir zwei Adressen erhalten haben – Allocation-Pool und SRVNET_RECV mit den Werten `0xffffd38439044000` bzw. `0xffffd3843654ddd8` – legen wir die srvnet-Basisadresse offen:

![](https://assets.kitploit.com/production/public/readmes/24502/403108b6e79960d84369827376e208b606ba05304ae052ec4ae251cd289399a7.png)

![](https://assets.kitploit.com/production/public/readmes/24502/239c0c59c0e492097bdaf2eac907812124a89ef8bb31841287202ae80f4d13d0.png)

Um den AcceptSocket-Zeiger zu lesen, gehen wir wie folgt vor:
1. Bereite Alloc A aus einer Lookaside-Liste so vor, dass der Bereich „User buffer“ mit Nullen gefüllt ist. Dieser Puffer wird später den zu lesenden Zeiger enthalten. Alloc A wird dabei aus dem Alloc verwendet, der der von uns offengelegten Allocation-Pool-Adresse entspricht. Daher beginnt der User-Buffer-Bereich von Alloc A bei der Adresse 0xffffd38439044050, da dieselbe Lookaside-Liste verwendet wird.
2. Bereite Alloc B aus einer anderen Lookaside-Liste vor, damit:
- Der Zeiger pMdl1 auf die Adresse des AcceptSocket-Zeigers minus 0x18 zeigt (da der Offset von MappedSystemVa in der MDL-Struktur 0x18 beträgt).
- Der Zeiger pMdl2 auf den Bereich „User buffer“ von Puffer A zeigt.
- Das Flags-Feld auf 0x03 gesetzt wird.

Somit lauten die Adressen der beiden Mdl-Zeiger: mdl1_ptr: `0xffffd3843654de68`, mdl2_ptr: `0xffffd38439045250`.

Wir können die Felder der SRVNET_BUFFER_HDR-Struktur überschreiben, indem wir sie aus einem größeren Puffer mithilfe der in [Observation #2](https://github.com/datntsec/CVE-2020-1206#observation-2-failing-the-decompression) beschriebenen Technik dekomprimieren.

Ich werde diesen Schritt direkt nach Schritt 4 näher erläutern.

3. Wenn Puffer B freigegeben wird, laufen folgende Vorgänge ab:
- Die MDL-Flags werden aus dem zweiten MDL in Puffer A gelesen. Wenn das MDL_PARTIAL_HAS_BEEN_MAPPED-Flag gesetzt ist, wird MmUnmapLockedPages aufgerufen, und das System kann abstürzen. Deshalb müssen wir den Puffer in Schritt 1 mit Nullen füllen.
- Der Bereich „User buffer“ von Alloc A wird modifiziert und enthält die Informationen, die wir lesen müssen.
4. Lese den AcceptSocket-Zeiger aus dem Bereich „User buffer“ von Puffer A.
- Verwende die bereits oben verwendete Technik zum Offenlegen von Adressen, um den AcceptSocket-Zeiger zu lesen.

Im Folgenden beschreibe ich die obigen Schritte genauer:

Schritt 1 ist recht einfach und analog zu oben, daher werde ich nicht weiter darauf eingehen.

In Schritt 2 erstellen wir zunächst ein Paket, das wir an den SMB-Server senden, mit folgendem Inhalt:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = -0x38
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x10138
Data = ‘A’ * 0x10138 + compress(mdl1_ptr  + ‘\x00’*0x10 + mdl2_ptr) + ‘\xff’*0x10

Wie wir sehen können, enthält OriginalCompressedSegmentSize einen negativen Wert und die Summe OriginalCompressedSegmentSize + Offset = 0x10100. Die Größe des Pakets, das der Client an den Server sendet, ist jedoch größer als 0x10100. Somit wird der anfängliche Alloc, den der Server vor der Dekomprimierung erstellt, größer sein als der Alloc, der die Daten nach der Dekomprimierung enthält. Der Wert von OriginalCompressedSegmentSize wird hier negativ gesetzt, um die Summe von OriginalCompressedSegmentSize und Offset exakt auf 0x10100 zu bringen, ohne die Position der komprimierten Daten zu beeinflussen, da diese vom Offset abhängt. 0x38 ist der Offset des Zeigers Mdl1 in der Struktur SRVNET_BUFFER_HDR.

Der Server wird also einen Alloc erstellen, der die Daten des Clients wie folgt enthält:

Als nächstes ruft es die Funktion SrvNetAllocateBuffer auf, um einen Alloc mit einer User-Buffer-Größe von 0x10100 zuzuweisen, also Alloc B gemäß den obigen Schritten:

Die Dekomprimierung wird durchgeführt, natürlich wird nur ein Teil der gültigen Daten dekomprimiert:

Basierend auf den obigen Abbildungen ist zu erkennen, dass die beiden Mdl-Zeiger im SRVNET_BUFFER_HDR von Alloc B auf die gewünschten Werte geändert wurden.

Ähnlich wie zuvor werden wir diesmal das Flag auf 3 setzen, indem wir den Offset des gesendeten Pakets wie folgt anpassen:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = -0x10
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x10110 Data = ‘A’ * 0x10110 + compress(‘\x00\x03’) + ‘\xff’*0x10
root@kitploit:~
Cuối cùng nó sẽ có dạng:

![](https://assets.kitploit.com/production/public/readmes/24502/467ea07dff7cfaca7062ed50d1605e40586c412f40bcadaf01aa37d862efd3bd.png)

Khi Alloc B được giải phóng thì các đoạn code sau sẽ được chạy:``` c
pMdl1->MappedSystemVa = (BYTE*)pMdl1->MappedSystemVa + 0x50;
pMdl1->ByteCount -= 0x50;
pMdl1->ByteOffset += 0x50;
pMdl1->MdlFlags |= 0x1000; // MDL_NETWORK_HEADER

pMdl2->StartVa = (PVOID)((ULONG_PTR)pMdl1->MappedSystemVa & ~0xFFF);
pMdl2->ByteCount = pMdl1->ByteCount;
pMdl2->ByteOffset = pMdl1->MappedSystemVa & 0xFFF;
pMdl2->Size = /* some calculation */;
pMdl2->MdlFlags = 0x0004; // MDL_SOURCE_IS_NONPAGED_POOL

Wie oben erwähnt, enthält pMdl1->MappedSystemVa (Offset 0x18) den Wert von pMdl1->MappedSystemVa + 0x50 = 0xffffd3843654de68 + 0x18 + 0x50 = 0xffffd3843654ded0.

Bevor Alloc B freigegeben wird, ist SRVNET_RECV:

Nachdem die ersten 4 Zeilen des obigen Codes ausgeführt wurden:

Bevor Alloc A freigegeben wird:

Nachdem der gesamte obige Code ausgeführt wurde:

Und die Bytes, die wir in Alloc A auslesen müssen, sind die blauen Bytes unten:

Somit müssen wir nur die oben beschriebene Technik des byteweisen Leakens anwenden, um die Adresse von AcceptSocket + 0x50 zu erhalten. In diesem Teil wäre das 0xffffd3843ea02418 → AcceptSocket: 0xffffd3843ea023c8

Analog werden wir vorgehen, um die Adresse AcceptSocket→srvnet!SrvNetWskConnDispatch zu leaken.

Wir müssen alles wie folgt vorbereiten:

Nachdem Alloc B freigegeben wurde, ändert sich alles wie folgt:

Die Bytes, die wir benötigen, um die Adresse von AcceptSocket->srvnet!SrvNetWskConnDispatch + 50 zu erhalten, befinden sich in Alloc A. Diese Bytes sind die im folgenden Bild blau markierten Bytes:

Somit ist AcceptSocket->srvnet!SrvNetWskConnDispatch 0xfffff80060e9d170. Nehmen wir an, wir kennen den Offset innerhalb des Moduls srvnet.sys, dann können wir die Basisadresse von srvnet berechnen.

In diesem Teil ist die Basisadresse von srvnet: 0xFFFFF80060E70000 mit einem Offset von srvnet!SrvNetWskConnDispatch von 0x2d170.

Als nächstes verwenden wir die Write-what-where-Primitive aus CVE-2020-0796, um beliebig in einen Speicherbereich zu schreiben.

Zuerst versuchen wir, die Basisadresse von ntoskrnl zu leaken, indem wir die Adresse der Funktion IoSizeofWorkItem leaken, die von srvnet importiert wird. Dazu erstellen wir zunächst zwei UNICODESTRING-Strukturen wie folgt:``` c // Destination unicode string desLength = 6; desMaximumLength = 6; desBuffer = allocation_pool_object_ptr + 0x1650 + 0x20 + 2;

// Source unicode string srcLength = 6; srcMaximumLength = 6; srcBuffer = srvnet_base_ptr + OFFSETS['srvnet!imp_IoSizeofWorkItem'];

root@kitploit:~
Mit `allocation_pool_object_ptr` als die geleakte Adresse des Allocation-Pools und `OFFSETS['srvnet!imp_IoSizeofWorkItem']` als Offset der von srvnet importierten Funktion IoSizeofWorkItem.

Diese beiden UNICODE_STRING-Strukturen werden an der Stelle `allocation_pool_object_ptr + 0x1650` durch die bei CVE-2020-0796 gefundene Write-what-where-Technik gespeichert.
Zuerst speichern wir den Ziel-Unicode-String in `allocation_pool_object_ptr + 0x1650`, indem wir das folgende SMB-Paket erstellen:``` c
Header:
-   Id = 0x424d53fc
-   OriginalCompressedSegmentSize = 0xffffffff
-   CompressionAlgorithm = 1
-   Flag = 0
-   Offset = 0x22
Data:
sentinel = os.urandom(2)  // 16 bits for verification
data = struct.pack('<HHIQ', desLength, desMaximumLength, 0, desBuffer)  // dest unicode string
data += struct.pack('<HHIQ', srcLength, srcMaximumLength, 0, srcBuffer) // src unicode string
data += sentinel
data_to_compress = os.urandom(0x1100 - len(data))
// 0x18 null bytes that override the struct.
data_to_compress += b'\x00'*0x18
// Target address.
data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650)
data = data + compress(data_to_compress)

Oben enthält die Daten einen Sentinel, der mit der Funktion os.urandom(2) erzeugt wurde. Dieser ist 2 Byte lang und diese 2 Byte helfen uns zu erkennen, ob die von uns geleakte Adresse tatsächlich die Adresse ist, die wir leaken müssen, indem wir sie nach erfolgreichem Leak vergleichen.

Wenn die Gesamtgröße des vom Client gesendeten Pakets größer als 0x1100 ist (dies hängt von den vor der Komprimierung zufälligen Daten ab), dann wird definitiv allocation_pool_object_ptr verwendet, um es auf dem SMB-Server zu speichern:

Als nächstes ruft der SMB-Server die Funktion SrvNetAllocateBuffer auf, um einen Speicherbereich für die Dekomprimierung zuzuweisen. Da die Summe von OriginalCompressedSegmentSize und Offset jedoch 0x21 beträgt, wird nur ein Bereich mit einem User-Buffer der Größe 0x1100 zugewiesen:

Der Heap-Überlauffehler tritt auf (wie in CVE-2020-0796 beschrieben) und nach der Dekomprimierung durch den SMB-Server (bevor das Kopieren der unkomprimierten Daten stattfindet) wird:

Somit zeigt der Zeiger UserBufferPtr auf den Anfang von allocation_pool_object_ptr + 0x1650 und während des Kopiervorgangs wird allocation_pool_object_ptr:

Analog dazu fügen wir einen weiteren Sentinel unten ein, indem wir das folgende Paket senden:``` c Header:

  • Id = 0x424d53fc
  • OriginalCompressedSegmentSize = 0xffffffff
  • CompressionAlgorithm = 1
  • Flag = 0
  • Offset = 0x2 Data: data = sentinel data_to_compress = os.urandom(0x1100 - len(data)) // 0x18 null bytes that override the struct. data_to_compress += b'\x00'*0x18 // Target address. data_to_compress += struct.pack('<Q', allocation_pool_object_ptr + 0x1650 + 0x28) data = data + compress(data_to_compress)
root@kitploit:~
So wenn der SMB-Server das Paket empfängt, wird er den entsprechenden Speicherbereich zuweisen. In diesem Fall wird der zugewiesene Speicherbereich `allocation_pool_object_ptr` sein und die Daten werden wie folgt enthalten:

![](https://assets.kitploit.com/production/public/readmes/24502/24cc306a702cdb5f575bd1d612ea6b35db87aaf875fd45fa64977e50c61e85ed.png)

Nach dem Entpackvorgang sehen die Daten so aus:

![](https://assets.kitploit.com/production/public/readmes/24502/d6442e5a936e1c46ebb9e3a4651e8859dca56c1aa09e9e79ed9155ea105dede2.png)

Somit haben wir zwei Unicode-Strings und zwei Sentinels erstellt, um die geleakten Daten zu verifizieren.

Als nächstes rufen wir die Funktion `RtlCopyUnicodeString` auf und übergeben ihr die beiden oben erstellten Unicode-Strings.

Um die Funktion `RtlCopyUnicodeString` aufzurufen, überschreiben wir zuerst den Zeiger `HandlerFunctions` mit der Adresse der Funktion `RtlCopyUnicodeString`. Diese Funktion wird vom Modul srvnet importiert und hat (nach meinem Modul) den Offset 0x32288.

Mit der Write-What-Where-Technik schreiben wir also die Adresse 0xFFFFF80060E70000 + 0x32288 - 0x8 in `HandlerFunctions`.

Zuerst leaken wir einen SRVNET_RECV-Zeiger (0xffffe00f0b593dd8).

![](https://assets.kitploit.com/production/public/readmes/24502/f7c5bde869eb5ba21a0b9dd4e2034417487150592178aa1eb1aef523e246a5c2.png)

Wir behalten die Verbindung bei, um weiterhin Pakete unten zu senden.

Als nächstes verwenden wir die Write-What-Where-Technik, um in den Zeiger `RtlCopyUnicodeString - 0x8` zu schreiben (der Grund für -0x8 ist, dass die Funktion `RtlCopyUnicodeString` die Funktion `Srv2ReceiveHandler` in `HandlerFunctions` ersetzt).

![](https://assets.kitploit.com/production/public/readmes/24502/a88b44518f26ed6f0958772bdd4bf124c7c2403a525944b060a37cf17309a092.png)

Anschließend schreiben wir nacheinander die beiden Zeiger der zuvor erstellten Unicode-Strings in die beiden Argumente von `HandlerFunction`.

![](https://assets.kitploit.com/production/public/readmes/24502/691bdb0ce599face8b1f35393db9cc37e1f85a075c4d259a7fb96adee9fb66d2.png)

Zu diesem Zeitpunkt wurde in derselben Verbindung die Funktion `Srv2ReceiveHandler` durch `RtlCopyUnicodeString` ersetzt. Wenn wir also ein Paket senden, wird die Funktion `RtlCopyUnicodeString` aufgerufen und kopiert den Unicode-String.

![](https://assets.kitploit.com/production/public/readmes/24502/2410149b163fd4ebbfb13fa5d2a127cafcfdc82146c33544b0a6cc6905c13d2b.png)

Als nächstes müssen wir 10 Byte Adressdaten von 0xffffd38439045670 bis 0xffffd3843904567a (einschließlich der beiden Sentinels an beiden Enden der zu leakenden Adresse) leaken. Dann prüfen wir, ob die beiden Bytes am Anfang und Ende der geleakten Adresse den Sentinels entsprechen. Wenn ja, haben wir korrekt geleakt (0xfffff8068152c380).

Nachdem wir die Adresse von `nt!IoSizeofWorkItem` (0xfffff8068152c380) geleakt haben, subtrahieren wir deren Offset (0x12C380), um die Basisadresse von ntoskrnl (0xfffff80681400000) zu erhalten.

Beachten Sie, dass die Offsets der einzelnen Moduldateien in verschiedenen Windows-Versionen unterschiedlich sind. Stellen Sie daher sicher, dass Sie die richtige Moduldatei auf dem Zielrechner haben.

Auf ähnliche Weise erhalten wir nach der ntoskrnl-Basisadresse die `MiGetPteAddress` (0xBA968) und die PTE-Basisadresse (MiGetPteAddress + 0x13):

![](https://assets.kitploit.com/production/public/readmes/24502/5c92df61b331c038bd90b2ccb878b7027bdbf6338602aecdda1b49a867eb42b1.png)

Im nächsten Schritt schreiben wir den Shellcode an die Adresse 0xFFFFF78000000800 mit der Write-What-Where-Technik. Anschließend berechnen wir die Shellcode-Adresse im PTE mit der untenstehenden Formel neu und löschen das NX-Bit, damit der Shellcode ausgeführt werden kann:``` c
shellcode_addr >>= 9
shellcode_addr &= 0x7FFFFFFFF8
shellcode_addr += pte_base

Schließlich schreiben wir die Shellcode-Adresse in allocation_pool_object_ptr + 0x50 + 0x1600 und rufen den Shellcode auf, indem wir diese Adresse mit HandlerFunctions ersetzen und die nt_base_ptr-Adresse an den Shellcode übergeben.

Viel Spaß mit RCE :))

Referenzen

  • SMBleedingGhost Writeup: Chaining SMBleed (CVE-2020-1206) with SMBGhost
  • SMBleedingGhost Writeup Part II: Unauthenticated Memory Read – Preparing the Ground for an RCE
  • SMBleedingGhost Writeup Part III: From Remote Read (SMBleed) to RCE
  • Exploiting SMBGhost (CVE-2020-0796) for a Local Privilege Escalation: Writeup + POC
  • lznt1.py
  • TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAING WRITEWHAT-WHERE VULNERABILITIES IN CREATORS UPDATE
  • VERGILIUS_MDL
  • Exploit Development: Leveraging Page Table Entries for Windows Kernel Exploitation
  • RtlUnicodeStringCopy function
  • UNICODE_STRING structure

DatntSec. Viettel Cyber Security.

Tool herunterladen
→ Allocation size ↓Logical Processor0x11000x21000x41000x81000x101000x201000x401000x801000x100100
Processor 1📝📝📝📝📝📝📝📝📝
Processor 2📝📝📝📝📝📝📝📝📝
...
Processor n📝📝📝📝📝📝📝📝📝