CTF Circle - Hack-A-Sat 2021 Qualifier CTF "groundead" Challenge Writeup Written by sen (@sarahemm) ############### ### Summary ### The 'groundead' challenge provided a file named 'challenge' and a server and port number, with no other information. Upon connecting to the port it said "ground station online" but nothing further, and there wasn't any indication as to what you were supposed to be attempting. ############################ ### Tools/Infrastructure ### Our team has a couple servers we use as a launching point for CTF work, so my work was done on these. I ended up using Ghidra for the reversing part of the challenge, and Ruby to write the solver. ########################### ### Phase 1 - Discovery ### To start off, I ran 'file' on the challenge file provided, which showed it was an ELF binary. I disassembled it using objdump just to get a rough overview of what I was dealing with, which showed symbols with mangled names like _Z23getSatellitePacketBytesPv. This usually means C++, but I hadn't done any reversing of C++ before this point, and had barely even worked with it at all in the forward direction. A bit of googling showed that Ghidra seemed to be a reasonable tool to use for C++ reversing work, and it had been on my todo list to learn to use for awhile anyways. I got that installed and ready to use (literally just unzipping it and ./ghidraRun, easier than I'd expected!), then turned to the server port to see what information I could get from there. Connecting to the port gave a message saying the ground station was ready, with ASCII art of a ground station. Entering any information generated a message saying "That sequence of hex characters did not work. Try again." Running the provided binary, it gave the same messages and output, so the binary seemed to be a copy of what was running on the challenge server. ########################### ### Phase 2 - Reversing ### At this point it seemed that the challenge wanted hex digits, and I had a copy of the challenge binary to reverse. Loading it into Ghidra, I figured out how to get it disassembled and decompiled. Looking through the different functions in the binary, I decided to focus on processSatellitePacketBytes as, after quickly looking through it, it seemed to reference both the error message I was getting about the hex digits as well as the flag. I would end up regretting focusing so narrowly so quickly, but that's how it goes sometimes when you're under time pressure. I looked at both the decompilation and the disassembly, and decided to work off of the decompilation as much as possible. With plain C it's easy enough to work off the disassembly, but I found that harder with C++ and this seemed more likely to get results in a quicker time. I began working from the top of the function down, renaming variables as I figured out what they were for, or at least started to suspect what they were for. As I was doing that I found references to things like APID, indicating this was looking for CCSDS format packets, a standard protocol for space applications. In addition, there was a reference to what looked like command 0x08 which would request emergency mode and emit the flag. case 8: puts("EMERGENCY_MODE: SPACECRAFT IS IN EMERGENCY_MODE"); puts("You made it!\nHere\'s your flag:"); puts(flag); exit(0); After thinking this through a bit more, I realized that the challenge was pretending to be a ground station rather than a satellite itself, so it would likely be expecting a satellite to send telemetry rather than commands. I worked up a quick tool to generate CCSDS headers, but every attempt I made at sending data to the service resulted in the same error message about hex characters not working. I'd hit a point where I was feeling stuck in the reversing, so started working up from the bottom of the function, reversing out a bunch of debug output that helped me further narrow down which variable was used for what. This got me a more complete understanding of how everything worked, but didn't seem to get me any closer to solving the specific error I was running into. I did find some code that validated various header fields, which would become important later on. At this point though even with these fields set appropriately it wasn't getting me anywhere. if (((fieldPVN == 0) && (fieldPktType == 0)) && ((fieldAPID != 0x7ff || (fieldSecHdr != 1)))) { // sets 'header is okay' flag ########################## ### Phase 3 - Debugger ### I'd spent several hours getting to this point, and was feeling fairly stuck. Trying a debugger seemed a reasonable next step, but using debuggers on stripped binaries isn't something I have a lot of experience with. I spent half an hour trying to get useful information out of gdb before deciding that I should step away from the challenge for a bit and try to get a fresh perspective. I took my dogs out for a walk and hoped some new ideas came to me while I was out. While I was out, it occurred to me that the error message I was getting might be generated from somewhere else in the binary, even though the text was identical. Even though my packet matched all the "rules" I could find in the processSatellitePacketBytes function, it could be another function entirely that was rejecting my packets with the same error message. ################################ ### Phase 4 - More Reversing ### I decided to go back to more familiar tools. Since much of my background is in embedded systems where a lot of debugging gets done via "printf debugging" and setting pins high or low depending on what areas of code are executing, I thought about how I could use these methods to figure out what was going on rather than using an interactive debugger. Looking through all the other functions, I found getSatellitePacketBytes was referencing the same error message I was getting. Ghidra keeps the cursor synced between the decompilation and disassembly windows, so putting my cursor on the "That sequence of hex characters..." string in the decompilation then flipping back to the disassembly showed me instructions that loaded the address of the string then executed the << C++ operator to add it to a buffer. 001036b2 48 8d 35 LEA RSI,[s_That_sequence_of_hex_characters_d_0010b... 57 80 00 00 001036b9 48 8d 3d LEA param_1,[std::cout] c0 c9 00 00 001036c0 e8 5b ee CALL ::std::operator<< ff ff This indicated that the instruction that referred to the string should be at 0x36B2, so changing the parameter should accomplish what I wanted. A minute with a hex editor changing the byte at 0x36B6 from 0x80 to 0x81 changed the location of the text it was retrieving to give the error, so that I'd be able to tell if the error I was getting was coming from the getSatellitePacketBytes function or from processSatellitePacketBytes. A quick test of the modified binary showed that now I got the "EMERGENCY MODE" message if the getSatellitePacketBytes function was rejecting my input, and the "hex characters" message if it was processSatellitePacketBytes. Now that I knew for sure what piece of code was rejecting my input, I started reversing the getSatellitePacketBytes function. I had found previously that the processSatellitePacketBytes function was reading characters out of a queue, but I hadn't thought much about where characters in the queue came from. This function was reading characters from the console and pushing them into the queue, and must have been doing some preliminary filtering as well. After a brief reversing session on this new function, I found a reference to 0x07 I thought looked promising. I tried throwing just a whole bunch of 07 at the challenge binary, and it got past the first check! The data was then passed to the processSatellitePacketBytes routine which output a bunch of debug information, then rejected the packet since it was just 07 over and over and not valid CCSDS. I reversed out the area around the 0x07 in the function a bit more, and it seemed like it only cared if the 7th byte in a packet was 0x07, the other bytes could be any values. The header is 6 bytes, which means the first data byte needed to be 0x07 to pass the check. I modified my test data to send 0x07 after sending the header, and it resulted in a "Handling test telemetry message"! There was no obvious way to get the flag though since the only status value I was allowed to send was 0x07 (test telemetry) and not 0x08 (emergency mode/flag), but it seemed likely that I was just one step away from the flag now. ######################### ### Phase 5 - Solving ### I played around a bit more with various patterns of digits, trying to send multiple bytes rather than one or add padding in various places, but didn't have any success as the "reader" function restricted the value of the 7th byte, but the "process" function only cared about the 7th byte (as long as the header passed its checks). Looking through the "reader" function a bit more, I noticed the number 0x1ACFFC1D in the code, which seemed significant. I had skipped over it initially thinking it was referring to an address or something, but googling this number brought up the same CCSDS documentation I'd been referencing which seemed unlikely to be cooincidental. This series of bytes is used between each frame of certain variants of CCSDS (called the Attached Sync Marker, or ASM), which made me think that maybe I could use it to pad the data I was sending. If both the "reader" and "processor" would ignore it for actual data parsing, it might shift my bytes around enough that the check for 0x07 would be checking a byte in the header rather than data. The header bytes have more flexibility in this case, so I could probably set one to 0x07 if that ended up being required to satisfy the check. I modified the header I was sending to prefix it with 0x1ACFFC1D, then worked out that the 7th byte in my data was now in the "sequence number" field of the header. This field is ignored by the processing function, so seemed like I would be able to set it to 0x07 to satisfy that check without affecting anything in the processing function. I changed this byte to be 0x07, and changed the data to 0x08 now that it shouldn't be blocked anymore. Putting this data into the challenge server finally resulted in the flag! I ended up sending the following (without the spaces). The first group is the ASM, the second group is the CCSDS TM header (ending in 01 meaning one byte of data follows), and the third group is the data following the header. 1acffc1d 0a2307000001 08 ################################## ### Lessons Learned/Reinforced ### - As with many CTF challenges, stepping away for a bit is always a good idea whenever I get stuck, it usually results in new ideas that are often helpful in getting me un-stuck. - Sometimes it's essential to start working with a new tool mid-CTF (Ghidra in this case), but sometimes it serves as too much of a distraction and wastes too much time when under the deadlines of a CTF (gdb in this case). I need to make sure to evaluate my use of new tools after a bit of time working with them, to make sure I'm not wasting time when I could be using tools I'm already familiar with instead. - Focusing too narrowly too soon caused me to waste some time in this one, I should have looked over all the functions in the challenge binary first rather than just looking at processSatellitePacketBytes.