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

2024-12-17 16:41:30 3点赞 6收藏 10评论

0 折腾原因

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

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

1 准备工作

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

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


可以看到该键盘是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……这部分。

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


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

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


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

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


可以看出,普通键码为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引用。这两个数据的读取方法应该就是基址+偏移。

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


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

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


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

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


大约可以看出,该函数有比较复杂的逻辑判断过程,猜测和处理按键信号有关。特别关注到判断条件里出现了几个疑似键码: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中:

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


巧合的是,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)两个分支:

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


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

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


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


汇编码中,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。

作者声明本文无利益相关,欢迎值友理性交流,和谐讨论~

展开 收起
10评论

  • 精彩
  • 最新
  • 感觉这是“炫术”贴,重新制造发动机啊,实用性不强! [抠鼻] (1)原机BIOS中可改FN与Ctrl键位;(2)FN键可使用FN+Esc快捷键锁住,防止误按;(3)原机按键可用0.1mm细漆包线,飞线焊接互换;(4)上或下位机的软件开发中,可在串口或USB HUD程序代码中直接修改传输值;(5)如果是通用程序,也不用这般改啊,因为一般服务器、PC,都没有Fn键,也用不到Fn键。

    校验提示文案

    提交
    [皱眉] 早知道要研究一个月,就不搞了

    校验提示文案

    提交
    单独按下fn键是不会发送信号的,另外诸如fn+空格调光之类的功能也不会发信号。而且习惯ctrl在左下角盲按了,所以固件层面交换按键的确是最优解 [苦恼]

    校验提示文案

    提交
    还有2条回复
    收起所有回复
  • BIOS里直接改不行吗?为啥这么麻烦?

    校验提示文案

    提交
    这是改了usb给其他电脑用的,原机应该bios能改吧

    校验提示文案

    提交
    哦哦哦,长见识了,直接在底层实现了换键,厉害了

    校验提示文案

    提交
    收起所有回复
  • 所以别的品牌电脑使用这个键盘的时候能不能实现FnLcok默认锁定呢?

    校验提示文案

    提交
    只要改固件,没有什么不能实现的。但是这个修改点需要自己去寻找了 [高兴]

    校验提示文案

    提交
    收起所有回复
提示信息

取消
确认
评论举报

相关文章推荐

更多精彩文章
更多精彩文章
最新文章 热门文章
6
扫一下,分享更方便,购买更轻松