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。

Thinkpad X1 Tablet gen2 键盘固件逆向实现Ctrl与Fn换位的更多相关文章

  1. [工控安全]西门子S7-400 PLC固件逆向分析(一)

    不算前言的前言:拖了这么久,才发现这个专题没有想象中的简单,学习的路径大致是Step7->S7comm->MC7 code->firmware,我会用尽量简短的语言把前两部分讲清楚, ...

  2. 【记录一个问题】thinkpad x1笔记本,安装ubuntu 16后,拔掉U盘,总是启动到windows,无法启动到ubuntu

    如题 昨天使用ubuntu 18没有这个问题 ============================= 12:38 1.安装完成出现重启后,一定要拔掉U盘 2.BIOS里面的security boo ...

  3. Thinkpad个性化设置:F1~F12恢复正常按键,Fn与Ctrl按键互换

    一.F1~F12恢复正常按键 联想Thinkpad的F1~F12键功能与其他笔记本是相反的! 也就是说,如果不按着Fn,按那几个功能键,实现的是属性设置的功能,比如直接按下F1键是静音,F2键是音量降 ...

  4. thinkpad W500S 如何换键盘?

    tHINKPAD的笔记本拆装有,123456789... 至少5种以上了,一般键盘去下都是边上撬就去下 来了.今天拆换W550S键盘就遇到劲敌了.拼了 老劲也去不下,冬天背上都冒汗(屋子热的吧). 终 ...

  5. thinkpad彻底消除"触摸键盘"图标

    输入“服务”,进入“查看本地服务”,找到“Touch Keyboard and Handwriting Panel Service”, 将其启动类型改为“禁用”,这样的话重启电脑之后也不会自动启动这触 ...

  6. ThinkPad X1 Carbon 2018 Windows 10无法关机的问题

    最近两天在工作中很多同事都遇到了自己的X1电脑关机时自动重启的现象,这个问题让我在知乎.微软支持.国外各种科技论坛找到了很多类似的症状. 但是针对同事们遇到的问题,解决方案异常的简单:就是下载联想驱动 ...

  7. (转)Thinkpad X1 Carbon 扩展硬盘

    http://tieba.baidu.com/p/5837920925 网上看到有人成功利用X1C空闲的4G模块来实现了扩充用的是东芝RC100或者建兴的T11 其实难点应该是2242这种尺寸的SSD ...

  8. thinkpad X1 extreme 安装Ubuntu 18.04.2 LTS

    1.安装的时候需要禁用:nouveau.modeset=0 2.安装完成之后需要添加:acpi=off ,ro后面加上3,直接进入终端 3.启动之后:安装nvdia驱动 $ ubuntu-driver ...

  9. 解决ThinkPad x1 发热的问题

    F1进入BIOS界面 将intel speedstep设置为禁用 将CPU Power Manager设置为禁用 重启电脑 电脑不再发热

  10. ThinkPad T430i,如何将WIN8换成WIN7???

    1. 启动时不断点击键盘上的F1键,进入BIOS 界面选择“Restart”→把 “OS Optimized Default”设置为 “disabled” ,(OS Optimized Default ...

随机推荐

  1. SparkSQL编程需注意的细节

    SparkSQL是把Hive转为字符串后,以参数形式传递到SparkSession.builder().enableHiveSupport().getOrcCreate().sql(Hive_Stri ...

  2. 题解:AT_cf16_exhibition_final_e Water Distribution

    题目链接:link. 这道题目我们有 \(3\) 个结论: 在最优情况下,最后所有的点上的水量都是一样的.因为水多的可以向水少的运水. 不存在间接运水的情况,这个由三角形的三边关系可以得到. 最优运输 ...

  3. ARCHIV_CREATE_FILE 员工头像上传

    *&---------------------------------------------------------------------* *& Report ZHRR_011 ...

  4. Result Maps collection does not contain value for java.util.HashMap

    前言 查询报错 Result Maps collection does not contain value for java.util.HashMap 原因 SQL XML中包含此返回类型 解决 第一 ...

  5. springBoot启动 Error running Application. Command line is too long. Shorten the command line via JAR manifest or via a classpath file and rerun.

    1. 打开SpringBoot启动配置 2.选择shorten command line 3.apply保存就行了

  6. 实战绕过某waf后缀检测内容检测

    本次测试为授权测试. 0x01 背景 弱口令进入某后台,存在任意文件上传,存在waf 测试aa后缀成功上传通过查看派单可以看到文件路径 直接上传aspx被拦截 0x02 绕过后缀名检测 先简单尝试大小 ...

  7. Python全栈应用开发利器Dash 3.x新版本介绍(3)

    更多Dash应用开发干货知识.案例,欢迎关注"玩转Dash"微信公众号 大家好我是费老师,在前两期文章中,我们针对Python生态中强大且灵活的全栈应用开发框架Dash,介绍了其3 ...

  8. Vue+ts 引用外部的js 文件

    export default defineComponent({ name: 'App', components: { Signin, Navbar, FooterPage, BackToTopBut ...

  9. EOMONTH 函数:计算当月/前后几个月的最后一天

    在处理excel的时候,往往需要根据当前的日期,计算出该月的最后一天日期.一种方法是通过DATE函数构造,公式如下: =DATE(YEAR(A1),MONTH(A1)+1,1)-1 这个公式就是获取下 ...

  10. 前端开发系列122-进阶篇之Floating point addition

    本文简单说明 JavaScript 中常见的进制转换函数以及浮点数计算的注意点. 如何把任意进制的数据转换为十进制? 假设我们有二进制数据110,如果要把该数据转换为十进制数据可以参考下面的处理过程. ...