Reverse engineering and fixing a bug in the firmware of a "ThinkPad Compact USB Keyboard with TrackPoint"

For years, I owned and liked my USB keyboard, A “Lenovo ThinkPad Compact USB Keyboard with TrackPoint” (KU-1255). It is small and with its trackpoint, you don’t even need to move your hands from the keyboard to move the mouse pointer. Unfortunately, Lenovo went far beyond what’s useful: They implemented middle-mouse button scrolling in hardware, but half-broken.

An annoying bug

The keyboard firmware supports different modes, on of them disables or activates the middle mouse button. If it is activated, you can use the middle mouse button. As a bonus, you get a kind-of middle-mouse scrolling: As long as you press the button, vertical trackpoint movements are translated to scroll events. But just vertical and in a really coarse way, so that you can only scroll line-wise, but not close to pixel-wise.

In addition, releasing the middle mouse button after scrolling triggers a middle mouse click event, ruining the scroll feature totally.

Linux works around this, for this particular keyboard, but on Windows or FreeBSD, you’re lost.

Lenovo didn’t recognise the problem, but sent me a new keyboard instead, assuming it was a hardware issue.

Reverse-engineering the firmware

Let’s start! First, we take the keyboard apart and look what’s inside: A Sonix SN8F2288FG. It’s an 8-bit microcontroller with 12k words ROM, 512 B RAM and a weird architecture.

On Lenovo’s website, you can download the updater for the keyboard. It contains the firmware binary. Download it first: tp_compact_usb_kb_with_trackpoint_fw.exe (MD5 2c128084412b917449a2c01da3c49451, SHA256 7116a3819ee094857d21e4671cb6cf953d582372126f0f6728f6b2421eda7bd4), then put it into the free version of IDA Pro.

After playing around a bit, I found the firmware. It is XORed with 0x5A. Search for Auto .....a..2015051111591234567817EF60478521SN8F2288 to find it.

Then, put that firmware binary into the free SN8 disassembler dissn8. When I started, I had to add a few instructions and patch some others to decode the binary correctly. To verify, I cross-checked by assembling and disassembling with SN8 C Studio.

Then, it was time to print the assembly on paper, sit down and understand it.

After a while, I found the place that implements the “feature” I disliked and patched it out. I also bumped the firmware version to be discern my modified version from the original:

--- orig/firmware-annotated-SN8CStudio.asm	2019-06-10 22:06:46.782287000 +0200
+++ patched/firmware.asm	2019-06-10 22:06:54.378983000 +0200
@@ -2188,13 +2188,16 @@
 label_06d5: ; <- 06d3
   CALL   iic_stop
 ; only apply speed factor if !middle_button_pressed (i.e. scrolling):
-  BTS0   flag_middle_button_pressed
-  JMP    tp_skip_apply_speed
+;  BTS0   flag_middle_button_pressed
+;  JMP    tp_skip_apply_speed
+	NOP
+	NOP
   CALL   tp_apply_speed_x
   CALL   tp_apply_speed_y
   CALL   copy_clamp_tp_mouse_pos ; tp_mouse_d? -> mouse_d?
 tp_skip_apply_speed: ; <- 06d7
-  BTS1   flag_middle_button_pressed
+;  BTS1   flag_middle_button_pressed
+  BSET   0x14.6 ; cool, enough space to fit that. so that button 3 is reported reported properly.
   RET
   MOV    A, tp_mouse_dx
   MOV    temp_maybe_tp_dx, A
@@ -2439,8 +2442,10 @@
   BCLR   mouse_buttons.1
   BTS0   tp_mouse_buttons.1
   BSET   mouse_buttons.1
-  BTS0   flag_Thinkpad_preferred_scrolling
-  JMP    label_0792
+;  BTS0   flag_Thinkpad_preferred_scrolling
+;  JMP    label_0792
+  NOP
+  NOP
   BCLR   mouse_buttons.2
   BTS0   tp_mouse_buttons.2
   BSET   mouse_buttons.2
@@ -2462,7 +2467,8 @@
   BSET   flag_middle_button_pressed
   JMP    label_07b6
 label_07a0: ; <- 079b
-  BCLR   mouse_buttons.2
+;  BCLR   mouse_buttons.2
+  NOP
   MOV    A, tp_mouse_buttons
   AND    A, #0x04
   CMPRS  A, 0x78
@@ -4701,7 +4707,8 @@
   DW     0x0017 ; ..
   DW     0x0047 ; .G
   DW     0x0060 ; .`
-  DW     0x0030 ; .0
+  ;DW     0x0030 ; .0
+  DW     0x0033 ; increment version
   DW     0x0003 ; ..
   DW     0x0001 ; ..
   DW     0x0002 ; ..

Patch the updater

That was the easy part: XOR it again with 0x5A and put it into the same place. in the binary. A hex editor suffices.

Use the patched updater and enjoy middle-button scrolling work as expected.

Unfortunately, I feel I can’t published the patched updater. But this is the patch you need to apply to it:

--- orig.hex	2019-06-10 22:50:40.986155000 +0200
+++ patched.hex	2019-06-10 22:50:53.745130000 +0200
@@ -21893,7 +21893,7 @@
 00057dc0  0d 0a 0d 0a 0d 0a 46 69  72 6d 77 61 72 65 20 75  |......Firmware u|
 00057dd0  70 64 61 74 65 20 63 6f  6d 70 6c 65 74 65 64 0d  |pdate completed.|
 00057de0  0a 43 75 72 72 65 6e 74  20 46 69 72 6d 77 61 72  |.Current Firmwar|
-00057df0  65 20 56 65 72 73 69 6f  6e 3a 20 56 33 2e 33 30  |e Version: V3.30|
+00057df0  65 20 56 65 72 73 69 6f  6e 3a 20 56 33 2e 33 33  |e Version: V3.33|
 00057e00  0d 0a 50 6c 65 61 73 65  20 72 65 70 6c 75 67 20  |..Please replug |
 00057e10  6b 65 79 62 6f 61 72 64  00 00 00 00 50 72 6f 74  |keyboard....Prot|
 00057e20  6f 63 6f 6c 20 48 61 6e  64 73 68 61 6b 65 20 45  |ocol Handshake E|
@@ -24452,8 +24452,8 @@
 00074200  74 9c 5a 54 7b 9c 0f 77  39 9c 01 9c 17 20 8e dc  |t.ZT{..w9.... ..|
 00074210  d3 9c da 5c 88 dc fe 9c  d3 9c 1f 45 fe 9c dd 9d  |...\.......E....|
 00074220  d3 9c 68 45 fe 9c d3 9c  a5 47 5b 4e 69 45 fe 9c  |..hE.....G[NiE..|
-00074230  d3 9c fd 9c 17 19 8f dc  17 11 74 9c 48 0c 81 dc  |..........t.H...|
-00074240  54 92 6b 92 ed 9d 48 04  5a 54 68 44 6e 45 69 44  |T.k...H.ZThDnEiD|
+00074230  d3 9c fd 9c 17 19 8f dc  17 11 74 9c 5a 5a 5a 5a  |..........t.ZZZZ|
+00074240  54 92 6b 92 ed 9d 4e 14  5a 54 68 44 6e 45 69 44  |T.k...N.ZThDnEiD|
 00074250  6f 45 6e 05 bd dc 6e 44  a5 47 5b 4e 6e 45 dc 38  |oEn...nD.G[NnE.8|
 00074260  6e 53 6f 05 b5 dc 6f 44  a5 47 5b 4e 6f 45 6e 44  |nSo...oD.G[NoEnD|
 00074270  6f 78 dc 28 4b dd 68 44  6e 45 69 44 6f 45 6e 05  |ox.(K.hDnEiDoEn.|
@@ -24475,10 +24475,10 @@
 00074370  6c 2d 2e dd 6c 49 2f dd  6c 45 69 44 6c 49 6c 44  |l-..lI/.lEiDlIlD|
 00074380  48 7e dc 20 5a 54 6c 45  a5 77 6a 49 4e 0f d9 dd  |H~. ZTlE.wjIN...|
 00074390  49 10 49 19 df dd 49 18  49 11 2d dd 5a 54 76 1a  |I.I...I.I.-.ZTv.|
-000743a0  1f 0a 76 12 76 1b 1f 0b  76 13 4e 0f c8 dd 76 18  |..v.v...v.N...v.|
+000743a0  1f 0a 76 12 76 1b 1f 0b  76 13 5a 5a 5a 5a 76 18  |..v.v...v.ZZZZv.|
 000743b0  1f 08 76 10 76 44 77 5d  cd dd 49 18 c0 dd 76 44  |..v.vDw]..I...vD|
 000743c0  77 45 49 10 4e 0f fa dd  48 1c 76 08 48 14 ec dd  |wEI.N...H.v.H...|
-000743d0  76 18 1f 44 5e 70 22 5d  fd dd 49 1c ec dd 1f 44  |v..D^p"]..I....D|
+000743d0  5a 5a 1f 44 5e 70 22 5d  fd dd 49 1c ec dd 1f 44  |ZZ.D^p"]..I....D|
 000743e0  5e 70 22 45 49 14 22 00  e9 dd 5a 77 72 45 5e 77  |^p"EI."...ZwrE^w|
 000743f0  73 45 48 14 ec dd 72 71  73 71 48 1c 5a 54 68 44  |sEH...rqsqH.ZThD|
 00074400  dc 2a 94 dd 49 10 74 0d  9c dd 68 44 74 49 68 0d  |.*..I.t...hDtIh.|
@@ -24728,7 +24728,7 @@
 00075340  5c 74 37 75 2d 35 04 d5  0b 74 2f 45 7a 77 cd 75  |\t7u-5...t/Ezw.u|
 00075350  13 d6 1a 77 cd 75 1c da  48 5a 5b 5a 5a 5a 58 5a  |...w.u..HZ[ZZZXZ|
 00075360  5a 5a 5a 5a 5a 5a 52 5a  b5 5a 4d 5a 1d 5a 3a 5a  |ZZZZZZRZ.ZMZ.Z:Z|
-00075370  6a 5a 59 5a 5b 5a 58 5a  5a 5a 5b 5a 53 5a 58 5a  |jZYZ[ZXZZZ[ZSZXZ|
+00075370  69 5a 59 5a 5b 5a 58 5a  5a 5a 5b 5a 53 5a 58 5a  |iZYZ[ZXZZZ[ZSZXZ|
 00075380  61 5a 5a 5a 58 5a 5b 5a  5a 5a fa 5a 68 5a 53 5a  |aZZZXZ[ZZZ.ZhZSZ|
 00075390  5e 5a 5a 5a 5a 5a 5b 5a  59 5a 5b 5a 5b 5a 5a 5a  |^ZZZZZ[ZYZ[Z[ZZZ|
 000753a0  53 5a 7b 5a 5a 5a 5b 5a  5a 5a 5b 5a 78 5a 0b 5a  |SZ{ZZZ[ZZZ[ZxZ.Z|