Mouse Adventures #2: Extracting the Firmware

Wherein I investigate the origins of the TeckNet mouse, find multiple OEMs selling variants of it, rip apart the configuration tool further, try to read too much meaning into PDB file paths, figure out part of the mouse's protocol and rip apart the firmware updater. The saga continues, for now!

Catching Up

In Part 1, I described the TeckNet Hypertrak Gaming Mouse and its awful software, disassembled some bits of the configuration tool, and figured out how it receives notifications from the mouse. It's a start, but we want to go further!

But first, a brief interlude... I came across a few URLs in the strings. There's a link to the firmware download on TeckNet's website. Right, that makes sense. You get it if you click on the Help button in the corner, so you can update easily. (Never mind the fact that the URL is http://www.tecknetonline.com/download/MouseKeyboard/M009_V2_For_Win_Driver_V1.0.zip, which includes a version number...)

Then you also get WWW.ARMAGGEDDON.NET and www.dare-u.cn. These two are just baffling! So, there's a big function that loads a lot of strings into a class, depending on the selected language. Presumably this is a shoddy attempt at supporting translations. For the English text, you get www.dare-u.cn; for the Chinese text, you get WWW.ARMAGGEDDON.NET.

The ARMAGGEDON Mice

If you go to the ARMAGGEDDON website, you're redirected to a website selling "The Ultimate Gaming Gear". They've got a version of this mouse called the NRO-5 STARSHIP III with a different logo and a nice pattern on the top. It's $70.90 "before tax & shipping". That's a yikes. It's more than twice as much as what I paid for the TeckNet version.

I downloaded the drivers too, because why not? They seem to have two almost-identical versions: "STARSHIP III" and "STARSHIP III 2017". I'm not sure why they did this. They install into the same directory, but with different EXE filenames. The UI is pretty much identical. The "STARSHIP III 2017" does not include Angle Snap; the 2017 does.

There's a few differences from the TeckNet software. Both of the STARSHIP III drivers include options to turn on/off the side button lights, which TeckNet don't (spoiler for later: the TeckNet firmware does support this even though their tool doesn't expose it). The English translations are a bit different. The button menus are ordered slightly differently.

They have different USB vendor/product IDs: the STARSHIP III 2017 is 04D9:A165, the STARSHIP III is 04D9:A0CB. Even though I don't own these mice, I can just look at the config tool to see what VID/PID they're expecting to see. They include firmware updates as well (so I could probably flash my mouse to behave like a STARSHIP if I wanted to).

The Dare-U Mice

So, how about Dare-U? Now, this is where it gets weird... They have a mouse called the EM920 which looks nigh-on identical. Then they've also got a DM60 which looks identical as well. The specs don't match up on either, though! The EM920 claims it has an ATG-4090 sensor. The DM60 claims it has a PMW3336 sensor. My TeckNet mouse has an A9800 sensor, as does the STARSHIP III mouse.

Obviously, the answer is to install more dodgy drivers! I first downloaded the EM920 driver. Poor show: there's no 64-bit version, like the TeckNet and Armaggeddon mice have. Furthermore, the tool itself won't start up on my machine -- it shows a splash screen and then does nothing. Oh well, I want to find out a bit more about this, so let's disassemble it!

I, too, love to look at... Dreivers. Yes. There's a few things I can gather from this:

That was something -- what about the other Dare-U mouse that looks like ours, the DM60? Let's install those drivers too... Well, they also refuse to fully start up, but we can still look at the executable for fun!

WHO DID THIS??? 😂😂😂😂😂

Alright, now we've seen like five different variants on the same damn mouse. Who actually made these things, for starters?

My TeckNet mouse has a mysterious inscription on the back: "Manufacturer's SKU: TM155U". That's a start. Wait, where have we seen TM155 before?

Every version of this configuration tool has two interesting pieces: 1) the Path to the PDB (debug info) on the developer's machine, and 2) the name of a mutex that's created internally. Let's collate all of these...

Mouse Mutex Path
HyperTrak MGTM155OEMVerMutex E:\Program\MG\TM155\OEM(20151225)\HidMouse\x64\Release\TeckNet M009-V2 setup.pdb
STARSHIP III 2017 MGTM155-4090-Singapore-3325Mutex E:\Program\MG\TM155-4090-Singapore\HidMouse\x64\Release\STARSHIP III 2017.pdb
STARSHIP III MGTM155SingaporeMutex E:\Program\MG\TM155\Singapore\HidMouse\x64\Release\STARSHIP III.pdb
EM920 TM155EM920_Mutex E:\Project\Dreivers\A-One\Mouse\Togran DAREU\TM155 4090 EM920 - Revise Gun - New\SourceCode\Bin\EM920.pdb
DM60 TM155L_Mutex E:\Kuaipan\Project\Dreivers\A-One\Mouse\TM155\SourceCode\Bin\DM60.pdb

Curiously, TM155 shows up in all of them. The Tecknet Hypertrak drivers and Armaggeddon STARSHIP drivers were probably compiled on the same computer; those paths are just a bit too much of a coincidence. What can we discover about the TM155 online?

A quick search for TM155 mouse brings up this page where a Chinese company named Tonstep (short for Dongguan Tonstep Electronics Technology Co., Ltd.) will happily sell you a bulk order of them. They claim it's got a PMW3336 sensor (like the DM60, but unlike my Hypertrak, the EM920 and the STARSHIP). And I mean... How could you pass this offer up? It's got 12 key progranmmable buttons with driver. It's Adjustablet.

shut up and take my money

Let me just recompose myself and look at one of the other search results. There's also this page where Togran also claims to make the TM155. A bit less hype than Tonstep's page. Wait, where have we seen Togran before? Oh, that's right, the PDB path for the EM920 mentions 'Togran DAREU'. And if you look at the About page on Togran's website, it mentions DareU as one of their brand names.

So maybe Togran made my mouse. Maybe Tonstep made it. I've genuinely got no clue: they both claim to manufacture devices. Oh well. I've got more pressing things to investigate, and ultimately, I feel like I've rambled on for far too long about this already.

That Damn Firmware Updater...

Wouldn't it be cool if we could look at the mouse's firmware? "Yes!", I thought, naively. I was rather hesitant to poke at the updater too much as I didn't want to risk bricking my mouse just yet, but I figured I'd look into it anyway. You get the following files inside the updater folder: EFORMAT.INI, FwUpdate.exe, HIDDLL.DLL, ISPDLL.DLL. What's inside the INI?

; This version is for Holtek internal memo since 20071114
; This file can only be used for I3000 and ISPDLL.dll
[Version]
DateCode=17:00,MAY23,2013
VersionCode=2

[Body]
count=7
body1 =HT66FB540, 2, 0x00EE, 0x0211, 0x0, 0x1000, 0x80000, 0x0020, 0x0, 0, 0x0, 0x0, 0x08, 0x01, 0000, 0x00, 0, 1, 1, 4, 11, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0
body2 =HT66FB550, 2, 0x00EE, 0x0212, 0x0, 0x2000, 0x80000, 0x0020, 0x0, 0, 0x0, 0x0, 0x08, 0x01, 00, 0x00, 0, 1, 1, 4, 11, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0 
body3 =HT66FB560, 2, 0x00EE, 0x0213, 0x0, 0x4000, 0x80000, 0x0020, 0x0, 0, 0x0, 0x0, 0x08, 0x01, 0000, 0x00, 0, 1, 1, 4, 12, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0
body4 =HT68FB240, 2, 0x00EE, 0x0219, 0x0, 0x1000, 0x80000, 0x0020, 0x0,0, 0x0, 0x0, 0x08, 0x01, 0000, 0x00, 0, 1, 1, 4, 11, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0
body5 =HT68FB540, 2, 0x00EE, 0x0215, 0x0, 0x1000, 0x80000, 0x0020, 0x0,0, 0x0, 0x0, 0x08, 0x01, 0000, 0x00, 0, 1, 1, 4, 11, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0
body6 =HT68FB550, 2, 0x00EE, 0x0216, 0x0, 0x2000, 0x80000, 0x0020, 0x0, 0, 0x0, 0x0, 0x08, 0x01, 00, 0x00, 0, 1, 1, 4, 11, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0
body7 =HT68FB560, 2, 0x00EE, 0x0217, 0x0, 0x4000, 0x80000, 0x0020, 0x0,0, 0x0, 0x0, 0x08, 0x01, 0000, 0x00, 0, 1, 1, 4, 12, 17, 13, 16, 2, 0, 0, 0, 0, 0, 0, 9.0

[PARTIALLOCK]
HT66FB540   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15}
HT66FB550   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15},0x1.0{16-17},0x1.1{18-19},0x1.2{20-21},0x1.3{22-23},0x1.4{24-25},0x1.5{26-27},0x1.6{28-29},0x1.7{30-31}
HT66FB560   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15},0x1.0{16-17},0x1.1{18-19},0x1.2{20-21},0x1.3{22-23},0x1.4{24-25},0x1.5{26-27},0x1.6{28-29},0x1.7{30-31},0x2.0{32-33},0x2.1{34-35},0x2.2{36-37},0x2.3{38-39},0x2.4{40-41},0x2.5{42-43},0x2.6{44-45},0x2.7{46-47},0x3.0{48-49},0x3.1{50-51},0x3.2{52-53},0x3.3{54-55},0x3.4{56-57},0x3.5{58-59},0x3.6{60-61},0x3.7{62-63}
HT68FB240   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15}
HT68FB540   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15}
HT68FB550   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15},0x1.0{16-17},0x1.1{18-19},0x1.2{20-21},0x1.3{22-23},0x1.4{24-25},0x1.5{26-27},0x1.6{28-29},0x1.7{30-31}
HT68FB560   =0x0.0{0-1},0x0.1{2-3},0x0.2{4-5},0x0.3{6-7},0x0.4{8-9},0x0.5{10-11},0x0.6{12-13},0x0.7{14-15}

[Programming]
; No =  DatClk,     AddClk,     shift
0  =      4,          4,          0    ; for 16bit programming <= 16Kword (power on ID : ADh)   
1  =     16,          5,          2    ; for HT86xxx (power on ID : 86h)
2  =     16,          4,          2    ; for 64bit programming <= 64Kword, eg. HT36RA1/RA4/RM4, HT46RU67 (power on ID : A3h)
3  =     16,          5,          2    ; for 64bit programming > 64Kword, eg. HT36RFA (power on ID : C5h)
4  =      4,          5,          0    ; for 16bit programming > 16Kword (power on ID : D6h, 59h)   
6  =     16,          5,          0    ; for Flash MCU & (OTP Type-6 >= 16K words)
9  =      0,          0,          0    ; for NULL    
10  =     0,          5,          1    ; for Flash Type-9
11  =     0,          5,          8    ; for Flash Type-9B
12  =     0,          5,          0x10    ; for Flash Type-9C
13  =     0,          5,          0x20    ; for Flash Type-9D
14  =     0,          5,          0x40    ; for Flash Type-9E

[EEData]
;No=StringLabel, addrress bit number
1=HT2201, 0
2=HT93LC46, 7
3=HT93LC56, 9
4=FlashType-2, 9
6=FlashType-5, 0

[MatchPattern]
1 = 26, 0x12, 0xA1, 0x3A, 0x00 
2 = 16, 0x65, 0xA9
3 = 6, 0x2D 
4 = 20, 0x65, 0xA9, 0x0
5 = 16, 0x64, 0xA9
6 = 16, 0x65, 0xA9
7 = 16, 0x94, 0xa6 ;for OTP TYPE 2A

[OSC_Calibration]
;delete for i3000 at 20130524

[Check]  

[ConditionalCheck]  
HT45RQ = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT45R0S = 0x9, 0x30, {0x00}, 0xA, 0xC0
HT45R0S-1 = 0x9, 0x30, {0x00}, 0xA, 0xC0 
HT46R01 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT46R01A = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT46R02 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT46R03 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R01 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R01A = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R064 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R064B = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R064D = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R02 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R03 = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R01-Q = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6 
HT48R02-Q = 0x1, 0x18, {0x00, 0x08}, 0x1, 0x6

[OTPTiming]
ClockWidth = 25
WritePulse = 500
Repeat = 10
Reset = 2000
Vpp5 = 200000
BeforePattern = 200000
AfterPattern = 200000

[MTPTiming]
ClockWidth = 25
WritePulse = 2000
Reset = 2000
Repeat = 10
WritePulse45F0V=2000
Type2Tps2 = 5
Type2Tps3 = 5

[PowerOn]
0=CSB_HIGH,RWB_LOW,CLK_HIGH,VDD,0x01,VPP,0x04,WAITm,0xc8,VPP,0x00,WAITm,0x2,VPP,0x01,WAITm,0xc8 
1=CSB_HIGH,RWB_LOW,CLK_HIGH,VDD,0x01,VPP,0x04,WAITm,0xc8,VPP,0x01,WAITm,0xC8   
2=CSB_HIGH,RWB_HIGH,CLK_HIGH,VDD,0x01,VPP,0x04,WAITm,0x96,VPP,0x00,WAITm,0x2,VPP,0x01,WAITm,0x3,Parallel_pattern,0x10,0xA965,WAITu,0x64   
3=CSB_HIGH,RWB_HIGH,CLK_HIGH,VDD,0x01,VPP,0x01,WAITm,0x28,parallel_pattern,0x10,0xA965,WAITm,0xC8,WAITm,0x20  ;2011/08/09 test OK!
4=CSB_HIGH,RWB_HIGH,CLK_HIGH,Set_AD4,0x0,VDD,0x01,VPP,0x01,Serial_pattern,0x10,0xA964,Dummy_Clock,0x400
5=CSB_HIGH,CLK_HIGH,Set_AD1,0x0,VDD,0x01,VPP,0x04,WAITm,0xC8,VPP,0x00,WAITm,0x1,VPP,0x04,WAITu,0x64 
6=CLK_LOW,Set_AD1,0x0,VDD,0x01,CLK_HIGH,Set_AD1,0x1,VPP,0x04,WAITm, 0x05
7=VDD,0x00,WAITm,0x80,CLK_LOW,Set_AD1,0x0,VDD,0x01,WAITm,0x0A,CLK_HIGH,Set_AD1,0x1,VPP,0x04,WAITm,0x0A
8=CSB_HIGH,RWB_HIGH,CLK_HIGH,VDD,0x01,VPP,0x01,WAITm,0x28,parallel_pattern,0x14,0x0A965,WAITm,0xC8,WAITm,0x20 ;2011/08/09 test ok!
9=VDD,0x00,VPP,0x00,WAITm,0x80,CSB_HIGH,RWB_LOW,CLK_HIGH,VDD,0x01,VPP,0x04,WAITm,0xc8,VPP,0x01,WAITm,0xC8   ; ht83f10 
10=VPP,0x00,VDD,0x00,WAITm,0x40,CLK_HIGH,Set_AD1,0x1,VDD,0x01,WAITm,0x05,VPP,0x04,WAITm, 0x05  ; only for OTP type 6  ; 
11=CLK_LOW,Set_AD1,0x0,VDD,0x01,WAITm,0x05,CLK_HIGH,Set_AD1,0x1,VPP,0x04,WAITm, 0x0A   ;only for  ht45fm03b  vdd wait=5ms
12=CLK_LOW,Set_AD1,0x0,VDD,0x01,CLK_HIGH,Set_AD1,0x1,VPP,0x04,WAITm, 0x05       ; modify from index=6(Flash type6A) not testing yet
13=VPP,0x00,VDD,0x00,WAITm,0x40,CLK_HIGH,Set_AD1,0x1,VDD,0x01,WAITm,0x05,VPP,0x04,WAITm, 0x05     ; modify from index=10(OTP type6A)  not testing yet
14=CSB_HIGH,RWB_HIGH,CLK_HIGH,Set_AD4,0x0,VDD,0x01,VPP,0x01,Serial_pattern,0x10,0xA694,Dummy_Clock,0x200  ; modify from index=4(OTP type2A) 
15=CLK_HIGH,Set_AD1,0x0,VDD,0x01,WAITm,0x05,CLK_LOW,WAITu,0xFF,CLK_HIGH,WAITu,0x30     ; modify from index=12(Flash type6A) 
16=CLK_LOW,Set_AD1,0x0,VDD,0x01,CLK_HIGH,Set_AD1,0x1,VPP,0x04,WAITm, 0x01     
17=CLK_HIGH,VDD,0x01,Set_AD1,0x1,WAITm,0x05,CLK_LOW,WAITu,0xFF,CLK_HIGH,WAITu,0x30

[TimeTable]
0=0x0,0x2,0x2,0x00,0x80,1,1,0,1,1,1,1,10,1,0x14,0,0x10,0x6,0x32,10,3,0,5.5,12.5,5.5,12.5    
1=0x0,0x2,0x2,0x00,0x80,1,1,0,1,1,1,1,10,1,0x15,0,10,0x10,0x32,10,3,0,5.5,12.5,5.5,12.5   
2=0,0,0,0x80,0x80,1,1,0,1,1,1,1,40,1,0x13,0,0x10,0x9,0x32,0x0,3,40,5.5,0,5.5,0     
4=0x400,0,0,0x80,0x80,1,1,1,1,1,1,1,40,1,0xB3,0,0x10,0x6,0x32,0,3,40,5.5, 0, 5.5,0    
5=0x400,0,0,0x80,0x80,1,1,0x08,10,1,1,1,50,1,0x73,0,0x10,0x6,0x32,0,3,40,5.0, 0, 5.0,0  ;modify for e-Writer pro
6=0x400,0,0,0x80,0x80,1,1,0x011,1,1,1,1,5,1,0xB7,0,0x5,0x6,0x1,0,3,40,5.5,12.5,5.5,12.5  ; OTP Type-6, TPG=250us
7=0x400,0,0,0x80,0x80,1,1,1,1,1,1,1,100,1,0xB3,0,0x10,0x6,0x32,0,3,40,5.5, 0, 5.5,0  ; only for ht45fm03B:writepulse=10ms
8=0x0,0x2,0x2,0x00,0x80,1,1,1,1,1,1,1,10,1,0x15,0,10,0x10,0x32,10,3,0,5.5,12.5,5.5,12.5  
9=0x200,0,0,0x80,0x80,1,1,0xC1,1,1,1,1,40,1,0xB3,0,0x10,0x6,0x32,0,3,40,5.5, 0, 5.5,0     ; modify from index=4(flash type6A)
10=0x200,0,0,0x80,0x80,1,1,0x0D1,1,1,1,1,5,1,0xB7,0,0x5,0x6,0x1,0,3,40,5.5,12.5,5.5,12.5      ; modify from index=6(OTP type6A),not testing yet
11=0x0,0x2,0x2,0x00,0x80,1,1,0x40,1,1,1,1,10,1,0x15,0,10,0x10,0x32,10,3,0,5.5,12.5,5.5,12.5   ; modify from index=1(OTP type 2A)     
12=0x0,0x2,0x2,0x00,0x80,1,1,0x41,1,1,1,1,10,1,0x15,0,10,0x10,0x32,10,3,0,5.5,12.5,5.5,12.5   ; modify from index=1(OTP type 2A)     
13=0x200,0,0,0x80,0x80,1,1,0xE1,1,1,1,1,40,1,0xB3,0,0x10,0x6,0x32,0,3,40,5.5, 0, 5.5,0     ; modify from index=9(flash type6A), funcmap1.5 for type9
14=0x400,0,0,0x80,0x80,1,1,0x41,1,1,1,1,40,1,0xB3,0,0x10,0x6,0x32,0,3,40,5.5, 0, 5.5,0
15=0x400,0,0,0x80,0x80,1,1,3,1,1,1,1,40,1,0xB3,0,0x10,0x6,0x32,0,3,40,5.5, 0, 5.5,0        ;modify from index=4, funcmap1.2 for fast Power ON  
16=NULL

[Trim_RC]
;delete for i3000 at 20130524
[Ignore]
HT66FB540=0x1A, 0x7F, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x3F, 0x00, 0x01
HT66FB550=0x1A, 0xFF, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x7F, 0x00, 0x01
HT66FB560=0x1A, 0x7F, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x7F, 0x00, 0x01
HT68FB240=0x1A, 0x7F, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x7F, 0x00, 0x01
HT68FB540=0x1A, 0xFF, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x7F, 0x00, 0x01
HT68FB550=0x1A, 0xFF, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x3F, 0x00, 0x01
HT68FB560=0x1A, 0xFF, 0x1B, 0x7F, 0x1D, 0x1F, 0x1E, 0x7F, 0x1F, 0x7F, 0x00, 0x01

[IgnoreLastWord]
HT45R0Y=1
HT45R11=1
HT45R15=1
HT45R16=1
HT45R0YB=1
HT45R0YB-1=1
HT45R11B=1
SY-002=1

[CheckRom]
 RomTable=0x00,0x15,0x2A,0x3F,0x07,0x12,0x2D,0x38,0x38,0x2D,0x12,0x07,0x3F,0x2A,0x15,0x00

[DriverChangeable]
HT46R064 = 1, HT46R064B
HT46R065 = 1, HT46R065B
HT46R066 = 1, HT46R066B
HT48R063 = 1, HT48R063B
HT48R064 = 1, HT48R064B
HT48R065 = 1, HT48R065B
HT48R066 = 1, HT48R066B

[ISPData]
HT66FB540 = 0, SYSH_
HT66FB550 = 0, SYSH_
HT66FB560 = 0, SYSH_
HT68FB240 = 0, SYSL_
HT68FB540 = 0, SYSH_
HT68FB550 = 0, SYSH_
HT68FB560 = 0, SYSH_

[ConvertToDIP]
0x00000002 = 16, 40, 41, 42, 43, 44, 45, 46, 47, 0, 1, 2, 3, 4, 5, 6, 7, 28, 29, 30, 31, 32, 33, 34, 35, 12, 13, 14, 15, 16, 17, 18, 19  ; ESKT16NSOPA
0x00000003 = 28, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19   ; ESKT28SSOPA
0x00000004 = 28, 41, 42, 43, 44, 45, 46, 47, 0, 1, 2, 3, 4, 5, 6, 13, 12, 11, 10, 9, 8, 7, 40, 39, 38, 37, 36, 35, 34, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19   ; ESKT28SOPA
0x00000006 = 10, 47, 42, 44, 46, 45, 3, 0, 1, 2, 4, 28, 29, 30, 31, 32, 15, 16, 17, 18, 19    ; ESKT10MSOPA
0x00000009 = 28, 43, 44, 42, 45, 41, 46, 40, 47, 6, 0, 5, 1, 4, 2, 10, 12, 9, 13, 8, 14, 7, 33, 39, 34, 38, 35, 37, 36, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19  ; ESKT30SSOPA
0x0000000A = 20, 40, 42, 41, 45, 38, 44, 39, 47, 36, 46, 13, 15, 12, 14, 11, 17, 10, 16, 9, 19, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19  ; ESKT20TSSOPA
0x00000011 = 40, 41, 42, 40, 43, 39, 44, 38, 45, 37, 46, 36, 47, 11, 0, 10, 1, 9, 2, 8, 3, 15, 20, 14, 21, 13, 22, 12, 23, 35, 24, 34, 25, 33, 26, 32, 27, 31, 28, 30, 29, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ; ESKT56SSOPA

Okay, clearly it's connected, since the top bit mentions ISPDLL.dll, and we've got one of those. What do those strings mean?

Turns out they're microcontrollers. Presumably one of these is inside my mouse. There's a bunch of fluff on the Holtek website about them, including datasheets and software. The specifications are incredible - 62 powerful instructions and up to 12-level subroutine nesting. I... I need a moment to relax after reading this.

Alright. Next move: let's throw the updater into a disassembler! Quick scan of the Imports to see what's going on...

We're pulling in a few functions from HID.dll: HidD_GetAttributes, HidP_GetCaps, HidD_SetFeature, HidD_GetFeature, HidD_GetPreparsedData. Fair enough - we do need to communicate with the mouse, so that makes sense.

We're also pulling in some stuff from ISPDLL.dll: ConnectToBootloader, LoadProgdata, GetChksum, GetMCUInfo, LockAll, EraseByPage, BlankCheck, GetTransProgress, Program, ExecuteProgramFrom, SwitchToUserProgram, DisConnectBootloader, GetBootloaderVer. These seem quite promising: something's clearly going on here that involves flashing.

First off, let's look at the HID code, as we've already dealt with that in the config tool. It turns out that the exact same function to open the HID devices is present in FwUpdate.exe: it opens a read handle to the device FF01:0001 and a read/write handle to the device FF00:FF00. The former is never used, but it's still opened!

So, our focus goes to the device FF00:FF00, which is the one we saw used in the config tool but didn't get around to looking into. Presumably, both tools use this for communication in at least some form.

There's only two functions that refer to the handle. One is the one that opens the devices. The other one is actually pretty straightforward, and I'll present a slightly annotated decompilation below:

int __usercall sendCommand@<eax>(unsigned __int8 *data@<esi>)
{
  int v1; // ecx
  int v2; // edx
  unsigned __int8 command; // bl
  int result; // eax
  char buffer[9]; // [esp+4h] [ebp-18h]
  int v6; // [esp+10h] [ebp-Ch]

  v1 = *(_DWORD *)data;
  v2 = *((_DWORD *)data + 1);
  *(_DWORD *)&buffer[4] = 0;
  *(_DWORD *)buffer = 0;
  *(_DWORD *)&buffer[1] = v1;
  LOBYTE(v1) = -1 - data[6];
  *(_DWORD *)&buffer[8] = 0;
  LOBYTE(v1) = v1 - data[5] - v2 - data[3];
  *(_DWORD *)&buffer[5] = v2;
  LOBYTE(v1) = v1 - data[2] - data[1];
  v6 = 0;
  command = *data;
  buffer[8] = v1 - *data;
  result = HidD_SetFeature((int)handle_vendor00, (int)buffer, (unsigned __int16)featureLength_vendor00);
  if ( (command & 0x80u) != 0 )
  {
    *(_DWORD *)buffer = 0;
    *(_DWORD *)&buffer[4] = 0;
    *(_DWORD *)&buffer[8] = 0;
    v6 = 0;
    HidD_GetFeature(handle_vendor00, buffer, (unsigned __int16)featureLength_vendor00);
    result = *(_DWORD *)&buffer[5];
    *(_DWORD *)data = *(_DWORD *)&buffer[1];
    *((_DWORD *)data + 1) = result;
  }
  return result;
}

We're building an array (buffer) using the supplied data and sending it as a feature report to the device using HidD_SetFeature. The report length is pulled from a global variable (which I named featureLength_vendor00). Conveniently, we know that this variable is always 9, as the function that opens the HID handles fails out if the FF00:FF00 device's feature report length is not 9.

If the top bit of the first byte in the supplied data is set ((command & 0x80u) != 0), then we immediately request some data from the device, and put it back into the data array, overwriting what was originally there.

So, let's just recap what happens here:

We can immediately make a couple of assumptions here. We see that the function only expects a reply from the device if the top bit of data[0] is set -- this suggests that this byte may identify what sort of operation is performed. It's calculating something based off the first 7 bytes and putting it into the last slot -- this might be a rudimentary checksum.

Armed with this knowledge, let's look at what commands are being sent. There's only two references to sendCommand. The first is just this code:

int send_cmd_A()
{
  char v1[16]; // [esp+4h] [ebp-18h]

  *(_DWORD *)&v1[4] = 0;
  *(_DWORD *)&v1[8] = 0;
  *(_DWORD *)&v1[12] = 0;
  v1[0] = 0xA;
  v1[1] = 0xAAu;
  v1[2] = 0x55;
  v1[3] = 0xCCu;
  v1[4] = 0x33;
  v1[5] = 0xBBu;
  v1[6] = 0x99u;
  return sendCommand((unsigned __int8 *)v1);
}

It sends a fixed command: {0x0A, 0xAA, 0x55, 0xCC, 0x33, 0xBB, 0x99}. No reply is expected. Okay, let's look at the other one.

int send_cmd_8A()
{
  void *v0; // eax
  volatile signed __int32 *v1; // edi
  int result; // eax
  char v3[12]; // [esp+Ch] [ebp-18h]
  int v4; // [esp+18h] [ebp-Ch]

  v0 = sub_406920();
  if ( !v0 )
    sub_401440(0x80004005);
  v1 = (volatile signed __int32 *)(*(int (__thiscall **)(void *))(*(_DWORD *)v0 + 12))(v0);
  *(_DWORD *)v3 = 0;
  *(_DWORD *)&v3[4] = 0;
  *(_DWORD *)&v3[8] = 0;
  v4 = 0;
  v3[0] = 0x8Au;
  result = sendCommand((unsigned __int8 *)v3);
  if ( _InterlockedDecrement(v1 + 3) <= 0 )
    result = (*(int (__stdcall **)(volatile signed __int32 *))(**(_DWORD **)v1 + 4))(v1);
  return result;
}

This one's constructing a MFC string for some reason, and then they send command 0x8A with all-zero arguments. This command gets a reply, since the top bit is set... but then they never bother doing anything with the reply. Pathetic. Just pathetic.

Alright, this seems straightforward enough. Both of these functions are only ever called from one particular function. It's quite long, but it's also very easy to follow the general structure of because it's full of status messages (presumably these are shown in the UI) and it also calls a bunch of imported functions from ISPDLL.dll (which are conveniently named, since they come frrom a DLL). Let's take a look at that...

int __usercall sub_402B70@<eax>(int a1@<edi>)
{
  void *v1; // eax
  HDC v2; // eax
  struct CDC *v3; // ebp
  RECT v4; // STC8_16
  _DWORD *v5; // esi
  LPCWSTR *v6; // eax
  HRSRC v7; // eax
  LPCWSTR *v8; // eax
  HRSRC v9; // eax
  LPCWSTR *v10; // eax
  HRSRC v11; // eax
  unsigned __int8 v12; // al
  LPCWSTR *v13; // eax
  HRSRC v14; // eax
  HRSRC v15; // eax
  HRSRC v16; // eax
  LPCWSTR *v17; // eax
  char *v18; // eax
  DWORD v19; // eax
  LPCWSTR *v20; // eax
  int v21; // eax
  int v22; // eax
  LPCWSTR *v23; // eax
  int v24; // eax
  LPCWSTR *v25; // eax
  LPCWSTR *v26; // eax
  LPCWSTR *v27; // eax
  LPCWSTR *v28; // eax
  int v29; // eax
  HRSRC v30; // ecx
  int v31; // eax
  LPCWSTR *v32; // eax
  LPCWSTR *v33; // eax
  int v34; // eax
  LPCWSTR *v35; // eax
  LPCWSTR *v36; // eax
  LPCWSTR *v37; // eax
  int result; // eax
  LPCWSTR *v39; // eax
  int v40; // eax
  int v41; // [esp+E8h] [ebp-8Ch]
  HRSRC hResInfo; // [esp+ECh] [ebp-88h]
  HGLOBAL hResData; // [esp+F0h] [ebp-84h]
  int v44; // [esp+F4h] [ebp-80h]
  int v45; // [esp+F8h] [ebp-7Ch]
  int v46; // [esp+FCh] [ebp-78h]
  int v47; // [esp+100h] [ebp-74h]
  int v48; // [esp+104h] [ebp-70h]
  int v49; // [esp+108h] [ebp-6Ch]
  int v50; // [esp+10Ch] [ebp-68h]
  struct tagRECT Rect; // [esp+110h] [ebp-64h]
  int v52; // [esp+120h] [ebp-54h]
  char ArgList[4]; // [esp+124h] [ebp-50h]
  int v54; // [esp+128h] [ebp-4Ch]
  int v55; // [esp+12Ch] [ebp-48h]
  int v56; // [esp+130h] [ebp-44h]
  struct HDC__ hdcSrc; // [esp+134h] [ebp-40h]
  HDC hdc; // [esp+138h] [ebp-3Ch]
  int v59; // [esp+170h] [ebp-4h]

  v1 = sub_406920();
  if ( !v1 )
    sub_401440(-2147467259);
  v41 = (*(int (__thiscall **)(void *))(*(_DWORD *)v1 + 12))(v1) + 16;
  v59 = 0;
  GetClientRect(*(HWND *)(a1 + 32), &Rect);
  v2 = GetDC(*(HWND *)(a1 + 32));
  v3 = CDC::FromHandle(v2);
  *(_QWORD *)&v4.left = *(_QWORD *)&Rect.left;
  *(_QWORD *)&v4.right = *(_QWORD *)&Rect.right;
  sub_401650((CDC *)&hdcSrc, v4);
  LOBYTE(v59) = 1;
  if ( *(_DWORD *)(a1 + 128) )
  {
    SelectObject(hdc, *(HGDIOBJ *)(a1 + 128));
    (*(void (__thiscall **)(struct HDC__ *, int))(hdcSrc.unused + 40))(&hdcSrc, a1 + 120);
    CDC::SetBkMode((CDC *)&hdcSrc, 1);
    (*(void (__thiscall **)(struct HDC__ *, signed int))(hdcSrc.unused + 48))(&hdcSrc, 0xFFFFFF);
  }
  if ( ConnectToBootloader() == -1 )
  {
    Sleep(0x3E8u);
    if ( init_comms((CWnd *)a1) != 1 && init_comms((CWnd *)a1) != 1 )
      goto LABEL_8;
    send_cmd_A();
    Sleep(5u);
    send_cmd_8A();
    Sleep(10000u);
    if ( ConnectToBootloader() == -1 )
    {
      if ( init_comms((CWnd *)a1) != 1 )
      {
LABEL_8:
        v5 = (_DWORD *)(a1 + 148);
        sub_405EA0(L"Please insert the mouse and try it again.");
LABEL_52:
        dword_44B764 = 0;
        dword_44B768 = 0;
        v39 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
        LOBYTE(v59) = 18;
        sub_403B50((CWnd *)a1, (int)&hdcSrc, *v39, dword_448B28, dword_448B2C);
        LOBYTE(v59) = 1;
        v40 = v50 - 16;
        if ( _InterlockedDecrement((volatile signed __int32 *)(v50 - 16 + 12)) <= 0 )
          (*(void (__stdcall **)(int))(**(_DWORD **)v40 + 4))(v40);
        (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
          &hdcSrc,
          *v5,
          *(_DWORD *)(*v5 - 12),
          &unk_448B38,
          37);
        BitBlt(*((HDC *)v3 + 1), 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdc, 0, 0, 0xCC0020u);
        ReleaseDC(*(HWND *)(a1 + 32), *((HDC *)v3 + 1));
        LOBYTE(v59) = 0;
        sub_401790((CDC *)&hdcSrc);
        v59 = -1;
        result = v41 - 16;
        if ( _InterlockedDecrement((volatile signed __int32 *)(v41 - 16 + 12)) <= 0 )
          result = (*(int (__stdcall **)(int))(**(_DWORD **)result + 4))(result);
        return result;
      }
      send_cmd_A();
      Sleep(5u);
      send_cmd_8A();
      Sleep(0x2710u);
      if ( ConnectToBootloader() == -1 )
      {
        v5 = (_DWORD *)(a1 + 148);
        sub_405EA0(L"Switch to ISP Mode Failed");
        goto LABEL_52;
      }
    }
  }
  dword_44B764 = 1;
  dword_44B768 = 1;
  v6 = (LPCWSTR *)sub_404920((int)&hResInfo, a1 + 132, L"/Skins/check1.png");
  LOBYTE(v59) = 2;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v6, dword_448B78, dword_448B7C);
  LOBYTE(v59) = 1;
  v7 = hResInfo - 4;
  if ( _InterlockedDecrement((volatile signed __int32 *)hResInfo - 1) <= 0 )
    (*(void (__stdcall **)(HRSRC))(**(_DWORD **)v7 + 4))(v7);
  v8 = (LPCWSTR *)sub_404920((int)&hResInfo, a1 + 132, L"/Skins/check1.png");
  LOBYTE(v59) = 3;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v8, dword_448B88, dword_448B8C);
  LOBYTE(v59) = 1;
  v9 = hResInfo - 4;
  if ( _InterlockedDecrement((volatile signed __int32 *)hResInfo - 1) <= 0 )
    (*(void (__stdcall **)(HRSRC))(**(_DWORD **)v9 + 4))(v9);
  v5 = (_DWORD *)(a1 + 148);
  sub_405D90((void **)(a1 + 148), L"Switch to ISP Mode successful", 29);
  sub_4058B0((void **)(a1 + 148), (void **)&v41);
  v10 = (LPCWSTR *)sub_404920((int)&hResInfo, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 4;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v10, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  v11 = hResInfo - 4;
  if ( _InterlockedDecrement((volatile signed __int32 *)hResInfo - 1) <= 0 )
    (*(void (__stdcall **)(HRSRC))(**(_DWORD **)v11 + 4))(v11);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  BitBlt(*((HDC *)v3 + 1), 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdc, 0, 0, 0xCC0020u);
  Sleep(0x3E8u);
  v12 = GetBootloaderVer();
  sub_401510((int)&v41, L"Bootloader version is:%d", v12);
  v13 = (LPCWSTR *)sub_404920((int)&hResInfo, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 5;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v13, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  v14 = hResInfo - 4;
  if ( _InterlockedDecrement((volatile signed __int32 *)hResInfo - 1) <= 0 )
    (*(void (__stdcall **)(HRSRC))(**(_DWORD **)v14 + 4))(v14);
  (*(void (__thiscall **)(struct HDC__ *, int, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    v41,
    *(_DWORD *)(v41 - 12),
    &unk_448B38,
    37);
  BitBlt(*((HDC *)v3 + 1), 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdc, 0, 0, 0xCC0020u);
  Sleep(0x3E8u);
  v15 = FindResourceW(0, (LPCWSTR)0x85, L"MTP");
  hResInfo = v15;
  if ( !v15 )
  {
    sub_405D90((void **)(a1 + 148), L"Find resource failed.", 21);
    goto LABEL_52;
  }
  hResData = LoadResource(0, v15);
  v16 = (HRSRC)SizeofResource(0, hResInfo);
  hResInfo = v16;
  if ( hResData && v16 )
  {
    v50 = (int)LockResource(hResData);
    if ( !v50 )
    {
      sub_405EA0(L"Res Buffer is Null");
      goto LABEL_52;
    }
    sub_401510((int)&v41, L"Res size is:%ld\n", hResInfo);
  }
  sub_405D90((void **)(a1 + 148), L"Load Program Data...", 20);
  v17 = (LPCWSTR *)sub_404920((int)&hResData, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 6;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v17, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  v18 = (char *)hResData - 16;
  if ( _InterlockedDecrement((volatile signed __int32 *)hResData - 1) <= 0 )
    (*(void (__stdcall **)(char *))(**(_DWORD **)v18 + 4))(v18);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  BitBlt(*((HDC *)v3 + 1), 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdc, 0, 0, 0xCC0020u);
  Sleep(0x3E8u);
  v49 = 0;
  v47 = 0;
  v45 = 0;
  v48 = 0;
  v46 = 0;
  v44 = 0;
  if ( LoadProgdata(v50, hResInfo, &v49, &v48, &v47, &v46, &v45, &v44) == -1 )
  {
    v19 = GetLastError();
    sub_401510((int)&v41, L"Error code:%d", v19);
    sub_405EA0(L"Load resource data failed.");
    goto LABEL_52;
  }
  sub_405EA0(L"Get Chksum...");
  v20 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 7;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v20, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  v21 = v50 - 16;
  if ( _InterlockedDecrement((volatile signed __int32 *)(v50 - 16 + 12)) <= 0 )
    (*(void (__stdcall **)(int))(**(_DWORD **)v21 + 4))(v21);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  BitBlt(*((HDC *)v3 + 1), 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdc, 0, 0, 0xCC0020u);
  Sleep(0x3E8u);
  v22 = GetChksum(1);
  if ( v22 == -1 )
  {
    sub_405EA0(L"GetChksum failed.");
    goto LABEL_52;
  }
  sub_401510((int)&v41, L"Chksum is:%ld", v22);
  sub_4058B0((void **)(a1 + 148), (void **)&v41);
  v23 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 8;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v23, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  v24 = v50 - 16;
  if ( _InterlockedDecrement((volatile signed __int32 *)(v50 - 16 + 12)) <= 0 )
    (*(void (__stdcall **)(int))(**(_DWORD **)v24 + 4))(v24);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  BitBlt(*((HDC *)v3 + 1), 0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, hdc, 0, 0, 0xCC0020u);
  Sleep(0x3E8u);
  v52 = 20;
  if ( !GetMCUInfo(&v52) )
  {
    sub_405EA0(L"GetMCUInfo failed.");
    goto LABEL_52;
  }
  sub_401510(
    (int)&v41,
    L"PageSize is:%d\nMaxProgramPage:%d\nMaxLockPage:%d\nBootloader Size:%d",
    *(_DWORD *)ArgList,
    v54,
    v55,
    v56);
  sub_405EA0(L"Lock MCU Flash... ");
  v25 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 9;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v25, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
  if ( LockAll() == -1 )
  {
    sub_405EA0(L"Lock MCU Flash failed.");
    goto LABEL_52;
  }
  Sleep(0x3E8u);
  sub_405EA0(L"Erase MCU Flash... ");
  v26 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 10;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v26, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
  if ( EraseByPage(0, v54, 0) == -1 )
  {
    sub_405EA0(L"Erase MCU Flash failed.");
    goto LABEL_52;
  }
  Sleep(0x3E8u);
  sub_405EA0(L"Verify MCU Flash... ");
  v27 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 11;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v27, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
  if ( BlankCheck(0) == -1 )
  {
    sub_405EA0(L"Blank Check failed.");
    goto LABEL_52;
  }
  Sleep(0x3E8u);
  sub_405EA0(L"Program MCU Flash... ");
  v28 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 12;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v28, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
  Sleep(0x3E8u);
  SetEvent(hHandle);
  while ( GetTransProgress() != 100 )
  {
    v29 = GetTransProgress();
    v30 = (HRSRC)v29;
    v31 = v29 / 10;
    hResInfo = v30;
    if ( v30 == (HRSRC)(10 * v31) && (signed int)hResInfo <= 100 )
    {
      sub_401510((int)&v41, L"/Skins/%d.png", v31);
      v32 = (LPCWSTR *)sub_404860(&v50, a1 + 132, &v41);
      LOBYTE(v59) = 13;
      sub_403B50((CWnd *)a1, (int)&hdcSrc, *v32, dword_448B18, dword_448B1C);
      LOBYTE(v59) = 1;
      sub_401290(&v50);
      sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
      sub_401510((int)&v41, L"%d%%", hResInfo);
      sub_4058B0((void **)(a1 + 148), (void **)&v41);
      v33 = (LPCWSTR *)sub_404920((int)&hResData, a1 + 132, L"/Skins/InfoBoard.png");
      LOBYTE(v59) = 14;
      sub_403B50((CWnd *)a1, (int)&hdcSrc, *v33, dword_448B28, dword_448B2C);
      LOBYTE(v59) = 1;
      sub_401290(&hResData);
      (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
        &hdcSrc,
        *v5,
        *(_DWORD *)(*v5 - 12),
        &unk_448B38,
        37);
      sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
    }
  }
  v34 = GetTransProgress();
  sub_401510((int)&v41, L"%d%%", v34);
  sub_4058B0((void **)(a1 + 148), (void **)&v41);
  v35 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 15;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v35, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  sub_401510((int)&v41, L"/Skins/%d.png", 10);
  v36 = (LPCWSTR *)sub_404860(&v50, a1 + 132, &v41);
  LOBYTE(v59) = 16;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v36, dword_448B18, dword_448B1C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
  Sleep(0x2710u);
  sub_405EA0(L"MCU Flash Update Finished");
  v37 = (LPCWSTR *)sub_404920((int)&v50, a1 + 132, L"/Skins/InfoBoard.png");
  LOBYTE(v59) = 17;
  sub_403B50((CWnd *)a1, (int)&hdcSrc, *v37, dword_448B28, dword_448B2C);
  LOBYTE(v59) = 1;
  sub_401290(&v50);
  (*(void (__thiscall **)(struct HDC__ *, _DWORD, _DWORD, void *, signed int))(hdcSrc.unused + 104))(
    &hdcSrc,
    *v5,
    *(_DWORD *)(*v5 - 12),
    &unk_448B38,
    37);
  sub_405FA0(0, 0, Rect.right - Rect.left, Rect.bottom - Rect.top, &hdcSrc, 0, 0, 0xCC0020u);
  sub_404E10(v3);
  LOBYTE(v59) = 0;
  sub_401790((CDC *)&hdcSrc);
  return sub_401290(&v41);
}

What's going on here? It first calls ConnectToBootloader. If that fails, it sleeps for a second, connects to the mouse, sends command 0xA, sleeps for 5ms, sends command 0x8A, sleeps for 10 seconds, and then tries to connect to the bootloader again. If that fails, it retries again. If even that fails, it tells the user to insert the mouse. (Sounds painful, but who am I to judge?) If it detects the mouse but the commands failed, then it reports "Switch to ISP Mode Failed".

Thanks to that message, we can make a reasonable assumption that these commands are supposed to switch the mouse to ISP Mode. (This stands for "In-System Programming", by the way -- it does not turn your mouse into the reincarnation of AOL. Sadly.)

If it worked, then it reports "Switch to ISP Mode successful", sleeps for a second, calls GetBootloaderVer, reports the result, sleeps for another second, pulls some data out of the Windows resources using FindResourceW, and then tries to load it using LoadProgdata.

There's a few more calls to things that are obviously related to flashing. I'm not going to bother with those, as I've already come across something interesting -- this looks like it could be the firmware itself!

Extracting it gets us a 32.1KB file; it's got 0x80C1 bytes. That looks suspiciously close to 0x8000, which suggests to me that it's probably 0x8000 bytes worth of firmware with a bit of extra metadata. How can we figure out the format?

There's also one more question left to answer: The EFORMAT.INI file suggests that this flashing code supports multiple kinds of Holtek microcontrollers, with different parameters, but there's no functions called to tell the flasher what chip to use. Perhaps the metadata in the MTP file explains this?

Tune in soon for Part 3 where I'll explore this and extract and disassemble the firmware!

If you've really enjoyed this series of posts and want to help fund my hot chocolate habit, feel free to send me a couple of pounds: Ko-fi | Monzo.me (UK) | PayPal.me


Previous Post: Mouse Adventures #1: Introduction
Next Post: Mouse Adventures #3: Writing a Disassembler