Case Study: LEGIT_00004

After winning the Cyber Grand Challenge and competing in the Defcon CTF with Mayhem, we have a lot to talk about. This post is the first in a series coming out in the coming weeks. Some will be more technical, and some less.

LEGIT_00004 was a challenge from Defcon CTF that implemented a file system in memory. The intended bug was a tricky memory leak that the challenge author didn’t expect Mayhem to get. However, Mayhem found an unintended null-byte overwrite bug that it leveraged to gain arbitrary code execution. We heard that other teams noticed this bug, but thought it would too hard to deal with. Mayhem 1 – Humans 0. In the rest of this article,  we will explain what the bug was, and how Mayhem used it to create a full-fledged exploit.

To better understand the bug in this post, we will look at the challenge source code that the author sent us after the CTF (Thanks Steve!). During the competition however, Mayhem did not have access to source and exploited the challenge solely by analyzing the binary.

The null-byte overwrite was in the “copy” functionality of the challenge. Below is a snippet of that function containing the relevant code. Note that the cmdline argument is user-controlled (and is up to ~1024-byte long).

[code language=”c”]
int copy( fileSystemType *fs, char *cmdline, unsigned int owner ) {
char sourcefile[FILENAME_SIZE];
char destfile[FILENAME_SIZE];
int x;

// skip over leading whitespace characters
while ( *cmdline != 0 && isspace(*cmdline) )

// if we hit the end of the line there were no filenames specified
if ( *cmdline == 0 ) {

x = 0;
while ( *cmdline != 0 && !isspace(*cmdline) ) {
if ( x < FILENAME_SIZE ) {
sourcefile[x] = *cmdline;


sourcefile[x] = 0;


The null-byte overwrite bug is on line 25. The loop right before iterates through all the bytes in cmdline, even though it copies only the first FILENAME_SIZE (or 20) bytes. Note that x keeps being incremented at each iteration. Therefore on line 25, we can make x take any value from 1 to 1024 even though sourcefile is only 20 bytes.

The bug enables us to write a single null byte on the stack. To see how this can be exploited, we need to look at the calling context and the assembly code. Let’s start with the calling context:

[code language=”c”]
int main(void) {
char command[1024];
while (1) {
bzero(command, 1024);
getline(command, 1024);

i = 0;
while (command[i] != ‘ ‘ && i < strlen(command)) {
command[i] = 0;

if ( strcmp(command, "list") == 0 ) {

else if ( strcmp( command, "copy" ) == 0 ) {
retcode = copy( currentFS, command+i+1, currentUser );


You can see the call to copy at line 17. You can also see that a local buffer (command) is passed as the cmdline argument to copy. In fact, the address of that buffer is loaded relative to ebp:

804cd85: 8d 8d b0 f4 ff ff lea -0xb50(%ebp),%ecx

It turns out that the copy function saves ebp on the stack, and Mayhem decided to overwrite the lowest byte of the saved ebp. Instead of restoring the original value 0xbaaaaff4, Mayhem made the program restore 0xbaaaaf00. Given that the command buffer address is computed relative to ebp, its address changed as well. It went from 0xbaaaa4a4 to 0xbaaaa3b0, which happens to be below esp.

On the next getline call, the program will start writing our data at 0xbaaaa3b0. The saved return instruction pointer is right above it at 0xbaaaa408. By giving enough data without a new line, Mayhem overwrites the return pointer, and gains arbitrary code execution when getline returns. Neat.

How long did this all take? LEGIT_00004 was released on round 65, at 14:10:25 UTC according to our database. Mayhem found the first crash at 14:32:28 and found a crash on a return instruction at 14:35:00. We had to wait until 15:55:26 to get a crash that mayhem could convert to a full fledged exploit. Overall, it took Mayhem 1h45m from looking at the program for the first time to developing the exploit described above.

10 thoughts on “Case Study: LEGIT_00004

  1. Nice write up. Could you provide more details on how the bug was discovered? Was it discovered using fuzzing, symbolic execution, or some other method? If fuzzing, how did you generate the initial input?

    1. I believe this bug was found with fuzzing. I’m not sure about the initial input… we use network seeds when available, but for all but the last (half) day of the contest, our network tap was broken due to compatibility issues with the DEFCON infrastructure. I’d guess that this was descended from a symbolic execution seed, or was fully fuzzed, but I’m not totally sure.

      1. Ah, our CRS had a provenance chain for all our PoVs. We could query it and see things like “KLEE->GRR->GRR->GRR->PSE->GRR->GRR” (with associated input ids so we could grab intermediate files). Does Mayhem not have something similar?

        1. A lot of that information gets discarded. We track which bug a crash comes from, but for checking the lineage of a seed or crash it is more difficult. This wouldn’t necessarily be a line, but a directed (possibly cyclic) graph. Given that no humans are involved in the process, and I’m not sure what an automated system would do with that information, we didn’t bother trying to store it during the events. (We do have some other research in the pipeline which keeps better track of this though.)

      2. Yeah, the crash that Mayhem managed to convert to an exploit was generated using fuzzing. We know that we did not use network seeds to generate the first exploit, since we didn’t have them yet. Mayhem however generated a better exploit (stealthy-ish memory leak using shellcode) after analyzing network seeds.

    1. Yep! Mayhem generated a hardened binary that protected against this vulnerability at 14:11:08 UTC, less than a minute after downloading the binary. Our defenses are usually pretty good against type 1 vulnerabilities (except in the rare cases when we have to fallback to less secure patches due to performance overhead or functionality failures).

  2. tl;dr How did Mayhem do against humans in CTF? Last place? Beating other computers doesn’t tell me anything. Maybe they all suck compared to humans.

    1. The results are not out yet unfortunately, so we don’t know whether Mayhem was last or not. Mayhem was 13th out of 15 for most of day 2. It started the third day in last place, and the orgs froze the scoreboard at that point. But Mayhem performed a lot better on the last day as we had resolved all of our technical issues. We know it was a close race for the 14th place 🙂

Comments are closed.