Studying the encryption on Chitu's 3D Print controllers
Now with decrypt and encrypt functionality.
x-python-script - 3.77 kB - 09/22/2019 at 18:29
This is the bootloader which decrypts the update files.
macbinary - 34.00 kB - 09/21/2019 at 04:24
Since my last update, I successfully added support for encrypting data such that round-trip produces identical files. Once I did this, I proceeded to make a minimal build of Marlin, figuring I could jump straight into working.
No dice. I can get the existing bootloader to flash data on. It turns out that it's storing data in the I2C eeprom regarding what's flashed, and won't re-flash the exact same firmware. You have to swap revisions to flash from one to the next, and I think the file key is what is used to figure out if it's the same thing. Since I use a known good file key right now, that doesn't work.
The resulting Marlin build flashes but never starts, and that lead me to notice a few details I hadn't seen before.
1. The Marlin build using STM32DUINO produces an NVIC with no reserved entries. I do not know why.
2. A super short firmware does NOT flash correctly. I don't know how many blocks are required, but an arduino sketch compiled for a generic F103 (which according to ST is bit-compatible) will not flash - it hits the error handler almost immediately.
3. There's a debug flag stored in ram at 0x2000000. If it's true, the bootloader traces operations. I have no way to enable it. I know the bootloader goes looking for a file that ends in .GCX but I don't know what it actually does.
4. Timer8 appears to be used to drive the speaker/buzzer.
The ST Arduino core doesn't have a variant for 103Z, so I have nothing to compare it to, yet, but I do see there's a feature request for a 103Z variant that's waiting.
So...right now, I need to do some round-trip tests I didn't think of before. I'll decrypt a known good firmware and re-encrypt it with a different file key. If this flashes on and boots up correctly, my encryption script is doing the right thing.
Now...if that's the case, what do we think is happening?
Well, with the M200 and Lerdge, I learned that the bootloader often interferes with the process of overwriting *everything* and re-initializing. I'll have to test a custom version which disables all interrupts (not needed in any normal boot up) and re-initializes, then turns on something like the onboard LED.
After quite a bit of creating and re-creating python functions to XOR the data together, I finally switched disassemblers and got a mildly different output - different enough that it showed me r12 being used in a way that I hadn't grasped before. With some work, I was able to decrypt Chitu/Tronxy firmware of several flavors, at least enough that Binary Ninja shows me complete functions, literal pools line up, and so on.
I'll add the script to the files section of this project. Next up will be to encrypt the same way and do a few end-to-end conversions and diff the resulting files. When those come back identical, it's time to try some custom firmware, probably starting with the classic "Make an LED blink."
Tonight, I'm just happy to be able to decrypt the firmware and confirm I understand how the bootloader works with it.
With some work, I've figured out how the Firmware CRC works.
The first 3 DWORDS of the file are, in order:
A signature - must be
The XOR CRC of all DWORDS after the header
An encryption seed
To calcuate the CRC, begin with a seed of 0xef3d4323. XOR each subsequent byte with this.
In production, it's highly likely that the way this works is that they write the header first, calculate the XOR value of the firmware, and the final XOR is with the seed, resulting in what's written to the header.
Now, moving on to the XOR encryption.
Here's the code that does it, taken from Binary Ninja
Here's the call:
The operation pretty much only operates in blocks of 0x800.
The first two parameters appear to be the data from the encrypted file and a working buffer which is where the data is written to.
The third parameter is the block counter, which goes up as blocks are decrypted.
The file size is fixed, and I believe the last parameter is the encryption seed.
The problem is, my python implementation of this never seems to actually use different values, so I'm missing something obvious.
So, I may have noted that the Chitu board exposes Boot0. It's also a much later variant of the STM32F103, with FSMC support. I posted in one of the Tronxy facebook groups looking for dead boards to desolder so I could trace down SWD and see if there was an easier way to get at the flash.
It turns out, there's no need to do this. As someone pointed out, all you have to do is pull the BOOT jumper, restart the board, and it comes up in DFU mode.
And read protection is NOT set.
Suffice it to say I extracted the Chitu bootloader. The encryption is complex (it is some sort of rolling encryption), but the firmware loads at 0x800880 (at least, that's where the bootloader tries to start it). There's a couple of things about this:
1. I've heard there's no firmware image from the Tronxy. There could be relatively easily.
2. We can get Marlin up and running just using DFU. I would still like to reverse engineer the bootloader to allow people to update without using the STM32CUBE programmer, but in essence, any DFU update will work.
So...I sort of spent a lot of time looking at hex dumps for nothing.
I mean, looking at hex dumps is always amusing, but I could have taken the fast train to getting the bootloader to disassemble. Sigh.
This particular log will be heavy on hex dumps, low on actual progress, but again, the goal here is to record what I'm seeing, how I'm approaching it, and so on. I've been unsuccessful in my attempts to find a dead Chitu/Tronxy board to desolder. I want to see if there's anywhere I can attach SWD/SWIO, because attacking the initializers would be easier.
So I went back to looking, specifically, at the Tronxy XY-3 firmware.
As noted in previous logs, the first four bytes seems to be a signature, and the fifth byte is what I call the XOR Key. It's what zero will be XOR'd with. My theory is that it's some form of rolling encyrption that incrments a certain distance and then XORs the rest with the XOR key (why should become obvious in a bit).
I wrote a quick python script to xor firmware with the fifth byte and went scrounging for strings, and I struck gold, fragments of text matching PID Autotune references.
Now, remember that ARM architecture, there's literal pools scattered before and after functions (and sometimes in the middle), so when we see a bunch of strings that reference PID tuning together, we smile - it's likely this is the literal pool for the autotune function.
There are odd spaces in the strings...at least until I compare to the source for Repetier, around 2015.
Then, suddely, the text begins to look like this:
D=-?”²ÚÌ)PID Autotune failed! Temperature too highE. 12ã?…ëQÿÂßëªá\PID Autotune failed! timeout„ W?÷[¦tPID Autotune finished ! Þp;Ÿ+4
So...odds are that Chitu's firmware is related to Repetier the same way the M200 firmware is 'related' to Marlin.
But by filling in the expected bytes, I was able to make cribs I could XOR with the source bytes to produce keys.
The following keys have zero for where I don't know the byte values.
PID Autotune finished ! Place the Kp, Ki and Kd constants in the Configuration.h or EEPROM 50494420 4175746F 74756E65 2066696E 69736865 64202120 506C6163 65207468 65204B70 2C204B69 20616E64 204B6420 636F6E73 74616E74 7320696E 20746865 20436F6E 66696775 72617469 6F6E2E68 206F7220 45455052 4F4D Encrypted Bytes: 7A9F95F6 16E181AA A9A4B9B1 F5B3BCBB BCA6BA14 534EF44B 0DB8ECC8 92DEF1C7 A695D7D4 759075C9 D5F50F09 DF214489 CD74A6EF 83D8F2C6 7F91FFCD B1C993A1 D6F92D77 F3040177 8A094965 88C3FA86 D848D992 C2934D93 D5F0 Key: 2AD6D1D6 5794F5C5 DDD1D7D4 D5D5D5D5 D5D5D271 376ED56B 5DD48DAB F7FE85AF C3B59CA4 59B03EA0 F594616D FF6A20A9 AE1BC89C F7B99CB2 0CB196A3 91BDFBC4 F6BA4219 956D6602 F8683D0C E7ADD4EE F827ABB2 87D61DC1 9ABD
By dialing back to the shared bytes, I can see there's a commonality in the way the bytes are XORd. :
00000000 00000000 00000000 94949494 9494D338 4EA42D79 6B979097 16D5B484 9C909695 00000000 00000000 00000000 94949494 94949330 762E9507 46F9AD89 1B 00000000 0ADBB387 9E919695 94949494 949493C0 C6EAFDDE 3BC925CD 39C3BF81 2D7660EF 2ACBBB8C 98929795 94949494 94949390 D6176428
And if we go back to what we think is the NIVIC, we see:
BB31949C 93093DC7 A58E9A93 90969594 94949494 94949494
So what do we think of this?
Well, it's clear that the once we begin a run of the XOR KEY, we continue it for a while. In the lines above we see taht there's actually some sort of cycle occuring - probably a rolling XOR encryption or one that takes the location in as a parameter of its execution.
If I can get to the point where I can attack the constant initializers, I can insert all zeros, exposing the XOR values and use all FFs to make sure bits aren't carried over. It could be that smarter people will simply see the pattern in what's happening here.
And in the mean time, I'll keep digging on strings and known values.
I'd looked at the Chitu series of print controllers long before, back when I was first working on getting Marlin running on the Malyan M200, and concluded a couple things:
1. They were overpriced.
2. I didn't have one working project, so I sure as heck didn't need a second.
Recently someone contacted me on facebook and offered me the mainboard from a Tronxy X5SA, which is closely related to the Chitu Mini F, an STM32F103 board with an TFT LCD. The board has a blown driver, so, after warning that this whole project might dead end with no results, OR by me killing the board, I said sure.
And I began hacking.
If, when I did the M200, it was in the dark, each discovery new, and the lerdge, an evolution, I came to the table this time with a fair set of knowledge. Warning - as before, these logs will probably start out heavy on hex dumps.
Speaking of which...let's look at the firmware updates from Chitu
443D2D3F 72F7BA97 2BA26ECA 1EA57353 36FB727A 8BA9727A 97A1727A BBA9727A D3D2727A 0F45DA21 43687C75 76707372 72727272 72727272 C774737A 4F64033F DB255F65 D5FD717B E775737A A754737A 2DFB727A 6D932BC5 44422D4A 34F77479 2CFB727A 2DFB727A 2DFB727A A3B413B3 5CC5D028 07EE787F'mmu init'
At this point, I can spot an NVIC in my sleep. More importantly, I know to look for the block of reserved addresses
Let's compare to a nice dump from an STM32F103 firmware (the Malyan M200...shock!)
38300020 B9610108 DD5E0108 F15E0108 195F0108 1B5F0108 1D5F0108 00000000 00000000 00000000 00000000 1F5F0108 215F0108 00000000 235F0108 CBE90008 41630108 41630108 41630108 41630108
See those zeros? I don't have a full corresponding block of...anything that could be that.
But I do have a couple of suspicious DWORDS. Those 0x72s follow a common rule - if you think something is XOR'ed or substituted, the one that there's the most of is probably zero. Or an XOR key.
So I ran the code through a quick XOR script.
Out the other side, buried in fragments of garbage:
That's a very specific string, and mmu initialization is something you do when dealing with SRAM. But the NVIC is still garbage, and we still have fragments of other strings.
Switching to other Chitu firmware images, we rapidly see that the first DWORD is constant - probably a signature.
The second DWORD, however, changes, and the high byte of it corresponds to whatever the 00 XOR is.
The same thing repeats, in that if I XOR a rom image with its fifth byte, I get fragments of strings that make sense:
See that last one? It's printing a string with a CR/LF.
So, we know it's an XOR. We know it's not always the same key. And that's what we know to start...