Wherein I delve into the internals of my no-name brand "Tecknet Hypertrak Gaming Mouse" in an attempt to write a cross-platform tool that can manipulate its configuration without having to use the awful Windows-only tool it's shipped with. In this first part we tear apart said tool to figure out how it communicates with the mouse.
After a couple of years of exclusively using a laptop, I built a desktop computer in early 2018. I picked up a cheap gaming mouse upon a recommendation from a friend -- the TeckNet HyperTrak Gaming Mouse (or the TeckNet M009-V2, as the drivers call it).
For the princely sum of £23, you get what appears to be a decent little peripheral: it's got a braided USB cable, customisable lights, adjustable weights, DPI switching, 12 programmable buttons (left, right, wheel click, wheel tilt left/right, DPI up/down, five side buttons), macros... Not bad for the price.
The software's a bit shit, but once again I suppose you can't complain too much for the price. It's a Windows-only tool that doesn't really inspire much confidence. Here's a screenshot and a quick overview of the options:
Selecting the Config button at the bottom left gets you a list of configurations, which you can save to and load from files. The currently-applied configuration is saved onto the mouse itself, so you don't need to have the software running for these features to work - you only need it to make changes.
Anyway, come October, I finally got tired of Windows and decided to install macOS High Sierra* on my desktop. To my dismay, I realised that this mouse doesn't actually expose its extra buttons as standard HID buttons, so they're practically useless without the configuration tool. I tried several tools like BetterTouchTool and USB Overdrive which promised to offer support for input devices with non-standard buttons, but none of them could detect anything when I pressed one of the extra buttons.
What's the obvious solution? To do it myself, of course...
* this machine has a GTX 1060 in it, and there's no reliable way to get graphics acceleration on 10.14 Mojave with one of those just yet - Apple's GeForce drivers don't support Pascal cards, and Nvidia's drivers don't support Mojave yet
I thought a good start would be to look at what sort of things the TeckNet drivers are doing, so I rebooted into my Windows installation again and looked into the files they contain.
res
contains some icons. Skins
contains images for the UI. record.ini
contains information about macros. The other INI files all seem to contain versions of the displayed configuration strings; I'm not sure why there's so many of them. configX.bin
contains the settings in a binary format. The X64
and X86
directories seem like they might have been intended for Windows drivers, but there's nothing inside them.
What's of most interest to us here are the configuration tool itself (TeckNet M009-V2 setup.exe
) and the Update
directory, which contains 32-bit and 64-bit executables for firmware updaters.
But, we'll look at that later... For now, let's throw the configuration tool into a disassembler! It's only 1.25MB, so it can't be that bad, right?
My usual tactic when I get something without symbols is to look at the executable's strings and imports for some leads as to where interesting things happen. It's very obviously a MFC application -- it's been compiled using Visual C++ with RTTI, so you get lots of helpful class names like CWnd
and CDialog
and COleControlSite
. That's a dead giveaway.
Many of the strings are generic UI fluff, but there's a few interesting things; the first that caught my eye was this set which is obviously related to device communication, as it references the mouse's USB vendor/product ID, 04D9:A118.
Following references to those format strings takes us to a promising function that's calling a bunch of device enumeration and HID functions. Definitely relevant to our interests! The decompiled output is a bit hard to follow because of all the MFC string class usage, but with a bit of work we can clean up into something that's mostly readable, and make some sense out of it.
signed __int64 init_classes()
{
HDEVINFO deviceInfoSet; // rbp
signed __int64 result; // rax
DWORD interfaceIndex; // er13
BOOL v3; // er12
struct _SP_DEVICE_INTERFACE_DETAIL_DATA_W *interfaceDetailData; // rsi
int v5; // ecx
signed __int64 v6; // rcx
WCHAR *v7; // rdi
bool v8; // zf
HANDLE v9; // rax
struct ATL::CStringData *(__fastcall ***v10)(CAfxStringMgr *__hidden, int, int); // rax
struct ATL::CStringData *(__fastcall ***v11)(CAfxStringMgr *__hidden, int, int); // rax
__int64 v12; // rax
signed __int64 v13; // rdi
__int16 v14; // ax
__int64 v15; // rdx
__int64 v16; // rdx
__int64 v17; // rdx
__int64 v18; // rdx
__int64 v19; // rdx
__int64 v20; // rdx
__int64 v21; // rdx
__int64 v22; // rdx
__int64 v23; // rdx
__int64 v24; // rdx
__int64 DeviceInterfaceData; // [rsp+20h] [rbp-F8h]
__int64 usagePage; // [rsp+20h] [rbp-F8h]
PSP_DEVINFO_DATA featureReportLength; // [rsp+28h] [rbp-F0h]
HANDLE inputReportLength; // [rsp+30h] [rbp-E8h]
__int64 outputReportLength; // [rsp+38h] [rbp-E0h]
__int64 v30; // [rsp+40h] [rbp-D8h]
DWORD RequiredSize; // [rsp+48h] [rbp-D0h]
__int64 v32; // [rsp+50h] [rbp-C8h]
HIDD_ATTRIBUTES hidAttrs; // [rsp+58h] [rbp-C0h]
__int64 preparsedDataPtr; // [rsp+68h] [rbp-B0h]
__int64 v35; // [rsp+70h] [rbp-A8h]
HIDP_CAPS hidCaps; // [rsp+80h] [rbp-98h]
struct _SP_DEVICE_INTERFACE_DATA interfaceData; // [rsp+C0h] [rbp-58h]
v35 = -2i64;
// find all devices that support HID collections
deviceInfoSet = SetupDiGetClassDevsW(&GUID_DEVINTERFACE_HID, 0i64, 0i64, 0x12u);// DIGCF_PRESENT | DIGCF_DEVICEINTERFACE
if ( deviceInfoSet == (HDEVINFO)-1i64 )
{
SetupDiDestroyDeviceInfoList((HDEVINFO)0xFFFFFFFFFFFFFFFFi64);
return 0i64;
}
interfaceData.cbSize = 32;
interfaceIndex = 0;
// loop through every interface in the set
while ( 1 )
{
v3 = SetupDiEnumDeviceInterfaces(deviceInfoSet, 0i64, &GUID_DEVINTERFACE_HID, interfaceIndex, &interfaceData);
if ( !v3 && GetLastError() == 259 )
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
return 0i64;
}
RequiredSize = 0;
SetupDiGetDeviceInterfaceDetailW(deviceInfoSet, &interfaceData, 0i64, 0, &RequiredSize, 0i64);
interfaceDetailData = (struct _SP_DEVICE_INTERFACE_DETAIL_DATA_W *)malloc(RequiredSize);
interfaceDetailData->cbSize = 8;
if ( !SetupDiGetDeviceInterfaceDetailW(deviceInfoSet, &interfaceData, interfaceDetailData, RequiredSize, 0i64, 0i64) )
{
free(interfaceDetailData);
SetupDiDestroyDeviceInfoList(deviceInfoSet);
return 0i64;
}
if ( interfaceDetailData == (struct _SP_DEVICE_INTERFACE_DETAIL_DATA_W *)-4i64 )
{
v5 = 0;
}
else
{
v6 = -1i64;
v7 = interfaceDetailData->DevicePath;
do
{
if ( !v6 )
break;
v8 = *v7 == 0;
++v7;
--v6;
}
while ( !v8 );
v5 = ~(_DWORD)v6 - 1;
}
sub_1400049E0((void **)&theDevicePath, interfaceDetailData->DevicePath, v5);
free(interfaceDetailData);
// open a handle to this device
// no reading or writing, just check metadata
v9 = CreateFileW(theDevicePath, 0, 3u, 0i64, 3u, 0, 0i64);
g_FF00_FF00_handle = v9;
if ( v9 == (HANDLE)-1i64 )
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
return 0i64;
}
HidD_GetAttributes(v9, &hidAttrs);
v10 = getAfxStringMgr();
if ( v10 == 0i64 )
throwAfxException(-2147467259);
v30 = ((__int64 (__fastcall *)(struct ATL::CStringData *(__fastcall ***)(CAfxStringMgr *__hidden, int, int)))(*v10)[3])(v10)
+ 24;
LODWORD(DeviceInterfaceData) = (unsigned __int16)hidAttrs.VersionNumber;
formatAfxString(
(__int64)&v30,
(__int64)L"PID:%04x\n,VID:%04x\n,VersionNumber:%04x\n",
(unsigned __int16)hidAttrs.ProductID,
(unsigned __int16)hidAttrs.VendorID,
DeviceInterfaceData);
// check for the Tecknet mouse VID/PID
if ( hidAttrs.VendorID == 0x4D9 && 0xA118u == hidAttrs.ProductID )
break;
tryNextInterface:
++interfaceIndex;
v16 = v30 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v30 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v16 + 8i64))();
if ( !v3 )
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
return 0i64;
}
}
if ( (unsigned __int8)HidD_GetPreparsedData(g_FF00_FF00_handle, &preparsedDataPtr) )
{
CloseHandle(g_FF00_FF00_handle);
v11 = getAfxStringMgr();
if ( v11 == 0i64 )
throwAfxException(-2147467259);
v12 = ((__int64 (__fastcall *)(struct ATL::CStringData *(__fastcall ***)(CAfxStringMgr *__hidden, int, int)))(*v11)[3])(v11);
v13 = v12 + 24;
v32 = v12 + 24;
if ( (unsigned int)HidP_GetCaps(preparsedDataPtr, &hidCaps) == 0xC0110001 )
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
if ( _InterlockedDecrement((volatile signed __int32 *)(v13 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)(v13 - 24) + 8i64))();
v23 = v30 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v30 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v23 + 8i64))();
return 0i64;
}
LODWORD(outputReportLength) = (unsigned __int16)hidCaps.OutputReportByteLength;
LODWORD(inputReportLength) = (unsigned __int16)hidCaps.InputReportByteLength;
LODWORD(featureReportLength) = (unsigned __int16)hidCaps.FeatureReportByteLength;
LODWORD(usagePage) = (unsigned __int16)hidCaps.UsagePage;
formatAfxString(
(__int64)&v32,
(__int64)L"DevicePath:%s,\n"
"Usage:0x%04x\n"
", UsagePage:0x%04x\n"
",FeatureReportByteLength:0x%04x\n"
",InputReportByteLength:0x%x\n"
",OutputReportByteLength:0x%x\n",
theDevicePath,
(unsigned __int16)hidCaps.Usage,
usagePage,
featureReportLength,
inputReportLength,
outputReportLength);
v14 = hidCaps.Usage;
if ( hidCaps.Usage == 1 )
{
if ( hidCaps.UsagePage != 0xFF01u || 0 == hidCaps.InputReportByteLength )
{
failedUsageCheck:
v15 = v32 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v32 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v15 + 8i64))();
goto tryNextInterface;
}
// GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, no security attribs, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, no template
g_readFrom1_FF01 = CreateFileW(theDevicePath, GENERIC_READ, 3u, 0i64, 3u, 0x40000080u, 0i64);
if ( g_readFrom1_FF01 == (HANDLE)-1i64 )
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
v17 = v32 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v32 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v17 + 8i64))();
v18 = v30 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v30 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v18 + 8i64))();
return 0i64;
}
SetEvent(Overlapped.hEvent);
g_inputReportLength = hidCaps.InputReportByteLength;
v14 = hidCaps.Usage;
}
if ( 0xFF00u == v14 && 0xFF00u == hidCaps.UsagePage && hidCaps.FeatureReportByteLength == 9 )
{
g_FF00_FF00_handle = CreateFileW(theDevicePath, 0xC0000000, 3u, 0i64, 3u, 0, 0i64);
if ( g_FF00_FF00_handle == (HANDLE)-1i64 )
{
SetupDiDestroyDeviceInfoList(deviceInfoSet);
v19 = v32 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v32 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v19 + 8i64))();
v20 = v30 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v30 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v20 + 8i64))();
result = 0i64;
}
else
{
g_have_FF00_FF00 = 1;
g_FF00_FF00_outputLength = hidCaps.OutputReportByteLength;
g_FF00_FF00_inputLength = hidCaps.InputReportByteLength;
g_FF00_FF00_featureLength = hidCaps.FeatureReportByteLength;
SetupDiDestroyDeviceInfoList(deviceInfoSet);
v21 = v32 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v32 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v21 + 8i64))();
v22 = v30 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v30 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v22 + 8i64))();
result = 1i64;
}
return result;
}
goto failedUsageCheck;
}
// if we got here then HidD_GetPreparsedData failed, so we fucked it
SetupDiDestroyDeviceInfoList(deviceInfoSet);
CloseHandle(g_FF00_FF00_handle);
v24 = v30 - 24;
if ( _InterlockedDecrement((volatile signed __int32 *)(v30 - 24 + 16)) <= 0 )
(*(void (**)(void))(**(_QWORD **)v24 + 8i64))();
return 0i64;
}
Here's a quick overview of what's going on here:
These devices allow the tool to communicate with the mouse. There's a pretty good description of how HID works here on MSDN, but in a nutshell: HID stands for Human Interface Device, and it's a standard, extensible way for things like mice and keyboards to communicate with OSes. HIDs can have input reports (data sent by the device in a pre-defined format), output reports (data sent to the device in a pre-defined format) and feature reports (arbitrary data sent by the host to the device, or requested by the host from the device).
Each device has a "Usage" which identifies what sort of device it is (mouse, keyboard, gamepad, joystick, golf club...) against a set of pre-defined device types and properties. These two usages in particular are both within the vendor-specific range, which you can essentially read as "device designers can do whatever the fuck they want"... which is precisely why we're in this mess -- all of this mouse's fancy features are configured through non-standard, vendor-specific feature reports!
So, we now have a rough idea of how the config tool communicates with the mouse. We know how it sets up that communication; that's a good starting point. If we look at the references to the device handles, then that'll probably shed some more light on precisely what it's doing!
We have device FF01:0001, which we only read from - we never write to it. Pretty simple, let's have a look. It's only read from one particular function, which runs inside a thread, and said thread is created right after the communication is initialised. Straightforward? Yes.
void __fastcall __noreturn readFromFF01Device_Thread(void *a1)
{
DWORD amountToRead; // ebx
struct ATL::CStringData *(__fastcall ***v2)(CAfxStringMgr *__hidden, int, int); // rax
DWORD NumberOfBytesTransferred; // [rsp+30h] [rbp-48h]
DWORD amountThatWasRead; // [rsp+34h] [rbp-44h]
char Buffer[40]; // [rsp+38h] [rbp-40h]
amountToRead = (unsigned __int16)g_inputReportLength;
*(_QWORD *)Buffer = 0i64;
*(_QWORD *)&Buffer[8] = 0i64;
*(_QWORD *)&Buffer[16] = 0i64;
*(_QWORD *)&Buffer[24] = 0i64;
*(_QWORD *)&Buffer[32] = 0i64;
amountThatWasRead = 0;
v2 = getAfxStringMgr();
if ( !v2 )
throwAfxException(-2147467259);
((void (__fastcall *)(struct ATL::CStringData *(__fastcall ***)(CAfxStringMgr *__hidden, int, int)))(*v2)[3])(v2);
while ( 1 )
{
ResetEvent(Overlapped.hEvent);
if ( g_have_FF00_FF00 != 1 || g_readFrom1_FF01 == (HANDLE)-1i64 )
{
// device isn't connected right now, so wait?
WaitForSingleObject(Overlapped.hEvent, 0xFFFFFFFF);
}
else
{
// request some data
ReadFile(g_readFrom1_FF01, Buffer, amountToRead, &amountThatWasRead, &Overlapped);
WaitForSingleObject(Overlapped.hEvent, 0xFFFFFFFF);
if ( g_have_FF00_FF00 )
{
// we've got it (hopefully?)
GetOverlappedResult(g_readFrom1_FF01, &Overlapped, &NumberOfBytesTransferred, 1);
if ( NumberOfBytesTransferred )
{
// send message WM_USER+2 to the dialog if the input report starts with 06 07
if ( Buffer[0] == 6 && Buffer[1] == 7 )
SendMessageW(g_dialogHwnd, 0x402u, (unsigned __int8)Buffer[2], (unsigned __int8)Buffer[3]);
}
}
}
}
}
Essentially it just keeps on reading input reports, over and over again. If it receives one beginning in 06 07, then the two bytes following that are sent to the main dialog box for processing using a non-standard Windows message WM_USER + 2
.
Can we find the processing for this? Usually we'd just look for a WndProc or a DlgProc, but this is a MFC application, so naturally it's a bit more complicated than that. A quick search brings me to documentation for ON_MESSAGE on MSDN, which is apparently The One True Way™ to handle user-defined messages in MFC. Judging by the code sample, there should be a message map table somewhere containing these. This is probably an array of structures somewhere.
Searching the executable for 02 04 (0x0402 in little endian) brings us to a possible candidate; there's a bunch of function pointers and zeroes and things that look like message IDs, in fixed intervals, which is a pretty good sign that you are looking at an array of structures. A bit of massaging in IDA and suddenly it looks pretty reasonable:
Yep, that's a message map alright. The handler we want is sub_1400145B0
. It takes three parameters: the implicit this
pointer (since it's a method), followed by wParam
and lParam
. We know these correspond to buffer[2]
and buffer[3]
from the data sent by the mouse, as those were passed into the SendMessage call.
That method is a pain to read as it's accessing a lot of fields inside the dialog object and also doing some MFC string manipulation, but we can still get a general idea of what's going on. The exact behaviour performed depends on what the two parameters are. There's one certainty: the function will always either do nothing, or it will do some stuff and then call Shell_NotifyIconW
to spawn a Windows notification.
I know the mouse can change between the different pre-configured DPI levels through button presses, and whenever this happens, the config tool pops up a Windows notification. With that in mind, I'm just going to assume that this code is related to this - it might even pop up different kinds of notifications I haven't seen before. We can always come back to it later after all.
So we now have an informed guess about the FF01:0001 device: it's used by the mouse to notify the software about certain actions including DPI setting changes and possibly other stuff. Next, we want to figure out the FF00:FF00 device - it uses both reading and writing, so that's likely to be where all the interesting configuration stuff happens.
I think this post has dragged on long enough for now, so I'm gonna split it up here. In part 2 I'll look into the origins of this cursed hardware, I'll investigate the protocol used to communicate with the mouse, and tear open the firmware updater.
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: Relaying OpenVPN through a Remote Server
Next Post: Mouse Adventures #2: Extracting the Firmware