Skip to content


Square sponsored the Grace Hopper 2017 CTF and I was asked by some colleagues to help out with their team since they knew that I enjoyed reverse engineering things. Together we solved all of the challenges and this page documents how I worked through some of them.

Floppy - x86 image

Our team of corn snakes formed an uneasy alliance with the office mice and slithered into the server room of Evil Robot Corp. They extracted an image of the VPN server before they had to retreat. Turns out sending a team of reptiles into a room whose main purpose is to keep its contents cold wasn’t the best idea. Can you figure out how this bootable floppy works and recover the secret data?

This one is worth 1000 points and in the worst case will involve real mode x86 assembly plus low-level architecture stuff. Hopefully not. First, let's see what floppy.img does in qemu while tracing with gdb:

        qemu-system-x86_64 -fda floppy.img -S -s &
        gdb -iex 'target remote localhost:1234' -iex 'display/i $pc'

This starts qemu in suspended mode (-S) and with debugging port on 1234 (-s). gdb will connect to qemu's tracing port and also set it to automatically print the current instruction. We can start the CPU with c and see what it prints on the screen:

        Challenge: 32

Ok, step 2, hit Control-C (in the gdb window) and see where we are (so that we can determine the loading address of the image):

        x0000fff0 in ?? ()
        1: x/i $pc
        => 0xfff0:  add    %al,(%eax)
        (gdb) c
        Program received signal SIGINT, Interrupt.
        0x00001f9d in ?? ()
        1: x/i $pc
        => 0x1f9d:  mov    %al,-0x1(%ebp)
        (gdb) x/8x 0x1f90
        0x1f90: 0x00006424  0xf8458900  0xfff303e8  0xff4588ff
        0x1fa0: 0xffffa5e9  0x0060b8ff  0x04c70000  0x00006024

Let's see if one of those words shows up in the image:

        xxd -e -g4 floppy.img | grep f8458900
        00001190: 00006424 f8458900 fff303e8 ff4588ff  $d....E.......E.

That looks good! So the load offset is 0 0x1f90 - 0x1190. We can now turn to the Hopper interactive disassembler to load the image at 0xe00 and start to walk through the code. A good starting point is the method at 0x1fd9, which must be some sort of input reading loop:

And it is clearly doing some sort of busy loop, in between calls to 0x12a0. Let's step into that one and disassemble it (in hopper you can bring up pseudo-code by hitting Alt-Enter):

        function sub_12a0 {
        eax = arg0 & 0xffff;
        asm{ in         al, dx };
        eax = eax & 0xff;
        return eax;

That's pretty clearly an INB instruction wrapper and if we go back to the function 0x1f9d we can see that it is being called with ports 0x64 and 0x60. A quick search turns up this stackoverflow question that reveals that those are the keyboard controller busy and data ports. While historically interesting, this is not what we're looking for so let's name the functions and move up the stack a function.

Now this is solid -- we see the calls to getc() and the constant strings "Challenge" and "flag-", so this is hopefully where we can find some answers. There appear to be string input/output functions and based on what is printed ("Challenge: XY\n"), we can guess that sub_1310() is probably a hex output function with a number of bytes to print. var_12 appears to be the challenge and it is read from io port 0x71. Again, let's do a quick search and find osdev's wiki page on the CMOS, which tells us that this is the real-time clock (RTC) port. Writing a 0x2 to 0x70 selects the minutes register and looking at my clock I see that it was currently 32 minutes past the hour. Cool.

Experimenting with the running image, I noticed that it will read four characters and then print "NOPE. That loop is counting up to 4 keystrokes and accumulating some sort of value from it into an array pointed to by %ebp. After the loop there is a bunch of math that appears to be doing some sort of mixing into the array at 0x2bb8, which starts as all zeros, from the non-zero arrays at 0x2b64 and 0x2bb8. Assuming we've guessed correctly about sub_1310, 8 bytes from that array are then printed as the flag.

So, let's see if we can jump into that branch by short circuiting the test at 0x1f17 by setting a breakpoint there, continuing the emulator and typing in AAAA for our input value:

        (gdb) br *0x1217
        Breakpoint 2 at 0x1217
        (gdb) c
        Breakpoint 2, 0x00001217 in ?? ()
        1: x/i $pc
        => 0x1217:  cmp    %edx,%ecx
        (gdb) info reg
        eax            0x215f   8543
        ecx            0xaaaa   43690
        edx            0x215f   8543

Ok, so our input was turned into %ecx=0xaaaa and the value from sub_19a0() returned 0x215f, which clearly doesn't match. What if we pretend that we input the right value?

        (gdb) set $ecx=0x215f
        (gdb) c

And we have a flag! Maybe it's right, maybe it's not. But the rules in this CTF don't penalize you for trying answers, so I went ahead and tested it. 1000 points for team chocolates after a few minutes of reverse engineering!

The Turing Agent - a small Gameboy CTF

The hamsters have triumphed! We found an open Github repository! It looks like some sort of game; perhaps they’re planning to trick our humans into playing it. We need to know if there’s any data hidden in the game that might harm our humans.

I had recently ported the gnuboy emulator to the esp32 for my SHA2017 Gamebadge project, so this one seemed like a good second challenge. Worth 500 points, so it was pretty valuable. There were hints files, but they appeared to be missing any actual addresses, so I ignored them. Step one was making sure it worked in sdlboy, which it did. The "game" was fairly short -- a fun intro, then a locked door that needed a code. After button smashing it prints "NO GOOD", so let's take a quick glance at the strings to see what was inside:

        strings -tx | less
           2a6e ?*#V
           2a91 LONG LIVE       KOJIMA.
           2ab7 #68#6
           2adf ++4!
           2ae5 6S#6
           2b02 6S#6|#6
           2b55 MDYP
           2b6d "r+^#V
           2b8b "r+*fo6
           2bd8 1ST FLAG IS
           2beb THAT WORKED.
           2bf8 I'M IN.
           2c1b NO GOOD.
           2c24 LET ME TRY SOME THING ELSE.
           2c47 ,"s+^#V

The "1ST FLAG IS" is tantalizing, but there isn't a string to go with it. The "LONG LIVE KOJIMA" looks interesting, so let's try the Konami code:


Which does indeed generate the expected message. This button pattern is somewhat distinctive, perhaps it appears as a static pattern in the binary somewhere? There are probably better ways to look for it, but we can try the easiest first -- grepping for the pattern on the hex dump:

        xxd -g1 \
         | grep '\(..\) \1 \(..\) \2 \(..\) \(..\) \3 \4' \
         | grep -v '\(..\) \1 \1 \1 \1 \1' \
         | head -5
        00000210: 58 c9 04 04 08 08 02 01 02 01 20 10 08 02 10 04  X......... .....
        000003e0: 01 01 01 01 00 01 00 00 00 00 00 01 01 01 01 01  ................
        000004b0: 01 01 01 01 00 01 00 00 00 00 01 01 00 00 00 00  ................
        00000a40: 1f 10 10 1f 1f 10 1f 10 1f 10 1f 10 eb 14 17 e8  ................
        00000c40: 80 ff 80 ff 80 ff 80 ff ff ff ff ff 01 ff 01 ff  ................

The first grep is to find the pattern, the second grep is to ignore repeated sequences of bytes. That first line looks quite interesting since 01, 02, 04, 08, 10, and 20 are all "one-hot" with only a single bit set. This was my suspicion - there is a bitmask of buttons, so if we map that to the Konami code:

        04 04 08 08 02 01 02 01 20 10 08 02 10 04
        UP UP D  D  L  R  L  R  B  A

This gives us the button to bit pattern Rosetta Stone, so let's see where the pattern goes from there by hex dumping that part of the code:

        00000210: 58 c9 04 04 08 08 02 01 02 01 20 10 08 02 10 04  X......... .....
        00000220: 20 10 04 08 08 10 04 01 02 01 20 08 01 01 01 00   ......... .....

The pattern after that start 08 02 10 04 20 10 ... 00, which translates to a 19 button pattern. Inputting that into the emulator results in:

Success! Putting this flag in wins team chocolates another 500 points!

Note that we got lucky -- if the Konami pattern had been spread out across multiple lines or encoded in a different way we would have had to do some actual reverse engineering. If you want more details on how to to that, Reverse Engineering a gameboy ROM with radare2 is a great introduction.

Sniffed off the wire

After weeks of perching, our avian operatives captured a suspicious network flow. Maybe there's valuable data inside?

PCAP files are great fun. First thing I did was run strings on it, because I'm curious:

        strings -tx sniffed-off-the-wire.pcap
        171 [21d [16;23HDe
        2ca [8;2HKe
        375 [5;19HAe
        421 [24;13HIe
        4ce [20;39HKe

Huh, that looks familiar. If you're old enough to have used a serial terminal or ANSI graphics, those look suspiciously like vt100 cursor positioning commands, just missing the leading escape character (since strings won't show non-printing chars).

Ok, wireshark time! It easily made sense of the TCP conversation and allowed me to confirm that only two hosts were communicating and that only one of them was sending anything of interest (the other was just sending ACKs). This makes the filtering easy. wireshark will do session extraction, but I find it to be very, very slow. Instead I used tcpflow, which will generate one file per host:

        tcpflow -r sniffed-off-the-wire.pcap -o flow/
        xxd -g1 flow/ | head -2
        00000000: 1b 5b 31 32 3b 35 48 5a 1b 5b 31 36 3b 32 33 48  .[12;5HZ.[00000010: 44 1b 5b 38 3b 32 48 4b 1b 5b 35 3b 31 39 48 41  D.[8;2HK.[5;19HA

Sweet. Let's cat that file and see what we get:


A big blob of what looks like base64. Ok, cut-n-paste that into base64 --decode | xxd -g1:

        00000000: 61 78 5a 56 f1 e5 43 3c dd 78 e2 d9 3b 18 99 c0  axZV..C<.x..;...
        00000010: b8 e3 68 c0 31 0a e4 11 09 cc 4a 92 e5 e2 20 74  ..h.1.....J... t
        00000020: af 31 8a a8 00 21 dc 52 bb 9f 58 97 e6 0e 90 42  .1...!.R..X....B
        00000030: 84 e9 d7 53 37 a1 8c 36 07 3f 13 0d 80 69 44 72  ...S7..6.?...iDr
        00000040: 00 57 a0 d0 83 30 6a 9e 0e f8 eb 29 e9 45 0a f8  .W...0j....).E..
        00000050: 6c 11 c6 2f 38 96 9f b5 fa c4 6a d3 a3 14 42 2d  l../8.....j...B-
        00000060: 52 93 80 50 73 a1 35 f5 0f 98 38 73 56 8a 92 02  R..Ps.5...8sV...
        00000070: d4 a8 ba 63 5f 89 8a 86 4c 60 d4 8d 77 e2 4c d1  ...c_...L`..w.L.
        00000080: 85 c1 d6 32 46 d7 14 4b 13 2a 6b 13 ca 37 27 15  ...2F..K.*k..7'.

That doesn't look like anything I recognize. No strings, no magic number. I don't think this is on the right track.

But when I cat'ed the file, there was a flash and it looks like it drew the screen twice. Maybe there is an earlier message? How many times are each characters drawn anyway? Let's do a quick search for the first cursor location, 12;5:

        xxd -g1 flow/  | grep '\[00000000: 1b 5b 31 32 3b 35 48 5a 1b 5b 31 36 3b 32 33 48  .[12;5HZ.[16;23H
        000062b0: 37 48 67 1b 5b 31 32 3b 35 48 69 1b 5b 32 30 3b  7Hg.[12;5Hi.[20;
        00009410: 1b 5b 32 33 3b 35 37 48 57 1b 5b 31 32 3b 35 48  .[23;57HW.[12;5H
        00015df0: 5b 31 37 3b 37 35 48 6d 1b 5b 31 32 3b 35 48 69  [17;75Hm.[12;5Hi
        00018de0: 32 3b 32 34 48 74 1b 5b 31 32 3b 35 48 65 1b 5b  2;24Ht.[12;5He.[
        00023500: 39 3b 34 36 48 57 1b 5b 31 32 3b 35 48 61 1b 5b  9;46HW.[12;5Ha.[
        00029330: 1b 5b 34 3b 34 30 48 62 1b 5b 31 32 3b 35 48 57  .[4;40Hb.[12;5HW

That's quite a few times. Maybe there is a message in the way things are drawn? I have a program in my toolkit called slowcat that does exactly what it sounds like -- it prints very slowly, one byte at a time:

        use warnings;
        use strict;
        use Time::HiRes 'usleep';

        my $delay = 10; # useconds between bytes
        $|++;  # unbuffered stdout

            for(split //)
                usleep $delay;

Running this on the flow produces a very Too Many Secrets effect:

If you like that effect, check out bartobri/no-more-secrets, a tool that recreates it for command line output.


We captured this image from an old database filled with messages from the first robot spies. Most of these took the form of dinosaur or dragon action figures. This one was sent in by a dinosaur bot, with the note "Stegasauruses are like Onions: they have layers, and like hiding in grass. Replace spaces with dashes and prepend "flag-" to the flag."

That's the image. file says "PNG image data, 32 x 32, 8-bit grayscale, non-interlaced". The PNG is only 511 bytes, but the raw pixel data 32x32x8bits should be 1KB, so it is compressed. My first guess is that the compressed data is not useful, so I converted it to a raw bitmap with Image Magick:

        convert stegasaurus.png pgm:- | tail -c 1024 > test.raw

The tail commands is based on this stackoverflow question since pgm has a bit of a header that has to be stripped. Now that we have raw bytes, let's try looking at the low bits (since most of the time steganographic data is stored in the LSB):

        use warnings;
        use strict;
        undef $/; # read the entire file at once

        print pack("b*",             # convert bit string into bytes
            join _,             # join bits into one string of 01010100...
            map { ord($_) & 1 }  # extract bottom bit of each byte
            split //, <>);       # split it into bytes

Running this on the raw file produces what looks like python:

        ./low-bits < test2.pgm | less
        import sys
        k=int(sys.argv[1](1));print("".join(chr(ord(c)^k) for c in "Fpvgpa8'M,m&!s,!"))^@^@^@^@

The ^@ are nul characters, which we can ignore. This takes an argument and XORs it against the constant string, which means that we would have to try all 256 possible values. That's too much work, so let's hack the program slightly:

        import sys;
        for k in range(0,255):
            print(str(k)+": "+"".join(chr(ord(c)^k) for c in "Fpvgpa8'M,m&!s,!"))

Now when we run it it outputs lots of possible values, one of which looks likely:

        0: Fpvgpa8'M,m&!s,!
        1: Gqwfq`9&L-l' r-
        20: Rdbsdu,3Y8y25g85
        21: Secret-2X9x34f94
        22: Pf`qfw.1[23: Qgapgv/0Z;z16d;6

Putting that into the CTF challenge website won another 10 points for team chocolates!


3.50 am, CET, Heidelberg, Germany
You weren't able to stay connected for very long. You weren't expecting to go unnoticed, but you were really hoping to keep a few shells around for a few hours at the very least. You feel some regret for not doing your recon right, you should have known what would go noticed and what wouldn't before starting all this.
At least you recovered some files. It seems one of these files was being used to manage passwords. You didn't think about copying the binaries and it seems they were running programs written in an esolang called Seventeen.
Luckily you stumbled upon some information about seventeen (see seventeen.txt). Is this going to be enough to figure out how passmgr.17 works and find sandflea's password?

This one was worth another 1000 points and sounded like a bit of work. Unpacking the tar file revealed a few files (including some OSX ones that we'll ignore):

        tar tvf ~/Downloads/seventeen.tgz
        -rw-r--r-- alok/staff     1310 2017-09-28 04:29 seventeen.txt
        -rw-r--r-- alok/staff      163 2017-09-28 04:45 .bash_history
        -rw-r--r-- alok/staff     3923 2017-08-11 13:00 passmgr.17
        -rw-r--r-- alok/staff     4204 2017-07-07 22:31 primes.17
        -rw-r--r-- alok/staff      121 2017-07-07 22:31 primes.out

The .bash_history had a nice clue in it -- the root password when parsed by passmgr.17 was "8a30347d2a" and the primes.out was generated with an input value of 150.

First thing to note is that the format is C like, with /* .. */ commenting, and used assembly style label: branch targets. Variables were not predeclared, so we'll need to build a symbol table on the fly. The seventeen.txt file helpfully tells us that the machine is stack based, that variables store single values and that there is a single array indexed with integers. It does not say anything about call stacks, nor the order of operands for the IFZ and IFG instructions.

Luckily primes.17 is heavily commented so that we can deduce the various instructions and their operations. Even more helpfully, it starts with three sanity check routines to ensure that your interpreter is working correctly. Getting these right took me a few tries, but saved me from having to debug while executing passmgr.17.

The instructions that I ended up using were read_num, print_num, print_byte, read_byte, dup, ifz, ifg, call, jump, add, sub, xor, mod, store, vstore, vload, and exit. My parser is fairly simple - it uses perl arrays for the stack, a hash table of instructions, a hash table of variables and an array for the vector. I used the same hash table for the variables as the branch targets, which simplified my lookup routines but might not be according to the spec.

        /* Check 3 */
        1 x store
        0 x store
        1 sub_assert call

The difficult test is #3, which ensures that the evaluation of variables happens when they are pushed, not when they are popped. This hit my interpreter since I was doing everything symbolically; instead I had to switch to an immediate evaluation unless the next instruction was a store, in which case the variable name must remain symbolic.

I heard from at least one other team that tried to work out the password by hand and were getting stumped at how the .bash_history file had ten characters of output for the four character input of "root". I gave them the hint that /bin/echo root produces five characters including the newline, which is being read by read_byte instruction.

The only remaining issue is that the challenge clue was asking for "sandflea"'s password, so it is time to get the flag:

        echo sandflea | perl ./parser passmgr.17


Our hamster allies took a break from racing on their wheels to scan Evil Corp’s websites with equal speed. They found this Perl script suspicious. Of course, hamsters find everything suspicious, so maybe it’s nothing…

Perl! That's where I'm a viking! (Seriously, years ago I inspired a new rule in The Perl Journal's obfuscated perl contest!). Anyway, back to the challenge... When you go to that URL, it prints a cryptic message:

        Sorry, try different query params.


So I tried and was rewarded with the source code for the challenge:

        print "Content-Type: text/plain\r\n\r\n";

        $_ = $ENV{'QUERY_STRING'};

        open F,$0 and print  and exit if (/[^a-zA-Z0-9=.](^a-zA-Z0-9=.)+/);

        s /(\D+)/lc($1)/eg;
        y /0-9A-Z/0-9N-ZA-M/s;
        $ s=$ _;
        $ s=substr(($s = substr $s,$s-1,$s),$s+1,$s);
        @ t=split /|m O_O m/, $s;
        $ c=($s|~$s)-1;
        @ o=[];
        foreach (qw('f 0 u r')) {
        $d=substr $s,$c++,2;
        push @o, substr $s,$d,$t[++$c](++$c);
        push @o, chr($o[2](2)+04).chr($o[3](3)+031);
        push @o, chr($o[1](1)+045).chr($o[4](4)+0151);
        push @o, chr($o[2](2)+01).chr($o[3](3)-04);
        push @o, chr($o[1](1)-053).chr($o[4](4)+0136);
        push @o, chr($o[2](2)-013).chr($o[3](3)+025);
        push @o, chr($o[1](1)+047).chr($o[4](4)+0100);

        join _,reverse(@o) =~ /\D+/;

        $& eq 'Grace Hopper' ? (open F,'/secrets/flag' and print "congrats!\n", ) : print "Sorry, try different query params.\n\n",
          $ENV{'REQUEST_URI'}, "\n", ' ' x length($ENV{'SCRIPT_NAME'}), '^' x (length($ENV{'REQUEST_URI'}) - length($ENV{'SCRIPT_NAME'}));

        # --Alok
        # flap-f382a1f8b36754c96bf0

Well, that's certainly perl. Let's break down the argument parsing part of the code:

        s /(\D+)/lc($1)/eg;      # make everything lower case
        y /0-9A-Z/0-9N-ZA-M/s;   # "squeeze" repeated digits to single digits
        $s=$_;                   # copy the argument into $s
        $s = substr $s,$s-1,$s;  # if $s starts with a number, skip that many bytes-1
                                 # forward and then extract that many bytes.
                                 # replace s with this new value
        $s = substr($s,$s+1,$s); # if the new $s starts with a number, skip that many bytes+1
                                 # forward and then extract that many bytes.
                                 # replace s with this new new value
        @ t=split //, $s;        # split $s into an array of characters (ignore the extra art work)
        $ c=($s|~$s)-1;          # $c will always equal -1

If the input string were something like 14aaaaaaaaaaa6bbbbbb012345, the first substr() would result in 6bbbb012345 and the second would result in 012345. @t=(0,1,2,3,4,5)

While I was sorting out this part of the code, one of my teammates was working from the other end and figured out that @o would have to contain (x,y,z,a,b,'re','pp','oH',' e','ca','rG') so that reverse(@o) =~ /\D+/; would result in "Grace Hopper". Since the first five values of @o are used to determine the last six strings, it is possible to determine what x, y, z, a and b must be based on this code:

        push @o, chr($o[2](2)+04).chr($o[3](3)+031);
        push @o, chr($o[1](1)+045).chr($o[4](4)+0151);
        push @o, chr($o[2](2)+01).chr($o[3](3)-04);
        push @o, chr($o[1](1)-053).chr($o[4](4)+0136);
        push @o, chr($o[2](2)-013).chr($o[3](3)+025);
        push @o, chr($o[1](1)+047).chr($o[4](4)+0100);

The initializer for @o=[] is deliberately obfuscated -- it doesn't make an empty list, but instead a single entry list with a reference to an empty array. So $o[0](0) = [], $o[2](2)+4 = ord('r') = 0x72, $o[3](3) + 031 = ord('e') = 0x65, $o[1](1) + 045 = ord('p') = 0x70 and $o[4](4) + 0151 = ord('p') = 0x70. Note that the leading 0 in the values is octal, This gives us the values that we must end up with:

        @o = ([], 75, 110, 76, 7); # solution

We're almost there! We now just need to meet in the middle and figure out the input that produces those values.

        foreach (qw('f 0 u r')) {
        $d=substr $s,$c++,2;
        push @o, substr $s,$d,$t[++$c](++$c);

Since $c=-1 to start with, this advances it to 0, then extracts the first two characters from the final $s to get an offset, and then reads as many characters as are stored in the array @t. That's messy.

Let's work out an example with our previous input, $s="012345"; @t=(0,1,2,3,4,5);. After the first substr, $d="01", and then $t[++c](++c) = $t[2](2) = 2, so it will push subtr $s, "01", 2 == "12" onto the array. It then reads the next three bytes and does a similar operation, subtr $s, "34", 5, except that this is off the end of the string so things won't work.

So we need to find a better input string... Something that contains the values that we need. First try is something like "132153182131a7511076", which 'would' work, except that the y/0-9/0-9/s early in the parsing.

So we need to find another way to represent 110. We went through several different attempts, perhaps leading 0 for octal 0156? Maybe 0x63? How about rounding 109.9? Could we make an integer overflow? None of those worked, but eventually one of our team mates suggested 1.1e2 and we were able to capture the flag for another 500 points:



After months of reading over our humans’ shoulders--or sitting on the clicky hard lapwarmers or crinkly thin lapcovers that attract so much of their attention--we realized that the androids are encoding messages to each other in human entertainment! They gave themselves away with this puzzle--their minds of metal and wheels think only of words related to computer security. Can you solve the crossword and find the hidden message?

This one was a team effort -- there was a breadth of knowledge required that really benefited from having everyone on team chocolates help out. We created a shared google spreadsheet and attacked the clues, while also digging into the source code to figure out how the clue was encoded.

        function validate(key) {
          // Save the grid
          var gridText = JSON.stringify( => => c.empty ? "" : (c.inp.value + " ")[0](0)).join(",")));
          localStorage.setItem('grid', gridText);

          // Display flag if the grid is "mostly" correct. Only look at intersections to keep things solvable.
        {name: "HMAC"},
          ).then(sig => {
        var r = [Uint8Array(sig)]( => i.toString(16)).join('')
        if (r.substr(0,10) == "d7d9fcd45b") {
          flag.textContent = "🚩  = flag-" + r.substr(10, 10);
        } else {
          flag.textContent = "";
          console.log("sorry, no bueno.");
          }).catch(err => console.log(err));

One of the first observations was that the validate() method only checked the intersections, which greatly reduced the number of clues that we had to solve. This helped in a few places where we weren't quite sure (although we were thinking about brute forcing it, too...) We thought for sure we were done in the shared one, but didn't get the flag, so I cleared my puzzle and typed in a fairly minimal set of clues, plus then tried a few different answers, which resulted in:

Success! A tiny flag appeared at the bottom of the page. If I hadn't zoomed out and been watching for it, I probably would have missed it.

While I take issue with Write-once, Read-never answer PERL, I do especially liked the disclaimer at the bottom of the code:

Disclaimer: this challenge is not intended to convey or constitute cryptographic advice. Always consult a cryptologist, then roll your own crypto.


  • Auto-creating the slack channel for each team was great.

  • Having a slack channel with the mods and puzzle creators was amazing!

  • Allowing both local and remote teams was a good idea. I liked that some of the puzzles required presence at GHC.
  • The mix of puzzles was quite good; I focused mostly on reverse engineering since that is what I do, but the other ones were fun too.
  • One week might have been too long. We finished the first weekend since we mis-read the due date...
  • Add a penalty for bad flag attempts? It was tempting to guess a few times.
  • Why a log-scale on the leaderboard graphs? It was very hard to tell who was winning.

One of my favorite CTFs so far! Thanks to the folks at Square for sponsoring it.

2017 Security

Last update: November 8, 2020