Novatouch TKL Reverse Engineering - Part 2

Novatouch TKL Reverse Engineering - Part 2

Adam Engström bio photo By Adam Engström Comment

As written in my first post on this subject I was not really happy with the default mapping of the keys in the Novatouch, this is a writeup of the steps I went through to patch the firmware in the keyboard for a different behaviour. The changes include patching the default key mapping and also adding a hook for entering the TI BSL when a specific key combination is pressed.

Dumping the original firmware

Originally I used a GoodFET for flashing but since then I switched to a Launchpad instead. This made it possible to use the mspdebug tool for remote debugging and flashing. The programming header pinout for JTAG / SWD can be found in the last post.

To be able to open the firmware file in IDA I used objdump from msp-gcc toolchain to create an ELF file from the raw binary dump. Should be possible to import raw binaries into IDA but I did not manage to get it to work in a timely manner. With the ELF I don’t need to go through setting up entry address and memory details every time I re-import the binary. Also, having the object files also makes it simple to link against new code we want to inject.

I have published some tools in my novatools repository with a Makefile that performs all the steps from reading out the firmware to creating the ELF file.

Basically what the Makefile does is:

  1. Read out the full contents of the main flash with mspdebug (0x8000 - 0xffff)
  2. Convert resulting ihex to raw binary
  3. Create separate binary files with the .text and .vectors section contents
  4. Do some pathing of the text section
  5. Compile code to inject into the final ELF
  6. Create object file from the section binaries
  7. Create MSP430X ELF file from the object files

The resulting ELF contains the correct entry address and processor model so things get a lot easier when using msp-gcc tools and IDA. Also it’s faster to flash the ELF because half of the contents of the raw binary is just 0xFF.

Firmware analysis

After spending some time in IDA identifying possible functions I got an overall picture of the program flow. Starting out with finding all the IO port manipulation instructions and going from there made things a lot easier (using the IO pin definitions I found out from the hardware analysis).

I also used the source code of the Texas Instruments MSP430 USB developers package as a reference to identify the USB functions of the firmware. My hopes that CoolerMaster was using the reference implementation from TI for USB was confirmed by checking for strings in the raw binary.

$ strings main.elf | grep -i devel
USB_DEVELOPERS_PACKAGE_3_20_00

It seems like the reference implementation is used mostly “as-is” with some minor modifications. Some changes seem to be back-ported from a later version of the reference implementation because there are some functions that look to be more from the 4_00_00 version than 3_20_00.

The firmware uses mostly global variables and barely any local variables, not even via function arguments. This made it easier to figure out each function whenever the purpose of a global variable was identified.

The next chapters describes the overall flow of the program. The flow charts exist just to give an high level understanding of the logic, each chapter describes a single function. All names for variables and functions are made up by me because all I have is the binary with addresses. It’s very possible I have gotten things wrong.

The sleep calls are called sleep_us() but I have no idea whether it’s actually microseconds. The function loops over a nop (3 instructions per iteration) the number of times specified in argument so might as well be nanoseconds, depends on what frequency the MSP is clocked in. Just something I guessed when naming the functions and never bothered to look up.

Main loop

Mainloop

The main loop does pretty much what’s expected, things like initialising variables and then start to call init functions for different subsystems, including USB.

A loop is then entered where it’s first checked if USB is suspended or not. When USB is in suspend mode the key matrix is still polled to see if any key is pressed. If a key is pressed an USB wakeup is sent to computer.

When USB is active the key sample function is continuously called.

Key sample function

Key sample function

The key sample function reads a single key from the matrix and then handles sending of pending USB reports. A number of USB endpoints are used but I won’t go into details about that. What’s interesting is that the USB key report used only has place for 6 keys (not including modifiers as they are kept as bits in the beginning of the report).

One of the “special features” of the Novatouch is the ability to press fn + F1-F4 for setting the repeat rate of the keyboard, called rapid fire. Totally useless function for me but I guess it’s something gamers find useful. The actual repeating is done at the end of this function, where the current repeat rate is compared with a timer to see of the currently pressed key should be sent again.

The mux_and_read function is were all the magic happens.

Key mux and read function

Key mux and read function

In this function the key mux circuits are controlled by setting the correct row and column for the key that is to be read. A counter keeps track of which key is currently read.

The key index counter is converted into column and row by the bit pattern xCCCCRRR (R = Row, C = Col). The first C controls which mux chip should be selected (right or left part of matrix), the two following CC bits controls the column of the selected chip. The RR bits selects the row. The counter is cleared when 0x80 is reached.

The current state of each key is kept in a bit packed array with 2 bits for each key. In this array 0 means key is not pressed, 1 means it has just been pressed / is in debounce state and 2 means key is fully pressed.

There seems to be some special handling of the key when it’s in debounce state, before the key state is read from cap sense controller the TP_prev IO is set and an extra sleep is performed.

After it has been determined if the key is fully pressed or not (key state == 2 or 0, debounce logic not shown in figure) the handle_key_* function is called to handle up / down press. Since the key down function is very similar to the key up function I have not done any description of that one, basically just the inverse of the key up function.

Key down function

Key down function

When a key is pressed some logic is done to determine what to do for the key. First thing that is done is that the key index is translated to a keycode with the help of table_1.

The keycode is used as index into another table (table_2) which contains which type of key the code corresponds to. The different types of keys can be seen in the figure above (normal, mod, led). Keys being of type 0 is passed through and directly put on the USB report queue to be sent to host.

The handling of rapid fire key combinations can also be found in this function. Whenever the fn key is pressed a flag is set. If the flag is set and the current key is F1-F4 a variable is set that is used to send keys repeatedly at a specific interval when pressed.

The actual sending of the keys is done at the end of the key sample function where a timer is checked to see if enough time has passed for the key to be sent again.

Patching

Now that we have an overview of the program flow we can do some changes to accomplish our goal; remap caps to ctrl. As can be seen in the overview in the previous chapter the first thing that happens when a key is pressed is that it’s looked up in table_1 to determine it’s keycode. Consequently we should be able to overwrite the keycode for caps in the table with the code for ctrl instead.

To determine which key index corresponds to which key we could either measure the electical connection between the mux circuits and each key on the PCB or use a debugger to break at each key press to check the global key index variable. To measure the connections between mux and key on PCB it would be necessary to fully disassemble the keyboard and remove the dome sheet, which I was not too keen on. So I went with the debugger route.

Using mspdebug to break on entry to the key pressed function I got the mapping between key index and keycode in the table below.

The row is the lowest 3 bits of key id and the column is the 4 bits above that.

(xCCCCRRR). I.e. tab = 5 + (2«3) = 21. Key id vs key location

Having this information it was easy to just replace the code for caps (at index 20) with the code for ctrl (found at index 17) and manually patch the binary.

After verifying that the new firmware worked I wrote a Python utility for doing the patching automatically, this utility can be found in the novatools repository.

Patched binary diff

Comparing the original and patched binaries we can see some differences.

First hunk at 0x830 is our call into our own BSL entry check function (see next chapter). Second hunk at 0x2590 is in table_1 where caps keycode is overwritten with the one for ctrl.

Last hunk switches the backspace and \ keys to get a layout closer to HHKBr keyboard.

Other than changing around keys we can also change the USB identifier strings and other fun stuff, see patch utility for the addresses of these.

BSL entry hook

To be able to do different key mappings in the future and upgrade the firmware without opening up they keyboard I added a hook for entering the BSL (Bootstrap Loader) by pressing a key combination. The BSL is pre-programmed on all MSP430 processors and can load new firmware via USB. Reading out the BSL memory area with GoodFET showed that the original BSL was still in place.

To enter BSL via code one should just call address 0x1000 according to the documentation from TI. I did a lot of investigation in the firmware to see if there was any way of entering the BSL without changing the original firmware. Unfortunately I could not find any way to do so.

Instead I had to inject some code into the firmware to call the BSL. The hook is called at the end of the fn + Fx key checking where I replaced a mov instruction with a call to my own function.

At the end of rapid fire checking in key pressed function we originally had:

...
    8836:	e2 d3 04 24 	bis.b	#2,	&0x2404	;r3 as==10
    883a:	d2 42 2f 25 	mov.b	&0x252f,&0x2530
    883e:	30 25
    8840:	d2 42 2f 25 	mov.b	&0x252f,&0x2531
    8844:	31 25
...

We replace this with a call to our own function which we place at the end of the .text section at address 0xa780:

...
    8836:	e2 d3 04 24 	bis.b	#2,	&0x2404	;r3 as==10
    883a:	b0 12 80 a7 	calla	#0xa780
    883e:	03 43           nop
    8840:	d2 42 2f 25 	mov.b	&0x252f,&0x2531
    8844:	31 25
...

The opcode for the call instruction was calculated with the help of the opcode documentation for the 430X instruction set. Because the call is two bytes shorter than the mov instruction we put a nop right after our jump.

#include <intrinsics.h>
#include <msp430f5510.h>

// Declare pointers to variables we access in Novatouch fw
unsigned char* const repeat_flags = (unsigned char*)0x2404;
unsigned char* const repeat_rate = (unsigned char*)0x252f;
unsigned char* const num_for_7x_c1 = (unsigned char*)0x2530;

void check_bsl_enter() {
    // We just replaced this copy to get here, perform it here instead
    // (although it seems to be redundant because it's never actually
    // read)
    *num_for_7x_c1 = *repeat_rate;

    // Enter BSL if fn + f1 + f4 is pressed
    if (*repeat_flags & 0x9) {
        __dint();
        // Maybe need to slow down clock to 8 MHz also, not sure what
        // is configured by Novatouch fw
        USBKEYPID = 0x9628;
        USBCNF &= ~PUR_EN;
        USBPWRCTL &= ~VBOFFIE;
        USBKEYPID = 0x9600;
        ((void (*)())0x1000)();
    }
}

This code snipped is compiled into an object file with msp430-gcc and linked together with our original firmware. For details look at the Makefile in novatools.

Unfortunately this did not help me a whole lot because OS X promptly refused to enumerate the HID device the BSL exposes and therefore I could not even route the USB device to a virtual machine. Will need to find a PC to do the upgrade the next time I want to change the firmware without opening up the case.

Further work

This concludes my current adventures in Novatouch land. In retrospective when looking over this post it feels like I could have left out half of it and just write about the interesting parts (patching and injecting code) instead of trying to explain the whole program flow. Well, since this is an experiment in trying to get better at writing at least I have something to better for the next post.

Therea are some things I would like to do further work on that I have not yet had time for:

  • Hook up oscilloscope to cap sense controller and try to understand how it works (need to find an oscilloscope first)
  • Write HHKB fn layer to have easier access to arrows and F keys
  • Or just port tmk_keyboard or some other open source firmware
  • Exploit USB stack to be able to enter BSL without ever opening the keyboard
comments powered by Disqus