NorthSec 2021 Badge Writeup
Solving all 10 flags + easter eggs within the NorthSec 2021 CTF badge
To learn more about the badge itself and the development behind the scenes I would recommend watching this YouTube video.
Although NorthSec was remote for the second year in a row the badges still formed the bases of a CTF. However the badge's challenges were completely unrelated to the actual competitive CTF which was one of the highlights from the event as far as I was concerned.
The badge itself hold 10 flags which is really impressive given the memory on the chip and the engineering behind it all. The different categories of the flag are trivia, general problem solving, and reverse engineering. All challenges are independent from each other, so you can look for flags in any order. The lore behind the badge follows the theme of the 2021 northsec: medieval
This was also my first time doing any type of hardware hacking. Because of this I have to thank vicious, Padraignix, and Eric Hogue for giving me gentle hints without spoiling any of the fun. Padraignix and Eric have writeup's of their own. If there is something I explain poorly go check them out and their approach.
Flag #1 / Intro
The first flag is one of those "warmup" flags. There is visibly a chest in the upper left hand side of the screen, visible upon booting up the game.
This is our first flag FLAG-W3lc0m2NSECxx
You submit the flags through the FLAGBOT on the official NorthSec Discord server and by some magic it syncs up to your badge.
Flag #8 / Toms Hut Part 1
I spent a good 20 minutes talking to all the NPC's and going around the map. Eventually I walked into a house presumably owned by someone named Tom.
Inside Tom's hut we are greeted with the following message
I also remember seeing a house with a WiFi symbol on it.
Once you enter that house you see the following:
Unfortunately we're only allowed to toggle the WiFi on and off without any connection to any network being established. Thankfully this badge has a CLI feature that I used to connect to the WiFi using the serial connection.
We'll use dmesg to see if the nsec badge is detected and sure enough it is!
So we'll just connect to it using the screen command to connect to the GUI. Below we see what commands we've got to play with.
Using the commands above we connected ourselves to our home WiFi
Now when you go back to Tom's hut you're welcomed with the following message:
So lets go ahead and get those files and see what Tom's got in store for us.
We will start with the rc101 challenge then solve the rc102. The rc101.zip contains a .elf in a Tensilica Xtensa format. This will be a first for me because I've never touched Xtensa architecture. After a fair amount of googling I was able to find a Ghidra module that could read this type of format.
With this module loaded, everything ran smoothly. So I went ahead and loaded up the app_main
from this we can get a rough idea of whats happening.
Its actually quite straight forward
- 0x400d2360 user supplied input
- 0x400d2381 makes a check to see input is whats expected
- 0x400d2386 if expected, succeed, then load flag in register
- 0x400d2390 if fail, jump to try_again
The Verify function is is kinda long. Essentially our offset is the flag. Just from looking at this function there is a glaring pattern present. If you stare at it for 30 seconds you'll see that there is a jump of 0x4 each time which is then validating our user input (a1). The validation is being performed against values loaded in the a8 to a15 registers.
So what we'll do is take the register values and build it back up sequentially one at a time... maybe there is a sick 1337 way to do it in Ghidra but I did in manually in a notepad. I'm sure there is a smarter quicker way to do this but this will have to do.
This leaves you with 11a922186c46496eb1317c128b361720
which we'll submit to the challenge as user input to receive our flag.
So the flag is: flag-REverSINg_xteNSA_is_NOt_that_HArd
Flag #9 / Toms Hut Part 2
Its not over yet! We still have another reverse challenge to do. So after some light recon we're going to be dealing with more or less the same architecture.
Lets jump in and look at our main.
For a second I thought that I loaded the wrong app because the main looks exactly the same but the verify function is very different.
The way I understand it is that something will move something from an offset of ctx to the register a9. So you are comparing a9 with the ascii of a8 before counting down to the next block just like before. Therefore we're going to move the asii value of a8 then compare both, if they're equal we jump to the next block, and if not it makes a jump, resets ctx and return from the function. This in my head is easier to understand then the previous challenge's verify function.
- move input from ctx to a9
- a8 = 0x66 = f
- compare a9 with 0x66
- if equal then move onto next block
This leaves us with f219e6cdb1fa4a48b160d00d61118f93
which we'll submit to the challenge as user input to receive our flag.
So the flag is: flag-this_is_a_big_huge_enormous_condition
Flag #2 / Konami
I'm actually impressed I thought of this one. After solving the first 3 flags I didn't really know what to solve next but there was a chest in the top left corner of the map that looked inaccessible.
For a while I thought I needed to find a bug with the collision detection but then I remembered this was a video game! And EVERY video game has cheats!... Right? At first I tried the most popular "walking through walls cheats" but had no success with those. However when I googled the most popular cheat codes did I find success.
Perhaps its because its the most popular one? But the Konami/Contra code opened the door on the first try. I'm not quite sure if a different cheat codes would have also worked but once entered a magical door will open!
Just go through the door to the chest to receive the second flag! FLAG-UuDdLrLrBA0000
Flag #5 / Cyber Punk
I kinda got this flag by accident. Right after getting flag #2 I randomly also got flag #4 and I was trying to figure out where it came from. I investigated by talking to the 10 or so NPC's scattered around the map to see if their text options had changed. Eventually I had to chase down the NPC with pink hair named "Punk_" (seen below).
Personally this NPC (like the dog) never stopped walking and my fat fingers stumbled but eventually I got to his ass.
When you talk to this absolute chad he's all about making you prove yourself to him. You need to be cool and have a lot of money, and be 1337... as seen below.
Since I'm none of the above I walked away a little too quickly, hitting the down key and triggering the following.
With that we get flag #5 I still don't know how I got flag #4 though... But we'll figure it out eventually. FLAG-KLJV490uhkEJF28
Flag #7 / 20:20 Vision
Due to the reset button being at the back of the badge accidentally rebooted the game multiple time as it laid flat on my desk.
In doing so I noticed the following message:
So I started going through the alphabet tab-completing like a maniac and eventually it completed to the_sword_of_azeroth! (a world of warcraft reference)
And with this we have the 7th flag FLAG-Cl1F0rFun&Pr0f1t
Flag #6 / Le Lack Du Quack
If you walk around the map you'll run into a pretty mean looking duck chilling in a pond.
He doesn't say much besides quack.
But I noticed that as soon as I started interacting with him that my CLI segfault. I connected to the CLI in a few different ways as seen below.
minicom --device /dev/ttyUSB0
screen /dev/ttyUSB0 115200
tio /dev/ttyUSB0
picocom -b 115200 /dev/ttyUSB0
But they all left me with a similar shell.
It appears that there was output before it segfaults we see it display "quickquickquick" which kinds sounds like "quack", the noise a duck would make. So I went ahead and piped this into a file.
At first I though my terminal was broken but I kept getting the same weird output. I ended up installing a new VM but nothing changed.
Its only when I decided properly check the end of those lines that I realize maybe there might be a message here...
After removing all the gunk I was left with this nice poem.
quackquackquickquack
quickquickquack
quickquack
quackquickquackquick
quackquickquack
quickquick
quackquack
quickquack
quackquickquick
quickquickquack
quackquickquackquick
quackquickquack
quack
quackquackquack
quackquackquick
quick
quack
quackquickquickquick
quickquackquick
quick
quickquack
quackquickquick
quickquickquack
quickquickquick
quick
quack
quickquickquickquick
quickquick
quickquickquick
quickquickquackquick
quickquackquickquick
quickquack
quackquackquick
quackquickquickquick
quackquick
quickquickquickquack
quackquackquickquick
quackquickquack
quickquack
quickquackquack
quackquack
quickquickquick
quackquickquackquack
quickquickquick
Now there are only two words present quick and quack and some spaces. So maybe the spaces before the newlines are a fluke and they're not suppose to exist. If they are then I'm thinking morse code could be a solution. Also maybe also we can turn the 0's and 1's into text.
Now when I was talking to some of the NPC's one of them really triggered me by what he said...
Everyone knows that PHP never left, and will never be die. So I wrote a little script in PHP to get us the output in 0's and 1's and later the mores code.
<?php
$data = "quackquackquickquackquickquickquackquickquackquackquickquackquickquackquickquackquickquickquackquackquickquackquackquickquickquickquickquackquackquickquackquickquackquickquackquackquackquackquackquackquackquickquickquackquackquickquickquickquickquackquickquickquickquackquackquickquickquickquickquackquickquickquickquickquackquickquickquickquickquickquickquickquickquickquickquickquackquickquickquackquickquickquickquackquackquackquickquackquickquickquickquackquickquickquickquickquackquackquackquickquickquackquickquackquickquackquickquackquackquackquackquickquickquickquackquickquackquackquickquickquick" ;
$data = str_replace( "quick" , "0" , $data);
$data = str_replace( "quack" , "1" ,$data);
echo $data = str_replace( " " , "." ,$data);
?>
This left us with two combinations:
1101001011010101001101100001101010111111100110000100011000010000100000000000100100011101000100001110010101011110001011000
0010110100101010110010011110010101000000011001111011100111101111011111111111011011100010111011110001101010100001110100111
This didn't really amount to much and cyber-chef sent me the following sms.
Its at this point that I realized that perhaps my gut feeling was right and went ahead to confirm my initial suspicion. The new lines did act as spaces after all. Morse code needs spaces so I made some tweaks to my robust PHP code and obtained the following message as seen below:
"QUACK IM A DUCK TO GET BREAD USE THIS FLAG BNVZKAWMSYS" so the flag is therefore FLAGBNVZKAWMSYS
Flag #3 / A Fisherman's Downfall
It at around this time that I start feeling like I'm going to have to swim in the deep end of the pool. Again I have no real hardware reversing experience. One thing I do want to do is extract all the textures from the application and see if I can reconstruct the map. But before we get to that there is a female NPC which gives us our next hint to a flag.
Whats important to note is that the NPC's don't ask questions they all speak in funny infosec anecdotes.
At this point I thought that I needed to render the other side of the shore and that maybe a flag was printed on it. In case it isn't obvious as far as I know the shore isn't visible / inaccessible by our character. Also we cant swim. I was a little stumped at this point so I had to ask for a light hint.
After some light googling I fell on this GitHub which looked to be exactly what I was looking for. As can be seen below I had a lot of issues in dumping the flash. It didn't seem to matter what I ran or how I ran it...
For the next several hours I kept failing and nothing was working. Some progress was made along the way however.
We know that the firmware is ~16M thanks to the function above. Then I just started playing around, lowering the Baud rate, switching cables and USB ports which proved to have its successes but no cigar.
At this point it seemed like dumping the whole firmware was impossible. I spend roughly 4h on this trying different techniques but nothing worked. Mentally I was here:
I started asking around on discord. The general consensus was that the CH340 chip wasn't the most reliable when it came to this type of thing. Thats when a hero named Padraignix came to my aid. He told me that he had a very similar experience and only got a full dump on his ~40th attempt. So not all hope was lot. After harping on discord for a while longer another hero appeared named vicious who laid down some general tips.
But it seems like I could reliably grab around 2MB of the firmware.
So lets just try and get 1MB and be happy...
And after all that struggle we got a flag?!?! FLAG-JTAGPower0verwhelming
Turns out this is just a bonus flag. Which makes sense because we're on the fisherman woman quest-line. Since I want to parse the textures I was able to find a open source tool that should get the job done. So first we'll list all of the available partition.
The only partition that really interests me is storage and perhaps factory. So lets use the same tool with the help of some flags to
Lets just use binwalk because that's the next logical thing to do really, lets hope there are a sufficient amount of images within.
At this point I thought that maybe I could get the firmware from the github? Since this is northsec and we're past the end of the CTF they should have open sourced it.
I was right, but the thing I didn't expect to see was the full map of the game!!!
And we can see in the bottom right corner that there is a chest! Now... listen... I know this is kind of "cheating" since this wasn't available during the actual CTF... but hey I cant dump the whole firmware to solve it the indented way (so far). Anyways from this image I think that we should be able to go out of bounds either at the bottom right of the map near the stone wall, or at the top right near the trees.
After some messing around I was right and you can go out of bounds right around here:
After going out of bounds you keep going down and start mashing ENTER and you'll open the chest.
So the flag is FLAG-MfoAkJu0TtD36
Now that was kinda cheating so I will also solve it the intended way. Since I wasn't able to dump the firmware myself ill yoink it from the GitHub.
Now that we have the firmware we will replete the previous steps. We'll start of by seeing what partitions we're dealing with and our output should be the same as before.
Now we have another binary file but I don't really know how to read it / what it really is so like any hacker we'll just fuck around and find out. Binwalk to the rescue!
Alright so lets just extract the images and see if we could have figured this without "cheating"
... Wait so they're corrupted?
At this point I wasn't quite sure what to do... so I went back to cheating a.k.a looking at the GitHub to try and get an understanding of the ESP32, and how it stores information and perhaps even if it has a filesystem / hierarchy. I did a lot of googling based off of what I saw in the GitHub... Like a lot. But one thing stood out was "spiffs".
Basically the ESP32 uses SPIFFS instead of storage cards like SD cards. This means that with SPIFFS, you can write your code in a separated file and save them on the ESP32 filesystem using magic. SPIFFS lets you access the flash memory like you would do in a normal filesystem in your computer, but simpler and more limited. Basically, you can read, write, close, and delete files. So it makes sense that binwalk could recognize some of the file structure but for a reason I don't genuinely fully understand wasn't able to extract the images successfully. My guess is because its in an "unknown state" where only headers are being recognized without their entire contents being present.
After even more research I discovered a tool called mkspiffs that essentially unpack or list all the files in the spiffs. Now this was a pain in the ass to configure and I don't know what my final make command looked like but finding the proper flags took me around 30 minutes to get right. Basically from the storage dump we're going to rebuild a directory structure in a folder named storage. note that a 1024 fs page size in bytes was the only thing that worked for me.
Now I spend a long 30 minutes just looking around running shitty forensics tool to get an idea of what I had. Through a hint from our previous hero Padraignix he told us to look into rpg which had 2 files. I didn't know what to make of this at fist but it almost seemed like a bit map I just needed to find the right dimensions.
What we end up getting is the following
As you can see it somewhat makes sense. But it feels like the areas that should be empty are filled with 0's and the areas that are full are filled with 1's. For example the castle at the bottom of the map doesn't make much sense in this bit array.
After A LOT, and I mean A LOT of fuckin about I finally got a map that made sense. With the path that we would have expected.
And I noticed something interesting with the castle there appears to be a way to get inside the castle!!!
Now there wasn't any flags within the castles but there was an "Easter egg" that made me shit my pants.
When you go into the building there a weird sound then your game just corrupts perpetually the more you move. I seriously thought I destroyed my badge and I was going to have to find a way to reinstall the firmware. But thankfully after the reboot everything was fine.
Flag #10 / I am 1337
I should remind everyone that at this point I am way out of my comfort zone. This flag can be best described by the following image.
During the previous flag when I was digging quite deep into the source code and I ended up learning a lot about the ESP32. When I was looking for anything pertaining to memory and files I fell face to face with the NVS (non volatile storage) term quite often. There is actually a library that goes in depth into NVS and ESP32.
Thankfully the esp32_image_parser.py that we've used before has a function to dump nvs. Ill make a quick note that I was given a 3 letter hint "NVS" which is one of the main reasons I investigated this avenue. So we will read the NVS partition from the full firmware dump and go from there. The way I understand it the goal of this exercise would be to read the key pair since NVS is essentially just a storage of key pairs.
Initially I was running shit like this and the output was making no sense
But thankfully I went back to read the documentation and was gifted with this snippet of information.
Now I have output that makes more sense?
One thing that was annoying is that you couldn't pipe this into jq or npx so I literally had to use an online tool to make this readable.
So after A LOT of pain I finally started understand. Basically when the flash is initialized, the page headers have been written to flash. These page have a valid sequence number. Pages that have some empty entries is where data can be written. No more than one page can be in this state at any given moment. So we need to find the right entry in NVS in which the flag counter is saved / data is written. In theory this should be where our "bitmap state: Written" entry is so we should understand what needs to be modified as far as data when we dump the NVS.
Again, the way I understand it is that I need to find the right entry since its just a key pair, therefore dumping the NVS partition will be our way to read them. From this point I'm assuming we need to patch it and push it to the firmware with a flag trigger that gives us the 10th flag.
Here is the catch 22. I was not able to dump my firmware myself so I had to use the firmware from the GitHub. That firmware doesn't have any flags solved and therefore saved in its NVS. The issue is that there is no way for me to know how to set a flag if no flags are set in the NVS.
I had no choice at this point to have someone help me. Thankfully a hero by the name of EricHogue came to my rescue. And gave me a firmware dump with 1 flag.
Then I ran python3 esp32_image_parser.py dump_nvs -partition nvs nsec2021.bin
and python3 esp32_image_parser.py dump_nvs -partition nvs ~/Desktop/badge.bin
to compare the two outputs.
nsec2021.bin (from GitHub) | badge.bin (from EricHogue) |
Line 15: Key : storage Line 25: Key : misc Line 36: Key : log Line 50: Key : log Line 63: Key : nvs.net80211 Line 74: Key : opmode Line 85: Key : sta.ssid Line 101: Key : sta.ssid Line 115: Key : sta.authmode Line 126: Key : sta.pswd Line 144: Key : sta.pswd Line 158: Key : sta.pmk Line 173: Key : sta.pmk Line 187: Key : sta.chan Line 198: Key : auto.conn Line 209: Key : bssid.set Line 220: Key : sta.bssid Line 234: Key : sta.bssid Line 248: Key : sta.lis_intval Line 259: Key : sta.phym Line 270: Key : sta.phybw Line 281: Key : sta.apsw Line 295: Key : sta.apsw Line 309: Key : sta.apinfo Line 366: Key : sta.apinfo Line 380: Key : sta.scan_method Line 391: Key : sta.sort_method Line 402: Key : sta.minrssi Line 413: Key : sta.minauth Line 424: Key : sta.pmf_e Line 435: Key : sta.pmf_r Line 446: Key : sta.btm_e Line 457: Key : sta.rrm_e Line 468: Key : ap.ssid Line 484: Key : ap.ssid Line 498: Key : ap.passwd Line 516: Key : ap.passwd Line 530: Key : ap.pmk Line 545: Key : ap.pmk Line 559: Key : ap.chan Line 570: Key : ap.authmode Line 581: Key : ap.hidden Line 592: Key : ap.max.conn Line 603: Key : bcn.interval Line 614: Key : ap.phym Line 625: Key : ap.phybw Line 636: Key : ap.sndchan Line 647: Key : ap.pmf_e Line 658: Key : ap.pmf_r Line 669: Key : lorate Line 680: Key : country Line 694: Key : country Line 708: Key : opmode Line 718: Key : phy Line 729: Key : cal_data Line 818: Key : cal_data Line 884: Key : cal_data Line 898: Key : cal_mac Line 912: Key : cal_mac Line 926: Key : cal_version Line 937: Key : opmode |
Line 15: Key : namespace_name Line 26: Key : key1 Line 294: Key : cal_data Line 359: Key : cal_data Line 372: Key : cal_mac Line 385: Key : cal_mac Line 398: Key : cal_version Line 408: Key : opmode Line 418: Key : save Line 433: Key : save Line 1418: Key : storage Line 1428: Key : misc Line 1439: Key : log Line 1453: Key : log Line 1466: Key : nvs.net80211 Line 1477: Key : opmode Line 1488: Key : sta.ssid Line 1504: Key : sta.ssid Line 1518: Key : sta.authmode Line 1529: Key : sta.pswd Line 1547: Key : sta.pswd Line 1561: Key : sta.pmk Line 1576: Key : sta.pmk Line 1590: Key : sta.chan Line 1601: Key : auto.conn Line 1612: Key : bssid.set Line 1623: Key : sta.bssid Line 1637: Key : sta.bssid Line 1651: Key : sta.lis_intval Line 1662: Key : sta.phym Line 1673: Key : sta.phybw Line 1684: Key : sta.apsw Line 1698: Key : sta.apsw Line 1712: Key : sta.apinfo Line 1769: Key : sta.apinfo Line 1783: Key : sta.scan_method Line 1794: Key : sta.sort_method Line 1805: Key : sta.minrssi Line 1816: Key : sta.minauth Line 1827: Key : sta.pmf_e Line 1838: Key : sta.pmf_r Line 1849: Key : sta.btm_e Line 1860: Key : sta.rrm_e Line 1871: Key : ap.ssid Line 1887: Key : ap.ssid Line 1901: Key : ap.passwd Line 1919: Key : ap.passwd Line 1933: Key : ap.pmk Line 1948: Key : ap.pmk Line 1962: Key : ap.chan Line 1973: Key : ap.authmode Line 1984: Key : ap.hidden Line 1995: Key : ap.max.conn Line 2006: Key : bcn.interval Line 2017: Key : ap.phym Line 2028: Key : ap.phybw Line 2039: Key : ap.sndchan Line 2050: Key : ap.pmf_e Line 2061: Key : ap.pmf_r Line 2072: Key : lorate Line 2083: Key : country Line 2097: Key : country Line 2111: Key : opmode Line 2121: Key : phy Line 2132: Key : cal_data |
As you can see there is a major difference! What stood out the most was a save key and the difference between the flags.
Entry 34
Bitmap State : Written
Written Entry 34
NS Index : 2
Type : BLOB_DATA
Span : 3
ChunkIndex : 0
Key : save
Blob Data :
Size : 36
Data :
00000000: 00 00 00 00 01 00 00 00 01 01 00 00 01 00 19 00 ................
00000010: 01 00 00 00 FF 00 FF 00 01 00 00 00 00 00 00 00 ................
00000020: 00 00 00 00 ....
Entry 37
Bitmap State : Empty
Written Entry 37
NS Index : 2
Type : BLOB_IDX
Span : 1
ChunkIndex : 255
Key : save
Blob IDX :
Size : 36
Chunk Count : 0
Chunk Start : 255
Now I was also able to obtain a firmware with 2 flags solved. And the solution because obvious as seen below.
Therefore to get all of the 10 flags should be this bitmap state's data should be:
00 00 00 00 01 00 00 00 00 00 01 01 01 00 19 00 01 00 00 00 ff 00 ff 00 01 01 01 01 01 00 01 01 01 01 00 00
So lets grab that copy of Erics's firmware, perform a NVS dumps, flipped the corresponding two bits and then write to flash.
Then ill open this in a hex editor and flip all those bits.
Now with that done we will write our new NVS to the flash of the badge and fucking pray to all gods that I don't destroy my badge.
Holly shit we did it!
Flag #6 / Who's Knocking?
This flag was literally hell. IT took me way to long to figure out what was going on. In case you don't know me that well I'm not great at WiFi pentests. I haven't done a lot of them and its not really an area that interests me. I don't have a WiFi card so the only way I could solve this challenge was by booting kali off of a USB. I spent roughly 3 solid days trying to make a workaround for my VM but VirtualBox simply cant do it.
Due to flag 8 and 9 (the reverse challenge) and having to connect to WiFi I knew there could be something funky going. My gut feeling was that I needed to capture some WiFi transmission. At first I thought that maybe the badge's interface is transmitting frames on lets say channel X and I just had to figure it out. When that was disprove I thought that maybe the badge had a secret SSID but that was also false. There were a lot of other theories that were tested and disproven.
This was clearly not one of my best moments, but it worked...
I need my setup to be this way since I need to create a hotspot WiFi from the same WiFi connection but since I couldn't do that because I don't own a USB WiFi card I created a WiFi connection from my a wired connection. Thanks to a stack overflow comment I discovered a tool called create_ap as seen below.
This created a ap0 interface which was on 198.51.100.42 which is what I needed. With this I will be able to send all the traffic into wireshark and know that whatever the badge would send once it established a connection I would receive.
Then I used the badges CLI to join the AP I just created.
And out of the short capture I noticed that once the badge authenticated to the IP
I would see that the badge was trying to reach out to 198.51.100.172 on port 4444 and even if someone doesn't know whats going on and is drunk on whisky 4444 screams reverse shell.
I setup a netcat listener on my localhost and sure enough after a few seconds I caught the connection
Our final flag of the journey is FLAG-spOkeTh3Hors