0xFF 免责声明
对设备的刷机操作有风险,使用本文提到任何工具产生的问题,还请自行承担!
0x00 起因
在某天罗技驱动提示升级后,毫不犹豫选择了升级。不巧几天后,发现k780键盘的某些组合键无法工作,连Ctrl+C都无法使用,这让只会复制黏贴的高级攻城狮情何以堪。接着很容易把锅甩给罗技的固件升级,想要重刷一下固件,在罗技官网一顿操作后,发现了一个叫做Firmware Update Tool的程序,结果这工具仅能做升级操作,同版本的固件都无法重新写入,更不要说用旧版本进行降级。无奈之下端起逆向大旗,废话不多说。下面以目前次新版本FirmwareUpdateTool_1.2.169_x86为例。
0x01 寻找界面突破口
我们先在界面上寻找一些特征,首先连接好优联(unifying)接收器,然后键盘通过优联与电脑连接。
如果键盘未连接,随便按个键唤醒
提示优联接收器可升级,会进入一个叫做YOUR RECEIVER IS READY TO UPDATE的确认窗口,这里点击update
就会进行升级,当然如果你的固件版本大于或者等于该升级程序的版本,会直接弹出没有设备需要升级的提示。
这里需要注意的是,优联接收器和K780键盘都是有单独的固件的,升级是分开的。
0x02 逆向分析
把FirmwareUpdateTool.exe
文件(官网下载的文件为rar自解压包,要先进行解压)拉入IDA进行静态分析,在函数窗口能找到一些Q
开头的函数,很明显是Qt框架写的界面程序,从文件中也能发现Qt的类库Qt5Core.dll
等文件。
尝试搜索界面上的字符串,发现并不能找到完全符合的,翻一下能看到:/translations/en.qm
,基本可以确定用了Qt框架的多国语言模块,还有welcome-header
、welcome-text
之类就是字符串索引key
,双击welcome-header
查看,sub_40CD40+115
引用了,直接进入sub_40CD40+115
。
0040CE5B处的call获取真正的字符串,接下来的通过QLabel::setText
设置label的文本。
在sub_40CD40函数是一个稍微复杂的switch case
的代码,按F5生成伪代码,可以看到根据a2
的值进行了不同操作,sub_40CD40函数主要功能是对界面进行操作。
int __thiscall sub_40CD40(QWidget **this, int a2)
{
...
switch ( a2 )
{
case 1:
v6 = (const struct QString *)sub_40CD10(&v261, "welcome-header", 0, -1);
v7 = v2[8];
LOBYTE(v293) = 1;
QLabel::setText(v7, v6);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v261);
v8 = (const struct QString *)sub_40CD10(&v233, "welcome-text", 0, -1);
v9 = v2[9];
LOBYTE(v293) = 2;
QLabel::setText(v9, v8);
...
case 2:
v15 = (const struct QString *)sub_40CD10(&v217, "detecting-devices-header", 0, -1);
v16 = v2[8];
LOBYTE(v293) = 8;
QLabel::setText(v16, v15);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v217);
v17 = (const struct QString *)sub_40CD10(&v219, "detecting-devices-text", 0, -1);
v18 = v2[9];
...
case 3:
v21 = (const struct QString *)sub_40CD10(&v198, "unplug-receivers-header", 0, -1);
v22 = v2[8];
LOBYTE(v293) = 11;
QLabel::setText(v22, v21);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v198);
v23 = sub_40CD10(&v259, "unplug-receivers-text", 0, -1);
LOBYTE(v291) = 32;
LOBYTE(v293) = 12;
QChar::QChar(&v169, v291);
...
case 6:
v44 = (const struct QString *)sub_40CD10(&v213, "devices-up-to-date-header", 0, -1);
v45 = v2[8];
LOBYTE(v293) = 26;
QLabel::setText(v45, v44);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v213);
v46 = (const struct QString *)sub_40CD10(&v251, "devices-up-to-date-text", 0, -1);
v47 = v2[9];
LOBYTE(v293) = 27;
QLabel::setText(v47, v46);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v251);
v276 = QString::fromAscii_helper(":/Images/options.png", 20);
LOBYTE(v293) = 28;
v48 = (const struct QPixmap *)QPixmap::QPixmap(&v175, &v276, 0, 0, v170, v171);
LOBYTE(v293) = 29;
QLabel::setPixmap(v290[12], v48);
LOBYTE(v293) = 28;
QPixmap::~QPixmap((QPixmap *)&v175);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v276);
v284 = QString::fromAscii_helper(":/Images/tick.png", 17);
LOBYTE(v293) = 30;
v49 = (const struct QPixmap *)QPixmap::QPixmap(&v189, &v284, 0, 0, v170, v171);
v2 = v290;
LOBYTE(v293) = 31;
QLabel::setPixmap(v290[13], v49);
LOBYTE(v293) = 30;
QPixmap::~QPixmap((QPixmap *)&v189);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v284);
v50 = (const struct QString *)sub_40CD10(&v197, "close-button", 0, -1);
v51 = v2[10];
LOBYTE(v293) = 32;
QAbstractButton::setText(v51, v50);
v14 = &v197;
goto LABEL_36;
case 7:
v52 = (const struct QString *)sub_40CD10(&v249, "keyboard-update-ready-header", 0, -1);
v53 = v2[8];
LOBYTE(v293) = 33;
QLabel::setText(v53, v52);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v249);
v54 = sub_40CD10(&v247, "keyboard-update-ready-text", 0, -1);
...
...
case 12:
v88 = (const struct QString *)sub_40CD10(&v266, "updating-keyboard-header", 0, -1);
v89 = v2[8];
LOBYTE(v293) = 57;
QLabel::setText(v89, v88);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v266);
v90 = sub_40CD10(&v231, "updating-keyboard-text", 0, -1);
...
case 13:
...
case 16:
v106 = (const struct QString *)sub_40CD10(&v204, "receiver-update-ready-header", 0, -1);
v107 = v2[8];
LOBYTE(v293) = 70;
QLabel::setText(v107, v106);
LOBYTE(v293) = 0;
QString::~QString((QString *)&v204);
v108 = (const struct QString *)sub_40CD10(&v223, "receiver-update-ready-text", 0, -1);
v109 = v2[9];
LOBYTE(v293) = 71;
QLabel::setText(v109, v108);
LOBYTE(v293) = 0;
...
}
}
上面对内部代码进行精简, 重点来关注一下case 7
中keyboard-update-ready-header
这个是键盘准备升级的状态,case 12
就是键盘正在升级的状态,case 16
是接收器准备升级的状态。
优联接收器刷机
我们先来看看case 16
是怎么进入的。查看函数sub_40CD40的引用有:sub_409C90+11
、sub_409C90+10C
,直接进入函数sub_409C90,也是个充斥着switch case的函数,直接F5伪代码查看,定位到关键代码:
也就是执行到state=16
就能进入case 16
,载入OD动态调试,将7E9D72位置的指令nop
掉,这样就能达到永远都能进入接收器升级的界面,F9运行起来。
成功提示该界面,选择升级即可刷新固件,这里注意一下,整个升级过程不需要外网,FirmwareUpdateTool中集成了固件,所以为什么官方需要发布新版本FirmwareUpdateTool,这也是一个原因,可能也考虑到了离线升级这种场景。
这样简单的爆破后,接收器的固件就可以任意刷了。
键盘刷机
根据上面分析继续查看函数sub_409C90,找到如下关键处:
可以看到v5
的值很关键,到这里你肯定想到直接给v5
设置个非0值不就行了,但是事情没这么简单,这边*(_DWORD *)(v3 + 0x88) = v5
对v5
进行了保存操作,说明这个值可能不是一个简单的bool类型,随便改为非0可能会对升级造成影响(这个也经过了测试证实了我们的猜测,会导致进入键盘升级界面,但是升级报错)。继续跟入函数sub_40AAA0,该函数内有大量的复杂操作,我们从返回值去快速定位到关键代码:
继续跟入函数sub_40A8D0,这个函数里面读取了设备信息,需配合OD动态调试,不然难以分析,这里就不演示了,现在回看一下,其实也比较容易猜到这边的代码的逻辑,分析结果如下:
找到关键跳转,将jb直接改为jmp即可。
载入OD,在OD中修改跳转,直接运行起来。
成功进入k780键盘准备更新界面,选择update后,进入升级界面,升级时键盘指示灯红绿交替。
到这里,你会发现,仅做了关键跳转的修改,优联接收器也能随便刷了,可以说明它们都使用函数sub_40A8D0进行版本判断,那就一举两得了
0x03 后记
虽然干得热火朝天,但是刷完之后,我的K780键盘故障依旧,其实是硬件问题(这就很尴尬),后面我会另外介绍我的k780是如何复活的。
文件
官方FirmwareUpdateTool_1.2.169_x86
FirmwareUpdateTool_1.2.169_x86修改版 可平刷降级
链接: https://pan.baidu.com/s/1xGec18il4IkoHm-dJoXRHA 提取码: g8si