二进制咸鱼的自我救赎

幸福往往是摸的透彻,而敬业的心却常常隐藏。

About RSS

戴记严选 GM3323D 驱动逆向,并实现在 Linux 上调整灯的颜色

#re

我手里有一个几年前买的戴记严选 GM3323D鼠标,这个鼠标为数不多的缺点就是他只能在 Windows 上进行设置鼠标的灯光特效,所以趁着现在有空赶紧对他的驱动进行逆向和移植。

逆向工程

查看安装目录可以看到 "GM3323D.exe" 和 "hiddriver_1.dll", "hiddriver_2.dll" 这几个明显的文件,还有 "DuiLib.dll" 这种第三方库,"res" 文件夹里放的是资源文件,包括程序布局,贴图。我们先都记下来。

安装目录下还放了第三方库的开源许可协议,给这个公司点赞!

GM3323D.exe

首先是逆向 "GM3323D.exe",配合安装目录下 "res/wirelesslightpage.xml" 文件里出现的 "wireless_lightmode1" 字符串,我们在 IDA 中 xref 定位到下面函数。这里我猜测程序是读取选项的下标,然后将下标塞入格式化字符串 L"wireless_lightmode%d",来选择对应模式,并通过 PostMessageW 传递给消息处理函数进行后续的操作。使用 IDA 定位字符串的时候,可以在 String 窗口右键选择 Setup 选项,勾上 "unicode c style" 选项,这样才能搜索到 utf16 字符串。

void __thiscall sub_40FF50(int this, int a2)
{
  int v3; // edi
  int v4; // esi
  int v5; // eax
  _BYTE v6[132]; // [esp+10h] [ebp-94h] BYREF
  int v7; // [esp+A0h] [ebp-4h]

  if ( *(_DWORD *)(a2 + 264) == *(_DWORD *)(this + 1692) )
  {
    v3 = *(_DWORD *)(a2 + 280);
    *(_DWORD *)(32872 * **(_DWORD **)(this + 1900) + *(_DWORD *)(this + 1900) + 2364) = v3;
    DuiLib::CDuiString::CDuiString(this: (DuiLib::CDuiString *)v6);
    v7 = 0;
    DuiLib::CDuiString::Format(this: (DuiLib::CDuiString *)v6, a2: L"wireless_lightmode%d", v3 + 1);
    v4 = **(_DWORD **)(this + 1692);
    v5 = DuiLib::CDuiString::operator wchar_t const *(this: (DuiLib::CDuiString *)v6);
    (*(void (__thiscall **)(_DWORD, int))(v4 + 196))(a1: *(_DWORD *)(this + 1692), a2: v5);
    sub_4107A0(a1: v3);
    PostMessageW(hWnd: *(HWND *)(this + 1904), Msg: 0x535u, wParam: 0, lParam: 0);
    v7 = -1;
    DuiLib::CDuiString::~CDuiString(this: (DuiLib::CDuiString *)v6);
  }
}

知道消息 ID 为 0x535 之后继续搜索,在程序的反编译结果里搜索 0x535 没结果,搜索 1333 定位到了下面的代码。

    case 1333:
      PostThreadMessageW(*(_DWORD *)(this + 3044), 0x670u, wParam, lParam);
      *(_DWORD *)a5 = 1;
      return 0;

再次将 0x670 转换为 1648 进行搜索,定位到了处理消息的函数。里面有些代码片段比较可疑,因为程序里默认的配置数量就是 5 个。

DWORD __stdcall StartAddress(char *lpThreadParameter)
{
  while ( GetMessageW(lpMsg: &Msg, hWnd: nullptr, wMsgFilterMin: 0, wMsgFilterMax: 0) )
  {
    if ( Msg.message == 1648
      && (*((_DWORD *)lpThreadParameter + 90518) == 1 || *((_DWORD *)lpThreadParameter + 90517) == 1)
      && *((_DWORD *)lpThreadParameter + 90520) != 1 )
    {
      // ...
      for ( i = 0; i < 5; ++i )
      {
        if ( lpThreadParameter[i + 214] == 1 )
          sub_416010(this: (int)lpThreadParameter, a2: i);
        if ( lpThreadParameter[i + 219] == 1 )  // lightmode 变化
          sub_416410(this: (int)lpThreadParameter, a2: i);// 组包 05 0D <profile+1> <lightmode> ...
        if ( lpThreadParameter[i + 224] == 1 )
          sub_4165B0(this: (int (__cdecl **)(_WORD *, int))lpThreadParameter, a2: i);
        if ( lpThreadParameter[i + 209] == 1 )
          sub_416500(this: (int)lpThreadParameter, a2: i);
      }
      // ...
    }
  }
}

我们接下来看一下 sub_416410 函数,里面有一个函数指针。

int __thiscall sub_416410(int this, int a2)
{
  // ...
  if ( v5 )
    v6 = (*(int (__cdecl **)(char *, int))(this + 60))(a1: v9, a2: 13);// hiddriver_2.SetFeature
  else
    v6 = (*(int (__cdecl **)(char *, int))(this + 32))(a1: v9, a2: 13);
  // ...
}

通过搜索 + 60 的偏移搜索不到赋值语句,我猜可能是 IDA 在反编译指针运算的时候抽风了。存放同一个地址,但是不同类型的指针,在运算的时候,得加上不同大小的偏移才能得到同一个地址,所以我按照 int* 指针运算的偏移搜索一下 + 15 看看,结果真看到了下面代码。可以猜测这个函数指针是指向了 "hiddriver_2.dll" 文件的 SetFeature 函数。

  // ...
  v3 = (const DuiLib::CDuiString *)DuiLib::CDuiString::operator+(v22, v20, L"hiddriver_2.dll");
  LOBYTE(v23) = 3;
  DuiLib::CDuiString::operator=((DuiLib::CDuiString *)v21, v3);
  LOBYTE(v23) = 1;
  DuiLib::CDuiString::~CDuiString((DuiLib::CDuiString *)v20);
  v4 = (const WCHAR *)DuiLib::CDuiString::operator wchar_t const *((DuiLib::CDuiString *)v21);
  LibraryW = LoadLibraryW(v4);
  *(this + 19) = LibraryW;
  // ...
    SetFeature = GetProcAddress(v18, "SetFeature");
    v19 = *(this + 19);
    *(this + 15) = (HMODULE)SetFeature;
  // ...

使用 x32dbg 动态附加到程序上,给 sub_416410 函数加上断点,发现前面的推理是正确的,并且得到了给 SetFeature 函数传递的参数。

0706FC54  05 0D 01 01 04 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°....... 

hiddriver_2.dll

SetFeature 函数朴实无华,调用系统函数 HidD_SetFeature 将 Feature 传递给鼠标,鼠标就可以接收。

int __cdecl SetFeature(PVOID ReportBuffer, ULONG ReportBufferLength)
{
  if ( HidDeviceObject != nullptr )
    return HidD_SetFeature(HidDeviceObject: HidDeviceObject, ReportBuffer, ReportBufferLength);
  else
    return 0;
}

接下来我通过给 sub_416410 里加上断点,并改变设置,来检查传递给 SetFeature 的数据找出规律。除了下面发现的变化,a1[2] 是配置索引,a1[6:9] 是颜色的 16 进制值,下面数据中的颜色是粉色 #FF48B0。数据长度则是 13 字节。

//                 vv= 改变了模式后,这里变了
0706FC54  05 0D 01 01 04 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
0706FC54  05 0D 01 02 04 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
0706FC54  05 0D 01 03 04 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
//                       vv= 改变了循环时间
06B3F884  05 0D 01 01 04 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
06B3F884  05 0D 01 01 04 06 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
06B3F884  05 0D 01 01 04 03 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
06B3F884  05 0D 01 01 04 01 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
//                    vv= 改变了亮度
06B3F884  05 0D 01 02 06 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
06B3F884  05 0D 01 02 08 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  
06B3F884  05 0D 01 02 03 08 FF 48 B0 06 01 00 00 00 00 00  ......ÿH°.......  

编写程序

通过分析 "hiddriver_2.dll" 我们可以知道程序是通过 HidD_SetFeature 函数将设置发送给设备的,在 Linux 下则是通过万能的 ioctl 往 /dev/ 下的设备里写入设置。下面的代码是通过 HID_ID 来找到鼠标设备,并通过 ioctl 函数将设置传给鼠标。

HIDIOCSFEATURE = lambda n: (3 << 30) | (n << 16) | (0x48 << 8) | 0x06


def find_dev(vid: int, pid: int):
    for folder in Path("/sys/class/hidraw/").iterdir():
        uevent = folder / "device/uevent"
        if f"HID_ID=0003:{vid:08X}:{pid:08X}" in uevent.read_text():
            return "/dev/" + folder.name


def write_report5(fd, buf: bytearray):
    assert len(buf) == 13 and buf[0] == 5
    n = ioctl(fd, HIDIOCSFEATURE(13), buf, True)
    return n

接下来的主函数就简单了,直接找到设备,读取要修改的参数,应用到设置模板里,写入到设备。

def main():
    path = find_dev(0x1D57, 0xFA60)
    if not path:
        print("Device not found")
        return
    parser = ArgumentParser()
    parser.add_argument("-p", "--profile", type=int, default=1)
    parser.add_argument("-m", "--mode", type=int, default=1)
    parser.add_argument("-c", "--color", default="FF 48 B0")
    parser.add_argument("-d", "--duration", type=int, default=4)
    parser.add_argument("-b", "--brightness", type=int, default=8)
    parser.add_argument("--battery", action="store_true")
    args = parser.parse_args()
    if args.battery:
        monitor_battery(path)
        return
    profile = int(args.profile)
    mode = int(args.mode)
    color = bytes.fromhex(args.color)
    duration = int(args.duration)
    brightness = int(args.brightness)
    tmpl = bytearray.fromhex("05 0D 01 02 04 08 FF 48 B0 06 01 00 00")
    tmpl[2] = profile
    tmpl[3] = mode
    tmpl[4] = duration
    tmpl[5] = brightness
    tmpl[6:9] = color
    print("Writing", tmpl, "to", path)
    with open(path, "rb+", buffering=0) as fd:
        n = write_report5(fd, tmpl)
        print(f"Write {n} bytes")

附加题:设备电量

设备电量有点迷惑,我在驱动程序里看到有显示电量数字和图标的地方,于是就搜索 "power" 相关字段,但是只能定位到和 GUI 相关的地方,然后再搜索 "%d%%" 才定位到 "sub_416C90" 函数,这个函数负责将电量数字显示到 GUI 上。通过 xref 找到上游 "sub_40B140" 函数,这个函数跑到 const CMainWnd::`vftable' 里去了。因为又有一堆的 switch, 所以我猜测又是一个处理事件的函数。

int __thiscall sub_40B140(int this, int a2, WPARAM wParam, LPARAM lParam, DuiLib::CMenuWnd *a5)
{
  // ...
    case 1331:
      sub_415B30(this: this + 2964, a2: (unsigned __int16)lParam ^ (wParam << 16));
      *(_DWORD *)a5 = 1;
  // ...
}

但是这一次搜索 PostMessage 还是 1331, 0x533 都搜索不到结果了,于是求助于大模型。

大模型:

你看程序在初始化的时候执行了 *(this + 11) = (HMODULE)Open_DevMonitor;,然后程序里又调用了 (*((void (__cdecl **)(_DWORD, int, int))lpParameter + 11))(*((_DWORD *)lpParameter + 89), 1331, 1332); 函数指针,等价于调用了 Open_DevMonitor 函数,并将 1331 作为第二个参数 a2 传递了。

再看看 Open_DevMonitor 函数里 dword_1001410C = a2; 有这样的赋值,最后在调用 PostMessageA(hWnd, dword_1001410C, (unsigned __int16)v5, v6); 的时候,就相当于把电池数据传递给了 sub_40B140

我:妙啊。我还在想看不出来就去动态调试了,没想到静态分析也能看出来这么离谱的函数指针调用。

按照这个思路在 "hiddriver_2.dll" 找到了 sub_10001A80 函数,这个函数读取了设备文件。我们也照猫画虎,直接读取设备文件并解析。

def monitor_battery(p: str):
    f = os_open(p, O_RDONLY)
    data = read(f, 64)
    sub = data[1]
    group = data[2]
    charging = bool(data[3])
    level = int(data[4])
    if group != CMD_GROUP:
        return
    print(f"Battery status: {level * 10}%, Charging: {charging}")

效果演示

img

总结

这次是是对一个不太熟悉的领域进行了逆向工程:Windows GUI 程序和驱动。学习到了硬件和系统的各种交互,也领会到了各种函数指针回调的坑爹。我只能说该动态调试的时候就动态调试,可能的地方全部下断点,程序总该停下来的,当然,现在还有大模型这种强力外援,能给人省下很多时间。

完整代码在这:https://github.com/chenx6/gadget/blob/master/gm3323d/mouse.py