InsoSystems was a pwn challenge from Insomni’hack teaser 2023.
TL;DR
Leak the base address of the binary with an md5 out of bounds read. Exploit a buffer overflow using ROP to leak the address of the libc then ret2main. And finally re-exploit this buffer overflow to call system and get a shell.
Reverse-engineering
The binary first sets stdin/stdout/stderr as unbuffered. It then malloc a function table containing 5 entries. And loop on getting an index from the user input and call the corresponding function.
auth
Here is the auth function :
It offers two authentication methods : using the clear password, or using the hash. We don’t know the content of the environment variables PASSWORD and PASSWORD_HASH but since the function sets the global variable g_is_logged_in at the beginning, we can bypass the auth by providing an auth_method different from 1 and 2.
is_logged_in
This function just prints if we are logged in or not.
upload_file
There are two vulnerabilities in the function upload_file. The first one is an out of bounds read : it computes the md5 sum of filename_maxlen bytes from the buffer filename. filename is read from stdin in the function read_buffer.
As you can see read_buffer reads at mostmax_size bytes and allocates a buffer of the effective size of the user input. So if filename_maxlen is greater than the size of the provided buffer, we can leak the content of the heap. There is also a null byte overwrite but it will not be useful for us.
The second vulnerability in this function is a buffer overflow, during the call to read_file_content.
1 2 3 4 5
structfile_s { char content[4096]; int size; };
This function reads at most maxsize + 999 bytes from stdin into file->content without taking into account the size of file->content which is always 4096. Moreover, file->size is placed just after file->content so we can overwrite it to get a relative write primitive as we control file->size during the memcpy.
read_file
Here we have the same vulnerability as in upload_file with the md5 sum, but is harder to exploit because if the file does not exists the program exits with the error message Error reading file.
Exploit
Leak exe base address
The binary is compiled with all the protections enabled.
I started to exploit the oob read to get some leaks from the heap. Remember the upload_file function.
If we provide a “big” integer as filename_maxlen (e.g. 0x28) but a small buffer such as abcd the md5_hex function will read 0x28 bytes from heap and produce a hash with them while the actual allocated buffer has a size of 0x5 (realloc allocated a chunk of 0x18 available bytes).
Here is the heap layout just before the call to md5_hex. rdi is filename.
So we can leak the base address of the binary one byte a time starting by reading 0x28 bytes, then 0x27 and so on until 0x20 bytes. We start by reading 0x28 bytes because of the null byte overwrite at the end of the read_buffer function. Each read will produce a hash, the path of the file. With 8 hashes we just have to bruteforce one byte per hash.
""" " leak exe """ hashes = [] for i inrange(8): path = upload(0x20 + 8 - i, b"abcd", 1, b"x") hashes.append(path)
leak = b"" for target in hashes[::-1]: for bf inrange(0x100): b = bytes([bf]) h = hashlib.md5() h.update(b"abcd".ljust(0x18, b"\x00") + p64(0x21) + leak + b) if target == h.hexdigest().encode(): leak += bytes([bf]) break
We now have the base address of the binary. We also found a stack-based buffer overflow. Let’s exploit it to control rip.
The vulnerability is in read_file_content it allows us to write a buffer of the size of our choice on the stack.
1 2 3 4 5 6 7 8 9 10 11
do { memset(s, 0, sizeof(s)); v4 = read(0, s, 1000uLL); // no check on maxsize // if maxsize > 4096 -> stack buffer overflow // -> we can overwrite file->size which is just after file->content memcpy(&file->content[file->size], s, v4); file->size += v4; } while ( v4 > 0 && maxsize > file->size );
file is defined as is :
1 2 3 4 5
structfile_s { char content[4096]; int size; };
We have to send 4096 bytes then the offset (positive or negative :) ) of our desired write, then pause, so that read will return and file->size gets overwritten. And finally send our payload.
As seen in gdb, at the time of the memcpy the current frame saved rip is located 0x38 bytes before our buffer. We have to overwrite file->size with -0x38, then send our new rip value and fill the buffer until file->size and overwrite it with a big value to exit the do-while :
Note that I also tried to overwrite the return address of the upload_file function at the offset 0x1078, it worked on local but not on the remote challenge :/ Bonus: I managed to leak a stack address with a bit of bruteforce (1/32) by overwriting the null byte at the end of filename_hash using the relative write but it was not useful for our exploit.
""" " leak exe """ hashes = [] for i inrange(8): path = upload(0x20 + 8 - i, b"abcd", 1, b"x") hashes.append(path)
leak = b"" for target in hashes[::-1]: for bf inrange(0x100): b = bytes([bf]) h = hashlib.md5() h.update(b"abcd".ljust(0x18, b"\x00") + p64(0x21) + leak + b) if target == h.hexdigest().encode(): leak += bytes([bf]) break