DEFCON 25 HHV Challenge - Part 3: The Firmware

NEON 12F1572

The Firmware

After several iterations I was able to get a complete disassembly of the firmware. My disassembler doesn’t disassemble every single instruction in the ISA, but it disassembles all the instructions in this firmware.

If you really want it, or just want to see how it works, you can find it here. I don’t make any claims about its code quality. It started off as a “quick test” and then morphed into the main tool I used.

At this point I also decided to scroll through the firmware just to see what’s in it, and at this point came across this:

[2FD]     RETLW       04 => .         	; Return with Literal in W
[2FE]     RETLW       42 => B         	; Return with Literal in W
[2FF]     RETLW       75 => u         	; Return with Literal in W
[300]     RETLW       69 => i         	; Return with Literal in W
[301]     RETLW       6C => l         	; Return with Literal in W
[302]     RETLW       64 => d         	; Return with Literal in W
[303]     RETLW       20 =>           	; Return with Literal in W
[304]     RETLW       46 => F         	; Return with Literal in W
[305]     RETLW       65 => e         	; Return with Literal in W
[306]     RETLW       61 => a         	; Return with Literal in W
[307]     RETLW       74 => t         	; Return with Literal in W
[308]     RETLW       75 => u         	; Return with Literal in W
[309]     RETLW       72 => r         	; Return with Literal in W
[30A]     RETLW       65 => e         	; Return with Literal in W
[30B]     RETLW       73 => s         	; Return with Literal in W
[30C]     RETLW       3A => :         	; Return with Literal in W

Ah ha! This solves the string mystery. They were encoded as RETLW instructions. After looking through the datasheet this is turns out to be one of two methods for accessing tables of data in program memory where you use the BRW instruction along with a counter, to continuously jump to another table entry and back. The other method which is the one the developer (or compiler) chose to use is to use the indirect memory access functionality INDF, which we’ll cover later.

Looking further, we see several strings that aren’t seen during startup or in the help menu.

[41E] lol, nice try though!
[50D] 33123121
[516] 41241342
[51F] 41323414
[528] unlockit

It looks like those codes represent the four buttons on the board. After trying them, I got different results but nothing spectacular happened. In the case of typing 14323414 we get the string lol, nice try though in the UART console.

There are a number of places we could start, such as the entry address is at 0x0 which looks something like this:

[00]      GOTO        3A              	; Go to address
// ..snip..
[3A]      GOTO        3B              	; Go to address
[3B]      CLRF        7B                ; Clear the contents of f and set the Z bit.
[3C]      MOVLW       20              	; Move literal to W.
// ...

We see that the entry address immediately jumps to 0x3A which immediately jumps to 0x3B.

Why does it issue two jumps? Honestly, I have no idea. Somethings will always remain a mystery.

Then we clear f:0x7B and start some processing. This is a perfectly plausible place to start, but the firmware quickly starts using pieces of data that I needed more context for to figure out what they were. Therefore, I decided to take a different approach before digging into main.

I started with the interrupt handler. The PIC12F1572 has only one interruption handler, and that handler lives at address 0x4. Since we know that things happen when we interact with the board, either through UART or the buttons, then we can work from the hardware MMIO, and interrupt flags, upward to gain meaning behind different events.

In the github repo, you can check out all of my notes inside of output.asm, but for now I’ll just highlight some key portions of the interrupt handler.

/* Branch if there's no receive interrupt */
[09]      BTFSS       f:PIR1, B:RCIF == 0	; Bit Test f, Skip if Set
[0A]      GOTO        26              	; Go to address

/* Branch if the recieve interrupt is not enabled. */
[0B]      MOVLB       01              	; Move literal to BSR.
[0C]      BTFSS       f:PIE1, B:RCIE == 0	; Bit Test f, Skip if Set
[0D]      GOTO        26              	; Go to address

/* Branch if there is no receive overrun. */
[0E]      MOVLB       03              	; Move literal to BSR.
[0F]      BTFSS       f:RCSTA, B:OERR == 0	; Bit Test f, Skip if Set
[10]      GOTO        1A              	; Go to address

These instructions handle a UART receive interrupt, i.e. the user has typed a new character.

We start by checking if the receive interrupt bit is set, the interrupt is enabled, and that there’s no receive buffer overrun. Then jump to 0x1A to save off the character received:

[1A]      MOVF        W = f:RCREG     	; Move f
[1B]      MOVWF       f:70 = W        	; Move W into F
/* f:70 is the most recent received character. */
[1C]      MOVF        W = f:70        	; Move f
/* Save a copy to 2B */
[1D]      MOVLB       00              	; Move literal to BSR.
[1E]      MOVWF       f:2B = W        	; Move W into F

These instructions save off that character into f:2B. From now on, we know that f:2B is a new UART character. The next few instructions check if what we typed was a return character (0x0D) or some other character.

[21]      BTFSS       f:STATUS, B:Z == 0	; Bit Test f, Skip if Set
[22]      GOTO        25              	; Go to address

/* 7B[04] indicates we have a new character. */
[23]      BSF         f:7B[04]        	; Bit set on F
[24]      GOTO        26              	; Go to address

/* 7B[06] indicates that "enter" was pressed */
[25]      BSF         f:7B[06]        	; Bit set on F

Now we can see that f:7B is going to be used as a bit field that will indicate some kinds of state, in this case, bit 4 will indicate that we have a new character we need to process and bit 6 will indicate that the user pressed “enter” and we need to process the command at this point.

[29]      MOVLB       01              	; Move literal to BSR.
[2A]      BTFSS       f:PIE1, B:ADIE == 0	; Bit Test f, Skip if Set
[2B]      GOTO        35              	; Go to address
/* Clear the interrupt. */
[2D]      BCF         f:PIR1, B:ADIF = 0	; Bit clar f
/* Get the Value of the ADC */
[2E]      MOVLB       01              	; Move literal to BSR.
[2F]      MOVF        W = f:ADRESEH   	; Move f
[30]      MOVWF       f:70 = W        	; Move W into F
[31]      MOVF        W = f:70        	; Move f
[32]      MOVLB       00              	; Move literal to BSR.
[33]      MOVWF       f:2A = W        	; Move W into F
/* 7B[07] Indicates we have a new ADC reading */
[34]      BSF         f:7B[07]        	; Bit set on F

This code processes the ADC (button presses). From the code above we can see that f:7B bit 7 indicates that a new button was pressed, and f:2A will contain the ADC value.

So up to this point we can start to develop a table of values we can use in the main logic:

  • f:2A - Most recent ADC value
  • f:2B - Most recently typed UART character
  • f:7B
    • bit 4 - There was a new character typed into UART
    • bit 6 - Return character was received
    • bit 7 - There’s a new ADC value available for processing

That’s pretty much the end of the interrupt handler, let’s start looking at address 0x3A or as I’ll call it, main

I’m going to give you the highlights of what the beginning part of main does as most of it is initialization.

One of the first things that main does is sets the FSR0L and FSR0H registers.

[3C]      MOVLW       20              	; Move literal to W.
[3D]      MOVWF       f:FSR0L = W     	; Move W into F
[3E]      MOVLW       00              	; Move literal to W.
[3F]      MOVWF       f:FSR0H = W     	; Move W into F
[40]      MOVLW       15              	; Move literal to W.
[41]      CALL        548             	; Call Routine

We haven’t talked about these yet, but these are used in indirect addressing and will be used a lot in main. In this case, the two registers are combined to create an address that can be referenced, in the case of address 0x3D this address is 0x0020. Note that these are the absolute addresses you see on the right-hand side of the memory bank chart, meaning you don’t have to switch memory banks to access these locations.

Then an INDF0 instruction (or INDF1 if FSR1* is used), will allow you to read or write from that address. Looking back to our memory map, we can see that this is the start of general-purpose memory.

Using this technique, we can also refer to program memory by setting the top bit of FSR1 to 1. This means we aren’t interacting with something in a memory bank, something in our code. We’ll see this used a lot when we start printing strings.

Finally, at 0x41 a call is issued. What this function does doesn’t matter, but this is a good place to talk about the calling convention. There is no calling convention defined by the ISA, but the compiler uses W as the first parameter if there’s only one parameter. Otherwise, the compiler will pick some general-purpose memory to use as its arguments. Sadly, it’s nothing as clean as ARM, or even x86. IN the example above, the value 0x15 is being passed as the first parameter to 0x548, which is a routine that simply clears (writes 0’s) the general-purpose memory starting at 0x0020 for W number of bytes.

The following steps and routines initialize the board:

  • The clock and pins are configured (0x4C1)
  • UART is set up (0x4DE)
  • The ADC is set up (0x436)
  • Clear the OPTION_REG and clear the timer
  • Write the string “SecureLock…” to UART (0x405)
  • Write the string “Build Features…” to UART (0x405)
  • Write the string “Rev …” to UART (0x405)
  • Write the string “Initializing …” to UART (0x405)
  • Blink the green LED (0x4D0) 6 times, and write a . (0x539) to UART. I found this funny because the chip isn’t doing anything special except sleeping.
  • Print the string “System Started”

Now we enter the main loop. It starts womewhere around 0x169:

/* Loop back to here from 223 */
[169]     BTFSS       f:7B, B:06 == 0 	; Bit Test f, Skip if Set
/* Take this jump if we have not seen a newline from UART */
[16A]     GOTO        17D             	; Go to address

/* Run this code if "Enter" has been pressed. */
/* Unset the bit indicating that enter was set. */
[16B]     BCF         f:7B, B:06 = 0  	; Bit clar f
[16C]     MOVLW       08              	; Move literal to W.
[16D]     MOVLB       00              	; Move literal to BSR.
/* f:38 starts off at 0. See 46 */
[16E]     XORWF       W = f:38 XOR W  	; XOR W with f
[16F]     BTFSC       f:STATUS[Z]    	; Bit Test f, Skip if Clear
/* Take this jump, if the number of characters entered so far is 0. */
[170]     GOTO        17D             	; Go to address

The first thing that is checked is the 6th bit in f:7B, which we’ve already identified as being a flag indicating that a return character was pressed. In this case if we do not have that flag set we will skip the rest of the logic and go straight to 0x17D. It’s likely that 0x17D is the end of any UART input processing, since this flag being unset skips to that point.

The bit is then cleared, and we check f:38 which up to this point hasn’t been identified yet, but early on in main gets set to 0. Judging from context this is likely a counter for the number of characters that have been typed. Beyond that we don’t know how that value is used.

As mentioned before 0x17D likely ends the UART processing section of the code, so let’s look at the instructions up to that point:

/* This logic saves off a character if one has been received and not processed.*./
/* 2B contains the most recent character. */
[171]     MOVF        W = f:2B        	; Move f
[172]     MOVWF       f:36 = W        	; Move W into F
[173]     MOVF        W = f:38        	; Move f
[174]     ADDLW       W = W + 2C      ; Add literal and W
/* FSR1L = 0x2C */
[175]     MOVWF       f:FSR1L = W     	; Move W into F
[176]     CLRF        f:FSR1H              ; Clear the comments of f and set the Z bit.
/* Move this character into 2C[i] where i comes from f:38 */
[177]     MOVF        W = f:36        	; Move f
[178]     MOVWF       f:INDF1 = W     	; Move W into F
[179]     MOVLW       01              	; Move literal to W.
[17A]     MOVWF       f:36 = W        	; Move W into F
[17B]     MOVF        W = f:36        	; Move f
[17C]     ADDWF       f:38 = W + f:38        ; Add W and F

This logic assumes that the 6th bit of f:7B was set and that the return character was not pressed. We read from f:2B, which we’ve identified as the latest character received from the board, which gets copied to f:36. Then we load in our number of characters received (f:38) into W and add it to the mysterious value 0x2C.

Looking at the next couple of lines reveals that the code is attempting to add the number of characters to the offset 0x2C and use indirect addressing to write the newest character. You can think of this code as doing something like this:

// Located at f:2B
char latest_rx;
// Located at f:38
int num_characters_received;

// Located at 0x002C 
char rx_buffer[8];

rx_buffer[num_characters_received] = latest_rx;

This means that 0x38 is an index into the 0x2C array, indicating where to place the next character.

Notice that the indirect addressing doesn’t start with a 0x80, meaning that the reference to 0x2C + index won’t reference program memory, but reference general purpose memory as shown in the memory bank mapping.

After the code increments the f:38 value, now come to a crossroads: If there are no ADC values, we jump to location 0x222, and process the UART information. If there are ADC values, then we process them. As we are already looking at UART, let’s skip the UART code for now and continue onto location 0x222 where we process input.

/* Check for a new receive on UART. */
[222]     BTFSS       f:7B, B:04 == 0 	; Bit Test f, Skip if Set
[223]     GOTO        169             	; Go to address
[224]     CALL        233             	; Call Routine
[225]     BCF         f:7B, B:04 = 0  	; Bit clar f
[226]     MOVLB       00              	; Move literal to BSR.
[227]     MOVF        W = f:38        	; Move f
[228]     BTFSC       f:STATUS[Z]    	; Bit Test f, Skip if Set
[229]     GOTO        169             	; Go to address
[22A]     MOVLW       01              	; Move literal to W.
[22B]     MOVLB       00              	; Move literal to BSR.
[22C]     SUBWF       f:38 = f:38 - W       ; Subtract W from f
[22D]     MOVF        W = f:38        	; Move f
[22E]     ADDLW       W = W + 2C      ; Add literal and W
[22F]     MOVWF       f:FSR1L = W     	; Move W into F
[230]     CLRF        f:07              ; Clear the comments of f and set the Z bit.
[231]     CLRF        f:01              ; Clear the comments of f and set the Z bit.
[232]     GOTO        226             	; Go to address

We once again check if we need to handle any new characters from UART. If not, we loop back up to 0x169 where our main processing loop started.

The above code looks a little trickier than it really is. The first part aborts and goes to the top of the main loop if we don’t have new data on UART, and then there’s a call to 0x233. We’ll get to that in a moment, but for now just know that 0x233 is responsible for identifying the typed command. And finally, the majority of the instructions are dedicated to clearing out the 0x002C memory location to reset the buffer as this only gets executed when a return character is typed.

The function at 0x233 is a simple command router. It calls the string comparison function at 0x399. Against several strings in program memory. For example, the program memory string “help” is located at 0x54E. The string comparison function directly checks the UART buffer at 0x002C against this passed in value. The pseudo code looks something like this:


char rx_buffer[8];

// astris stored in f:73, f:72
// rx_buffer is stored in W
// At this point f:72 and f:73 are OR'd together. These must be 1 in order
// for the strings to be a match.
bool strcmp(char* str, char* rx_buffer) {
	// ... typical string comparison stuff ...
}

FUN_233_cmd_router() {
	if (strcmp(cmd, "help")) {
		// Print the help commands.
		0x7b[3] = 1;
	}
	if (strcmp(cmd), "ledtest")) {
		// Blink the LEDs.
		0x7b[3] = 1;
	}
	if (strcmp(cmd), "lockit")) {
		0x7b[2] = 0;
		0x7b[3] = 1;
			
	}
	if (strcmp(cmd), "unlockit")) {
		0x7b[2] = 1;
		0x7b[3] = 1;
	}
	if (0x7b[3] == 0) {
		// Print "Unknown Command"
	}
}



And here we see some interesting stuff. The first thing that stands out are the two strings that we saw previously in the firmware dump, but not in the help menu, lockit and unlockit. In fact, we never saw lockit as a discrete string, and the developer cleverly hid the command by indexing two bytes into the unlockit string.

Finally, we can see two new additions to the 0x7b bit field, we can fill out our table a bit more:

  • f:7B
    • bit 2 - Set to 1 if the board is in the unlocked state, and 0 if the board is in the locked state.
    • bit 3 - Set to 1 if a command was matched, 0 otherwise. If zero, we print “unknown command”
    • bit 4 - There was a new character typed into UART
    • bit 6 - Return character was received
    • bit 7 - There’s a new ADC value available for processing

Now a new bit seems interesting, this unlock bit. Remember when I said in the introduction to this project that I didn’t remember there being any instructions? This might have been the point where they came in handy, as we’ll see in just a moment.

Let’s go back to the location in the code where we ran into our crossroads, and let’s look at the code that handles the ADC. The next chunk of code checks bit 7 of f:7B to indicate if there are any new ADC values.

[17D]     BTFSS       f:7B, B:07 == 0 	; Bit Test f, Skip if Set
[17E]     GOTO        222             	; Go to address
[17F]     BCF         f:7B, B:07 = 0  	; Bit clar f
[180]     MOVLB       00              	; Move literal to BSR.
[181]     MOVF        W = f:2A        	; Move f
[182]     CALL        298             	; Call Routine

The most recent value from the ADC, is stored into 0x2A from the interrupt controller, and the function 0x298 as its own parameter.

I won’t go into the details of what 0x298 does, and save it as an exercise for the reader to practice reverse engineering this ISA. Instead, I’ll give you a high-level summary of what it’s doing to help guide you:

  1. Correlate the ADC reading to which button was pressed.
  2. Convert that ADC value to an ASCII number representing the button pressed (‘1’ for the far left, ‘4’ for the far right)
  3. Toggle the red LED
  4. Save the most recent ADC ASCII value into program memory at 0x0020[f:29] where f:29 is the number of buttons that have been pressed.

When the function returns, we check to see if f:29 is equal to 8. Since f:29 is incremented every time we press a button, this would mean we have 8 total inputs.

This is looking familiar to the three number strings we found earlier.

Maybe one of them does something interesting.

[50D] 33123121
[516] 41241342
[51F] 41323414

Each one contains only numbers 1-4 and are all 8 numbers long.

Continuing through the assembly we see the ADC and the UART interrupts are disabled, and we see a check for the unlockit bit.

[18B]     BTFSS       f:7B, B:02 == 0 	; Bit Test f, Skip if Set
[18C]     GOTO        1C8
/* In this case, the unlockit bit has been set. */
/* 516 = 41241342 */
[18D]     MOVLW       16              	; Move literal to W.
[18E]     MOVWF       f:72 = W        	; Move W into F
[18F]     MOVLW       85              	; Move literal to W.
[190]     MOVWF       f:73 = W        	; Move W into F
[191]     CALL        343             	; Call Routine
[192]     XORLW       W = W XOR 00    	; XOR W and literal
[193]     BTFSC       f:STATUS[Z] == 1 	; Bit Test f, Skip if Clear
[194]     GOTO        1A9             	; Go to address

If we are currently in a locked state (f:78 B:02 == 0) then we go to 0x1C8. Since we start up in that state, let’s follow it for now.

[1C8]     MOVLW       1F              	; Move literal to W.
[1C9]     MOVWF       f:72 = W        	; Move W into F
[1CA]     MOVLW       85              	; Move literal to W.
[1CB]     MOVWF       f:73 = W        	; Move W into F
/* The values in f:72 and f:73 51F go to 14323414. */
/* If we type in 1432341, before being unlocked, do this. */
[1CC]     CALL        343             	; Call Routine
[1CD]     XORLW       W = W XOR 00    	; XOR W and literal
/* Jump to 1D5 if no match. */
[1CE]     BTFSC       f:STATUS[Z] == 1   ; Bit Test f, Skip if Clear
[1CF]     GOTO        1D5             	; Go to address
[1D0]     MOVLW       1E              	; Move literal to W.
[1D1]     MOVWF       f:73 = W        	; Move W into F
[1D2]     MOVLW       84              	; Move literal to W.
[1D3]     MOVWF       f:74 = W        	; Move W into F
/* Prints "lol, nice try though" */
[1D4]     CALL        405             	; Call Routine

In the case we are not unlocked, we will call a new function 0x343. This function is responsible for checking our typed button sequence against a sequence passed into the function via f:72, f:73. This function will return a 1 if the sequence matches and a 0 if they don’t match.

If we follow the case where we do match the 14323414 sequence, we will always get the message “lol, nice try though”. So this code seems to be a red herring.

I didn’t realize this until the end, but that sequence is the version number that’s spit out through the UART. I guess it was put there in case someone tried that as the unlock code.

A similar pattern repeats for the sequence 33124121:

[1D5]     MOVLW       0D              	; Move literal to W.
[1D6]     MOVWF       f:72 = W        	; Move W into F
[1D7]     MOVLW       85              	; Move literal to W.
[1D8]     MOVWF       f:73 = W        	; Move W into F
/* Check against 33124121 */
[1D9]     CALL        343             	; Call Routine
[1DA]     XORLW       W = W XOR 00    	; XOR W and literal
[1DB]     BTFSC       f:STATUS[Z] == 1    	; Bit Test f, Skip if Clear
[1DC]     GOTO        1F2             	; Go to address
/* Set the unlock bit */
[1DD]     BSF         f:7B[02]        	; Bit set on F
[1DE]     MOVLW       01              	; Move literal to W.
[1DF]     MOVLB       00              	; Move lite

In this case something useful happens. If we did type 33124121 then the unlock bit (f:7B[02]) gets set, and the next set of code turns on the red LED.

There’s similar logic for the button sequence 41241342 which re-locks the device.

The Sad Letdown

At this point, I was kinda out of instructions to look at. At least I was out of meaningful instructions. Did I complete the task? I was honestly expecting some other layer to this whole thing, or at minimum a “success” message printed out to the screen.

I hadn’t done any research on the internet about this board yet, and I still didn’t want to spoil anything for myself, so I called a friend and he was able to track down the original source code for the board (you can find it here). I explained to him my solution and he was able to tell me if there was more to the challenge or not, and I was happy to be informed that there wasn’t. I had already completed the challenge.

At this point I took a look at the github repo and saw that there were in fact instructions, and I was supposed to have read (or maybe did and forgot about them), which indicated that the goal was to “unlock” the device.

While crawling through the documentation I noticed that the green LED was supposed to light up when the device was unlocked, but my red LED lit up instead. I must have switched the red and green LED locations on the board.

Conclusion

This was a fun project. I got to do the full stack of my skill set, which is rare these days. I wish that there was more of an “ending” to this project but after reading the instructions, and given the suggested level of difficulty, I suppose that wasn’t the purpose.