Thinkpad X1 Tablet gen2 键盘固件逆向实现Ctrl与Fn换位

0 折腾原因

一直想有一个键盘+红点+触摸板的桌面组合放在办公室用。键盘+红点操作效率高,触摸板在看文档网页时翻页顺滑。几经转折发现了Thinkpad X1 Tablet gen2原装键盘,除了太薄手感一般之外,完美满足需求,而且这款键盘折叠部分里的排线很容易折断,导致价格非常便宜,很适合用来改装USB。很久之前改装了两个,然而这款键盘Fn和Ctrl位置不能交换,导致我一直误操作,所以全部出掉了。前段时间手痒又改装了一个,这次打算彻底解决Fn和Ctrl换位的问题,了却心愿。另本文主要记录思路,以便类似情况参考,并不是软件使用教程。

1 准备工作

首先需要了解该键盘的基本构造,好在之前折腾的时候拆过一个坏键盘,拆机图如下:

可以看到该键盘是usb协议的,核心是一个Sonix单片机,型号为:SN32F237FG。推测该单片机通过GPIO接收按键、红点和触摸板信号,转化为USBHid发给电脑。
这样的话,就需要更改单片机固件实现更改键位了。

2 工具准备

1.该单片机的文档(Sonix官网):SN32F237_V2.20_SC.pdf
2.该键盘的固件(Lenovo官网):n1olk08w.exe
3.USB用途表(USB-IF官网):hut1_12v2.pdf
4.逆向工具ghidra:https://github.com/NationalSecurityAgency/ghidra
5.其他工具:在研究过程中,我还用Keil研究并编译了该单片机的例程,并用bindiff将编译出的axf文件与固件对比,匹配出了一些函数名。但事后总结思路,对这次逆向并未起到决定性作用,所以这次不再赘述,有兴趣的同学可以自行研究。

3 逆向过程

3.1 导入固件

固件解压出3个文件:一个exe,一个Config.ini,一个hex文件,不难看出hex文件就是本次要逆向的固件,导入ghidra,根据官网上的信息,选择指令集为ARM:LE:32:v7,完成导入。

3.2 寻找键码

按照一般经验,键盘固件中会有一个键码表,用于将接收到的信号转变为键码,对应USB用途表,就是04=A 05=B 06=C……这部分。

一般键盘固件中键码都是连续存储的,观察固件hex,在比较靠后的部分,找到了疑似区域(键码中间隔着01、02之类的):

每个键的键码有4位,前两位有01、02、81三种,后两位就是USB用途表中的键码。对照USB用途表,大概解析出键码分布如下(红色的是改建验证过的,绿色的是还没验证的,供参考)。:

可以看出,普通键码为01开头,如0104=A;带FN功能的键码为02开头,如023a=F1;Ctrl、Shift、Alt、徽标键键码为81开头,如8102=左Shift。
按照常理分析,在键码表中将Fn和Ctrl对换即可达成目的。研究到这里,是不是感觉即将大功告成?其实这才是入坑的开始。以我浅薄的研究,该键盘的固件逻辑稀烂。
首先,在键码部分压根没有找到Fn键的踪影。其次,左右Ctrl也没有出现。其实上图中绿色的8101和8110,推测应为左右Ctrl,然而修改后烧录验证,这两个键码根本没有生效。而把A键由0104改为8101,则变成左Ctrl键。所以推测,Fn和Ctrl键,在读键码表之前就被判定了。我们只能继续分析。

3.3 寻找Fn

在ghidra中载入固件之后,可以发现,在我们找到的键码表开头的地方,有DAT_00005a8c和DAT_00005a8d两个数据,分别被FUN_000020f0和FUN_000020fa引用。这两个数据的读取方法应该就是基址+偏移。

顺藤摸瓜,观察FUN_000020f0和FUN_000020fa,都被FUN_00000e80引用。推测这两个函数分别负责读取键码前两位和后两位:

来到FUN_00000e80,反编译为伪代码:

大约可以看出,该函数有比较复杂的逻辑判断过程,猜测和处理按键信号有关。特别关注到判断条件里出现了几个疑似键码:0x6e(Esc)、0x3d(空格)、0x51(End,且结果分支里出现了0x49即Insert)。其中End键极为关键,该键盘Fn+End正是Insert键,故确认FUN_00000e80包含了Fn快捷键的判定和处理。经过多次更改后烧录,大概扒出了该函数的功能。

ulonglong FUN_00000e80(uint param_1,int param_2)

{
  byte bVar1;
  char *pcVar2;
  byte bVar3;
  uint uVar4;
  byte *pbVar5;
  int iVar6;
  uint uVar7;
  uint uVar8;
  bool bVar9;
  
  uVar4 = GetScanCodeHyperByte(param_2);
  bVar1 = *(byte *)(DAT_00001264 + param_2);
  uVar7 = (uint)bVar1;
                    /* 键码前两位为02 */
  if ((uVar4 & 0x7f) == 2) {
                    /* Fn+ESC=FnLock */
    if (uVar7 == 0x6e) {
      if ((*DAT_00010268 == '\0') && ((*DAT_0000126c & 8) != 0)) {
        *(undefined *)(DAT_00001260 + 0x2d) = 1;
      }
      else {
        *(undefined *)(DAT_00001260 + 0x2d) = 0;
      }
    }
                    /* Fn+空格 控灯 */
    if (uVar7 == 0x3d) {
      *(undefined *)(DAT_00001260 + 0x2d) = 0;
      if ((*DAT_00001268 == '\0') || ((*DAT_00001270 & 0x10) != 0)) {
        *DAT_00001274 = '\x04';
        pcVar2 = DAT_0000127c;
        if ((*DAT_00001278 & 2) == 0) {
          *DAT_0000127c = *DAT_0000127c + '\x01';
          if (*pcVar2 == '\x03') {
            *pcVar2 = '\0';
          }
          pbVar5 = DAT_00001278;
          *DAT_00001278 = *DAT_00001278 & 0xf;
          *DAT_00001278 = *DAT_0000127c << 4 | *pbVar5;
          FUN_000032ca(6,*DAT_0000127c);
          NotPinOut_GPIO_init();
        }
        else {
          GPIO_Init();
        }
      }
      else {
        uVar4 = 1;
      }
    }
    else {
                    /* Fn+4 待机 */
      if (uVar7 == 5) {
        if ((*DAT_00001268 == '\0') || ((*DAT_00001270 & 0x10) != 0)) {
          if (*DAT_00001280 != '\x01') {
            if ((*DAT_0000126c & 4) == 0) {
              FUN_00000d44();
            }
            else if (*DAT_00001284 == '\0') {
              *DAT_00001284 = '\x01';
              FUN_00000d44();
            }
          }
        }
        else {
          uVar4 = 1;
        }
      }
      else if (*(char *)(DAT_00001260 + 0x2d) == '\0') {
        uVar4 = 1;
      }
      else {
                    /* Esc */
        uVar8 = uVar7 - 0x6e & 0xff;
        if ((uVar8 == 0) && (*DAT_00001268 != '\0')) {
          uVar4 = 1;
          uVar7 = uVar8;
        }
        else {
                    /* F12 */
          if (uVar8 == 0xd) {
            iVar6 = FUN_00000a68();
            if (iVar6 == 0) {
              HID_GetReportInputEvent();
            }
            *DAT_00001274 = '\x02';
            iVar6 = DAT_00001260;
            *(undefined *)(DAT_00001260 + 0x2c) = 3;
            *(undefined *)(iVar6 + 0x1c) = 3;
            *(undefined *)(DAT_00001260 + 0x1d) = *(undefined *)(DAT_0000128c + 0x34);
            *(undefined *)(DAT_00001260 + 0x1e) = *(undefined *)(DAT_0000128c + 0x35);
            *(undefined *)(DAT_00001260 + 0x1f) = *(undefined *)(DAT_0000128c + 0x36);
            uVar7 = uVar8;
          }
          else {
            iVar6 = FUN_00000a68();
            if ((iVar6 == 1) || (*DAT_00001288 != '\0')) {
              *DAT_00001288 = '\x01';
              uVar4 = 1;
              *DAT_00001290 = 0;
              uVar7 = uVar8;
            }
            else {
              bVar9 = uVar7 != 0x7c;
              uVar7 = uVar8;
              if (bVar9) {
                if (*DAT_00001274 != '\x02') {
                  HID_GetReportInputEvent();
                }
                *DAT_00001274 = '\x02';
                iVar6 = DAT_00001260;
                *(undefined *)(DAT_00001260 + 0x2c) = 3;
                *(undefined *)(iVar6 + 0x1c) = 3;
                *(undefined *)(DAT_00001260 + 0x1d) = *(undefined *)(DAT_0000128c + uVar8 * 4);
                *(undefined *)(DAT_00001260 + 0x1e) = *(undefined *)(DAT_0000128c + uVar8 * 4 + 1) ;
                *(undefined *)(DAT_00001260 + 0x1f) = *(undefined *)(DAT_0000128c + uVar8 * 4 + 2) ;
              }
            }
          }
        }
      }
    }
  }
                    /* 键码前两位01或81 */
  if ((uVar4 & 0x7f) == 1) {
    *(undefined *)(DAT_00001260 + 0x2c) = 0;
    bVar3 = GetScanCodeLowerByte(param_2);
    if ((*DAT_00001274 == '\x02') && (uVar7 != 0x7c)) {
      FUN_00000d90();
    }
    *DAT_00001274 = '\x01';
    if ((uVar4 & 0x80) == 0) {
      if ((int)param_1 < 8) {
        *(byte *)(DAT_00001260 + 0x14 + param_1) = bVar3;
        if ((*DAT_00001268 == '\0') || ((*DAT_00001270 & 0x10) != 0)) {
                    /* Fn+End=Insert(0x49) */
          if (uVar7 == 0x51) {
            *(undefined *)(DAT_00001260 + 0x14 + param_1) = 0x49;
          }
          else if (uVar7 == 0x20) {
            *(undefined *)(DAT_00001260 + 0x14 + param_1) = 0x9a;
          }
          else if (uVar7 == 0x26) {
            *(undefined *)(DAT_00001260 + 0x14 + param_1) = 0x47;
          }
          else if (uVar7 == 0x1a) {
            *(undefined *)(DAT_00001260 + 0x14 + param_1) = 0x48;
          }
          else if (uVar7 == 5) {
            *(undefined *)(DAT_00001260 + 0x14 + param_1) = 0x4f;
          }
          else if (uVar7 == 0x32) {
            pbVar5 = (byte *)(DAT_00001260 + 0x14);
            pbVar5[param_1] = 0x48;
            *(byte *)(DAT_00001260 + 0x14) = *pbVar5 | 1;
          }
          else if (uVar7 == 0x6e) {
            FUN_00000dc0();
            *DAT_00001274 = '\x04';
          }
          else if (uVar7 == 0x7c) {
            *DAT_00001274 = '\x02';
            iVar6 = DAT_00001260;
            *(undefined *)(DAT_00001260 + 0x2c) = 3;
            *(undefined *)(iVar6 + 0x1c) = 3;
            *(undefined *)(iVar6 + 0x1d) = 8;
            *(undefined *)(iVar6 + 0x1e) = 0;
            *(undefined *)(iVar6 + 0x1f) = 0;
          }
          else if (((uVar7 < 0x70) || (0x7b < uVar7)) &&
                  (iVar6 = FUN_00000a68(), iVar6 != 1)) {
            *(undefined *)(DAT_00001260 + 0x14 + param_1) = 0;
            *DAT_00001274 = '\x04';
            *DAT_00001290 = 0;
          }
        }
        param_1 = param_1 + 1 & 0xff;
      }
    }
    else {
      *(byte *)(DAT_00001260 + 0x14) = *(byte *)(DAT_00001260 + 0x14) | bVar3;
    }
  }
  if ((((uVar4 & 0x80) == 0) && ((*DAT_00001270 & 0x20) == 0)) &&
     ((*DAT_00001268 != '\0' && (((*DAT_0000126c & 4) != 0 && ((*DAT_00001270 & 0x10) != 0)))))) {
    *DAT_00001270 = *DAT_00001270 & 0xaf;
    FUN_000032ca(1);
    *DAT_0000126c = (*DAT_0000126c & 0xfc) + 1;
    FUN_00000dc0();
    UT_DelayNms(2);
  }
  return (ulonglong)CONCAT14(bVar1,param_1);
}

在上述代码中,我们可以观察到,每次执行Fn功能前,都要判定DAT_00001268=>2000001D是否为0。而经过烧录测试,Fn按下时内存2000001D为0,反之为1。至此,可以推断内存2000001D为Fn状态。

3.4寻找Ctrl

继续观察FUN_00000e80,发现FUN_00000a68出现了多次,应该也是一个重要的判定条件。反编译FUN_00000a68。

undefined4 FUN_00000a68(void)

{
  undefined4 uVar1;
  
  if (((((*DAT_00000e2c == '\x01') || (*DAT_00000e30 == '\x01')) ||
       ((*(byte *)(DAT_00000e28 + -0xc) & 2) != 0)) ||
      (((*(byte *)(DAT_00000e28 + -0xc) & 0x20) != 0 || ((*(byte *)(DAT_00000e28 + -0xc) & 4) != 0 ))
      )) || (((*(byte *)(DAT_00000e28 + -0xc) & 0x40) != 0 ||
             (((*(byte *)(DAT_00000e28 + -0xc) & 8) != 0 ||
              ((*(byte *)(DAT_00000e28 + -0xc) & 0x80) != 0)))))) {
    uVar1 = 1;
  }
  else {
    uVar1 = 0;
  }
  return uVar1;
}

看到这个函数,我们大胆联想到USB键盘8字节数据包的第一个字节的定义:

BYTE1 --
|--bit0: Left Control是否按下,按下为1
|--bit1: Left Shift 是否按下,按下为1
|--bit2: Left Alt 是否按下,按下为1
|--bit3: Left GUI 是否按下,按下为1
|--bit4: Right Control是否按下,按下为1
|--bit5: Right Shift 是否按下,按下为1
|--bit6: Right Alt 是否按下,按下为1
|--bit7: Right GUI 是否按下,按下为1

找一个二进制换算器即可得出0x1=左Ctrl 0x2=左Shift 0x4=左Alt 0x8=左徽标 0x10=右Ctrl 0x20=右Shift 0x40=右Alt 0x80=右徽标。Shift、Alt、徽标在这个函数中都出现了,那么DAT_00000e2c和DAT_00000e30代表什么也就呼之欲出了,经过验证分别为左Ctrl和右Ctrl的按下状态,他们在内存中对应2000001E和20000027。

3.5 寻找写入上述变量的函数

上述结论,验证了Fn和Ctrl在走键码表之前就被单独判定的猜想。那么写入内存2000001D(Fn)、2000001E(左Ctrl)和20000027(右Ctrl)的函数就显得至关重要。在变量表中,每个变量都对应多个DAT数据,对涉及的函数逐个分析,发现FUN_00001ce0(Fn)、FUN_00001c68(左Ctrl)、FUN_000018a2(左Ctrl、Fn)、FUN_00001b48(右Ctrl)对上述变量进行了写入操作。而这四个函数,都出现在了FUN_00001e2e中:

巧合的是,FUN_00001e2e引用了FUN_000011f6,FUN_000011f6引用了FUN_00000e80。那么有理由推断,FUN_00001e2e开头对Fn、左右Ctrl这三个不走键码表的按键进行了判定,然后再进入FUN_00000e80判定其他按键。

3.6 寻找修改部位

上面的工作大致定位了需要修改的位置,注意到上图中对DAT_00001f64=>2000002b进行判定后区分了FUN_00001ce0(Fn)、FUN_00001c68(左Ctrl)两个分支。而在FUN_000018a2中对DAT_00001ae4=>2000002b进行判定后区分了DAT_00001ae8=>2000001E(左Ctrl)、DAT_00001af0=>2000001D(Fn)两个分支:

那么我们将涉及的两处判定反过来,即可实现Fn和Ctrl的换位。即修改如下两处:


汇编码中,bne=d1 beq=d0,d1和d0互换,if就会反过来。修改后经验证,成功实现Fn和Ctrl换位。

3.7 顺手修改大雷

修改固件的过程中,发现Fn+4=待机,为了防止误触,顺手取消掉该功能。前面FUN_00000e80中可以看出,将键码头两位02改成01,即将固件中5abe位置的0221修改为0121,即可避免FUN_00000e80中进入待机分支。

4 吐槽和恰饭

根据逆向的结果,这款键盘的固件源码条理性不是很理想,实在想不出为什么要将Fn和Ctrl单独拿出来判断。因为我之前也没有逆向的基础,浪费了我一个月的时间,真是得不偿失。期间有一次刷成砖了,根据文档将主控第4脚接地后上机后即可识别,重新烧录救回来了。如果嫌麻烦不想搭建各种环境,也可以直接找我购买修改后的固件,抚慰一下我受伤的心灵。海鲜市场carrothu。

posted @ 2024-11-28 16:53  carrothu  阅读(102)  评论(0编辑  收藏  举报