let's play with the firmware

For those who have just connected, I’m talking about the platform for VR games, how to integrate with it and how to get to its sensors directly.

I have already noticed some glitches and problems with the sensors – which means it would be nice to fix them. But in order to correct them, you need to understand what kind of beast they are, and whether they can be changed somehow. Due to the huge size of the article, we had to divide the already small abstract into even smaller pieces – so today we will just look inside the sensor and learn how to change its firmware in a simple and convenient way.

An autopsy will show

Studying something is usually a chaotic process with approaches from different sides, the so-called “scientific poking method.” You can never tell in advance where the ideal path is until you look at the outcome of the process in retrospect. Therefore, you should not take this opus as a guide to action, but rather as a small overview of what you can look at and where to look for bits of usefulness. As a rule, all the many paths help each other and ultimately lead to understanding.

Let's open it...

Let's open it…

But what is definitely good almost always (when possible) is looking inside. If we look inside the foot sensor, we will immediately notice a large module labeled HY-40R201C. This is a BLE5.0 module based on TI CC2640R2. That is, it is a processor (more precisely, two) plus a radio module. It can be used either by itself, loading your own firmware into it, or in tandem with an external processor.

Let's turn it over on our tummy...

Let's turn it over on our tummy…

There are no other processors visible on the board; on the reverse side there is nothing except an optical mouse module A9800 not detected.

Conclusion: the module is used directly as a logical processor. There are two battery ports on the board, but only one is connected. There are Reset and Boot buttons. Now we can always open the datasheet to understand where what ports and hardware addresses are, but we know that this is an ARM Cortex M3 with 128kb ROM, 8kb frame. Not very much, but clearly enough.

Unlike the Nordic module I used, there is no direct USB support. But it is visible on the board CH9326which has the same name as the DLL located in the gateway folder.

Thus, we either found out or confirmed that the sensors communicate via a USB-HID converter chip, run on ARM, and are based on Texas Instruments SIMPLELINK-CC2640R2-SDK. Still comes with SDK “Academy”, in which in a concise form you can read about BLE, how to prepare it inside the SDK, and a few examples are considered. In any case, since there is no devkit, you won’t be able to play around with the sensor itself; otherwise, you’ll have to use full-text search inside the installed SDK.

What's in my ROM for you?

External inspection is good, but you can’t understand what’s inside just by looking. On the board there are spots marked “VRCMG”, where G is clearly Ground, everything else is a question. You can call, of course, but… I still don’t have JTAG.

Here, fortunately, we can remember that Gateway can flash sensors. When I connected them for the first time, he told me that the firmware was old and I needed to upload a new one. And poured it in. Conclusion – Gateway firmware is available. Review through the eyes of the files in the gateway folder:

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

Cool, where's C2?!

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

Hm. Loading dotPeek again, Ctrl+Alt+T, “.hex”… Ha!

search by dothex

search by dothex

    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(...)
    }

Great, that is, to flash the firmware we will need a couple of calls from KatvrFirmwareHelper and hex firmware. But as a rule, you need to load BIN into disasm, not HEX. However, this can be solved in many ways. I just ran 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

Please note that all files are exactly 128kB, with hexes of different sizes, which means the firmware is made up of several sections, and, probably, parts are missing or used for settings, or something else. Let's just keep this in mind for the future.

By the way, if we haven’t opened the sensor yet and don’t know what kind of processor it is, you can try to poke into the binary via binwalk:

$ binwalk --disasm ./katvr_direction.bin 

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

or cpu_rec:

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

Where we make sure that yes, it is ARM and mostly in Thumb mode.

Poking additionally through 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}
Заметки на полях

Поиск полезных данных никогда не бывает лишним. Например, вот этот вывод strings, в котором есть странные строки — “inputGyroRv” и “inputNormal”. Поиск по ним на github дал сходу интересную вещь, что позволило разметить часть функций и структур, которыми пользуется сенсор направления. Для ног однако подобной фкусности не обнаружилось.

Заливка прошивки на ходу

Разглядывание документации на Serial Boot Loader показывает, что бутлоадер просто готов принимать прошивку если в него перезагрузиться, 0x55 синхронизует скорость, в общем, всё как всегда. Мы, впрочем, не можем пользоваться родным прошивальщиком, так как у нас нет последовательного интерфейса — у нас есть HID2Serial, так что прошивка должна идти через него. Но, как мы уже выше выяснили, функции прошивки экспортированы наружу, так что можно просто попробовать вызвать их.

Создадим простой C# проект, куда импортируем KatvrFirmwareHelper и katvr_firmware.dll на которую он ссылается:

    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();  }

Let's run it – there are some debugging printouts in the console, an error. Um… Oh, well, yes, we hold down the Flash button, press Reset, launch it again – dots run across the screen. After about a minute and a half, it's ready. The sensor is still alive. True, the left light blinks instead of the right, as it blinked before. Oops. The settings have been erased.

Patching the firmware

But I would like comfortable patching. Hydra has the ability to export the current file as Hex or Raw, but everything is exported, including frames and areas that were not in the original hex. Conclusion – you need to patch the HEX file directly, but how?

To begin with, you need to train on something. The easiest way is to make a simple binary patch – change the “KATVR” line to “KAT-F”, that is, the device will advertise itself as “KAT-F” (type Foot). Open with any hex editor, for example, WinHex, and we edit. Then we take the diff:

> 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

Great, we have a patch that is easy to read as is or convert from C#:

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

One of the tasty tricks that I managed to find for C# (I very rarely touch it) is casting a tuple to a structure, which allows you to reduce multi-books in static arrays. It just doesn’t automatically parse the structure, but we can add a constructor and an implicit operator that will cause the cast:

    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 that we have the patch in a convenient machine-readable form, we need to apply it to HEX.

Let's take HexIO library and add a fixer. You don’t need to know much: we monitor the current address, if the read line includes the patch address, we correct it. Unfortunately, HexIO does not monitor changes in the structure and does not update the checksum – we had to get out of it by re-creating the record. Not very pretty, but fast and 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 apply the patch and use the patched hex instead of the original:

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

We flash it, look at what is visible in the Bluetooth environment: ha! there is a “KAT-F” device.

Side notes:

When I later needed to add some code to the firmware, I had to tinker with inserting new lines. To do this, instead of the error in Else, instead of the error about Gap, I added a piece that directly constructs new records and displays them immediately:

    } 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

Since I want to make patches that people can use, I need to format the firmware as a script.

In principle, as already mentioned, porting C# to PowerShell is trivial:

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()

This is already quite good, it allows you to flash any patch or roll back the firmware. But after flashing the firmware, the sensor loses its settings (left-right), which means that the pairing mode needs to be restored.

Borrowing the pairing definition code from previous scripts, essentially you just need to add searching for a sensor and sending a command

Moreover, both ReadDeviceId and WriteDeviceId are already ready:

...
$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"
}

If you add cmd batch files:

:: 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

Then the user will only need to run the script.

We patch patches to patch patches

Strictly speaking, we do not own the original firmware, and therefore distributing it is somehow not very good. On the other hand, all users already have a gateway, that is, we can simply use the original firmware – we just need to apply a patch on top of it. I would like to avoid binaries or an overly complex script (it would be possible to transfer all the logic of the HEX patch from C# to PowerShell). Conclusion – must be done simple patching.

In other words, to patch a patch, you need to apply a patch. Well, you understand, right?

We need to go deeper

We need to go deeper

We now have two HEXs: the original one, and the one obtained after applying a binary patch to it. You can take a text 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 to apply it? Well, that is, there is diff (fc.exe) but there is no patch! Well, since the patch format is simple and in our case no intelligent overlay is required (it needs to be applied 1-in-1, this will also confirm that the source firmware is correct), then we can do it simply: read the file, check for each line in the source equality, for each output line we simply print it.

Or you can actually turn this diff into a script that will directly patch. I don’t know which is easier, to be honest, but I took the second path (to be honest – so as not to bother with reading two files, but simply work as a pipe handler). The converter is made by a simple state machine:

  • “Comparing files” line => display the title, go to wait for the patch

  • “*****” waiting for the patch => go to waiting for the lines of the original

  • “*****” while waiting for the original lines => go to waiting for the patched lines

  • another line waiting for the original lines => print if on the line number and its contents

  • “*****” waiting for patched lines => go to waiting for a patch

  • waiting for patched lines => print it

It seems simple.

One of the important things that I had to become familiar with: by default, the redirect does not work in the encoding that was at the input and not in utf8, but in utf-16. So you have to send a redirect to Out-File -Encoding Ascii. But this doesn't bother me much.

So, now you can feed him the patch and see what happens:

> 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 $_
  }
}

Great! Let's check:

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

Finishing touches

Let's fix the script, add a restore call and code for applying a patch. Let's realize that the scripts folder is a mess, so… Refactoring time! We leave normal file names for .cmd – for users, so that they are at the beginning.

Let's move both working scripts (firmware and restore settings) to the end, calling them y-$script.ps1. And we'll put the patches in z_patch_$sensor.ps1.

Another note in the margins:

Call the script indirectly through the ampersand variable. Well, that is, applying the patch goes through:

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

The resulting pulp can already be considered the finale.

In the next episode

Now that we have prepared the material base, we can get down to the most interesting part – parsing the scabs and editing them for real, and not just directly editing the constants. Don't switch!

Links

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *