原文始发于Anton Fedorov:KatWalk C2: p.5: overclocking and bugfixing or how to use Ghidra to analyse ARM firmware
KatWalk C2: p.5: overclocking and bugfixing or how to use Ghidra to analyse ARM firmware
As I’ve reminded in the last part (where I’ve described how to patch the firmware — I’m describing what’s the KatWalk C2 treadmill, how to integrate with it and how to connect to its sensors directly.
正如我在上一部分中提醒的那样(我描述了如何修补固件——我正在描述什么是 KatWalk C2 跑步机,如何与它集成以及如何直接连接到它的传感器。
The original receiver refreshes sensors data at speed of about 86Hz, while technical limit is 133 Hz — which significantly lowers the latency, but the connection was unstable.
原始接收器以大约 86Hz 的速度刷新传感器数据,而技术限制为 133 Hz,这显着降低了延迟,但连接不稳定。
Let’s deep dive into sensors — learn about the game ghidra_11.0_PUBLIC which I have installed into `C:\Games` on my PC and peek into the sensors’ firmware and poke it: make some patches, fix some race conditions, fix some bugs… Be ready for the deep dive.
让我们深入了解传感器——了解我安装在 PC 上的“C:\Games”中的游戏 ghidra_11.0_PUBLIC,并窥视传感器的固件并戳它:制作一些补丁,修复一些竞争条件,修复一些错误……准备好进行深潜。
This time — it’s serious.
这一次——很严重。
Ghidra: how to feed a dragon with raw ARM binary
Ghidra:如何用原始 ARM 二进制文件喂养龙
Since buying full-featured IDA doesn’t fit into my fun budget, and IDA Freeware doesn’t support ARM, I had to fallback to the alternate options. The obvious choice was Ghidra, which are quite famous since last few years open-source reverse-engineering tool — powerful, feature-rich, with scripting support and knowledge of multiple architectures.
由于购买功能齐全的 IDA 不符合我的娱乐预算,而且 IDA 免费软件不支持 ARM,我不得不回退到替代选项。显而易见的选择是 Ghidra,它自去年以来就非常有名 开源逆向工程工具 – 功能强大,功能丰富,具有脚本支持和多种架构的知识。
Basically, I am finally have an excuse to learn it!
基本上,我终于有借口学习它了!
So, I’ve downloaded it into `C:\Games`, unpacked and run `C:\Games\ghidra_11.0_PUBLIC\ghidraRun.bat`. Create new project… Go to “File=>Import File” to import raw firmware binary. Ghidra support many formats, but somehow HEX is not one of them — it’s good that I’ve made it BIN already.
因此,我已将其下载到“C:\Games”中,解压缩并运行“C:\Games\ghidra_11.0_PUBLIC\ghidraRun.bat”。创建新项目…转到“File=>Import File”以导入原始固件二进制文件。Ghidra 支持多种格式,但不知何故,HEX 不是其中之一——我已经把它变成了 BIN 真是太好了。
It also looks like Ghidra doens’t detect architecture on its own — well, again, I know it already: ARM, Cortex M3, little endian:
看起来 Ghidra 本身也无法检测架构——好吧,再说一次,我已经知道了:ARM、Cortex M3、little endian:
Now double-click on the firmware in the project to open the disassembler itself.
现在双击项目中的固件以打开反汇编器本身。
Ghidra asks to analyze the file — let’s agree. But there is almost nothing useful, clean slate:
Ghidra 要求分析文件——让我们同意。但几乎没有任何有用的、干净的石板:
Memory map 内存映射
Well, RAW binaries typically need some extra work. So, let’s get our hands dirty! First of all — let’s follow the memory map found somewhere inside of TI manuals:
好吧,RAW二进制文件通常需要一些额外的工作。所以,让我们亲自动手吧!首先,让我们按照 TI 手册中的内存映射:
Go to “Windows => Memory Map” to fix it. The existing region should be renamed to “flash”, got “W” checkbox removed (it’s non-writeable memory). By using green plus button from the top-right tools panel adding one more region, name it “rom”, make start address 10000000, length 0x20000 (more precisely — 0x1CC00 but that’s not important), with access modes R and X (also, unckeck W). Then add the third region “ram”, started 20000000 with size 0x5000 and R/W (but no X).
转到“Windows => Memory Map”进行修复。现有区域应重命名为“flash”,删除“W”复选框(它是不可写内存)。通过使用右上角工具面板中的绿色加号按钮再添加一个区域,将其命名为“rom”,使起始地址为 10000000,长度为 0x20000(更准确地说 — 0x1CC00但这并不重要),访问模式为 R 和 X(也未解 W)。然后添加第三个区域“ram”,从 20000000 开始,大小为 0x5000 和 R/W(但没有 X)。
With knowledge of the memory map we see that first we have pointer to the begin of stack, pointing to the ram 0x20004000 as it should be. Then there is a vector to the entry point of the compiler-generated prologue, then interrupt vectors — all pointing inside of the ROM region, which we don’t have, and two last vectors doesn’t look like vectors at all.
了解内存映射后,我们看到首先我们有指向堆栈开头的指针,指向内存0x20004000应有的位置。然后有一个向量到编译器生成的序幕的入口点,然后是中断向量——所有向量都指向 ROM 区域内部,我们没有,最后两个向量看起来根本不像向量。
Although, we’ve not yet done with the memory map. ARM processors also hve a “Peripherals” regions: read and writes to/from it give access to the on-chip hardware. Most convenient way to deal with it is to download CMSIS-SVD package, where are hardware descriptions for many ARM chips collected together in machine-readable format. To use it, we should download SVD-Loader-Ghidra, then go to “Window=>Script Manager”, click onto third form the right icon on the toolbox (something looking like burger menu, left from X and red cross, in the hover text it called “Manage Script Directories”) and press the green plus inside of the script directories manager to add a path to the downloaded plugin (`$USER_HOME/Documents/GitHub/SVD-Loader-Ghidra` in my case). Close then the directories manager, enter into filter field “SVD” to find added plugin:
虽然,我们还没有完成内存映射。ARM 处理器还具有“外设”区域:读取和写入/写入它允许访问片上硬件。最方便的处理方法是下载 CMSIS-SVD 包,其中有许多 ARM 芯片的硬件描述以机器可读格式收集在一起。要使用它,我们应该下载 SVD-Loader-Ghidra,然后转到“Window=>Script Manager”,单击工具箱上右侧的第三个图标(看起来像汉堡菜单,从 X 和红叉向左,在悬停文本中称为“管理脚本目录”),然后按脚本目录管理器内部的绿色加号以添加下载插件的路径(在我的情况下为“$USER_HOME/Documents/GitHub/SVD-Loader-Ghidra”)。关闭目录管理器,进入过滤器字段“SVD”以查找添加的插件:
Run script by double-clicking it, it’ll ask for the SVD file. Let’s choose `…GitHub\cmsis-svd-data\data\TexasInstruments\CC26x0.svd`.
双击运行脚本,它会要求输入 SVD 文件。让我们选择“…GitHub\cmsis-svd-data\data\TexasInstruments\CC26x0.svd’。
Script will process and add hardware regions inside of the 0x40000000 address space for us.
脚本将为我们处理和添加0x40000000地址空间内的硬件区域。
Add symbols into unavailable ROM
将符号添加到不可用的 ROM 中
When one works with raw binary, every bit of knowledge is precious. So, go, download the SDK; unpack and start digging. We know already that it supports debugging, so there should be symbols somewhere. But where?
当一个人使用原始二进制文件时,每一点知识都是宝贵的。所以,去下载SDK;打开包装,开始挖掘。我们已经知道它支持调试,所以某处应该有符号。但是在哪里呢?
Let’s start with search for the interrupt address: 1001c901. Let’s switch back to WSL and:
让我们从搜索中断地址开始:1001c901。让我们切换回 WSL 并:
$ cd /mnt/c/ti/simplelink_cc2640r2_sdk_5_30_00_03
$ find . -name '*map' | xargs grep -i '1001c90'
./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/rtos_rom.map: 1001c900 00000020 arm_m3_Hwi_asm_rom.obj (.text:ti_sysbios_family_arm_m3_Hwi_excHandlerAsm__I)
./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/rtos_rom.map:1001c901 ti_sysbios_family_arm_m3_Hwi_excHandlerAsm__I
./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/rtos_rom.map:1001c901 ti_sysbios_family_arm_m3_Hwi_excHandlerAsm__I
Great, looks like what we need! Now, how to load them? Ghidra already has a plugin `ImportSymbolsScript.py`, but it’s not good enough after trying it out. So I’ve made an updated version ArmImportSymbolsScript.py. Main point is to make function pointers start at the even address and if the address is pointing to unknown region, make a word here — so the jmp tables points to the same symbol. That makes symbols more convenient and useful for auto-analysis afterward and code reading.
太好了,看起来像我们需要的!现在,如何加载它们?Ghidra 已经有一个插件“ImportSymbolsScript.py”,但尝试后还不够好。所以我 ArmImportSymbolsScript.py 做了一个更新版本。要点是使函数指针从偶数地址开始,如果地址指向未知区域,请在此处创建一个单词 – 以便 jmp 表指向相同的符号。这使得符号对于之后的自动分析和代码读取更加方便和有用。
Although, we still need to turn the symbols into compatible format: <name> <address> <type: f or not>,
虽然,我们仍然需要将符号转换为兼容的格式: <name> <address> <类型:f or not>,
Also, while the ROM part addresses is correct, it doesn’t look like everything under the 1xxxxxxx is anyhow valid — for example at address `00001a25 main` — not a main at all!
此外,虽然 ROM 部分地址是正确的,但看起来 1xxxxxxx 下的所有内容都无效——例如地址“00001a25 main”——根本不是主!
So, let’s not bother digging deeper into this discrepancies of the map file and just filter out data out of the binary itself:
因此,我们不必费心去深入研究映射文件的这种差异,而只是从二进制文件本身中过滤掉数据:
$ cd /mnt/c/ti/simplelink_cc2640r2_sdk_5_30_00_03
$ mkdir symbols
$ objdump -t ./kernel/tirtos/packages/ti/sysbios/rom/cortexm/cc26xx/r2/golden/CC26xx/CC2640R2F_rtos_rom_syms.out | perl -ne '@a=split;print "$a[-1] $a[0] $a[2]\n" if $a[1] eq "g"' > symbols/rom.txt
Now open Scripts Manager, double click on `ArmImportSymbolsScript.py`, choose the just generated `C:\ti\simplelink…\scripts\rom.txt`:
现在打开脚本管理器,双击“ArmImportSymbolsScript.py”,选择刚刚生成的“C:\ti\simplelink…\scripts\rom.txt”:
And it starts to make sense! Although, it’s not the whole ROM yet. We also need a bluetooth stack, symbols for which we can find in `source/ti/ble5stack/rom/ble_rom_releases/cc26xx_r2/Final_Release`.
它开始有意义了!虽然,它还不是整个 ROM。我们还需要一个蓝牙堆栈,我们可以在“source/ti/ble5stack/rom/ble_rom_releases/cc26xx_r2/Final_Release”中找到其符号。
$ for k in source/ti/ble5stack/rom/ble_rom_releases/cc26xx_r2/Final_Release/*symbols; do
objdump -t $k | perl -ne '@a=split;print "$a[-1] $a[0] $a[2]\n" if $a[1] eq "g"' > symbols/${k##*/}.txt
done
Now, import also `ble_r2.symbols.txt` and `common_r2.symbols.txt`.
现在,还可以导入“ble_r2.symbols.txt”和“common_r2.symbols.txt”。
Digging for API SDK symbols
挖掘 API SDK 符号
As memory map states, at address 0x1000 there are “TI RTOS ROM Jump Table”. Indeed, at this address there are many pointers (not jmp instructions, though)… But nothing of real interest — although, from the SDK sources there are another address table — “ROM_Flash_JT”, which, apparently, is not located at 0x1000, but can be found easily: just back-reference already labelled from ROM function, I use HCI_bm_alloc:
如内存映射所述,在地址0x1000有“TI RTOS ROM Jump Table”。事实上,在这个地址有很多指针(虽然不是jmp指令)……但没有什么真正有趣的东西——尽管从 SDK 源代码中还有另一个地址表——“ROM_Flash_JT”,它显然不在0x1000,但很容易找到:只是已经从 ROM 函数标记的反向引用,我使用HCI_bm_alloc:
That’s the table we want, so we can mark it’s beginning as “ROM_Flash_JT”, and compare it to the contents of `rom_init.c` file (full path in my system) `C:\ti\simplelink_cc2640r2_sdk_5_30_00_03\source\ti\blestack\rom\r2\rom_init.c`), to make lot more symbols properly named. I’ve created another script ROM_Flash_JT which can do that — run it, select rom_init.c, enjoy:
这就是我们想要的表格,因此我们可以将其开头标记为“ROM_Flash_JT”,并将其与“rom_init.c”文件(我的系统中的完整路径)“C:\ti\simplelink_cc2640r2_sdk_5_30_00_03\source\ti\blestack\rom\r2\rom_init.c”)的内容进行比较,以正确命名更多符号。我创建了另一个可以做到这一点的脚本ROM_Flash_JT – 运行它,选择 rom_init.c,享受:
Unpack ROM2RAM 打开ROM2RAM
The next important pool of information is the initial RAM contents. Whenever program has mutable structures / arrays statically initialized, they need to be initialized in RAM. So whenever code says something like:
下一个重要的信息池是初始RAM内容。每当程序具有静态初始化的可变结构/数组时,它们都需要在RAM中初始化。因此,每当代码说这样的话时:
const char * str = "MyString";
char * str2 = "OtherString";
Then `str` could be a pointer to ROM, but `str2` is a pointer to RAM, which means compiler should make a code to copy this string into RAM. Similar happens to various structures, static variables and so on. To do so, compiler inserted prologue code, before calling the main() function, unpacks constant data from ROM into RAM. I believe different compilers do this differently, but GCC used in TI SDK (as part of the XDC Tools) does it at the very first step by using a table storing unpackers and table storing pairs of ROM source — RAM target addresses. To unpack, get the arm-romtotram script, then go to the address pointed by Reset vector:
那么 ‘str’ 可能是指向 ROM 的指针,但 ‘str2’ 是指向 RAM 的指针,这意味着编译器应该编写代码以将此字符串复制到 RAM 中。类似的情况也发生在各种结构、静态变量等上。为此,编译器在调用 main() 函数之前插入序幕代码,将常量数据从 ROM 解压缩到 RAM 中。我相信不同的编译器会以不同的方式执行此操作,但 TI SDK(作为 XDC 工具的一部分)中使用的 GCC 在第一步就通过使用存储解压缩器的表和存储 ROM 源 – RAM 目标地址对的表来做到这一点。要解压,请获取 arm-romtotram 脚本,然后转到 Reset vector 指向的地址:
Create function there pressing “F”, rename it into “resetHandler” (hotkey “L”) and look at the several calls in a row. First of these functions is the one that does unpacking. Let’s rename it to “unpackRomToRam”:
在那里按“F”创建函数,将其重命名为“resetHandler”(热键“L”),然后连续查看几个调用。这些功能中的第一个是拆包功能。让我们将其重命名为“unpackRomToRam”:
Now, from this function we can trivially make three required for the script labels:
现在,从这个函数中,我们可以简单地为脚本标签创建三个必需的标签:
- “ROMtoRAMtable” — initial loop value,
“ROMtoRAMtable” — 初始循环值, - “ROMtoRAMtableEnd” — loop end condition value (ghidra doens’t allow to create label there from the loop itself: double click on it, create there pointer pressing “P” and then create label here with “L”),
“ROMtoRAMtableEnd” — 循环结束条件值(ghidra 不允许从循环本身在那里创建标签:双击它,在那里创建按“P”的指针,然后在这里用“L”创建标签), - “ROMtoRAM_Processors” at the pointer to the array of handlers used inside fo the loop.
“ROMtoRAM_Processors”,指向循环内部使用的处理程序数组。
The function should look like this now:
该函数现在应如下所示:
void unpackRomToRam(void)
{
byte **ppbVar1;
for (ppbVar1 = (byte **)&ROMtoRAMtable; ppbVar1 < &ROMtoRAMtableEnd; ppbVar1 = ppbVar1 + 2) {
(*(code *)(&ROMtoRAM_Processors)[**ppbVar1])(*ppbVar1 + 1,ppbVar1[1]);
}
xdc_runtime_Startup_exec__E();
return;
}
Check the processors table: it should have only three functions, where one does something LZ-style (copy byte or copy N bytes starting from M bytes in the past), function that calls memcpy and function that calls memzero. The table rom2ram just contains pairs of addresses, the length of the regions etc stored inside of the address pointer by the ROM source.
检查处理器表:它应该只有三个函数,其中一个执行 LZ 样式的操作(复制字节或复制 N 字节,从过去的 M 字节开始)、调用 memcpy 的函数和调用 memzero 的函数。表 rom2ram 仅包含 ROM 源存储在地址指针内的地址对、区域长度等。
Once three labels ROMtoRAM* created, just run “arm-romtoram” script and whoosh — RAM is split into initialized regions following the rules.
一旦创建了三个标签 ROMtoRAM*,只需运行“arm-romtoram”脚本,然后嗖 — RAM 按照规则被拆分为初始化区域。
Code analysis 代码分析
Now. once we’ve got all support information, time to deep dive into the code. AFirst of all, the two pointers “SysTick” and “IRQ” doesn’t look like pointers at all. That’s definitely code, not pointers, so let’s reset their status with “C”, rename “SysTick” to “Begin” and make it code with F12 and then function with “F”.
现在。一旦我们获得了所有支持信息,就可以深入研究代码了。首先,两个指针“SysTick”和“IRQ”看起来根本不像指针。这绝对是代码,而不是指针,所以让我们用“C”重置它们的状态,将“SysTick”重命名为“Begin”,让它用 F12 编码,然后用“F”运行。
That’s something that looks like an initialization sequence:
这看起来像一个初始化序列:
FUN_0000c9d0(&DAT_20001130,&DAT_20001150);
ti_sysbios_knl_Queue_construct(&DAT_2000118c,0);
DAT_20001154 = &DAT_2000118c;
FUN_0000cdfc(&DAT_2000120c,&LAB_000100e2+1,0,8);
FUN_0000cdfc(&DAT_20001230,&LAB_000100e2+1,3,4);
FUN_0000cdfc(&DAT_20001254,&LAB_000100e2+1,0,0xb);
FUN_0000cdfc(&DAT_200011e8,&LAB_000100e2+1,0,3);
DAT_20002038 = FUN_00009730(&DAT_20002040,&DAT_20002064);
if (DAT_20002038 == 0) {
do {
/* WARNING: Do nothing block with infinite loop */
} while( true );
}
FUN_00005bf4(0x10,0xe165,0x1f,6);
uVar20 = 0;
local_5c = 9;
local_5e = 8;
local_53 = 0;
local_56 = 0;
local_5a = 100;
local_58 = 1000;
FUN_0000363c(0x306,2,&local_56);
FUN_0000363c(0x308,0x10,&DAT_200011a4);
FUN_0000363c(0x307,7,&DAT_20001174);
FUN_0000363c(0x310,1,&local_53);
FUN_0000363c(0x311,2,&local_5e);
FUN_0000363c(0x312,2,&local_5c);
FUN_0000363c(0x313,2,&local_5a);
FUN_0000363c(0x314,2,&local_58);
FUN_00005bf4(0x10,0xe165,6,0xa0);
FUN_00005bf4(0x10,0xe165,7,0xa0);
FUN_00005bf4(0x10,0xe165,8,0xa0);
FUN_00005bf4(0x10,0xe165,9,0xa0);
local_64 = 0;
local_50 = 0;
local_52 = 1;
local_51 = 1;
local_4f = 1;
FUN_00005bf4(0x10,&DAT_00003fb5,0x408,4,&local_64);
FUN_00005bf4(0x10,&DAT_00003fb5,0x400,1,&local_52);
FUN_00005bf4(0x10,&DAT_00003fb5,0x402,1,&local_51);
FUN_00005bf4(0x10,&DAT_00003fb5,0x403,1,&local_50);
FUN_00005bf4(0x10,&DAT_00003fb5,0x406,1,&local_4f);
FUN_00005bf4(0x10,&LAB_0000f3f8+1,&DAT_2000117c);
Would be good to understand it. Let’s search thru constants list in SDK (using Notepad++ or good ol’ grep):
最好理解它。让我们在 SDK 中搜索常量列表(使用 Notepad++ 或 good ol’ grep):
datacompboy@NUUBOX:/mnt/c/ti/simplelink_cc2640r2_sdk_5_30_00_03$ find . -name '*.h' | xargs grep 0x306
./kernel/tirtos/packages/gnu/targets/arm/libs/install-native/arm-none-eabi/include/elf.h:#define NT_S390_LAST_BREAK 0x306
./source/ti/blestack/profiles/roles/cc26xx/broadcaster.h:#define GAPROLE_ADV_EVENT_TYPE 0x306 //!< Advertisement Type. Read/Write. Size is uint8_t. Default is GAP_ADTYPE_ADV_IND (defined in GAP.h).
./source/ti/blestack/profiles/roles/cc26xx/multi.h:#define GAPROLE_ADVERT_OFF_TIME 0x306
./source/ti/blestack/profiles/roles/cc26xx/peripheral.h:#define GAPROLE_ADVERT_OFF_TIME 0x306
./source/ti/blestack/profiles/roles/peripheral_broadcaster.h:#define GAPROLE_ADVERT_OFF_TIME 0x306 //!< Advertising Off Time for Limited advertisements (in milliseconds). Read/Write. Size is uint16. Default is 30 seconds.
Oh, cool! We looking at the firmware of a sensor, which is most probably a peripheral. Let’s load constants into ghidra from there:
哦,太酷了!我们查看传感器的固件,它很可能是外围设备。让我们从那里将常量加载到 ghidra 中:
File=>Parse C source=>green plus=>”c:\ti\simplelink_cc2640r2_sdk_5_30_00_03\source\ti\blestack\profiles\roles\cc26xx\peripheral.h”=>Parse to program.
File=>Parse C source=>green plus=>“c:\ti\simplelink_cc2640r2_sdk_5_30_00_03\source\ti\blestack\profiles\roles\cc26xx\peripheral.h”=>解析到程序。
It’ll complain about something, but constants will be there now. Point to the “0x306”, press “E” and see GAPROLE_ADVERT_OFF_TIME in the list of possibilities — apply it with double-click. Wait for re-decompile. Repeat for others. Below we see other constants — 408/400/402… We can repeat with them as well, but let’s look at why the function calls are not lablled.
它会抱怨一些事情,但现在常量会在那里。指向“0x306”,按“E”并在可能性列表中查看GAPROLE_ADVERT_OFF_TIME – 双击应用它。等待重新编译。对其他人重复。下面我们看到其他常数 — 408/400/402…我们也可以重复它们,但让我们看看为什么函数调用没有打字。
Let’s grep thru samples about GAPROLE_SCAN_RSP_DATA:
让我们通过有关GAPROLE_SCAN_RSP_DATA的示例:
$ find . -name '*.c' | xargs grep GAPROLE_SCAN_RSP_DATA
./examples/rtos/CC2640R2_LAUNCHXL/blestack/multi_role/src/app/multi_role.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/project_zero/src/app/project_zero.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData), scanRspData);
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_broadcaster/src/app/simple_broadcaster.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof (scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_np/src/app/simple_np_gap.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_np/src/app/simple_np_gap.c: status = GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, len, pDataPtr);
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral/src/app/simple_peripheral.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral/src/app/simple_peripheral_dbg.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_oad_offchip/src/app/simple_peripheral_oad_offchip.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_oad_onchip/src/app/simple_peripheral_oad_onchip.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_oad_onchip/src/persistent_app/oad_persistent_app.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
./examples/rtos/CC2640R2_LAUNCHXL/blestack/simple_peripheral_secure_fw/src/app/simple_peripheral_dbg.c: GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
Okay, promising. What’s inside of simple_peripheral.c:
好吧,很有希望。simple_peripheral.c 的内部内容:
// Setup the Peripheral GAPRole Profile. For more information see the User's
// Guide:
// http://software-dl.ti.com/lprf/sdg-latest/html/
{
// By setting this to zero, the device will go into the waiting state after
// being discoverable for 30.72 second, and will not being advertising again
// until re-enabled by the application
uint16_t advertOffTime = 0;
uint8_t enableUpdateRequest = DEFAULT_ENABLE_UPDATE_REQUEST;
uint16_t desiredMinInterval = DEFAULT_DESIRED_MIN_CONN_INTERVAL;
uint16_t desiredMaxInterval = DEFAULT_DESIRED_MAX_CONN_INTERVAL;
uint16_t desiredSlaveLatency = DEFAULT_DESIRED_SLAVE_LATENCY;
uint16_t desiredConnTimeout = DEFAULT_DESIRED_CONN_TIMEOUT;
GAPRole_SetParameter(GAPROLE_ADVERT_OFF_TIME, sizeof(uint16_t),
&advertOffTime);
GAPRole_SetParameter(GAPROLE_SCAN_RSP_DATA, sizeof(scanRspData),
scanRspData);
GAPRole_SetParameter(GAPROLE_ADVERT_DATA, sizeof(advertData), advertData);
GAPRole_SetParameter(GAPROLE_PARAM_UPDATE_ENABLE, sizeof(uint8_t),
&enableUpdateRequest);
GAPRole_SetParameter(GAPROLE_MIN_CONN_INTERVAL, sizeof(uint16_t),
&desiredMinInterval);
GAPRole_SetParameter(GAPROLE_MAX_CONN_INTERVAL, sizeof(uint16_t),
&desiredMaxInterval);
GAPRole_SetParameter(GAPROLE_SLAVE_LATENCY, sizeof(uint16_t),
&desiredSlaveLatency);
GAPRole_SetParameter(GAPROLE_TIMEOUT_MULTIPLIER, sizeof(uint16_t),
&desiredConnTimeout);
}
Wow, sequence is 1-to-1: GAPROLE_ADVERT_OFF_TIME, GAPROLE_SCAN_RSP_DATA, GAPROLE_ADVERT_DATA, GAPROLE_PARAM_UPDATE_ENABLE…
哇,顺序是 1 对 1:GAPROLE_ADVERT_OFF_TIME、GAPROLE_SCAN_RSP_DATA、GAPROLE_ADVERT_DATA、GAPROLE_PARAM_UPDATE_ENABLE……
So, we can rename FUN_0000363c => GAPRole_SetParameter, and look further:
因此,我们可以重命名 FUN_0000363c => GAPRole_SetParameter,并进一步查看:
// Set the Device Name characteristic in the GAP GATT Service
// For more information, see the section in the User's Guide:
// http://software-dl.ti.com/lprf/sdg-latest/html
GGS_SetParameter(GGS_DEVICE_NAME_ATT, GAP_DEVICE_NAME_LEN, attDeviceName);
// Set GAP Parameters to set the advertising interval
// For more information, see the GAP section of the User's Guide:
// http://software-dl.ti.com/lprf/sdg-latest/html
{
// Use the same interval for general and limited advertising.
// Note that only general advertising will occur based on the above configuration
uint16_t advInt = DEFAULT_ADVERTISING_INTERVAL;
GAP_SetParamValue(TGAP_LIM_DISC_ADV_INT_MIN, advInt);
GAP_SetParamValue(TGAP_LIM_DISC_ADV_INT_MAX, advInt);
GAP_SetParamValue(TGAP_GEN_DISC_ADV_INT_MIN, advInt);
GAP_SetParamValue(TGAP_GEN_DISC_ADV_INT_MAX, advInt);
}
So right after batch of GAPRole_SetParameter calls should be a call to GGS_SetParameter and then a batch of GAP_SetParamValue. But in the decompilation no single call, nothing that resembles GGS_SetParameter. So… GATT service calls was removed/commented out, that’s why we can’t use sensors directly.
因此,在一批GAPRole_SetParameter调用之后,应该是对GGS_SetParameter的调用,然后是一批GAP_SetParamValue。但是在反编译中,没有一个调用,没有类似于GGS_SetParameter的东西。所以。。。GATT 服务调用被删除/注释掉,这就是我们不能直接使用传感器的原因。
Another issue is: GAP_SetParamValue should only have two arguments, but we see calls with 4:
另一个问题是:GAP_SetParamValue应该只有两个参数,但我们看到带有 4 的调用:
FUN_00005bf4(0x10,0xe165,6,0xa0);
FUN_00005bf4(0x10,0xe165,7,0xa0);
FUN_00005bf4(0x10,0xe165,8,0xa0);
FUN_00005bf4(0x10,0xe165,9,0xa0);
Let’s check, is this indeed the right calls?
让我们检查一下,这确实是正确的电话吗?
$ find . -name '*.h' | xargs grep TGAP_LIM_DISC_ADV_INT_MAX
./source/ti/blestack/inc/gap.h:#define TGAP_LIM_DISC_ADV_INT_MAX 7
Yup, indeed they are. So, perhaps, GAP_SetParamValue is not a function but a macro?
是的,确实如此。那么,也许GAP_SetParamValue不是一个函数,而是一个宏?
$ find . -name '*.h' | xargs grep GAP_SetParamValue
./source/ti/ble5stack/icall/inc/ble_dispatch_lite_idx.h:#define IDX_GAP_SetParamValue JT_INDEX(152)
./source/ti/ble5stack/icall/inc/icall_api_idx.h:#define IDX_GAP_SetParamValue GAP_SetParamValue
./source/ti/ble5stack/icall/inc/icall_ble_api.h:#define GAP_SetParamValue(...) (icall_directAPI(ICALL_SERVICE_CLASS_BLE, (uint32_t) IDX_GAP_SetParamValue , ##__VA_ARGS__))
./source/ti/ble5stack/icall/inc/icall_ble_apimsg.h: * @see GAP_SetParamValue()
./source/ti/ble5stack/inc/gap.h: * Parameters set via @ref GAP_SetParamValue
./source/ti/ble5stack/inc/gap.h:extern bStatus_t GAP_SetParamValue(uint16_t paramID, uint16_t paramValue);
./source/ti/ble5stack/rom/map_direct.h:#define MAP_GAP_SetParamValue GAP_SetParamValue
That’s why! Depending on compilation settings, it’s either direct call, indirect via jump table (with index 152), or indirect with direct function address. Let’s check that ICALL_SERVICE_CLASS_BLE == 0x10:
这就是原因!根据编译设置,它可以是直接调用,通过跳转表间接调用(索引为 152),或者是间接调用直接函数地址。让我们检查一下 ICALL_SERVICE_CLASS_BLE == 0x10:
$ find . -name '*.h' | xargs grep ICALL_SERVICE_CLASS_BLE
...
./source/ti/blestack/icall/src/inc/icall.h:#define ICALL_SERVICE_CLASS_BLE 0x0010
./source/ti/blestack/icall/src/inc/icall.h:#define ICALL_SERVICE_CLASS_BLE_MSG 0x0050
./source/ti/blestack/icall/src/inc/icall.h:#define ICALL_SERVICE_CLASS_BLE_BOARD 0x0088
...
And rename FUN_00005bf4 into icall_directAPI, mark address 0xe165 as GAP_SetParamValue. And read simple_peripheral.c further:
并将FUN_00005bf4重命名为icall_directAPI,将地址0xe165标记为GAP_SetParamValue。并进一步阅读 simple_peripheral.c:
GAPBondMgr_SetParameter(GAPBOND_PAIRING_MODE, sizeof(uint8_t), &pairMode);
GAPBondMgr_SetParameter(GAPBOND_MITM_PROTECTION, sizeof(uint8_t), &mitm);
GAPBondMgr_SetParameter(GAPBOND_IO_CAPABILITIES, sizeof(uint8_t), &ioCap);
GAPBondMgr_SetParameter(GAPBOND_BONDING_ENABLED, sizeof(uint8_t), &bonding);
GAPBondMgr_SetParameter(GAPBOND_LRU_BOND_REPLACEMENT, sizeof(uint8_t), &replaceBonds);
Yes, that’s our 408, 400, etc. Rename 0x3fb5 => GAPBondMgr_SetParameter.
是的,那是我们的 408、400 等。重命名 0x3fb5 => GAPBondMgr_SetParameter。
And the decompiled code further doesn’t look like parameters table initialization either. So either it’s a wrong sample or one more proof that GATT services were commented out.
而且反编译的代码看起来也不像参数表初始化。因此,要么这是一个错误的样本,要么是 GATT 服务被注释掉的又一个证据。
Side note: I reiterate on usefulness of extra data bits and pieces! As I said in the previous article, search by strings sometimes give interesting things. For example, “inputGyroRv” and “inputNormal” strings — search over github gives an interesting library which is indeed exactly the one used in the direction sensor. Unfortunately, nothing of interest for the feet sensor…
旁注:我重申额外数据零碎的有用性!正如我在上一篇文章中所说,按字符串搜索有时会带来有趣的东西。例如,“inputGyroRv”和“inputNormal”字符串 – 在 github 上搜索会给出一个有趣的库,它确实正是方向传感器中使用的库。不幸的是,脚传感器没有任何兴趣……
Anyway, we can drill down as long as we like, but let’s focus on changing logic of the firmware.
无论如何,我们可以根据需要向下钻取,但让我们专注于更改固件的逻辑。
Playing with the firmware
使用固件
As an experiment, we already patched sensor by changing “KATVR” to “KAT-F” (“Feet”). But that’s no fun: each foot has its type (left/right), so let’s try to make it dynamic to announce KAT-R or KAT-L. To find this out, we should find where the type is stored.
作为一项实验,我们已经通过将“KATVR”更改为“KAT-F”(“脚”)来修补传感器。但这并不好玩:每只脚都有自己的类型(左/右),所以让我们试着让它动态地宣布 KAT-R 或 KAT-L。要找出这一点,我们应该找到类型的存储位置。
We know that setting is set via USB, and packets are started with 0x55/0xAA sequence. So by simple scrolling down I fond this piece of code:
我们知道该设置是通过 USB 设置的,数据包以 0x55/0xAA 顺序启动。因此,通过简单地向下滚动,我喜欢这段代码:
case 0xc:
cVar6 = *(char *)(puVar23 + 1);
pcVar26 = *(char **)(puVar23 + 2);
FUN_0000af16(&DAT_200011c8,0,0x1f);
if ((*pcVar26 == 'U') && (pcVar26[1] == -0x56)) {
DAT_200011cb = 0;
DAT_200011c8 = 0x55;
DAT_200011c9 = 0xaa;
cVar4 = (char)local_48;
cVar7 = DAT_200011c0;
if (cVar6 == '\x01') {
....
Which smells like the USB packet processing event handler. WriteDeviceId is command 0x04, so:
这闻起来像 USB 数据包处理事件处理程序。WriteDeviceId 是命令0x04,因此:
if (cVar6 == '\x03') {
DAT_200011cc = '\x03';
cVar4 = DAT_200011cc;
cVar7 = DAT_200011c1;
goto LAB_000009b8;
}
if (cVar6 == '\x04') {
DAT_200011c1 = pcVar26[5];
PostMsg(7,0,0);
DAT_200011cd = 0;
DAT_200011cc = '\x04';
FUN_0000fa20();
FUN_0000f8ec();
break;
}
Given ReadDeviceId is a command 0x03, we may conclude that DAT_200011c1 is the device ID parameter. Right-click on it, search, references to it. There are few references, but this one looks promising:
假设 ReadDeviceId 是命令0x03,我们可以得出结论,DAT_200011c1 是设备 ID 参数。右键单击它,搜索,引用它。参考资料很少,但这个看起来很有希望:
void FUN_0000f8ec(void)
{
if (ParamDeviceId == '\x03') {
DAT_20001132 = 5;
}
else {
DAT_20001132 = 4;
}
return;
}
This function called from the Begin() twice — once early during initialization phase (obviously after load of the parameters) and once right after the call to WriteDeviceId. Ideal injection point!
此函数从 Begin() 调用两次 — 一次是在初始化阶段的早期(显然是在加载参数之后),一次是在调用 WriteDeviceId 之后。理想的注射点!
Let’s look at it’s assembly code:
让我们看一下它的汇编代码:
*************************************************************
* FUNCTION
*************************************************************
undefined FUN_0000f8ec ()
undefined r0:1 <RETURN>
FUN_0000f8ec XREF[2]: 0000038c (c) , 000009a4 (c)
0000f8ec 04 49 ldr r1,[DAT_0000f900 ] = 20001130h
0000f8ee 91 f8 91 00 ldrb.w r0,[r1,#0x91 ]=>ParamDeviceId
0000f8f2 03 28 cmp r0,#0x3
0000f8f4 14 bf ite ne
0000f8f6 04 20 mov.ne r0,#0x4
0000f8f8 05 20 mov.eq r0,#0x5
0000f8fa 88 70 strb r0,[r1,#0x2 ]=>DAT_20001132
0000f8fc 70 47 bx lr
0000f8fe c0 ?? C0h
0000f8ff 46 ?? 46h F
DAT_0000f900 XREF[1]: FUN_0000f8ec:0000f8ec (R)
0000f900 30 11 00 20 undefine 20001130h ? -> 20001130
Function loads address of some object to r1 from nearby stored constant; then loads to r0 param id as object+offset inside. Then does comparison to 0x03 (left) and depending on the value stores 4 or 5 using “ite ne”.
函数将某个对象的地址从附近存储的常量加载到 r1;然后加载到 R0 参数 ID 作为 Object+Offset 内部。然后与0x03(左)进行比较,并根据值使用“ite ne”存储 4 或 5。
That’s a very nice mechanism of branchless conditional execution. In the full ARM mode they “.ne” and “.eq” and others are parameters of the instructions, but in the thumb mode the “ite” is an real operator (I)f,(T)then,(E)lse, which could also be “IT” (if-then) or “ITT” (if-then-then”) and so on — it sets mode for the next up to 4 instructions to be then or else, while the first one after it is always “then”.
这是一个非常好的无分支条件执行机制。在完整的 ARM 模式下,它们 “.ne” 和 “.eq” 等是指令的参数,但在 thumb 模式下,“ite”是一个实数运算符 (I)f,(T)then,(E)lse,它也可以是 “IT” (if-then) 或 “ITT” (if-then-then“) 等等——它为接下来最多 4 条指令设置模式,否则, 而它之后的第一个总是“然后”。
So, it stores to r0 either 4 or 5 depending on the leg into another parameter in the object, and then it branches back to lr (“return”). Since the function only touches r0 and r1, there is no push/pop instructions.
因此,它根据 leg 将 4 或 5 存储到 r0 到对象中的另一个参数中,然后分支回 lr(“return”)。由于该函数仅接触 r0 和 r1,因此没有推/弹出指令。
Since after the return there are two unused bytes (alignment), we can directly change bx lr + c0/46 to a jump somewhere, where we’ll store our code.
由于在返回后有两个未使用的字节(对齐),我们可以直接将 bx lr + c0/46 更改为某个地方的跳转,我们将在那里存储我们的代码。
Right after the ROMtoRAM table there are some free space available, all-zero. Let’s move a little bit down from it, to some round number (0x12e10).
在ROMtoRAM表之后,有一些可用空间,全部为零。让我们从它往下移动一点,到一些整数 (0x12e10)。
What should we write here? We want to make “KATVR” to be “KAT-L” or “KAT-R” depending on the setting. So we may make R0 to be “L” or “R” and then store it — but where? “KATVR” is twice in the image now — once in ROM and once in RAM:
我们应该在这里写什么?我们希望根据设置将“KATVR”设置为“KAT-L”或“KAT-R”。因此,我们可以将 R0 设置为“L”或“R”,然后将其存储在何处?“KATVR”现在在映像中出现了两次——一次在 ROM 中,一次在 RAM 中:
DAT_200011a4 XREF[1]: 000000ee (*)
200011a4 06 ?? 06h
200011a5 09 ?? 09h
200011a6 4b ?? 4Bh K
200011a7 41 ?? 41h A
200011a8 54 ?? 54h T
200011a9 56 ?? 56h V
200011aa 52 ?? 52h R
200011ab 05 ?? 05h
So we want to make equivalent of:
因此,我们想使等价物:
if(left) {
scanRsp[6] = 'L';
} else {
scanRsp[6] = 'R';
}
scanRsp[5] = '-'; // scratch that, it could be ROM patch as it fixed
At the function end we already have R0 either 4 or 5, and R1 is pointing to 20001130. The target, 200011A9, is only 0x79 away from R1, and flags are already set from the last comparison, so we may do something like this:
在函数端,我们已经有 R0 4 或 5,并且 R1 指向 20001130。目标 200011A9 距离 R1 只有 0x79 个距离,并且从上次比较中已经设置了标志,因此我们可以执行以下操作:
ite ne
mov.ne r0,#'L'
mov.eq r0,#'R'
strb r0,[R1,#0x7A]
mov r0,#'-'
strb r0,[R1,#0x79]
bx lr
To edit code in Ghidra one should clear the instructions (“C”), then via Ctrl+Shift+G run an assembler for the current line (it’ll complain about it being beta). Enter instruction, arguments, and sometimes it understands what I want to do — sometimes not. F.e. it doesn’t want to translate “mov.ne r0,#’L’” (nor just mov). So I fallback to online assembler when that happened: enter there instructions and just copy translated bytes. “mov r0, #’L’” => “4f f0 4c 00”… No no, give me 16bit command. “movs r0, #’L’” => “4c 20” — yup, that one. “movs r0, #’R’” => “52 20”. And so on…
要在 Ghidra 中编辑代码,应该清除指令 (“C”),然后通过 Ctrl+Shift+G 为当前行运行汇编程序(它会抱怨它是 beta 版)。输入指令、参数,有时它能理解我想做什么——有时不能。例如,它不想翻译“mov.ne r0,#’L’”(也不只是 mov)。因此,当这种情况发生时,我回退到在线汇编器:输入那里的说明并复制翻译后的字节。“mov r0, #’L’” => “4f f0 4c 00”…不,给我 16 位命令。“movs r0, #’L’” => “4c 20” — 是的,就是那个。“movs r0, #’R’” => “52 20”.等等……
So, it’s done, but Ghidra clearly out of its mind:
所以,它已经完成了,但 Ghidra 显然不在意:
MoveFeetNumNew
00012e10 14 bf ite ne
00012e12 4c 20 mov.ne r0,#0x4c
00012e14 52 20 mov.eq r0,#0x52
00012e16 81 f8 7a 00 strb.eq.w r0,[r1,#0x7a ]
00012e1a 81 f8 79 00 strb.eq.w r0,[r1,#0x79 ]
00012e1e 70 47 bx.eq lr
I have no idea why it stuck, but definitely, flags/state tracking got broken. Nevermind, we can ignore it for now, export binary, make a patch, flash it…
我不知道为什么它卡住了,但可以肯定的是,标志/状态跟踪被破坏了。没关系,我们可以暂时忽略它,导出二进制文件,制作补丁,刷新它……
Whoops. Not working. 🙁 哎 呦。不工作。:(
Ah, right! Just change a structure is not enough, we should also inform the BLE stack about it; let’s add a call GAPRole_SetParameter with GAPROLE_SCAN_RSP_DATA.
啊,对!仅仅改变一个结构是不够的,我们还应该通知BLE堆栈;让我们添加一个带有GAPROLE_SCAN_RSP_DATA的呼叫GAPRole_SetParameter。
Okay, to do so we need to get the pointer to the structure into R2, and, since we just need to return afterwards, we can just use tail jump to it:
好的,要做到这一点,我们需要将指向结构的指针放入 R2 中,并且,由于我们只需要在之后返回,我们可以只使用尾跳转到它:
ite ne
mov.ne r0,#'L'
mov.eq r0,#'R'
adds.w r2,r1,#0x74
strb r0,[R2,#6]
movw r0,#0x308
movs r1,#0x10
b.w GAPRole_SetParameter
Okay, mistracking of the code flow has gone to a new level of uselessness so let’s fix it. First, right-click onto “b.w” instruction and use “modify instruction flow” to change it to “CALL_RETURN”. That makes it better. To fix the “.eq” we can right-click onto the first broken instruction (adds.eq.w) and select “Clear Flow and Repair”, then press F12 to recreate code here — now code looks much better:
好的,代码流的误跟踪已经达到了一个新的无用水平,所以让我们来修复它。首先,右键单击“b.w”指令,然后使用“修改指令流”将其更改为“CALL_RETURN”。这让它变得更好。要修复“.eq”,我们可以右键单击第一个损坏的指令 (adds.eq.w) 并选择“清除流程和修复”,然后按 F12 在此处重新创建代码——现在代码看起来好多了:
MoveFeetNumNew XREF[1]: MoveFeetNumSmt:0000f8fc (j)
00012e10 14 bf ite ne
00012e12 4c 20 mov.ne r0,#0x4c
00012e14 52 20 mov.eq r0,#0x52
00012e16 11 f1 74 02 adds.w r2,r1,#0x74
00012e1a 90 71 strb r0,[r2,#0x6 ]
00012e1c 40 f2 08 30 movw r0,#0x308
00012e20 10 21 movs r1,#0x10
00012e22 f0 f7 0b bc b.w GAPRole_SetParameter undefined GAPRole_SetParameter()
-- Flow Override: CALL_RETURN (CALL_TERMINATOR)
And the decompilation is also looks good now:
而且反编译现在看起来也不错:
void MoveFeetNumSmt(void)
{
if (DeviceId == '\x03') {
DAT_20001132 = 5;
UNK_200011aa = 0x52;
}
else {
DAT_20001132 = 4;
UNK_200011aa = 0x4c;
}
GAPRole_SetParameter(0x308,0x10);
return;
}
Well, semi-good. Let’s right-click onto the GAPRole_SetParameter, “Edit Function” and add three arguments:
嗯,半好。让我们右键单击GAPRole_SetParameter“编辑函数”并添加三个参数:
Now the code looks exactly as intended:
现在,代码看起来完全符合预期:
void MoveFeetNumSmt(void)
{
if (DeviceId == '\x03') {
DAT_20001132 = 5;
UNK_200011aa = 0x52;
}
else {
DAT_20001132 = 4;
UNK_200011aa = 0x4c;
}
GAPRole_SetParameter(0x308,0x10,&scanRspData);
return;
}
Repeat the procedure (File=>Export Program; “fc.exe /b katvr_foot_orig.bin katvr_foot.bin”, etc). Flash it. Scan surrounding, excellent! We see the “KAT-R” and “KAT-L” in the range, perfect.
重复该过程 (File=>Export Program;“fc.exe /b katvr_foot_orig.bin katvr_foot.bin”等)。闪烁它。扫描环绕,优秀!我们在范围内看到了“KAT-R”和“KAT-L”,完美。
Okay, let’s stop child’s play and go mad scientist mode.
好吧,让我们停止儿戏,进入疯狂的科学家模式。
Overclock the KatWalk C2 sensors to 133 Hz
将 KatWalk C2 传感器超频至 133 Hz
To get the sensor problems, one should think like a sensor. We know, that sensors send data as Notification packets. Let’s look for it. On the left side there is a “Symbol tree” with a search field under it, enter there “Notofi” and double-click on “GATT_Notification”. Hm. No references? Perhaps, an indirect call.
要解决传感器问题,应该像传感器一样思考。我们知道,传感器以通知数据包的形式发送数据。让我们寻找它。左侧有一个“符号树”,下面有一个搜索字段,输入“Notofi”,然后双击“GATT_Notification”。嗯,没有参考资料?也许,一个间接的电话。
Let’s search for its address 0x10010045 (thumb mode, so address+1), and we can find the only one place, where some preparation is going on and then call to it:
让我们搜索它的地址0x10010045(拇指模式,所以地址+1),我们可以找到唯一一个正在进行一些准备工作的地方,然后调用它:
_DAT_20001186 = 0x14;
_DAT_20001188 = (undefined *)thunk_EXT_FUN_10018404(DAT_20001142,0x1b,0x14,0,in_r3);
cVar1 = DAT_20000521;
if (_DAT_20001188 != (undefined *)0x0) {
_DAT_20001184 = 0x2e;
if (_DAT_20001146 == 0) {
...
cVar1 = icall_directAPI(0x10,(int)&GATT_Notification + 1,DAT_20001142,&DAT_20001184,0);
0x2E is indeed the handle number used, so we found our place.
0x2E确实是使用的句柄编号,所以我们找到了自己的位置。
Simplified, the code of the function looks like this:
简化后,函数的代码如下所示:
void KatSendNotification() {
out = malloc(...);
if (out) {
if (packetNo == 0) {
out->_type = 0; // status packet
fill_charge_levels(out);
} else {
out->_type = 1; // data packet
if (!DATA_READY || !DATA_OK) {
out->_x = 0;
out->_y = 0;
} else {
DATA_READY = false;
out->_x = DATA_X;
out->_y = DATA_Y;
}
out->status = STATUS;
}
if (something) {
out[0] = 0; out[1] = 1; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 1;
}
attHandleValueNoti_t notification = { 0x2e, 0x14, out };
if (!GATT_Notification(0x2E, ¬ification, 0)) { free(out); }
if (++packetNo == 500) {
packetNo = 0;
}
}
Every 500 packets sensor sends its charge level (plus firmware version and its ID), and all other packets contain the movement data. If the movement data is not ready or invalid, it sends zeros. If there some flag is set, then at the beginning of the packet sequence 0–1–0–1–0–1 is written over.
每 500 个数据包传感器发送其充电级别(加上固件版本和 ID),所有其他数据包都包含移动数据。如果移动数据未就绪或无效,则发送零。如果设置了一些标志,则在数据包序列的开头会写入 0–1–0–1–0–1。
Now let’s inspect how often the notification is sent. There are only two references to (as I called it) “KatSendNotification” function found, both from the main thread inside of the event processing loop:
现在,让我们检查一下发送通知的频率。只有两个对(我称之为)“KatSendNotification”函数的引用,都来自事件处理循环内的主线程:
while(!QueueEmpty(...)) {
Entry* event = QueueGet(...);
switch (event->id)
{
...
case 4:
KatSendNotification();
ClockStop(...);
break;
case 6:
if (Flag1 == 1 || Flag2 == 1) {
KatSendNotification();
}
break
...
}
}
Event #4 should be posted by a timer, but the timer is not started anywhere, and even if it would — it stops right after single processing. No other references to the timer found, so, that’s not our entry point.
事件 #4 应该由计时器发布,但计时器不会在任何地方启动,即使它会启动,它也会在单次处理后立即停止。没有找到对计时器的其他引用,所以,这不是我们的切入点。
Event #6 is generated inside of a callback fired by the BLE stack and data is only sent when two flags are up. One of the flags is set after the connection is just established and the second flag is set as part of another BLE processing sequence. That is consistent with the earlier observation, that data updates flow starts only after the connection is established and connection parameters have been changed.
事件 #6 在 BLE 堆栈触发的回调中生成,并且仅在两个标志启动时发送数据。其中一个标志在刚刚建立连接后设置,第二个标志设置为另一个 BLE 处理序列的一部分。这与前面的观察结果一致,即只有在建立连接并更改连接参数后,数据更新流才会启动。
It’s uncertain, but looks like packets are generated every time the previous one is sent, basically, with the refresh request rate. Okay, let’s see how the data gets updated. Cross-reference to DATA_OK flag leads us to another thread:
这是不确定的,但看起来每次发送前一个数据包时都会生成数据包,基本上是刷新请求速率。好的,让我们看看数据是如何更新的。对 DATA_OK 标志的交叉引用将我们引向另一个线程:
void ReadSensorData(...)
{
do {
Semaphore_Pend(SensorSemaphore, -1);
Task_sleep(700);
GPIO_SET(..., 0);
SPI_Send(0x50);
Task_sleep(10);
char* out = &SensorData;
for (int i = 0x0C; i; --i) {
*(out++) = SPI_Recv();
}
GPIO_SET(..., 1);
Task_sleep(0.1);
DATA_OK = 0;
if ((SensorData[0] & 0x80 != 0) && (SensorData[0] & 0x20 != 0)) {
DATA_OK = 1;
}
DATA_READY = 1;
SensorReads++;
If (SensorReads > 99) {
// refresh something
SensorReads = 0;
}
if (SomeFlag == 0) {
Semaphore_Post(SensorSemaphore);
}
} while(true);
}
That thread continuously refreshes the sensor data until SomeFlag wouldn’t set. As deeper look shown, “SomeFlag” is actually “SleepMode”, it is set when sensor decides to go to sleep. Semaphore is initialized at the beginning of the main thread and set up, so the sensor basically continuously read the sensor as it wakes up and until it goes to sleep, with a delay of 700+10+13byteSPI plus refresh of some other data every 100 reads. SPI is set up to 4 megabode. Sleep is works in 10s of microseconds. So together, sensor data is refreshed every ~7.11 milliseconds, so around 140–141 Hz. Looks like there should be enough the fresh data to send updates at 133 Hz — but experiments have already shown that we do have problems getting zeros frequently.
该线程会持续刷新传感器数据,直到 SomeFlag 不设置。如更深层次的观察所示,“SomeFlag”实际上是“SleepMode”,它是在传感器决定进入睡眠状态时设置的。信号量在主线程的开头初始化并设置,因此传感器基本上在唤醒时连续读取传感器,直到进入睡眠状态,延迟为 700+10+13byteSPI,每 100 次读取刷新一些其他数据。SPI 设置为高达 4 兆波。睡眠在 10 微秒内起作用。因此,传感器数据每 ~7.11 毫秒刷新一次,因此约为 140-141 Hz。 看起来应该有足够的新数据以 133 Hz 的速度发送更新——但实验已经表明,我们确实在频繁获得零时存在问题。
Observed behavior: we see zero packets during the continuous movement (like, in Gateway dot suddenly jumps to the center).
观察到的行为:在连续移动过程中,我们看到零数据包(例如,在网关中,点突然跳到中心)。
This behavior is understandable: since the sensor refreshes at ~140Hz and reads happen at 133Hz, they are close which makes the probability of hitting the race case when we just formed the packet and at the same time data refresh happened quite high. We see the typical race condition between two threads when there is no synchronization between them.
这种行为是可以理解的:由于传感器以 ~140Hz 刷新,读取以 133Hz 发生,因此它们很接近,这使得当我们刚刚形成数据包时击中竞争情况的概率非常高,同时数据刷新发生得相当高。当两个线程之间没有同步时,我们会看到它们之间的典型争用条件。
The solution is obvious: we should synchronize them. We only need new data to send it out, so only when there is a connection. The packets are requested at regular intervals, so, what if… We move `Semaphore_Post` from `ReadSensorData` to `KatSendNotification`? That should work great: ReadSensorData gets fresh data and goes to sleep, and once KatSendNotification fetch this data — it’ll wakes the ReadSensorData thread to prepare for the next packet. Ideal.
解决方案是显而易见的:我们应该同步它们。我们只需要新数据来发送它,所以只有当有连接时。数据包是定期请求的,所以,如果……我们将“Semaphore_Post”从“ReadSensorData”移动到“KatSendNotification”?这应该很好用:ReadSensorData 获取新数据并进入睡眠状态,一旦 KatSendNotification 获取此数据,它将唤醒 ReadSensorData 线程为下一个数据包做准备。理想。
One more thing: the KatSendNotification is run once previous packet sent out, which means the delay inside of ReadSensorData is still needed, but I would reduce it a little bit. Since the data requested with frequency between 86 Hz and 133 Hz, the actual sampling moment is not that critical as long as the delays are the same — plus/minus few milliseconds won’t matter.
还有一件事:KatSendNotification 在发送前一个数据包后运行,这意味着仍然需要 ReadSensorData 内部的延迟,但我会减少一点。由于请求的数据频率在 86 Hz 和 133 Hz 之间,因此只要延迟相同,实际采样时刻就不那么重要——正负几毫秒无关紧要。
So, let’s run the notepad and plan for the patch.
因此,让我们运行记事本并计划补丁。
First step: reduce the delay:
第一步:减少延迟:
000079d0 41 f6 58 31 movw r1,#7000 # The delay
=>
000079d0 41 f2 88 31 movw r1,#5000
Second step: short-cut the loop before it updates SensorSemaphore. We have many ways to do so: we may change conditional jmp from the if to unconditional, just NOP-fill the call, or replace condition with goto:
第二步:在更新 SensorSemaphore 之前快捷循环。我们有很多方法可以做到这一点:我们可以将有条件的 jmp 从 if 更改为无条件的,只需 NOP 填充调用,或者将 condition 替换为 goto:
LAB_00007ab0 XREF[1]: 00007a54 (j)
00007ab0 28 78 ldrb r0,[r5,#0x0 ]=>SleepState = 52h
00007ab2 00 28 cmp r0,#0x0
00007ab4 98 d1 bne LAB_000079e8
00007ab6 68 68 ldr r0,[r5,#0x4 ]=>SensorReadSem
00007ab8 f9 f7 7e fa bl Semaphore_post undefined Semaphore_post()
00007abc 94 e7 b LAB_000079e8
=>
LAB_00007ab0 XREF[1]: 00007a54 (j)
00007ab0 9a e7 b LAB_000079e8
Third step: expand `KatSendNotification` function. Luckily, we’ve just learned how to play with the function expansion, so let’s do it the same way. Go get some free space (right after the previous patch, 00012e40), and replace the return (which is implemented as pop in this case) with branch to the extender:
第三步:展开“KatSendNotification”功能。幸运的是,我们刚刚学会了如何使用函数扩展,所以让我们以同样的方式进行操作。去获取一些可用空间(紧接在上一个补丁 00012e40 之后),并将 return(在本例中实现为 pop)替换为扩展器的分支:
LAB_00006b14 XREF[1]: 00006a08 (j)
00006b14 f8 bd pop {r3,r4,r5,r6,r7,pc}
00006b16 c0 ?? C0h
00006b17 46 ?? 46h F
=>
LAB_00006b14 XREF[1]: 00006a08 (j)
00006b14 0c f0 94 b9 b.w KatSendNotificationTail
And then form the continuation to do the same code as we cut out of the `ReadSensorData`. Comparing to the patching for the L/R, we need to plan for a gap required for the code before we can place the constant from where we can read the pointer to the semaphore. Same as before, we may need to fix Repair Flow and or adjust jump tracking, but once all the code is assembled and written in, it should be like that, and the decompilation show it beautiful:
然后形成延续以执行与我们从“ReadSensorData”中剪切的相同代码。与 L/R 的修补相比,我们需要规划代码所需的间隙,然后才能放置常量,从中读取指向信号量的指针。和以前一样,我们可能需要修复 Repair Flow 和/或调整跳转跟踪,但是一旦所有代码都组装并写入,它应该是这样的,并且反编译显示它很漂亮:
=>
KatSendNotificationTail XREF[1]: KatSendNotification:00006b14 (j)
00012e40 03 4d ldr r5,[->SleepState ] = 20001584
00012e42 28 78 ldrb r0,[r5,#0x0 ]
00012e44 00 28 cmp r0,#0x0
00012e46 02 d1 bne LAB_00012e4e
00012e48 68 68 ldr r0,[r5,#0x4 ]
00012e4a ee f7 b5 f8 bl Semaphore_post undefined Semaphore_post()
LAB_00012e4e XREF[1]: 00012e46 (j)
00012e4e f8 bd pop {r3,r4,r5,r6,r7,pc}
PTR_SleepState_00012e50 XREF[1]: 00012e40 (R)
00012e50 84 15 00 20 addr SleepState = 52h
Generate patch, upload… Wow, it works! From the first shot! 133Hz receiver working great, slow continuous move no longer has any breaks. Hooray!
生成补丁,上传…哇,它有效!从第一枪开始!133Hz接收器工作良好,缓慢的连续移动不再有任何中断。万岁!
Fix the bug (throw away forgotten debug appendix)
修复 bug(扔掉被遗忘的调试附录)
When I was almost happy with the result, Utopia Machina (yes, one more time many thanks for the thorough testing!) raised an issue that his direction sensor requires resetting it frequently to be able to use it. It gets stuck showing some angle and only minimally changes from it as you turn it, plus stops reporting its charge level. Another issue was once data from one of the feet disappeared… And this is not reproduced (at least not easily) on the original receiver.
当我对结果几乎感到满意时,Utopia Machina(是的,再次感谢您的全面测试!)提出了一个问题,即他的方向传感器需要经常重置才能使用它。它被卡住,显示一些角度,当你转动它时,它的变化很小,而且停止报告它的电量。另一个问题是,一旦其中一只脚的数据消失了……这在原始接收器上无法复制(至少不容易)。
I wasn’t able to reproduce the issue either until we realized that I keep sensors attached to the power, while he is not. Okay, I screw the sensor out of the backplate, and let it be for a couple of hours periodically touching it to see if is it still alive. And yes, the problem was reproduced!
我也无法重现这个问题,直到我们意识到我将传感器连接到电源上,而他没有。 好的,我把传感器从背板上拧出来,让它定期触摸它几个小时,看看它是否还活着。是的,问题重现了!
Once it happened, Wireshark confirmed by suspicion: that direction packets packets has 0–1–0–1–0–1 sequence at the beginning. The rest of the packet was normal, so a little bit of the quaternion was changing, leading to the little visible difference in the angle as you turn it.
一旦它发生,Wireshark 怀疑地证实:方向数据包数据包在开始时有 0-1-0-1-0-1 序列。数据包的其余部分是正常的,所以四元数的一点点变化,导致你转动它时角度的可见差异很小。
So yes that was an explosion of this time bomb:
所以是的,这是这颗定时炸弹的爆炸:
if (something) {
out[0] = 0; out[1] = 1; out[2] = 0; out[3] = 1; out[4] = 0; out[5] = 1;
}
That explains also how this lead to loss of signal from one of the feet — once the same code gets triggered in the foot sensor, while coordinates do not get corrupted (they are much further in the packet than direction), but battery level and sensor number are.
这也解释了这如何导致其中一只脚的信号丢失——一旦在脚传感器中触发相同的代码,坐标不会被损坏(它们在数据包中比方向更远),但电池电量和传感器编号却会损坏。
Great, so we know what happened — but why? Luckily, the “something” flag has only one write reference to it, inside of the timer that fires every second; the write happens when some counter gets to 1800 and doesn’t get reset along the way. Reset happens when some condition related to the battery level happens. So… That’s some debugging code forgotten for sensor battery consumption optimization. I also found USB commands to reset or adjust the constants for these events.
太好了,所以我们知道发生了什么——但为什么呢?幸运的是,“something”标志只有一个写入引用,在每秒触发一次的计时器内;当某个计数器达到 1800 并且在此过程中没有重置时,就会发生写入。当发生与电池电量相关的某些情况时,就会发生重置。所以。。。这是一些被遗忘的调试代码,用于传感器电池消耗优化。我还找到了用于重置或调整这些事件的常量的 USB 命令。
So, since this is not production-critical in any way code, we can just throw it away. Again, there are many ways to throw it away: change if, wipe with NOPs… I just NOP’ed the flag change operation.
因此,由于这在任何代码中都不是生产关键代码,我们可以将其丢弃。同样,有很多方法可以扔掉它:如果更改,用 NOP 擦拭……我刚刚取消了标志更改操作。
Fixed firmwares has none of these issues anymore.
修复固件不再有这些问题。
What about other issues 其他问题呢?
Yes, fixed sensor firmware is compatible with the original receiver… Almost. Before, the optical sensors got read at fixed ~140Hz and now they are read with each receiver update (86..133 Hz).
是的,固定传感器固件与原始接收器兼容……几乎。以前,光学传感器以固定的 ~140Hz 读取,现在每次接收器更新 (86..133 Hz) 都会读取它们。
Optical sensors return distance measured (in some magic units) since the last read.
光学传感器返回自上次读取以来测量的距离(以某些神奇单位)。
The original receiver requests data every 86 Hz from the foot sensor, but the foot sensor reads its optics every 140 Hz. So gateway uses the data not as a distance, but as a sampling of speed. This way we lose some read packets, but as we request fast enough, we can recover the original speed with a good enough precision.
原始接收器每 86 Hz 从脚踏传感器请求一次数据,但脚踏传感器每 140 Hz 读取一次光学元件。因此,网关不是将数据用作距离,而是用作速度的样本。这样,我们会丢失一些读取数据包,但是由于我们的请求速度足够快,我们可以以足够高的精度恢复原始速度。
The patched sensor reads its optics with the same frequency as it gets requested, which gives more accurate data for distance passed, but the absolute values are different from what we can see with fixed refresh frequency. At 86 Hz it’ll be higher by ~163%, and with 133 Hz refresh rate it only be ~105%.
贴片传感器以与请求相同的频率读取其光学元件,从而为通过的距离提供更准确的数据,但绝对值与我们在固定刷新频率下看到的值不同。在 86 Hz 时,它会高出 ~163%,而在 133 Hz 刷新率下,它只会提高 ~105%。
Is this an issue? It depends on how to use the data. For direct speed estimation. ignoring refresh rate (how the Gateway does it, unfortunately), then it’s yes and no. No, because at 133 Hz refresh rate, the data is almost the same, and one doesn’t feel any difference in the speed but it makes life much better with lower latency to react onto steps (especially at 120 fps). Yes, because the use of the patched sensor with the original receiver leads to significantly higher speeds, requiring to adjust settings in the Gateway for each game.
这是一个问题吗?这取决于如何使用数据。用于直接速度估算。忽略刷新率(不幸的是,网关是如何做到的),那么它是和否。不,因为在 133 Hz 刷新率下,数据几乎相同,并且感觉不到速度有任何差异,但它以更低的延迟(尤其是在 120 fps 时)让生活变得更好。是的,因为将修补的传感器与原始接收器一起使用会导致速度显着提高,因此需要在网关中为每个游戏调整设置。
Can it be fixed? Yes, of course, in many ways: patch for the gateway, patch for the original receiver, more complicated sensor patch… But that’s a topic for another time.
可以修复吗?是的,当然,在很多方面:网关的补丁,原始接收器的补丁,更复杂的传感器补丁……但这是另一个时间的话题。
What’s next 后续步骤
The next step is to get rid of the gateway at least for the native games. Right now one can’t make a standalone game using KAT SDK, since it windows only and requires the gateway to be run in the background. Since I already know how to communicate with the receiver directly and made a receiver that can be attached to the headset… I have everything required to make the real standalone SDK — and that’s a perfect fit for the game that Utopia Machine doing.
下一步是摆脱网关,至少对于原生游戏而言。目前,人们无法使用 KAT SDK 制作独立游戏,因为它仅在窗口中运行,并且需要网关在后台运行。由于我已经知道如何直接与接收器通信,并制作了可以连接到耳机的接收器……我拥有制作真正的独立 SDK 所需的一切——这非常适合 Utopia Machine 所做的游戏。
That means, there are work ongoing to make UE SDK with direct access to KatWalk C2 data for both windows and native on Quest 2/3. 🙂 Stay tuned!
这意味着,UE SDK的工作正在进行中,可以直接访问Windows和Quest 2/3上的原生KatWalk C2数据。:)敬请关注!
Links 链接
- KatWalk C2 Saga, EP1: “playing actually” on [Habr], [Medium] and [LinkedIn
KatWalk C2 Saga,EP1:在[Habr],[Medium]和[ LinkedIn上“实际播放” - KatWalk C2 Saga, EP2: “to speak alien” on [Habr], [Medium] and [LinkedIn].
KatWalk C2 Saga,EP2:在[Habr],[Medium]和[LinkedIn上“说外星人”。 - KatWalk C2 Saga, EP3: “cutting the wire” on [Habr], [Medium] and [LinkedIn].
KatWalk C2 Saga,EP3:在[Habr],[Medium]和[LinkedIn上“切断电线”]。 - KatWalk C2 Saga, EP4: “playing with firmware” on [Habr], [Medium] and [LinkedIn].
KatWalk C2 Saga,EP4:在[Habr],[Medium]和[LinkedIn上“玩固件”]。 - KatWalk C2 Saga, EP5: “overclocking and bugfixing” on [Habr], [Medium] and [LinkedIn]
KatWalk C2 Saga,EP5:[Habr]、[Medium]和[LinkedIn上的“超频和错误修复”
转载请注明:KatWalk C2: p.5: overclocking and bugfixing or how to use Ghidra to analyse ARM firmware | CTF导航