2020 NSA Codebreaker CTF — Parts 1–5 Solutions
By: Mitch Talmadge
Tags: Capture The Flag, Hacking, Ctf, Cryptography, Programming

Task 5: Where Has the Drone Been?

1300 Points | Reverse Engineering, Cryptography

A rescue team was deployed to the criminal safehouse identified by your efforts. The team encountered resistance but was able to seize the location without causalities. Unfortunately, all of the kidnappers escaped and the hostage was not found. The team did find two important pieces of technology left behind: the journalist’s Stepinator device, and a damaged surveillance drone. An analyst retrieved some encrypted logs as well as part of the drone’s GPS software. Your goal for this task is to identify the location of the criminal organization’s base of operations.

This one was TOUGH. Something that bothers me when most people give their CTF solutions is that you only see the successful bits. You never hear about them struggling to find the answer. You never hear about how they accidentally stay up all night working on the problem, throwing off their sleep schedule, and missing classes as a result, which only added to their depression and anxiety. You never hear about how the problem obsessed them and caused them to neglect their college assignments. You never hear about the stress and the imposter syndrome, many times wondering “am I good enough to do this?” I am here to tell you that I experienced all of this and more. Problems like these can be a huge undertaking, and it was NOT easy to solve this one. However, I learned so much from it, and I’d do it again in a heartbeat. Be strong; power through; it’s worth it.

Now — onto the problem. We are given two downloads:

Interesting looking binary ( gpslogger )

) Recovered log files ( logs.tgz )

This includes four encrypted log files from the drone, and the software that produced them:

Let’s run file on one of the log files and see what it knows:

Oh.. “data” ..that’s not useful. This is definitely a type of encryption that does not include metadata. Probably something simple like AES. Let’s also run file on the binary:

This is an aarch64 ELF executable. In other words, it was compiled for a 64 bit ARM processor, which I wouldn’t be surprised to find inside a drone. This is a similar processor to what powers the Raspberry Pi 4, or your smart phone — low on battery consumption with decent performance.

I tried many ideas to crack this encryption, and I will go through some of them here. First, I like to run strings to see what hardcoded information exists in the binary. If we are really lucky, the encryption key would be hardcoded. Here’s some of the things I found.

These function names reveal that we are looking for a key and IV which are generated inside the code. setup_cipher also looks useful.

These function names suggest that we will be working with AES CBC, ASM, or GCM encryption.. I wonder which.

Ooh! Look at this one. The IV depends on some longitude coordinate, and the key depends on some latitude coordinate.

That last one really got me thinking… where have I seen latitude and longitude coordinates before? Oh yeah, the map from Task 4!

I bet that will be useful later.

Okay, at this point I have some information but definitely not enough. I know that those coordinates play into the key and IV somehow, but I don’t know the exact details.

Let’s see if we can run the binary. Since it’s ARM64 based, I will need a device with this kind of processor. Remember when I mentioned phones? I have a spare OnePlus 6 Android phone laying around, so I installed Termux as well as Andronix, two apps which allow me to install and run Kali linux on my phone, even without root. I set up an SSH server and then remoted in from my desktop. After uploading the binary, I tried to run it:

Hmm.. missing libraries and symbols. Let’s see:

Ok, we’re missing libgps.so and libc.musl-aarch64.so.1 . After installing these missing libraries, I still could not run the binary because of the GPSNMEA: symbol not found error. I think this binary won’t work unless I have an actual hardware GPS module hooked up, and I do not have one, and don’t know how to use the one my phone has built-in. I gave up on running the binary at this point.

Let’s finally disassemble the binary and look inside, something I was sort-of hoping to avoid. I used the command objdump -D gpslogger > gpslogger.asm . I opened the file and jumped to main.main which I knew about from strings :

Hey, do you see that? <go.string.*+0x6fe0> … This binary was written in golang! This is actually going to end up being obnoxious because golang does a lot of interesting things which make the assembly look fairly different from traditional c-lang type binaries, like c and c++. Here’s some of the things I learned while I read through the assembly and scoured Google:

golang passes all parameters into functions through the stack. They do not use registers for this, unlike c/c++. (This will change in golang 1.16 or 1.17)

golang functions can return multiple values.

golang functions return their values on the stack, not through registers. They do this by placing the return values after all the parameters. So if you pass parameters into a function at SP + 0x08 and SP + 0x10 , then expect return values starting at SP + 0x18 .

and , then expect return values starting at . Strings are returned as two words; a pointer and a length.

I could go on forever, but instead here’s a great page explaining more if you want to learn.

I decided to go take a look at the main.generate_key and main.generate_iv functions.

There’s a lot going on here. I can see something which I bet will be valuable, though: strings.Repeat . Let me narrow things down for you:

Here we store to SP + 8 , SP + 16 , and SP + 24 . Then the function is called and we load from SP + 32 and SP + 40 . Remember what I was talking about with return values coming after the parameters? 32 comes directly after 24. So it looks like we pass in 3 parameters and get 2 out. Let’s look at the method signature for strings.Repeat :

So it takes in a string, and a count of how many times to repeat it. It returns a string. Remember also what I said about strings taking up two slots: a pointer and a length? So SP + 8 is the input string pointer, SP + 16 is the input string length, and SP + 24 is the count.

We can see in the screenshot that orr x0, xzr, #0x4 will cause x0 to contain 0x04 , or simply 4 . x0 is then stored in the count parameter position. So this string will repeat 4 times.

The return value is the repeated string; the pointer and the length. These are immediately returned from our main.generate_key function.

main.generate_iv works very similar, except that we repeat 3 times, and then we load runtime.rodata+0x3c7a0 , which turns out to be a single 0 character. This is appended onto the end of our repeated string via the concatstring2 function call. I am guessing this is just a single character of padding, since IVs need to be the exact length of one plaintext block.

Where are main.generate_key and main.generate_iv called from? The answer is main.setup_cipher .

Here I have jumped right into the middle of it, but you can see that it is calling both functions in succession.

Still in main.setup_cipher , there are two very important function calls. One to crypto/aes.NewCipher and one to crypto/cipher.NewCBCEncrypter . This tells us that we are using AES CBC encryption! Now that we know which cipher and mode are being used, we can also infer some things:

AES-128 requires blocks to be 16 bytes in length, AES-192 requires 24, and AES-256 requires 32. Our key has to match this length, which means that the string we repeat 4 times must be of length 4, 6, or 8 bytes. We don’t know yet whether 128, 192, or 256 is being used.

CBC requires that the IV be the same length as one block. This means our IV has to be either 16, 24, or 32 bytes in length. To figure out which one we are using, let’s consider the equation 3x + 1 = y . Here, x represents the length of our input string that is multiplied by 3. 1 is for our single added 0 char padding. y is either 16, 24, or 32. Also, x must be an integer. The only way to solve this equation is with x = 5, y = 16 . Therefore, we know that our block size is 16 bytes, so we are using AES-128 with a 16 byte key and a 16 byte IV.

. Here, represents the length of our input string that is multiplied by 3. is for our single added char padding. is either 16, 24, or 32. Also, must be an integer. The only way to solve this equation is with . Therefore, we know that our block size is 16 bytes, so we are using AES-128 with a 16 byte key and a 16 byte IV. To summarize, our key input must be 4 bytes long, and our IV input must be 5 bytes long.

This is really good information. All that’s left to do is figure out where these inputs come from. I already have a guess; remember the strings from the binary which mention that the key is based on latitude, and IV on longitude? Let’s keep that in mind.

Something I discovered while looking through setup_cipher is that it will branch to a panic if certain conditions fail. The panic message is quite valuable:

!!! EXPECTED TO FIND NMEA $GNGGA HEADER !!!

Wow, what a message. So much excitement in that error. It reveals something super useful to us, though! This setup_cipher function takes in a $GNGGA NMEA header. This is raw information that GPS modules receive from satellites. Here’s an example of one of these headers:

$GNGGA,054157.013,2307.1261,N,12016.4308,E,1,6,1.93,34.9,M,17.8,M,,*76

And here’s the formatting:

Truncated a bit. Source: https://gpsd.gitlab.io/gpsd/NMEA.html#_gga_global_positioning_system_fix_data

Look! Latitude and longitude. Look what else! 4 latitude characters (key) and 5 longitude characters (IV). If you dig into the main.setup_cipher code a bit, you will find that it splits on , and . characters in a couple places. This allows it to extract the first 4 characters of the latitude and the first 5 characters of the longitude (stripping off the decimals).

Keeping with the theme, I wrote a golang program using the same libraries in order to decrypt the log files. I plugged in 0513 as the latitude and 02459 as the longitude inputs, which come from the map image from many paragraphs ago. I then tried decrypting each log file. The newest two according to the dates in the file names were successfully decrypted!

The very first block was messed up ( $EHFKH is not a valid NMEA header) and I guess that’s because my IV was incorrect somehow. I never took the time to figure out the real IV because it didn’t matter; all the rest of the data was correct and had the info I needed.

Let’s remember our goal: find the coordinates of the kidnappers’ headquarters. If I had to guess, I’d say that these two newer files are from when the drones were flying around looking for our journalist before the kidnapping. The two older files must be from flights taken at the headquarters.

I suppose that I could try graphing the NMEA files and see if that leads me to their coordinates, but I had an easier idea: bruteforce! It’s only a 4 character key and I don’t need the IV, so I just guessed all values from 0000 to 9999 and checked if the output looked right. Here’s what I wrote:

In less than a second, I had the correct key to the older log files: 0521 . Not far off from our original 0513 ! They must be nearby.

I opened the decrypted log file and obtained these decimal coordinates:

0521.073024 N 02418.706995 W