Jailbreaking the Microsoft fitness band
This semester I got a Microsoft fitness band for a project. The original goal was not hard: understand the client communication. So I decided to pwn it for fun. Thanks to my friends in OSIRIS lab who supported me a lot, thank you mates. And also my mentor, mongo, inspired me so much and taught me a lot, thanks man!
So first, we need to figure out some of the basic behaviours of the fitness band, like how it upgrades firmware, uploads user status etc. Luckily the client is just a windows binary, so we don’t need to set up environment to reverse the Android app or IOS app. The Windows client is available here. Since the client was written in c#, we can use tools like ILSpy, JustDecompile, dnSpy and dotPeek to debug & decompile it easily!
After reversing the client a bit, I found that the client downloads FirmwareUpdate.bin from the cloud and stores it into folder:
1 |
c:/Users/IEUser/AppData/Local/Microsoft/CargoDevice/u_9e35ffc6-2859-4332-b89c-9110db164d9c/d_ffff1300ffffffff1830454e06000e30/FirmwareUpdate |
Now that I know where the firmware update will be, my next step was to try and get the binary itself. My device was already fully updated, so I needed to figure out a way to get it. Unfortunately, since our fitness band’s firmware is updated, I couldn’t download new firmware update anymore! Maybe I could cheat the sever?

I used Burp to modify the request’s version number to an older one and got it to download FirmwareUpdate.bin successfully, which allowed me to continue on to the more interesting stuff. First I needed to analyze the FirmwareUpdate.bin a bit.
1 2 3 4 5 6 7 8 9 |
b0n0n@DM ~/MS_band $ binwalk FirmwareUpdate.bin DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 29668 0x73E4 LZMA compressed data, properties: 0xA2, dictionary size: 2097152 bytes, uncompressed size: 1966105 bytes 30069 0x7575 CRC32 polynomial table, little endian 244521 0x3BB29 SHA256 hash constants, little endian 853728 0xD06E0 LZMA compressed data, properties: 0xA2, dictionary size: 2097152 bytes, uncompressed size: 1966105 bytes 939669 0xE5695 CRC32 polynomial table, little endian |
After searching online for formats similar to what binwalk shows, I found nothing interesting, so I decided to reverse the FirmwareUpdate.bin format and make guesses on what the different sections of the binary were for. I noticed that the FirmwareUpdate.bin size, 0x16D67E bytes, was stored in the header at offset 0x13:
1 2 3 4 5 6 7 8 9 10 11 |
00000000 5F C3 01 09 00 00 00 00 00 00 00 00 00 00 00 21 00 00 00 7E D6 16 00 C3 23 1E 43 00 C0 65 01 00 00 00 00 01 00 01 C0 65 01 01 00 00 _..............!...~....#.C..e.........e.... 0000002C B0 0E 00 02 C0 65 B1 0F 00 68 03 00 00 42 00 CD B4 0F 00 02 73 01 00 2B 00 CF 27 11 00 30 10 00 00 2C 00 FF 37 11 00 30 0C 00 00 2D .....e...h...B......s..+..'..0...,..7..0...- 00000058 00 2F 44 11 00 30 04 00 00 2E 00 5F 48 11 00 30 10 00 00 2F 00 8F 58 11 00 30 04 00 00 30 00 BF 5C 11 00 30 08 00 00 31 00 EF 64 11 ./D..0....._H..0.../..X..0...0..\..0...1..d. 00000084 00 30 04 00 00 32 00 1F 69 11 00 30 24 00 00 33 00 4F 8D 11 00 30 04 00 00 34 00 7F 91 11 00 30 04 00 00 6C 00 AF 95 11 00 30 04 00 .0...2..i..0$..3.O...0...4.....0...l.....0.. 000000B0 00 2A 00 DF 99 11 00 6A 06 00 00 29 00 49 A0 11 00 00 03 00 00 46 00 49 A3 11 00 40 0A 00 00 36 00 89 AD 11 00 80 28 00 00 3F 00 09 .*.....j...).I.......F.I...@...6......(..?.. 000000DC D6 11 00 40 EF 00 00 41 00 49 C5 12 00 97 B6 00 00 28 00 E0 7B 13 00 D8 15 00 00 49 00 B8 91 13 00 4E 2C 00 00 4A 00 06 BE 13 00 40 ...@...A.I.......(..{......I.....N,..J.....@ 00000108 2C 00 00 4E 00 46 EA 13 00 2C 30 00 00 4C 00 72 1A 14 00 10 2F 00 00 4F 00 82 49 14 00 E2 2D 00 00 50 00 64 77 14 00 E6 2D 00 00 4B ,..N.F...,0..L.r..../..O..I...-..P.dw...-..K 00000134 00 4A A5 14 00 06 2F 00 00 4D 00 50 D4 14 00 BA 2C 00 00 24 00 0A 01 15 00 0D 66 00 00 25 00 17 67 15 00 FB 95 00 00 6D 00 12 FD 15 .J..../..M.P....,..$......f..%..g......m.... 00000160 00 6C D9 00 00 9F 84 42 00 03 00 0A 00 E8 0C 00 00 00 30 00 00 00 00 01 00 15 05 FF 1F 64 07 C4 91 00 02 FF 1F FF FF FF FF FF FF FF .l.....B..........0..........d.............. 0000018C FF 00 00 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............................................ 000001B8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ............................................ |
Since binwalk said there was a CRC32 polynomial table, I decided to look for the 4 byte CRC checksum that would be included in the header for integrity checking. You can see in the header that there are four “random” bytes of data at 0x17, 0x431E23C3. After replacing those 4 bytes with ‘\0’ and calculating the CRC for the rest of the data, we got the exactly same value:
1 2 |
Found at offset dec=23 hex=00000017 CRC=431E23C3 |
So it seems that the binary might be using the CRC checksum to check the FirmwareUpdate.bin’s integrity! My next step was to try pushing modified firmware to the fitness band. I modified version number stored in FirmwareUpdate.bin as a test, to see if the fitness band would accept an arbitrarily modify FirmwareUpdate.bin. The version number was an easy way to see if the fitness band accepted the modified update, as you could easily check it on the band itself.
1 2 |
0001EC6C 00 4D 69 63 72 6F 73 6F 66 74 20 43 6F 72 70 6F 72 61 74 69 6F 6E 00 00 00 31 30 2E 33 2E 33 33 30 34 2E 30 00 25 75 00 00 30 37 32 .Microsoft Corporation...10.3.3304.0.%u..072 0001EC98 37 00 00 00 00 20 4C 45 00 9F 38 02 00 80 C3 C9 01 0B 2A 02 00 39 39 02 00 FF F7 E8 FE 06 20 28 70 60 68 C5 F8 05 00 01 E0 09 20 28 7.... LE..8.......*..99....... (p`h....... ( |
Unfortunately I realized that any time you start the updating process, the client would check to ensure that the firmware update has not been corrupted/tampered with. To bypass this, I used Dnspy to set break points and modify the binary right before the client sent it to the fitness band.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
private bool PushFirmwareUpdateToDeviceInternal(FirmwareUpdateInfo updateInfo, CancellationToken cancellationToken, KdkFWUpdateProgress progressTracker) { bool result = false; this.CheckIfUpdateValidForDevice(updateInfo.FirmwareVersion); Logger.Log(LogLevel.Info, "Verified that the firmware update is valid for the device", new object[0]); cancellationToken.ThrowIfCancellationRequested(); string text = Path.Combine("FirmwareUpdate", "FirmwareUpdate.bin"); if (this.storageProvider.FileExists(text)) { int.Parse(updateInfo.SizeInBytes); this.UploadDeviceFirmware(text, progressTracker); } string value = this.FirmwareVersions.ApplicationVersion.ToString(); if (updateInfo.FirmwareVersion.Equals(value)) { result = true; Logger.Log(LogLevel.Info, "Verified that the firmware update is successfully installed on the device", new object[0]); } return result; } |
C# function that sends the firmware update to the fitness band
However after finishing the process, I didn’t see the firmware installing screen on the fitness band’s screen, which meant it failed. It seemed like they had other mechanisms to check the firmware update’s integrity. This meant that I had to go back to reversing the firmware update, which seemed to have some code in the chunks that binwalk identified as LZMA data. The first issue was that I didn’t know the loading address of the file. After reading up a bit about Reversing Embedded ARM of Cortex M Series, I luckily found a table that contains the correct loading address of 0x05c8bf!
1 2 3 4 5 |
000d2230 00 bf c8 05 00 11 c9 05 00 4f c9 05 00 8d c9 05 |.........O......| 000d2240 00 93 c9 05 00 99 c9 05 00 f1 c9 05 00 51 ca 05 |.............Q..| 000d2250 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 000d2260 00 01 00 00 00 14 00 00 00 00 00 00 00 02 00 00 |................| 000d2270 00 04 00 00 00 00 00 00 00 03 00 00 00 06 00 00 |................| |
Finally I was able to drop the binary into IDA. By searching for uses of immediate value 0x17(offset of CRC position), I found the main CRC checking function, and by getting cross references to it I found the IntegrityCheck function, as well as other checking functions. The whole Integrity checking procedure is:
1.check main CRC

2.check section CRC

3.check if update’s version number bigger than original one

So all I had to do was patch the version number to be bigger than the one currently on the fitness band, patch the firmware update with whatever else I wanted, and recalculate the section CRCs, which I found the locations of based on the reversed code, and the main CRC.
For convenience, I wrote a parser for some of the binary format of the bin,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Offset: 0x00000000 Magic number: 0xC35F Offset: 0x00000002 Unkown byte: 0x1 Offset: 0x00000003 Unkown flag: 0x00000009 (looks like GPIO port output speed) Offset: 0x00000007 Zero padding Offset: 0x0000000F Total number of sections: 0x00000021 Offset: 0x00000013 Firmware size: 0x0016D67E Offset: 0x00000017 CRC of whole firmware: 0x431E23C3 Section table: Offset 0x0000001B section type 0xC000: Section offset 0x00000165, size 0x00010000 [ends at 0x00010165] Offset: 0x00000004 Version number: 0x000A0003.0x00000CE8 (shown as 10.3.3304) Offset: 0x00000010 Section size: 0x00010000 Offset: 0x00000018 Section CRC: 0x91C40764 Offset 0x00000025 section type 0xC001: Section offset 0x00010165, size 0x000EB000 [ends at 0x000FB165] Offset: 0x00000004 Version number: 0x000A0003.0x00000CE8 (shown as 10.3.3304) Offset: 0x00000010 Section size: 0x000EB000 Offset: 0x00000018 Section CRC: 0xE46EFF7D ... |
Next I wrote a simple patcher to patch the version number first, and then search for & patch the string I want to modify.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
def Patch(filename): f = open(filename, 'r+') Patched = f.read() Patched = VersionNumPatch(Patched, "10.6.3304") text1 = TextPad("No new texts, check back in a few.") #patch the empty text string text2 = TextPad("pwned by ------------- mongo&b0n0n") Patched = SearchPatch(Patched, text1, text2) Patched = CalSectCrc(Patched) # recalculate the section CRCs Patched = CalMainCrc(Patched) # recalculate the main CRC with open('FirmwareUpdate.Patch', 'w+') as f: f.write(Patched) Patch("FirmwareUpdate.bin") |
You can find the scripts on my github.
After patching the update, I used the same method as above to push the patched update to the device,

After the device receives the file, it checks its integrity, and if the new update passes all the checks, you will see the installing sign like this:

And then the fitness band will reboot and you can check the version number and anything else that was patched.


With this you can then modify any of the code and data in the update to whatever you like!
I have not bought a Microsoft fitness band 2 to test if the jailbreak will work on it yet. For people who are interested in looking into this further, there is an interesting function in the fitness band windows client, but you need to unblock it first!
1 |
public byte[] EFlashRead(uint address, uint numBytesToRead) |
If you see any mistakes in my article, please let me know, thanks!