..

IMK键盘固件逆向实现VIA

00. 背景

买的键盘不支持 VIA,改建只能找客服,这家店的技术有点忙,还有点傻。等了三天,没给我改成功 🤬,哥们是什么善茬吗?直接 Ghidra 启动!

01. 基本按键映射修改

芯片是南京沁恒的 CH582 ,用的一个32位的RISC处理器,支持RV32IMAC指令集。客服给的固件是 intel hex 格式的,肯定是没有任何符号信息了,但是根据字符串拿官方库 bindiff 固件可以恢复 90% 的符号。

研究了一下,代码都是从第一行指令开始执行,最开始的代码都是把一堆东西映射到 0x20000000 这个地址,这个地址应该是 RAM 地址,为了把常用的东西放到 RAM 里提升效率,代码里看到 0x20000000 的地址自己映射一下就行了,本文实际上没用到,不再展开。

基本按键映射比较简单,一般来说都有一个数组来保存像这样,所以修改基本按键映射应该不用分析

image.png

我尝试搜索 14 26 08 15 17 (即,QWERT)但是搜索不到,拿到原版和修改版之后 diff 一下。

0x40(这个键实际上是右边的 alt) → 0x35(~`)

0x20(这个键实际上是右边的shift) → 0x40(F7)

0x4C(DEL) → 0x41(F8)

image.png

提取出来之后发现是按列存储的,如下图

ba543114d96d2ae068c1d3a63d9a1263.png

其中红色的部分是键盘中不包含的按键,未知按键中 0xfe 为Fn,其他的按下图中解释即可。

image.png

这里上图中的 8 个并不是正常的 value,在协议中和正常的按键位置不同,所以在代码中必然有针对他们的判断,配置好内存映射,直接交叉引用就看到了。

image.png

逻辑基本上就是判断位置,这里我们把要修改成不是控制按键的地方全都改成 0xff 让他永远都不成立就行了。

image.png

剩下的修改基本按键映射,修改这个数组中的对应位置即可。

02. Fn 组合键修改

基本按键映射修改好了,但是更关键的是组合键。为此,我看了一下 Github 上一些开源键盘的 Fn 组合键的实现方式,一般是类似基本按键也有一个映射表。如果是这样就简单了,和上面类似只需要diff 一下找到这个映射表,对着修改就行了,但是很不幸这个开发者没有这样做。

要分析代码实现了吗?有点绝望,突然发现一个字符串 f12_deal 这不就是我要找的 Fn 组合键处理逻辑吗!因为这是一个左移 64 的小键盘没有 f1 - f12 ,需要用 Fn + 数字键实现。

image.png

f12_deal 这个没有找到交叉引用(使用 Binary Ninja 查看有交叉引用),但是和它相邻的 f10_deal 有。直接跟过去看看:

image.png

发现相邻的很多函数都是 Fn 组合键处理逻辑,但是这些函数找交叉引用都找不到,合理怀疑是用函数指针调用的,因为一般实现都是有个表,直接 010 Editor 搜一下:

image.png

很明显的结构:

strcut fn_layer_handler {
	uint32_t idx;
	void* fn_ptr;
};

fn_layer_handler fn_layer_handler_arr[32];

交叉引用看看对这个表的处理,首先是对 fn_layer 所支持按键的扫描处理:

image.png

其中 fn_layer_key 是我自己命名的名字,数据是一堆 key-code。把里面的数据拿出来,根据卖键盘的给我的说明书翻译一下,刚好包含了说明书里的所有 Fn 组合键。

Keyboard w and W   -- 2.4g 无线模式
Keyboard e and E   -- 蓝牙模式
Keyboard q and Q   -- 有线模式
Keyboard k and K   -- 说明书里没写
Keyboard t and T   -- 配对2.4g
Keyboard / and ?   -- 静音
Keyboard , and <   -- 音量减     
Keyboard . and >   -- 音量加
Keyboard z and Z   -- RGB 灯开关 
Keyboard x and X   -- RGB 灯模式+
Keyboard c and C   -- RGB 灯模式-
Keyboard v and V   -- RGB 灯速度变化
Keyboard b and B   -- 开启默认随机灯效模式
Keyboard n and N   -- 开启/关闭电磁阀
Keyboard m and M   -- 开启/关闭蜂鸣器
Keyboard 1 and !   -- 以下为 F1 -- F12
Keyboard 2 and @
Keyboard 3 and #
Keyboard 4 and $
Keyboard 5 and %
Keyboard 6 and ∧
Keyboard 7 and &
Keyboard 8 and *
Keyboard 9 and (
Keyboard 0 and )
Keyboard - and (underscore)
Keyboard = and +
Keyboard ESCAPE   -- ~`Keyboard a and A  -- 长按换第一个蓝牙设备
Keyboard s and S  -- 长按换第二个蓝牙设备
Keyboard d and D  -- 长按换第三个蓝牙设备
Keyboard f and F  -- 长按换第四个蓝牙设备

扫描完之后就是处理了,和说明书里对应的非常完美刚好是最后 4 个是长按。

image.png

这就比较明朗了,只需要修改 fn_layer_key 数组里的值和对应位置的 fn_layer_handler_arr 的函数的实现即可实现对 Fn 层快捷键的 VIA。

03. Patch 实现

keystone 不支持 RISC-V,直接用编译器生成。

image.png

然后修改一下 ld 文件,把 .patch_code 段放到指定地址,然后提出来直接用即可。

image.png

下面是完整实现

from HID_const import *
import struct

def write_byte(file, addr, value):
    file.seek(addr)
    file.write(struct.pack('<B', value))

def write_dword(file, addr, value):
    file.seek(addr)
    file.write(struct.pack('<I', value))

def disable_rshift(file):
    file.seek(0x729c + 2)
    file.write(bytes.fromhex('f00f'))

def disable_ralt(file):
    file.seek(0x728a + 2)
    file.write(bytes.fromhex('f00f'))

keymap_offset = 0x32ca4
fn_layer_key_arr_offset = 0x30de0
fn_layer_handler_arr_offset = 0x30cc8

with open('./3M_2X', 'rb') as f:
    content = bytearray(f.read())

patched_f = open('./3M_2X_patched', 'wb')
patched_f.write(content)  # 写入原始内容

# 对基础按键的修改
disable_ralt(patched_f)
write_byte(patched_f, keymap_offset + 7 * 9 + 7 - 1, HID_KEYBOARD_GRV_ACCENT)
disable_rshift(patched_f)
write_byte(patched_f, keymap_offset + 7 * 11 + 6 - 1, HID_KEYBOARD_F7)
write_byte(patched_f, keymap_offset + 7 * 13 + 6 - 1, HID_KEYBOARD_F8)

# 对 fn_layer 的修改
write_byte(patched_f, fn_layer_key_arr_offset + 8, HID_KEYBOARD_GRV_ACCENT)
write_byte(patched_f, fn_layer_key_arr_offset + 9, HID_KEYBOARD_LEFT_ARROW)
write_byte(patched_f, fn_layer_key_arr_offset + 10, HID_KEYBOARD_RIGHT_ARROW)
write_byte(patched_f, fn_layer_key_arr_offset + 11, HID_KEYBOARD_UP_ARROW)
write_byte(patched_f, fn_layer_key_arr_offset + 12, HID_KEYBOARD_DOWN_ARROW)
write_byte(patched_f, fn_layer_key_arr_offset + 28, HID_KEYBOARD_LEFT_BRKT)
write_byte(patched_f, fn_layer_key_arr_offset + 29, HID_KEYBOARD_RIGHT_BRKT)
write_byte(patched_f, fn_layer_key_arr_offset + 30, HID_KEYBOARD_SEMI_COLON)
write_byte(patched_f, fn_layer_key_arr_offset + 31, HID_KEYBOARD_SGL_QUOTE)

# 对 修改映射和功能的
# Insert
write_byte(patched_f, fn_layer_key_arr_offset + 27, HID_KEYBOARD_I)
# 修改 fn_layer_handler_arr[27] 为输入 Insert
handler_insert_addr = 0x33000
write_dword(patched_f, fn_layer_handler_arr_offset + 27 * 8 + 4, handler_insert_addr)

# Delete
write_byte(patched_f, fn_layer_key_arr_offset + 13, HID_KEYBOARD_D)
# 修改 fn_layer_handler_arr[13] 为输入 Delete
handler_delete_addr = 0x3305e
write_dword(patched_f, fn_layer_handler_arr_offset + 13 * 8 + 4, handler_delete_addr)

# PrintScreen
write_byte(patched_f, fn_layer_key_arr_offset + 14, HID_KEYBOARD_P)
# 修改 fn_layer_handler_arr[14] 为输入 PrintScreen
handler_print_screen_addr = 0x330bc
write_dword(patched_f, fn_layer_handler_arr_offset + 14 * 8 + 4, handler_print_screen_addr)

# 插入代码
patched_f.seek(0x33000)
code = bytes.fromhex("""
01 11 06 CE 22 CC 26 CA 83 07 15 00 02 C2 02 C4
23 06 01 00 A1 E3 83 07 05 00 89 C7 93 07 90 04
A3 03 F1 00 25 64 B7 74 00 20 93 07 44 4F 13 85
84 83 82 97 A1 47 63 FF A7 00 05 46 4C 00 13 85
84 83 13 04 A4 53 02 94 21 46 93 05 51 00 13 85
84 83 02 94 F2 40 62 44 D2 44 05 61 82 80 01 11
06 CE 22 CC 26 CA 83 07 15 00 02 C2 02 C4 23 06
01 00 A1 E3 83 07 05 00 89 C7 93 07 C0 04 A3 03
F1 00 25 64 B7 74 00 20 93 07 44 4F 13 85 84 83
82 97 A1 47 63 FF A7 00 05 46 4C 00 13 85 84 83
13 04 A4 53 02 94 21 46 93 05 51 00 13 85 84 83
02 94 F2 40 62 44 D2 44 05 61 82 80 01 11 06 CE
22 CC 26 CA 83 07 15 00 02 C2 02 C4 23 06 01 00
A1 E3 83 07 05 00 89 C7 93 07 60 04 A3 03 F1 00
25 64 B7 74 00 20 93 07 44 4F 13 85 84 83 82 97
A1 47 63 FF A7 00 05 46 4C 00 13 85 84 83 13 04
A4 53 02 94 21 46 93 05 51 00 13 85 84 83 02 94
F2 40 62 44 D2 44 05 61 82 80
""")
patched_f.write(code)

patched_f.close()

import intelhex

intelhex.bin2hex('./3M_2X_patched', './3M_2X_patched.hex')

Patch 完之后烧写测试发现功能和预期完全一样,不禁大喊一声:高玉灿牛逼 😋