KatWalk C2: p.4: playing with firmware

Anton Fedorov
13 min readApr 1, 2024

--

If you just see this for the first time — I speak about VR treadmill, how to integrate with it, and how to directly communicate with its sensors.

I already found a few issues with the sensors — which means, they are due to be fixed. To fix sensors it would be great to understand what are they and how to change something there. Due to the large size of the article I’ve had to split this tiny part into two, so today we’ll just peek inside of the sensor and learn how to change its firmware (in a user-friendly way).

Autopsy

Learning about something usually a chaotic process with different attempts to poke the surface from different directions. The so-called “scientific poke method”. It’s impossible to foresee the ideal path beforehand, but usually, it’s quite obvious in a retrospective. That means one shouldn’t treat this article as a guide — more like a review of possible ways to start poking around. Many ways and many attempts in the end lead to the understanding.

Open up…

But no matter way, an autopsy is the best way to learn the truth about how something died. This one — looks like from the autopsy. (Just kidding, the

first opening wasn’t that destructive, but this sensor was faulty before, I promise). Looking inside the foot sensor we see large module labelled

HY-40R201C. That’s BLE5.0 module based on TI CC2640R2. So this is a CPU (actually, two) plus a radio. It can be used as-is, with firmware uploaded directly inside or in tandem with an external CPU.

Turn to belly

No other CPUs in sight on the board. The other side only has an optical mouse module A9800.

That means the module IS the CPU. The board also has two battery ports, but only one is used. There are Reset and Boot buttons. Now we can look into its datasheet whenever we want to know hardware ports and addresses, we know it’s an ARM Cortex M3 with 128kB ROM and 8kB RAM. It ain’t much, but it’s honest MCU!

In contrast to Nordic unit I’ve used, there is no USB support on the MCU module. So it’s not a surprise to see a chip, marked CH9326 — USB-HID converter, and there is also “ch9326” named DLL and some function names in the Gateway.

So, we’ve learned and cross-validated that sensors can communicate with computers using a USB-HID conversion chip, they are based on an ARM CPU and run

using Texas Instruments SIMPLELINK-CC2640R2-SDK. The SDK is accompanied by an “Academy” where one can learn in a compressed form what is BLE, how to use it with the SDK plus an explanation for the samples. Since I don’t have TI’s DevKit and there is no equivalent of Nordic’s USB Stick, there is no point in trying to go through the academy at least right now; but the SDK is still worth installing to search inside for the code samples, constants and so on.

To the ROM and back

Just looking around is a good way to start learning, but to really get it one needs to look inside. There are contact points “V R C M G” with “G” looking like a ground, but the meaning of others is not clear. Yes, I can trace them to the CPU module, but… I don’t have JTAG connector anyway.

This is a good time to remember, that the gateway actually can update the firmware of sensors. There is a page showing current firmware versions and the update button. Additionally, I’ve actually had to update my sensors the first time I set up the treadmill. So, let’s go and look for the firmware images around:

C:\>dir /b /d "C:\Program Files (x86)\KAT Gateway\*bin"
loco_ankle_by_embeded.bin
loco_receiver_by_embeded.bin
loco_sensor_group_application_foot_release_by_embeded_engineer.bin
loco_sensor_group_application_waist_release_by_embeded_engineer.bin
loco_s_ankle_by_embeded.bin
loco_s_foot_by_embeded.bin
loco_s_receiver_by_embeded.bin
loco_s_waist_by_embeded.bin
loco_waist_by_embeded.bin
walk_c_foot_by_embeded.bin
walk_c_hall_by_embeded.bin
walk_c_receiver_by_embeded.bin
walk_c_v2_foot_by_embeded.bin
walk_c_v2_hall_by_embeded.bin
walk_c_v2_receiver_by_embeded.bin

Hmm.. That’s for the other KAT devices. Where’s C2 firmware?

C:\>dir /b /d "C:\Program Files (x86)\KAT Gateway\*hex"
katvr_direction.hex
katvr_foot.hex
katvr_receiver.hex

Okay. Need to confirm. Let’s run dotPeek again, Ctrl+Alt+T, “.hex”… ahha!

Search for usage of hex…

Indeed, these hex files are the firmware. Great, let’s quick-peek thru the way the gateway updates firmware:

    byte index = 0;

int num1 = (int) KatvrFirmwareHelper.ch9326_find();
assert(num1 != 0)

int num2 = (int) KatvrFirmwareHelper.ch9326_open(Update_Firmware_Upgrading_Form.vid, Update_Firmware_Upgrading_Form.pid);
assert(num2 != 0)

int num21 = KatvrFirmwareHelper.ch9326_set_gpio(index, (byte) 15, (byte) 15)
assert(num21 != 0)

int num3 = (int) KatvrFirmwareHelper.ch9326_connected(index);
assert(num3 != 0)

int num4 = (int) KatvrFirmwareHelper.flash(_hex_path, device_type, device_state);
assert(num4 == 1)

KatvrFirmwareHelper.ch9326_ClearThreadData();
KatvrFirmwareHelper.close_ch9326();

/* Write MACs of sensors into receiver if we updated receiver */
if (Update_Firmware_Upgrading_Form.deviceType == C2FirmwareUpdaeManager.C2DeviceType.Receiver) {
KATSDKInterfaceHelper.WriteSensorPair(...)
}

Basically, the `KatvrFirmwareHelper` can do all the jobs required to put the flash into the sensor. But to be able to analyze the firmware (and to look inside) I’ll need a BIN, not the HEX. There are many ways to convert, but I use one I have already on my PC — run the Linux in the WSL:

$ cd /mnt/c/Program\ Files\ \(x86\)/KAT\ Gateway/
$ for k in foot direction receiver; do objcopy --input-target=ihex --output-target=binary katvr_$k.hex katvr_$k.bin; done
$ ls -la kat*bin
-rwxrwxrwx 1 datacompboy datacompboy 131072 Mar 29 21:46 katvr_direction.bin
-rwxrwxrwx 1 datacompboy datacompboy 131072 Mar 29 21:46 katvr_foot.bin
-rwxrwxrwx 1 datacompboy datacompboy 131072 Mar 29 21:46 katvr_receiver.bin

So, all the firmware binaries are exactly 128kB in size, while their HEX parts are not equals, which means there are few sections inside of the firmware and, perhaps, there are gaps, settings parts and so on. Just keep in mind, useful thought.

By the way, if we haven’t opened the sensor and not yet know what’s the CPU there, we could analyze the binary with binwalk:

$ binwalk --disasm ./katvr_direction.bin 

DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 ARM executable code, 16-bit (Thumb), little endian, at least 1624 valid instructions

And/or cpu_rec:

$ python cpu_rec.py ./katvr_direction.bin 
./katvr_direction.bin full(0x20000) None chunk(0x10000;32) ARMhf

So yes, indeed, that’s an ARM code, mostly in Thumb mode.

We can also peek at it with strings:

$ strings -n 10 katvr_direction.bin
inputNormal
FinputGyroRv
executable
N]_]>CNUW]>@FUm
`(i0a(}0uh}pu
[USQOMKIFCA?<:8
p>"`hBp !`h
k +# p(F#p
!i"hQ\)pch!i
pGpGpGpGpG
{unknown-instance-name}
{empty-instance-name}
F{static-instance-name}

Side note: it’s always pays off to search for extra data. For example this “strings” output. There are strange “inputNormal” and “inputGyroRv”. Search for them on github instantly gives a pointer to library sources that indeed were used for the direction sensor, which helped to mark some functions and structures inside. Unfortunately, nothing like that for the foot sensor.

Quick firmware upload

Quick-reading through the datasheet for Serial Boot Loader explains that the module is ready to be reflashed once it booted with the flash button pressed, so it should be quite safe to play around with the firmware; 0x55 is used to auto-tune to the transfer speed, business as usual. But since sensors is based on USB-HID instead of the USB-Serial support chip, we can’t use an official flasher from TI, so we have to rely on the provided flasher (or write our own, or find another way). Since we already saw that the functions are provided, let’s just use them.

So, let’s create a clean console C# application, add to it `KatvrFirmwareHelper` code and `katvr_firmware.dll` referenced from it; and then:

    static void Main(string[] args)
{
uint vid = 0xC4F4u;
byte device_state = 0;
byte index = 0;
uint pid = 28471u;
byte device_type = 3;
string hex_path = "C:\\Program Files (x86)\\KAT Gateway\\katvr_foot.hex";

if (KatvrFirmwareHelper.ch9326_find() == 0)
{
Console.WriteLine("ch9326_find failed");
return;
}

if (KatvrFirmwareHelper.ch9326_open(vid, pid) == 0)
{
Console.WriteLine("ch9326_open failed");
return;
}

if (KatvrFirmwareHelper.ch9326_set_gpio(index, (byte)15, (byte)15) == 0)
{
Console.WriteLine("ch9326_set_gpio failed");
return;
}

if (KatvrFirmwareHelper.ch9326_connected(index) == 0)
{
Console.WriteLine("ch9326_connected failed");
return;
}

if (KatvrFirmwareHelper.flash(hex_path, device_type, device_state) != 1)
{
Console.WriteLine("KatvrFirmwareHelper.flash failed");
Console.ReadKey();
return;
}

KatvrFirmwareHelper.ch9326_ClearThreadData();
KatvrFirmwareHelper.close_ch9326();
}

Run it — see some debug prints on the screen, error. Hmm… Ah, right, the bootloader mode. Press the “Flash” button with a toothpick, then push the “Reset” with another toothpick, now run the program — yup, it works, dots running through the screen… In about one and a half minutes — finished. The sensor is still alive! The only oops is it blinking with the left LED instead of the right one as before. So… Settings were wiped out.

Let’s patch the firmware

Being able to upload the original firmware is good, but I definitely want to find a comfortable way to upload modified firmware. Ghidra can export a current module as a Hex or a Bin file, but it includes everything including RAM and the areas that were missing in the original Hex. So, I need to patch the hex, but how?

Let’s make some changes to the firmware to make sure we indeed change it. The simplest way — let’s change some of constants. F.e. “KATVR” replace with “KAT-F”, so it’ll announce it over BLE as “KAT-F” (like, foot). Open with any hex editor, f.e. WinHex, change it, save, diff it:

> fc.exe /b .\katvr_foot_orig.bin .\katvr_foot.bin
Comparing files .\katvr_foot_orig.bin and .\KATVR_FOOT.BIN
000129E9: 56 2D
000129EA: 52 46

Cool. We have a patch, which we can trivially turn into C# code:

    static readonly PatchEntry[] PatchFoot = {
( 0x000129e9, 0x56, 0x2D ), // V => -
( 0x000129ea, 0x52, 0x46 ), // R => F
};

One of the nice tricks I’ve learned along the way for C# (yes, I write on C# too rare… basically, never) — way to make an automagic conversion of
a tuple into a struct, which makes the patch quite compact. Since C# doesn’t want recognize it automagically, we can help it by adding an explicit
constructor and implicit conversion operator:

    struct PatchEntry {
readonly public int addr;
readonly public byte orig;
readonly public byte patch;
public PatchEntry(int addr, byte orig, byte patch) {
this.addr = addr;
this.orig = orig;
this.patch = patch;
}
public static implicit operator PatchEntry((int addr, uint orig, uint patch) tuple) {
return new PatchEntry(tuple.addr, (byte)tuple.orig, (byte)tuple.patch);
}
};

Now we have a machine-readable patch which we should apply to a hex file.

Let’s import HexIO library and make a quick-fix. We need to do quite a little work ourselves: track the address and if the line we just read is within range apply the patch. Unfortunately, HexIO doesn’t update the CRC on data updates. But we can always just create it as a new one and then it’ll make it right. Not the best approach but it works:

    static string PatchHex(string input, PatchEntry[] patch)
{
string output = System.IO.Path.GetTempFileName() + ".hex";

IIntelHexStreamReader hexInput = new IntelHexStreamReader(input);
using (StreamWriter hexOutput = new StreamWriter(output))
{
uint offset = 0;
var patch_i = 0;
do
{
IntelHexRecord rec = hexInput.ReadHexRecord();
if (rec.RecordType == IntelHexRecordType.Data)
{
while (patch_i < patch.Length) {
var pe = patch[patch_i];
if (pe.addr >= offset + rec.Offset)
{
long idx = pe.addr - offset - rec.Offset;
if (idx >= rec.RecordLength)
{
break;
}
if (rec.Data[(int)idx] != pe.orig)
{
Console.WriteLine("File data doesn't match expected.");
throw new InvalidDataException();
}
rec.Data[(int)idx] = pe.patch;
rec = new IntelHexRecord(rec.Offset, rec.RecordType, rec.Data);
patch_i++;
}
else
{
Console.WriteLine("Can't apply patch to a gap.");
throw new InvalidDataException();
}
};
}
else if (rec.RecordType == IntelHexRecordType.ExtendedLinearAddress && rec.RecordLength == 2)
{
offset = (uint)((rec.Data[0] << 8 | rec.Data[1]) << 16);
}
else if (rec.RecordType == IntelHexRecordType.EndOfFile)
{
if (patch_i < patch.Length)
{
Console.WriteLine("Not all patch was applied!");
throw new InvalidDataException();
}
}
else
{
Console.WriteLine(rec.ToString());
throw new InvalidDataException();
}
hexOutput.WriteLine(rec.ToHexRecordString());
} while (!hexInput.State.Eof);
};

return output;
}

Now, let’s apply the patch and update the sensor:

    static void Main(string[] args)
{
string orig_hex = "C:\\Program Files (x86)\\KAT Gateway\\katvr_foot.hex";
string hex_path = PatchHex(orig_hex, PatchFoot);
...
}

Run, wait, scan the surroundings… Yup! We have one “KAT-F” device in range.

Side note: later, once I’ve started to make bigger changes, I hit the error “can’t apply patch to a gap”. So I’ve had to write this branch. To make it work, instead of throwing the error construct the new records as needed and write them directly to the output:

    } else {
int start = pe.addr;
List<byte> data = new List<byte>();
do
{
data.Add(patch[patch_i++].patch);
} while (patch_i < patch.Length &&
patch[patch_i].addr - 1 == patch[patch_i-1].addr &&
patch[patch_i].addr - start < 0x20);
if (start - offset >= 0x10000)
{
Console.WriteLine("Can't inject a record: cross boundary");
throw new InvalidDataException();
}
var newrec = new IntelHexRecord((ushort)(start - offset), rec.RecordType, data);
hexOutput.WriteLine(newrec.ToHexRecordString());
}

User-friendly patching

Well, since I want to make the patch so other players would be able to benefit from the fixes, I should make it user-friendly. As I’ve shown in the previous article, it’s trivial to call C# from PowerShell, so simple script like this:

param (
[string]$firmware = "",
[int]$dvid = 0xC4F4,
[int]$dpid = 28471,
[int]$type = 3,
[int]$index = 0
)

Add-Type -Path "C:\Program Files (x86)\KAT Gateway\KAT_WalkC2_Dx.dll"

if ($firmware -eq "") {
$firmware = $katPath + "C:\Program Files (x86)\KAT Gateway\katvr_foot.hex"
}
Write-Host "Want to flash $firmware"

if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_find() -eq 0) {
throw "ch9326_find failed"
}
if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_open($dvid, $dpid) -eq 0) {
throw "ch9326_open failed"
}
if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_set_gpio($index, 15, 15) -eq 0) {
throw "ch9326_set_gpio failed"
}
if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_connected($index) -eq 0) {
throw "ch9326_connected failed"
}
if ([KAT_WalkC2_Dx.KatvrFirmwareHelper]::flash($firmware, $type, 0) -ne 1) {
throw "KatvrFirmwareHelper.flash failed"
}
[KAT_WalkC2_Dx.KatvrFirmwareHelper]::ch9326_ClearThreadData()
[KAT_WalkC2_Dx.KatvrFirmwareHelper]::close_ch9326()

Good enough, we can upload a new hex or revert to the original hex. The only oddity is sensor loses configuration if it’s left or right, which means it is worth adding a fixer.

By borrowing parts from the installation scripts I’ve written before, with a slight expansion of adding sensors’ configuration detection and upload the problem was fixed. In the end, the ReadDeviceId and WriteDeviceId are already there:

...
$id = -1
[IBizLibrary.KATSDKInterfaceHelper]::ReadDeviceId($dev.serialNumber, [ref]$id)
$sensor = New-Object IBizLibrary.KATSDKInterfaceHelper+sensorInformation
[IBizLibrary.KATSDKInterfaceHelper]::GetSensorInformation([ref]$sensor, $dev.serialNumber)
$leftmac = [IBizLibrary.KATSDKInterfaceHelper]::receiverPairingInfoSave.ReceiverPairingByte[7..12]
$rightmac = [IBizLibrary.KATSDKInterfaceHelper]::receiverPairingInfoSave.ReceiverPairingByte[13..19]
if(-not(Compare-Object $leftmac $sensor.mac)) {
[IBizLibrary.KATSDKInterfaceHelper]::WriteDeviceId($dev.serialNumber, 2)
Write-Host "Made the sensor to be Left Foot"
}
elseif(-not(Compare-Object $rightmac $sensor.mac)) {
[IBizLibrary.KATSDKInterfaceHelper]::WriteDeviceId($dev.serialNumber, 3)
Write-Host "Made the sensor to be Right Foot"
}
else {
throw "The sensor's mac is not paired to the treadmill"
}

We can also throw two cmd scripts:

:: restore-foot.cmd
powershell.exe -ExecutionPolicy Bypass -File flush-feet-sensor.ps1

:: update-foot.cmd
powershell.exe -ExecutionPolicy Bypass -File flush-feet-sensor.ps1 --firmware .\my-foot.hex

So the user will only need to double-click the `update-foot.cmd` or `restore-foot.cmd` to get things done. Neat.

Let’s patch the patch to patch a patch with the patch.

Since I don’t own the firmware, I don’t want to redistribute it. On the other hand, every potential user already bought the treadmill and has the gateway installed — so has a copy of the firmware already. That means I can just use the original firmware — all I need is to actually apply a patch before flashing the firmware. Basically, do what I did in the first place — but I don’t want to distribute binary flash uploader nor too complex PowerShell script (what would be the case if I reimplement the hex patch logic directly). That means — I need to make a simple patcher.

Basically, I need to write a patcher that can apply a patch. You’ve got the idea, right?

We need to go deeper

Let’s see: I have two HEX files: an original one and the ready-to-be-flashed one. What if I get a direct textual diff between them:

> fc.exe /l /n 'C:\Program Files (x86)\KAT Gateway\katvr_foot.hex' patch.hex
Comparing files C:\PROGRAM FILES (X86)\KAT GATEWAY\katvr_foot.hex and PATCH.HEX
***** C:\PROGRAM FILES (X86)\KAT GATEWAY\katvr_foot.hex
2463: :2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2
2464: :2029E8005456520512089F000900020A039F1902AF9607B039043D4FF703636505771064CC
2465: :202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2
***** PATCH.HEX
2463: :2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2
2464: :2029E800542D460512089F000900020A039F1902AF9607B039043D4FF70363650577106401
2465: :202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2
*****

But how that will help? While Linux often has diff and patch, Windows only has diff (as fc.exe) but not a patch counterpart. Well… The format is trivial, there is no need to do fuzzy logic, it’s the other way round: it should be a 1-to-1 exact application to make sure it works, so it can be trivial: read the patch, compare line-by-line with the expected lines, and write output lines for patched ones.

Or even simpler: we can turn this patch into a script that will do that. I don’t know which one is simpler, but I’ve chosen the second path, so all the work can be done with simple pipes without the need to deal with keeping two files open. The converter can be described as a simple state machine:

  • Initial state, “Comparing files” line => print output script header, enter the “wait” state
  • Wait state, “*****” line => enter “source” state
  • Source state, “*****” line => enter “dest” state
  • Source state, any other line => print comparison for line number and line content
  • Dest state, “*****” line => enter “wait” state
  • Dest state, any other line => print the output line

Looks trivial.

The only bits worth mentioning here: by default pipe redirect into a file works not in input encoding nor in utf8 but in utf-16. So one has to forward output into `Out-File -Encoding Ascii`. Not too bothersome.

Now let’s try it out to see how it works:

> fc.exe /l /n 'C:\Program Files (x86)\KAT Gateway\katvr_foot.hex' patch.hex | .\fc-text-to-patcher.ps1 | Out-File -Filepath foot-patch.ps1 -Encoding Ascii
> cat .\foot-patch.ps1
$in_line = 0
$Input | ForEach-Object {
$in_line++
$skip = 0
if ($in_line -eq 2463) {
if ($_ -ne ':2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2') { throw 'File content mismatch'; }
$skip = 1
}
if ($in_line -eq 2464) {
if ($_ -ne ':2029E8005456520512089F000900020A039F1902AF9607B039043D4FF703636505771064CC') { throw 'File content mismatch'; }
$skip = 1
}
if ($in_line -eq 2465) {
if ($_ -ne ':202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2') { throw 'File content mismatch'; }
$skip = 1
}
if ($in_line -eq 2465) {
Write-Output ':2029C80000010203AEC2E6ED0001C3AA9D00F802CF01060302040F02013F050609FF4B41D2'
}
if ($in_line -eq 2465) {
Write-Output ':2029E800542D460512089F000900020A039F1902AF9607B039043D4FF70363650577106401'
}
if ($in_line -eq 2465) {
Write-Output ':202A0800115ED1F9E86567676215F40000E979FA175FBD027BA63075FFFF79BF19070080C2'
}
if ($skip -eq 0) {
Write-Output $_
}
}

Let’s prove the result is indeed what we want:

> cat 'C:\Program Files (x86)\KAT Gateway\katvr_foot.hex' | .\foot-patch.ps1 | Out-File -Filepath out.hex -Encoding ascii
y> fc.exe .\out.hex .\patch.hex
Comparing files .\out.hex and .\PATCH.HEX
FC: no differences encountered

Final touch

So, let’s update the script by adding a call to settings restore, and add support to apply a patch. Since the script’s directory become a mess…

Time for refactoring!

  • Keep proper names for cmd scripts — for users’ convenience, so they are first in the folder.
  • Both major scripts (flasher and settings restoration) move to the end, renaming them into `y-$script.ps1`.
  • Patches will follow naming `z_patch_$sensor.ps1`.

One more side note: to call a script by its name stored in a variable one should use an ampersand. I mean, that’s what that part looks like:

$newfw = $ENV:TEMP + "\katvr_" + $orig + "_patch.hex"
$patchscript = ".\z_patch_" + $patch + ".ps1"
Get-Content $firmware | & $patchscript | Out-File -FilePath $newfw -Encoding ascii

That one is tidy enough to be called a release-ready version.

In the next episode

The saga continues! We’ve built the main base, and are ready to send our investigation troops. Next time we’ll dive into the world of analysis of raw binaries, doing proper changes like logic expansion instead of in-place raw constant change. Stay tuned!

Links

--

--

Anton Fedorov

Multitool: Sr. SWE-SRE. I have tendency to cause all sort of problems, so learned how to solve them.