An Nmap easter egg: yoloscan

Looking around on Reddit, I read a comment from someone who noticed an easter egg in Nmap, the ever popular network scanner. When executed with the -y argument, it outputs a reference to a classic World of Warcraft meme.

$ nmap -y
LEEROY JENKINS!!!
Starting Nmap 7.80 ( https://nmap.org ) at 2019-12-21 18:26 -03
WARNING: No targets were specified, so 0 hosts scanned.
Nmap done: 0 IP addresses (0 hosts up) scanned in 0.36 seconds

This behavior exists only in the latest version (7.80). Older versions do not recognize the option:

nmap: unrecognized option '-y'
See the output of nmap -h for a summary of options.

OK, pretty neat! But the fun wasn't over for me. I wanted to see where and how this was implemented.

Reconnaissance

My first instinct was to use grep, expecting to find the string "LEEROY JENKINS!!!" somewhere in the code. So I cloned the nmap repo, which is mirrored on GitHub. No dice. Of course, the easter egg's author wants us to have a little more fun.

A better idea: the function that outputs the text might be the same one that prints the following line (Starting Nmap 7.80...). So I went grepping for it.

$ grep -n Starting *.cc
FPEngine.cc:1205:    log_write(LOG_PLAIN, "Starting IPv6 OS Scan...\n");
nmap.cc:1564:  log_write(LOG_STDOUT | LOG_SKID, "Starting %s %s ( %s ) at %s\n", NMAP_NAME, NMAP_VERSION, NMAP_URL, tbuf);
nmap.cc:2479:        error("Warning: You asked for --resume but it doesn't look like any hosts in the log file were successfully scanned.  Starting from the beginning.");
service_scan.cc:2346:      log_write(LOG_PLAIN, "Starting probes against new service: %s:%hu (%s)\n", svc->target->targetipstr(), svc->portno, proto2ascii_lowercase(svc->proto));

The second match, at line 1564, resembles the initialization message. log_write is the function I'm looking for. I also assumed that the easter egg is in the same file, nmap.cc. One grep -n log_write nmap.cc later, I found an interesting match in a conditional block in the parse_options function, with some pointer arithmetic (line 919):

void parse_options(int argc, char **argv) {
    if (strcmp(long_options[option_index].name, "max-os-tries") == 0) {
        /* omitted: other conditionals */
    } else if (strcmp(long_options[option_index].name, (char*)k) == 0) {
        log_write(LOG_STDOUT, "%s", (char*)(k+3));
        delayed_options.advanced = true;
    }
    /* omitted: even more conditionals */
}

Seems like this is it! Just to confirm, I commented out that block and recompiled nmap (./configure && make).

$ ./nmap -y
Unknown long option (yoloscan) [email protected]#!$#$
QUITTING!

yoloscan? Now we're onto something.

Analysis

Alright, let's recap. Here's all the relevant code, including the declaration of the k variable.

void parse_options(int argc, char **argv) {
    int option_index;
 #ifdef WORDS_BIGENDIAN
   int k[]={2037345391,1935892846,0,1279608146,1331241034,1162758985,1314070817,554303488};
 #else
   int k[]={1869377401,1851876211,0,1380271436,1243633999,1229672005,555832142,2593};
 #endif
    struct option long_options[] = {
        {(char*)k, no_argument, 0, 0},
        /* omitted: other options */
    }
    /* omitted: loop over options; other conditionals */
    if (strcmp(long_options[option_index].name, (char*)k) == 0) {
        log_write(LOG_STDOUT, "%s", (char*)(k+3));
        delayed_options.advanced = true;
    }
}

An array of integers is used to obfuscate the option and its output. Based on what I've seen so far, the code should be equivalent to this:

void parse_options(int argc, char **argv) {
    int option_index;
    struct option long_options[] = {
        {"yoloscan", no_argument, 0, 0},
        /* omitted: other options */
    }
    /* omitted: loop over options; other conditionals */
    if (strcmp(long_options[option_index].name, "yoloscan") == 0) {
        log_write(LOG_STDOUT, "%s", "LEEROY JENKINS!!!\n");
        delayed_options.advanced = true;
    }
}

PS: The option struct is from libc. It's not important here; it just informs us that the option has no arguments.


How does casting an integer array to a character pointer work out? Two things that I noticed about the arrays in the code:

  • There are two strings, one at (char*)k, and one at (char*)(k+3). The first string has a clearly visible null terminator: the third element of the array.
  • There is a preprocessor conditional that declares a different array depending on the system's endianness. This means that there's some specific byte/bit ordering going on.

A few more things to note:

  • My system, like all others using the x86 architecture, uses little-endian formats.
  • My system, like most others, has 8-bit characters.
  • My system, like most others, has 32-bit integers.

Let's work on the numbers from the little endian version of the easter egg. Here's the first number, converted to binary, padded to 32 bits, and split up into 8-bit segments. Each one of the segments is a character.

Decimal:    1869377401
Binary:     01101111011011000110111101111001
Segmented:  01101111 01101100 01101111 01111001
ASCII:      o        l        o        y

Because of the endianness, the order is reversed. But we can clearly see the first four characters of the string, yolo, but it's not terminated yet. The second number contains the rest, scan, and the third is the terminator. By repeating the process for the entire array, we get the expected strings.

Wrap up

This was a great refresher for my basic C skills (well, C++ technically), and it inspired me to finally write a new blog post. In hindsight, it's pretty basic, but it was fun nonetheless!

PS: I hope that, by now, it's alright to write about the easter egg; it has been a while since it was inconspicuously added (2018-10-01).