Introducción al Reversing – 0x07 Use After Free

Buenos días continuamos con otro reto este se llama uaf y lo encontraréis en la página pwnable.kr Tenemos una aplicación programada en C++ donde nos muestra un menu con 3 opciones. Antes de centrarnos en que es lo que hace cada uno de las opciones del menu, vamos a reversear la función main().

1. Use

Antes de empezar desde el principio de la función me quiero centrar en el bucle switch donde esta el menu donde nosotros como usuarios de la aplicación podemos interactuar. Por tanto manos a la obra!.
Sabemos que Use After Free es un bug que permite controlar un puntero que previamente se ha liberado (free) y poder ejecutar la llamada a una función que nos interese en nuestro proceso de explotación. Pero como ya sabéis esa parte os la dejo a vosotros, nos centraremos en aplicar ingeniería inversa sin disposición del código fuente y saber o entender la lógica de la aplicación.
Sigamos con nuestra primera opción use. En esta opción debemos introducir un breakpoint justo en la dirección de memoria donde realiza el salto condicional una vez hayamos elegido la opción número 1.

[0x7f842cb75210]> db 0x00400fcd
[0x7f842cb75210]> dc
1. use
2. after
3. free
1
hit breakpoint at: 400fcd

Hacemos un pequeño desensamblado y vemos nuestro rip apuntando a la siguiente instrucción a ejecutar.

;-- rip:
0x00400fcd b 488b45c8 mov rax, qword [local_38h]
0x00400fd1 488b00     mov rax, qword [rax]
0x00400fd4 4883c008   add rax, 8
0x00400fd8 488b10     mov rdx, qword [rax]
0x00400fdb 488b45c8   mov rax, qword [local_38h]
0x00400fdf 4889c7     mov rdi, rax
0x00400fe2 ffd2       call rdx

Según vemos mueve el contenido de la variable local_38h al registro rax, como estamos haciendo ingenieria inversa deberemos ver en memoria que esta ocurriendo y anotar en un documento o en un papel dicha información que será de carácter vital a la hora de poder entender la lógica. También si habéis visto un poco el código desensamblado esta orientado a objetos y programado en C++ lo que hará que nuestro reversing sea algo distinto al habitual jeje y bajo mi punto de vista más tedioso.
Observamos en memoria que sucede.

[0x00400fcd]> px@rbp-0x38
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffdbdff1258 a01e 6800 0000 0000 f01e 6800 0000 0000 ..h.......h.....

La variable local_38h situada rpb-0x38 tiene de contenido 0x00681ea0. ¿Qué es este valor hexadecimal? Bueno seguimos indagando.

[0x00400fcd]> px@0x681ea0
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00681ea0 7015 4000 0000 0000 1900 0000 0000 0000 p.@.............
0x00681eb0 881e 6800 0000 0000 3100 0000 0000 0000 ..h.....1.......
0x00681ec0 0400 0000 0000 0000 0400 0000 0000 0000 ................
0x00681ed0 0000 0000 0000 0000 4a69 6c6c 0000 0000 ........Jill....
0x00681ee0 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x00681ef0 5015 4000 0000 0000 1500 0000 0000 0000 P.@.............
0x00681f00 d81e 6800 0000 0000 1104 0000 0000 0000 ..h.............
0x00681f10 332e 2066 7265 650a 0a00 0000 0000 0000 3. free.........

Parece que almacena una dirección de memoria 0x00401470 y vemos también el nombre de Jill. Hemos obtenido dos valores de suma importancia que anotaremos en nuestro cuaderno o documento y continuamos!. Haciendo un poco de trampa y usando el debugger GDB jeje vemos con info proc mappings que esa dirección corresponde al segmento heap. Con radare2 también podemos saberlo gracias a un plugin que desarrollaron @javierptrd y @n4x0r_ del equipo CTF Amnesia. El comando es dmh.

Malloc chunk @ 0x670250 [size: 0x11c11][allocated]
Malloc chunk @ 0x681e60 [size: 0x31][allocated]
Malloc chunk @ 0x681e90 [size: 0x21][allocated]
Malloc chunk @ 0x681eb0 [size: 0x31][allocated]
Malloc chunk @ 0x681ee0 [size: 0x21][allocated]
Malloc chunk @ 0x681f00 [size: 0x411][allocated]
Malloc chunk @ 0x682310 [size: 0x411][allocated]
Malloc chunk @ 0x682720 [size: 0xe8e1][allocated]
Malloc chunk @ 0x691000 [corrupted]
size: 0xffffffffffffffff
fd: 0xffffffffffffffff, bk: 0xffffffffffffffff

Top chunk @ 0x1dc850fc085 - [brk_start: 0x670000, brk_end: 0x691000]

Si hacemos un desensamblado de la memoria la vemos ahora la parte que nos interesa completa.

[0x00400fcd]> px@0x681e60
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00681e60 0000 0000 0000 0000 3100 0000 0000 0000 ........1.......
0x00681e70 0400 0000 0000 0000 0400 0000 0000 0000 ................
0x00681e80 0000 0000 0000 0000 4a61 636b 0000 0000 ........Jack....
0x00681e90 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x00681ea0 7015 4000 0000 0000 1900 0000 0000 0000 p.@.............
0x00681eb0 881e 6800 0000 0000 3100 0000 0000 0000 ..h.....1.......
0x00681ec0 0400 0000 0000 0000 0400 0000 0000 0000 ................
0x00681ed0 0000 0000 0000 0000 4a69 6c6c 0000 0000 ........Jill....
0x00681ee0 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x00681ef0 5015 4000 0000 0000 1500 0000 0000 0000 P.@.............
0x00681f00 d81e 6800 0000 0000 1104 0000 0000 0000 ..h.............
0x00681f10 332e 2066 7265 650a 0a00 0000 0000 0000 3. free.........

Bueno tenemos ahora como reversers más información.

Nombre: Jack
Edad: 0x19 = 25 años (La edad la obtenemos al ejecutar el binario)
Dirección de memoria: 0x00401570 ?¿
Nombre: Jill
Edad: 0x15 = 21 años
Dirección de memoria: 0x00401550 ?¿

Parece ser que tenemos dos objetos cargados en memoria donde tiene un nombre y una edad, también vemos una dirección de memoria vamos a ver lo que contiene.

[0x00400fcd]> px@0x401570
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00401570 7a11 4000 0000 0000 d212 4000 0000 0000 z.@.......@.....

Seguimos tirando de la cuerda y ahora nos encontramos con dos direcciones de memoria más 0x40117a y 0x4012d2. Suponiendo que es un objeto vamos a elegir el primero que es Jack cuya dirección de memoria es la vista anteriormente. Al desensamblar al objeto tenemos dos nuevas direcciones de memoria vamos a ver que son.

[0x00400fcd]> px@0x40117a
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x0040117a 5548 89e5 4883 ec10 4889 7df8 bfa8 1440 UH..H...H.}....@

Y bien, tenemos un volcado hexadecimal de instrucciones rápidamente lo identificamos con el 0x55 que significa push rbp al comenzar una función. Por lo tanto hemos deducido que esa dirección de memoria es una función. Si introducimos afl descubrimos que estabamos en lo cierto 0x0040117a 1 24 sym.Human::give_shell.

Give_shell es un método de la clase Human, información bastante importante jeje.
Hacemos lo mismo con la otra dirección de memoria y obtenemos que es un método de igual modo 0x004012d2 1 54 sym.Man::introduce.

Vemos que la clase Human usa dos métodos. Como podéis observar hemos podido obtener bastante información solo al entrar en la primera opción del switch y me comentareis, ¿por qué tenemos en el segmento heap tanta información? Pues es debido a que si veis el código al iniciar la función main() se instancia el objeto pasándole dos parámetros (nombre, edad).
Seguidamente cuando nuestro rip apunta a la dirección de memoria 0x00400fd4, tenemos que el registro rax vale 0x00401570 correspondiendo a esa dirección de memoria que vimos en el heap. Si ejecutamos una instrucción mas sumara 8 bytes a rax valiendo ahora 0x00401578. Pero antes de continuar vamos a ver que significa ese desplazamiento de 8 bytes en memoria.

[0x00400fcd]> px@0x401578
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00401578 d212 4000 0000 0000 0000 0000 0000 0000 ..@.............

Ahhh vale, ahora justo vale el método introduce() y antes valia el método give_shell() os podéis hacer una idea entonces.
Este objeto va a usar un método de la clase Human denominado introduce() debido a esta suma de bytes y su correspondiente desplazamiento en memoria ya que ahora apunta a esa dirección.
Ahora vemos como rdx vale la dirección de memoria al método introduce y rdi la dirección del heap donde esta nuestro objeto Jack 0x401570

[0x00400fcd]> px@rax
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00681ea0 7015 4000 0000 0000 1900 0000 0000 0000 p.@.............

Finalmente llama a rdx que apunta al método para redireccionar el flujo de ejecución a la clase Human y así poder usarlo.
Luego realiza el mismo procedimiento para el objeto de Jill así que obviamos este paso. Aquí el resultado del uso de la primera opción.

[0x00400fcd]> dc
My name is Jack
I am 25 years old
I am a nice guy!
My name is Jill
I am 21 years old
I am a cute girl!

3. Free

Ejecutamos de nuevo el debugger y seguidamente elegimos la opción número tres pero antes de nada colocamos un breakpoint en la dirección de memoria 0x00400f92 y ejecutamos. Como tenemos el ASLR activado las direcciones de memoria son aleatorias por lo tanto ahora son distintas a lo anteriormente visto pero bueno no cambiará realmente nada, continuamos Malloc chunk @ 0x1220e60 [size: 0x31][free].

Al liberar ya no vemos más el objeto correspondiente a Jack.

[0x00400f92]> px@0x1220e60
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x01220e60 0000 0000 0000 0000 3100 0000 0000 0000 ........1.......
0x01220e70 0000 0000 0000 0000 0400 0000 0000 0000 ................
0x01220e80 ffff ffff 0000 0000 4a61 636b 0000 0000 ........Jack....
0x01220e90 0000 0000 0000 0000 2100 0000 0000 0000 ........!.......
0x01220ea0 0000 0000 0000 0000 1900 0000 0000 0000 ................

2. After

Ahora vamos a ver la opción número dos. Con el mismo breakpoint al inicio del switch nos permite siempre valorar todo el contenido en memoria de la ejecución de una opción y volver a seleccionar otra, por lo tanto os aconsejo ponerlo ahí jeje.
Esta opción es realmente diferente porque no usa más ningún objeto para allocar o liberar así que manos a la obra! También evitaré spoilear en esta parte porque como ya dije el objetivo es hacer ingenieria inversa y no hacernos el exploit, eso os lo dejo a vosotros aunque como dije en la pasada entrada podéis contactarme por privado para la resolución del reto o directamente preguntarlo evitando la medida de lo posible el spoiler, en el grupo de telegram Hispapwners.
Ejecutamos la opción dos y ahora colocamos otro breakpoint en la dirección de memoria 0x00401000 pero justo dentro de la opción dos para ir debuggeando, por lo tanto ahora tener en cuenta que va a ejecutar por segunda vez la opción after.

0x00401000 b 488b45a0  mov rax, qword [local_60h]
;-- rip:
0x00401004 4883c008    add rax, 8
0x00401008 488b00      mov rax, qword [rax]
0x0040100b 4889c7      mov rdi, rax
0x0040100e e80dfdffff  call sym.imp.atoi ; int atoi(const char *str)

Vemos que llama a la función atoi() que lo que hará será convertir nuestro argumento de string a integer. Nosotros a la hora de ejecutar radare2, lo hicimos de este modo r2 -d uaf 4 flag. Le pasamos dos argumentos y ahora veréis porque.

[0x00400fdb]> px@rax
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffd372ea334 3400 666c 6167 004c 535f 434f 4c4f 5253 4.flag.LS_COLORS

Vemos que rax es la string 4 y los siguientes offset es la string flag. La string flag quiere decir el nombre del fichero cuyo contenido es «AAAA».

0x00401013 4898        cdqe
0x00401015 488945d8    mov qword [local_28h], rax
0x00401019 488b45d8    mov rax, qword [local_28h]
0x0040101d 4889c7      mov rdi, rax
0x00401020 e84bfcffff  call sym.operatornew___unsignedlong
;-- rip:
0x00401025 488945e0 mov qword [local_20h], rax

Una vez llama a la siguiente función nuestro registro rax contiene la dirección de memoria del heap donde esta nuestro objeto Jack, pero lo liberamos ¿recuerdan?, por lo tanto estará a cero.

[0x00400fe4]> px@rax
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x01220ea0 0000 0000 0000 0000 1900 0000 0000 0000 ................
0x01220eb0 880e 2201 0000 0000 3100 0000 0000 0000 ..".....1.......

Esto se pone más interesante porque seguidamente llama a la función open() que abrirá en modo lectura nuestro fichero flag leerá 4 bytes con la función read() que es lo que le indicamos en el primer argumento. Pero vamos a verlo mejor con el debugger.

0x00401029 488b45a0    mov rax, qword [local_60h]
0x0040102d 4883c010    add rax, 0x10
0x00401031 488b00      mov rax, qword [rax]
;-- rip:
0x00401034 be00000000  mov esi, 0
0x00401039 4889c7      mov rdi, rax
0x0040103c b800000000  mov eax, 0
0x00401041 e87afdffff  call sym.imp.open ; int open(const char *path, int oflag)

Ejecutamos unas instrucciones y justo cuando rip vale 0x00401034 nuestro valor del registro rax vale la string flag para indicarle por argumento a la función open() que fichero abrir.

[0x00401029]> px@rax
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x7ffd372ea336 666c 6167 004c 535f 434f 4c4f 5253 3d72 flag.LS_COLORS=r

Por último va a leer 4 bytes del fichero flag y lo va a escribir justo en la dirección de memoria del objeto en el heap. Y lo observamos una vez finaliza de llamar a la función read().

Boom! Ahí estan nuestras A’s.

[0x00401029]> px@0x01220ea0
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x01220ea0 4141 4141 0000 0000 1900 0000 0000 0000 AAAA............
0x01220eb0 880e 2201 0000 0000 3100 0000 0000 0000 ..".....1.......
0x01220ec0 700e 2201 0000 0000 0400 0000 0000 0000 p.".............
0x01220ed0 ffff ffff 0000 0000 4a69 6c6c 0000 0000 ........Jill....

Hasta aquí ya hemos visto en un vistazo rápido el funcionamiento de la aplicación ahora toca explotarlo ;))
Un saludo, naivenom.