64 bit ELF with Partial RELRO, stack canary & NX enabled, No PIE.
The program is a simple tweet-chat service:
As we can see the service allow us to sign up users, sign in and tweet message (send public message). We can also DM other users. In the above example, after userA send a message to userB, we can sign in as userB and check the DM:
It will print the sender's name and its message.
Users and messages are stored in the program with two different kinds of data structures:
Notice that there's some limitations while setting user->name: the maximum name length is 32, and the first character must be a printable character (check by the isprint function). This effects the functionality of the Change UserName : If you change a user's name, and the new name isn't a valid user name, it will remove the user.
So what if we remove a user after we send a DM to another user?
We can see that if we show userB's DM after we remove userA, the sender's name will become a strange value. This is because while removing userA, it will free the userA->name pointer, but the program is still able to access the pointer by showing userB's DM ( accessing userB->messsage->sender->name ). A typical Use-After-Free vulnerability.
So now there's a dangling pointer in the program. If we can arrange the heap memory chunk carefully, and make a user's name buffer overlapped with userB->messsage->sender:
We can then modify the value of pointer p_name by editing userC->name, and then leak some address by viewing userB's DM ( sender's name ). This can be done easily if you're familiar with the glibc malloc's fastbin allocation. By changing p_name into email@example.com ( 0x603040, which its first character is 0x40, a printable character ), we can then leak the libc's base address.
Now we still need to find a way to do the "write-anywhere" attack. It's kind of hard to find such vulnerability by just reversing the binary, so I decided to start fuzzing the binary, while examine the heap memory at the same time. Finally ( and luckily ! ), I notice that I've made the heap memory chunk arranged like this:
I found that I can corrupt the header of unsortbin chunk 0x1234060 by overflowing the userC->name buffer ! Later I realized that this is because program use strdup to allocate the buffer of userC->name. If we set the name length of userC less than 24, it will allocate a buffer with size 0x20 ( fastbin ) . But when we change a user's name, it allow us to input at most 32 characters, which will overflow the name buffer !
By corrupting the meta data and change the chunk size from 0x21 to 0xa1 ( the size of a message structure ), we can allocate a fake chunk (0x1234060, size = 0xa1), and forge the data structure at 0x1234090 ( a user structure ), change the userC->name pointer from 0x1234050 into another memory address, then we can do the "write-anywhere" attack ( ex. GOT hijacking ) by changing userC's name .
So to sum up:
Overflow userC->name, change the unsortbin chunk size into 0xa1.
Post a tweet, this will allocate the memory from unsortbin.
Craft the tweet message, forge a fake user structure (modify the userC->name pointer).
Change the name of userC to overwrite the memory.
Looks simple huh? Except it's not. To successfully change a user name, both the old user name and the new user name's first character has to be printable. For example, if we want to hijack free's GOT:
We can see that free's GOT ( 0x603018 ) stores the address 0x7eff8fd9bd00. Its first character is 0x00, which is not printable, making us unable to change the content of 0x603018. Even if its first character is printable, the system's offset in the libc is 0x46590 -- another non-printable first character, which will make the program remove (freeing) the user name and crash the program ( for trying to free a GOT entry ).
So how are we gonna bypass the check? Well it's a little bit tricky, but also very interesting. I notice that the GOT entry of stack_chk_fail stores the address 0x4007f6. Although 0xf6 is non-printable, the third character 0x40 is a printable character. Hmmmm, if only I can make 0x40 to our user name's first character...
That's right ! If we change the userC->name pointer into 0x60302a, we can start overwriting the content from 0x60302a. We first filled the GOT entry of stack_chk_fail with some printable characters ( now the first character of new user name is printable ! ), then we can start hijack some GOT !
Here I decided to hijack strchr's GOT so when the program call strchr(buf, 10) ( buf stores our input ) it will call system(buf) instead.
First time solving a 500 points pwn challenge ! WOOHOO !