Open VladLupashevskyi opened 2 years ago
IC164 is from the same generation, found here one more example:
https://youtu.be/ap3CYx5iMIU?t=116
SW Version: 2519021500_00E1 Seed: 93 8B 6F F8 7D 54 81 B2 Key: 1E FE 30 34
Hi @jglim here is the algorithm for all these clusters implemented in Rust: https://github.com/VladLupashevskyi/ki211-seed-key
It needs however different key for each SW version. Most probably there is some logic for generating those as well which is based on SW or something.
There is a table of root keys for each access level for example for 0095 SW Version:
(0) 13151416 (1) 541D378F (2) 35500E9F (3) 3DA7C96F (4) 71CFD8FF (5) 1AC9EA4F (6) 4884F65F (7) 2A58122F
If you take a look at how 8 bytes seed is used for key calculation you will see that only half of it is actually used. The other dummy bytes are actually generated based on 4 bytes seed and lvl. 2 key, so they might indicate something.
I tried to find out whether these access levels depend on each other by the same algorithm (as one root key would be seed and second a key) and actually there was match for lvl key pairs (0,1), (1,2), (2,3), but no more and the brute forced key was different always, so not sure whether it was just coincidence.
However, coming back to this issue #13, I can confirm that these algorithms are build in CBF files and I managed to emulate 171 cluster Flash procedure with Teensy, where I've got key calculated for my given seed. I have also tried to emulate 211 cluster, but it was not requesting any seed from emulated cluster, but I'm pretty sure it has those capabilities, given strings that are stored in that file. Unfortunately I'm not sure how to catch which calls it does to the c32s.dll, maybe you could help with that some day.
@VladLupashevskyi Awesome! I've added that as KIAlgo1
using your reference implementation, along with entries for 0095
. Thanks for your contribution!
I've tried searching for the above constants (e.g. 541D378F
, endian inverted, 8F371D54
) inside KI211.CBF, but I haven't found any matches. This probably means that I'm looking at the wrong place, or the root key might be directly embedded in the flash file.
If these keys can be extracted from the CFF files, I'm okay with manually dumping them as it should hopefully be a short list (and thus less painful than figuring out the backing logic). However, I haven't been able to find a matching CFF file for the 0095
SW block. Would you be able to point me to the correct CFF filename so that I can search and download it?
On the last bit on tracing c32s.dll calls, I can recommend drltrace for bulk tracing of library calls. This tool came in handy while figuring out the internals of SMR-D/F in my other ODB project. Again, this is a bulk strategy which slows down the process a lot and generates fairly chunky logs. If you have specific functions of interest, it may be quicker to stick a debugger on them instead, or write an "injectable" library to hook them.
@jglim thanks for implementation :)
just couple of corrections, these clusters (except for 171) are using 27 01 for seed (which is generic for all access levels) and 27 02 for key response of all access levels and the key response is formed as follows 27 02 (access level 1 byte) (key 4 bytes) (dongle ID 2 bytes)
so there shouldn't be only odd numbers for access levels and they just go 1,2,3,4,5,6,7. Effectively the only 07 makes sense since it gives the all access.
Please also take a look at test section of rust.rs file, there are also keys for other SW such as ki209 1057, ki211 0095, ki211 00A9 and 171 AEJ 07/1 (pay attention to the response for 171). I might add it later here, when I have access to my computer, but just if you faster than me :)
What can be also useful if somebody has an EEPROM programmer and some clusters is that the default access level is actually stored in EEPROM and can be changed which allows to get a firmware with potential keys table. The address in EEPROM for ki211 that I checked was 0x0DE, 0x0DF which is for default lvl 2 is 0xA2C4 and for level 7 it's 0x5779. They use some obfuscation to store it on EEPROM as you can see.
I did look those keys in cbf files, and also did find anything. I don't have any CFF files for 211 cluster, and not sure whether they actually exist. What I did however is I took some dummy CFF file with ID name of 5 characters like KI171 (or IC171 can't remember now) and replaced all occurrences with KI211 + I have patched c32s CRC check so it returns always true and that's how I was able to use any CFF file with 5 chars ID for flashing with my simulator. I cannot remember whether I have used only KI171 off file for KI171 sim or also some other patched CFF file. Need to check that again. So if it actually calculates key for non original CFF file, then we can pretty sure tell that generation is done only by CBF.
What I noticed in almost all CBF files where we know exact key and can find it in CBF, the endian inverted key is prefixed with 0x43 and 0xFE byte goes almost always after key. So I found the place in 211 cbf in Flash routine section exactly before seed key related strings and it's 0xF93197A0
which has 0x43 before and 0xFE after it. Gut feeling tells me that it might be actually used to calculate that root key for specific software version.
Thanks for pointing me to drltrace, it looks like exactly what I need for that. I will check that out as soon as I find time for that. And also I will check whether seedkey is gonna be generated for 171 if I take some dummy patched CFF file and post an update here.
Got it, thanks for taking time to explain how the levels work. The db.json definitions have been updated based on the rust tests, so there are a total of 4 entries now:
I've yet to include 171_AEJ_07_1
, as the output format and length is different from the rest. I'll update that at a later date when I think of a solution.
Looking at 2 random eeprom dumps, A2 C4
can be found at address 0xDE, but 57 79
is not present anywhere in either files. I'm still unsure of the significance of these 2-byte values as I assume that root keys should be 4-bytes.
Here's a dump of the first 16 bytes, starting from 0xDE.
A2115405711:
A2 C4 00 59 47 05 11 00 02 53 02 18 63 05 02 36
A2115405611:
A2 C4 00 59 46 99 10 00 10 09 25 18 63 04 02 30
That's a creative workaround to test the algo. The pattern 43 F9 31 97 A0 FE
also exists on the CBF file that I am using. Specifically, it resides within the KI211_WVC_FlashProg
embedded script.
When the script is dumped into a standalone file, the F9 31 97 A0
pattern can be found at position 0x2C1D, which appears to be inside the script function "SeedKeyBerechnung". I don't know enough about the scripting engine, though it seems that you are on the right path where F93197A0 is potentially a root key. Hopefully your technique will work out!
This is the list of functions inside KI211_WVC_FlashProg
(EP: entrypoint offset):
Fn: LDeleteDiagServiceIO Ordinal: 13 EP: 0x378F0009, InParam: 9 @ 0x340D, OutParam: 1 @ 0x14275
Fn: LokalSetPrepParam Ordinal: 1 EP: 0xAC, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LokalGetPresParam Ordinal: 2 EP: 0xAC, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LIsDiagServiceError Ordinal: 3 EP: 0xAE, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LokalGetPresType Ordinal: 4 EP: 0x1DE, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LCreateDiagServiceIO Ordinal: 5 EP: 0x436, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LSetPrepParam Ordinal: 6 EP: 0x49A, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LDoDiagService Ordinal: 7 EP: 0x4DA, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LGetPresParam Ordinal: 8 EP: 0x54A, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LDeleteDiagServiceIO Ordinal: 9 EP: 0x5D8, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: LDiagService Ordinal: 10 EP: 0x650, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: ProgrammingMode Ordinal: 11 EP: 0x8E6, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: Unlock Ordinal: 12 EP: 0xD4A, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: EraseRoutine Ordinal: 13 EP: 0x16FE, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: CheckRoutine Ordinal: 14 EP: 0x1A36, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: EcuReset Ordinal: 15 EP: 0x2AC6, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: SeedKeyBerechnung Ordinal: 16 EP: 0x2BA4, InParam: 0 @ 0x0, OutParam: 0 @ 0x0 <--------
Fn: FlashProg Ordinal: 17 EP: 0x6AC, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: FlashProg_Boot Ordinal: 18 EP: 0x746, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: FlashProg_Boot1 Ordinal: 19 EP: 0x7D6, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
Fn: FlashProg_Boot2 Ordinal: 20 EP: 0x85C, InParam: 0 @ 0x0, OutParam: 0 @ 0x0
@jglim re EEPROM, thanks for confirming that A2C4 exists on other EEPROMs, that's great news!. Sorry I might not expressed myself correctly - this value has nothing to do with root key. It's just default access level that is gonna be used after boot. On these clusters there is a function Fn_Set_sec_lvl
which is basically records current access level to EEPROM, so if you do seed key challenge for lvl 7 and then execute Fn_Set_sec_lvl
you will never need to do a seedkey and the cluster will be always unlocked. So for lvl 7 the value at address 0xDE
gonna be 0x5779
.
That's the way it gets computed:
((lvl + 3 ^ 0xF) & 0xF) << 4 | (lvl & 0xF)
for 0xDE
((lvl + 1 ^ 0xF) & 0xF) << 4 | (lvl + 2)
for 0xDF
So one can desolder that small EEPROM change to value 0x5779 and be able to read the firmware via diagnostics, which is located starting at 0x44800
until 0x100000
.
And thanks for confirmation of the location of that 4 byte value in CBF that motivates me :)
Nice, that's the first time that I've heard of persistent security levels. Appreciate the explanation.
For what it's worth, I took a look at _MIInterpreter
in c32s again; The 43
in the pattern 43 F9 31 97 A0 FE
helpfully appears to be an opcode for a 32-bit immediate push instruction (something like push 0xA09731F9
), so that's another good sign.
I got the same answer through KI211 0095 DLL and your calculation, your algorithm is very correct
If this topic is still actual, here is the list of the hashes for the sw ID's of security level 7.
"03": "0F379E18",
"06": "CF6A3B16",
"0A": "0FC62329",
"14": "3FB43C63",
"22": "0F07553D",
"23": "3FE4DE5E",
"43": "3FF83224",
"63": "DF49A948",
"65": "F9319720",
"66": "F9319720",
"67": "F93197A0",
"68": "F9319720",
"69": "F93197A0",
"6A": "C57B09A9",
"6B": "EA62570C",
"73": "1AF33E02",
"74": "13EE4F28",
"75": "CE2C3000",
"76": "FB137234",
"77": "EF8E7812",
"86": "4FC3814A",
"88": "6F286476",
"89": "1F769C2F",
"8A": "8FD64A72",
"93": "DFB8D13B",
"94": "3F2F6370",
"95": "2F12582A",
"A9": "BFB0046A",
"AF": "9F854122",
"B5": "DF27AC30",
"C3": "3FE4D358",
"D3": "3F04892F",
"D4": "2F7BD775",
"31": "0D76984F",
"34": "0D76984F",
"35": "0D76984F",
"3A": "0D76984F",
"3C": "0D76984F",
"42": "1A993E77",
"44": "9F841711",
"56": "FFB11339",
"72": "2F48AB50",
@WSorban Cool, thank you very much!!! Did you get them just from firmware or was there some logic that generates them based on SWID?
@jglim we need to reverse endianness to make it work with current implementation
Looks like "56": "FFB11339"
is the same as for 1056
which is 209 version of cluster
Some of them are same between different sw numbers. Also one sw number can be found in multiple ki's. For example 0x86 is found in both 211 and 164
Yeah, I noticed that.
Another interesting thing is that I cannot find these exact SW version numbers (2nd line on photo) in any diagnostics responses and it seems it's only obtainable from engineering menu in cluster.
Since diagnostic app should be able to determine the version for root key, It's possible that it actually depends on other SW version (1st line for example) that is present in diagnostic responses.
Request 31 FB 00 then sw I'd will be on 6th index
Added, Thank you @WSorban for this huge list of keys!
Hey @jglim, some updates from my side.
Tried with drltrace, but somehow Vediamo hangs after startup I cannot even connect to ECU :(
As you might remember I could not get seed key calculation function to be called during flash, but using your amazing app which decodes fn offsets, I just replaced offset of function that is getting called with seed key calculation function and got this nice output in logs:
Thanks to the person that has added such a nice output for logs :D
It does not request seed, just does calculation (and 33...33 is just some value that's stored in the memory where seed should be).
As we can see it does just exactly what I have implemented in algorithm, and nothing else to do with SW dependant key generation. In fact the value that I had gut feeling about (0xF93197A0
) is actually a root key and it was shared already by @WSorban.
With KI171 it's more interesting...
First of all after running 171 cbf file trough your DSCContext loader I found that there is actually a constant which is called "Key Constant" the value is: 2FABAD10
.
Then there is another constant in the beginning of seed calculation function in Flash routine: 6D4EC641
. I tried to change it and resulting key was changing, however not according to the logic of the algorithm. Given a seed: 00...01
the last half of this constant (as represented in cbf, i.e values from 0x00000000
to 0x0000FFFF
) just does xor with 1st and 2nd bytes of resulting key, another half does something different. Need to check it out further.
In original flash sequence before doing seed key calculation it get Boot ID via request 1A 9E
and that's how it decides that it's 171 AEJ 07/1
. I've tried to play with response, but it always defined it as 07/01
, since I don't have real 171 cluster I cannot really get a proper response, and I think it just falls back to the last possible SW revision of the cluster, if there is nothing specific in the response.
But then I tried to do that trick with replacing fn offsets and I replaced "get boot id" function with calculate key function and the resulting key was different. I assume it took the first SW in this case which is probably: AEJ 05/1
as it stays on the first place in strings of boot id fn, however it could have just informative meaning and some different value from Boot ID response is taken to decide which flavour of algorithm to use.
For the default constant value in the beginning of the function and seed 00..01
the resulting key was 40 EA 4E 9E
and I couldn't brute force root key for it. With constant reset to zeros for the same seed key was 012C0001
, and I could brute force key for it: 2FF40004
.
So it looks like current version of algorithm will not work for 171 cluster and it needs more digging.
However good news are that we pretty sure that the algorithm is just in CBF file and nothing is used from CFF files for these clusters at least. CFF files that I took had nothing to do with those clusters.
I have also found out that _MIInterpreter function looks for dll which has interesting name: InterpreterDebugPanel.DLL
. Probably it is not possible to obtain it, but it doesn't look that it has that many functions, maybe implementing some simple wrapper could shed some light of what is getting passed there.
At last I found that DSCContext was failing when reading PAL files and I had to update on line 16 in DSCContext.cs
from:
new MemoryStream(dscContainerBytes)
to
new MemoryStream(dscContainerBytes, 0, dscContainerBytes.Length, true, true)
Your function offset hack is brilliant, and makes a lot of sense in hindsight — most functions take 0 parameters and return nothing so they should be interchangeable. I'm still trying to figure out more uses for your technique, and might report back if I find anything new.
On KI171_WVC_FlashProg
, I've seen the variable Key_Konstante
in other CBF files too. Sometimes they are populated with a value like the KI171, many other times they are uninitialized. I'm inclined to believe that this algo is intended for firmware flashing, and might be a (new?) variant of DaimlerStandardSecurityAlgo
. That being said, I'm hoping that it's something else, which would be exciting.
InterpreterDebugPanel seems to only export 3 functions. There might be more callbacks from the dll, that are returned when the panel is opened but I haven't understood enough of it. Also, there are additional checks on the PAL header to determine if debugging is enabled (e.g. before the dll is loaded). I saved some notes but never finished the dll:
// this will require a *.def file to ensure that exported names are not mangled
// the function params might not be visible in ida if they are unused
/*
unk1: c string, either "unknown" or something else depending on some field within the script's header
interpreter_context: handle to the currently running vm, allocated at MIInterpreterCreate (structure, size=0x55)
mi_debug_set_breakpoint_callback: offset to the "set breakpoint" function
mi_debug_clear_breakpoint_callback: offset to the "clear breakpoint" function
*/
__declspec(dllexport) void* __cdecl ControlPanelOpen(const char* a_unk1, void* interpreter_context, void* mi_debug_set_breakpoint_callback, void* mi_debug_clear_breakpoint_callback)
{
// returns some sort of handle, no idea
return 0;
}
/*
open_result seems to be the return value of ControlPanelOpen
a2 might be an address..?
can't confirm if it returns anything.
*/
__declspec(dllexport) void* __cdecl ControlPanelBreakpoint(void* open_result, int a2)
{
__asm
{
int 3
}
return 0;
}
/*
this needs to exist even if it's probably not useful
no idea what the signature is; will definitely crash
a debugger exception should be raised *first* to find the caller
*/
__declspec(dllexport) void* __cdecl ControlPanelClose()
{
__asm
{
int 3
}
return 0;
}
filter.config
file. The default settings will intercept/log too much, to the point where most applications will seize up.Hello again, here's a small release that might help when working with the interpreter:
I've written a dll C32NativeExtension_221215_2.zip that hooks into c32s.dll, and traces every instruction that goes through MIInterpreter. At the very least, they will show up like this, with the current opcode, program counter, PAL file offset, and the stack pointer:
OP: 0000035E PC: 06E45EB1 F: 00000137 SP: 06041046
Some instructions may be accompanied with a description on their behavior and the values as they are used or modified:
OP: 00000003 PC: 070B43ED F: 00000C73 SP: 06E460A6
push absolute: 41C64E6D
This dll expects an exact c32s.dll, version 3.2.6.2, sha1: 59E29B37C76451A2BB63F1B5776D239D103D479D
, which ships with Vediamo 04.02.02. It has to be loaded after c32s.dll, and before MIInterpreter-related functions are called.
This project was built via a vanilla vs2019. It should build without much trouble as there are few third-party dependencies.
Any dll-loading technique (e.g. injection) should be fine, though CFF Explorer is especially recommended as a tool to add C32NativeExtension.dll as an import under c32s.dll, so that it is automatically loaded whenever c32s.dll is loaded by any process. When using this method, remember to put C32NativeExtension.dll in the same directory as c32s.dll after building it.
Whenever the library is loaded, it will create/overwrite a log file c32s.txt
in the same directory as c32s.dll, which will contain the instruction trace.
To understand how the trace might be helpful, A CRD3 is emulated to log its unlocking behavior (I am having difficulty emulating a KI171 as I am not sure of the messages that it expects). After initiating contact, an unlock script is run through Services › Execute Function › FN_SG_Entriegeln_9A
. Given a seed of 11 22 33 44 55 66 77 88
, I have first manually calculated the key 0E 66 48 A9
. The next section shows a snippet of the instruction trace, while the script was doing the same.
CRD3 (CRD3S2SEC9A):
k = 3F 9C 71 A5 3F9C71A5
kA = 41 C6 4E 6D 41C64E6D
kC = 30 39 3039
seed = 11 22 33 44 55 66 77 88 1122334455667788
key = 0E 66 48 A9 0E6648A9
seedA = 11223344
seedB = 55667788
intermediate1 = kA * seedA + kC
intermediate1 = 41C64E6D * 11223344 + 3039
intermediate1 = A1C68BF4 + 3039
intermediate1 = A1C6BC2D
intermediate2 = kA * seedB + kC
intermediate2 = 41C64E6D * 55667788 + 3039
intermediate2 = 903C54E8 + 3039
intermediate2 = 903C8521
seedKey = intermediate1 ^ intermediate2 ^ k;
seedKey = A1C6BC2D ^ 903C8521 ^ 3F9C71A5
seedKey = 31FA390C ^ 3F9C71A5
seedKey = 0E6648A9
https://github.com/jglim/UnlockECU/blob/main/UnlockECU/UnlockECU/Security/DaimlerStandardSecurityAlgo.cs
--------
OP: 00000003 PC: 070B43ED F: 00000C73 SP: 06E460A6
push absolute: 41C64E6D
OP: 00000003 PC: 070B43EF F: 00000C75 SP: 06E460AA
push absolute: 11223344
OP: 0000004F PC: 070B43F1 F: 00000C77 SP: 06E460AE
mul stack 41C64E6D *= 11223344 = A1C68BF4
OP: 00000003 PC: 070B43F2 F: 00000C78 SP: 06E460AA
push absolute: 00003039
OP: 00000047 PC: 070B43F4 F: 00000C7A SP: 06E460AE
add stack A1C68BF4 += 00003039 = A1C6BC2D
OP: 00000079 PC: 070B43F5 F: 00000C7B SP: 06E460AA
store stage1, dest. stack absolute: 02, type: 03
OP: 00000079 PC: 070B43F7 F: 00000C7D SP: 06E460AA
store stage2: writing unmodified value A1C6BC2D with type 03
OP: 00000003 PC: 070B43F8 F: 00000C7E SP: 06E460A6
push absolute: 41C64E6D
OP: 00000003 PC: 070B43FA F: 00000C80 SP: 06E460AA
push absolute: 55667788
OP: 0000004F PC: 070B43FC F: 00000C82 SP: 06E460AE
mul stack 41C64E6D *= 55667788 = 903C54E8
OP: 00000003 PC: 070B43FD F: 00000C83 SP: 06E460AA
push absolute: 00003039
OP: 00000047 PC: 070B43FF F: 00000C85 SP: 06E460AE
add stack 903C54E8 += 00003039 = 903C8521
OP: 00000079 PC: 070B4400 F: 00000C86 SP: 06E460AA
store stage1, dest. stack absolute: 06, type: 03
OP: 00000079 PC: 070B4402 F: 00000C88 SP: 06E460AA
store stage2: writing unmodified value 903C8521 with type 03
OP: 00000003 PC: 070B4403 F: 00000C89 SP: 06E460A6
push absolute: A1C6BC2D
OP: 00000003 PC: 070B4405 F: 00000C8B SP: 06E460AA
push absolute: 903C8521
OP: 0000009F PC: 070B4407 F: 00000C8D SP: 06E460AE
xor stack A1C6BC2D ^= 903C8521 = 31FA390C
OP: 00000015 PC: 070B4408 F: 00000C8E SP: 06E460AA
push absolute (negative pc): 3F9C71A5
OP: 0000009F PC: 070B440A F: 00000C90 SP: 06E460AE
xor stack 31FA390C ^= 3F9C71A5 = 0E6648A9
The log for the entire script execution can be viewed here: c32s_SecurityAccess_9New.txt. As global variables come with names, it helpfully describes what it is doing, such as:
27 09
request (F: 00000F26
)F: 00000F36
)F: 00000BB9
)Key
bytearray (F: 00000CA3
)Many of the instructions do not come with descriptions, as I only fixed up enough for the CRD3 to produce meaningful traces. Hope this still comes in handy!
Extra stuff
"minimum" CRD3 for emulation at a j2534 level (PassThruWriteMsgs/PassThruReadMsgs)
> .. .. .. .. 10 01
< 00 00 07 E8 50 01 00 14 00 C8
> .. .. .. .. 10 03
< 00 00 07 E8 50 03 00 14 00 C8
> .. .. .. .. 3E 00
< 00 00 07 E8 7E 00
> .. .. .. .. 22 F1 00
< 00 00 07 E8 62 F1 00 02 21 31 03
> .. .. .. .. 22 F1 54
< 00 00 07 E8 62 F1 54 00 40
> .. .. .. .. 22 00 01
< 00 00 07 E8 7F 22 00 01 00 00
> .. .. .. .. 27 09
< 00 00 07 E8 67 09 11 22 33 44 55 66 77 88
> .. .. .. .. 27 0A .. .. .. ..
< 00 00 07 E8 67 0A
Additional info on bench ki211: Build : ki211+ZGW211 ( possibly 203\209 will also work) Since theres need to get SW Version to get right seed, theres alternative for bench mode by sending raw can. Basically i emulate eis position 1, and MRM keypressing.
ID | SIZE | Data | Comment |
---|---|---|---|
000 | 6 | 03 00 02 00 00 00 | periodic 10ms , ignition pos 1 |
1A8 | 2 | 01 00 | UP btn. |
1A8 | 2 | 02 02 | Down btn. |
1A8 | 2 | 03 03 | Menu in btn. |
1A8 | 2 | 04 04 | Menu out btn. |
Just sharing some findings about IC211 here, IC209 is gonna be similar...
Found in this video combination for IC211 https://youtu.be/DMf5G9hdKAA?t=191
SW Version: 00A9 Seed: 84 7D 94 EA 99 27 9F 96 Key: 6A E3 96 C3
I was asking the author of this video how did he get the algorithm - he said he was reverse engineering the IC firmware.
Here guy is selling the tool with all possible calculators for 203, 209, 211 instrument clusters: https://ancartech.com/sw_skctool/