Page 1 of 1

UDS-Handler in Convers+ Firmware

Posted: 05 Mar 2023, 22:38
by Go4IT
In this thread i'd like to share my findings of UDS handler functions and what they do.

Locating MS-CAN functions

First we need to get an starting point where to look for in the firmware. I choose to go by hardware-route. I know that µC has serveral CAN-Ports and by routing back the signals of the board that CAN port D is connected to MS-CAN of the car. I also know that CAN port D control registers are located at memory address 0xFC0A 0000 onwards.

To map the addresses of the Memory-Mapped-IO ports to names, i've created an segment for that memory range in IDA and named it "IOMAP":
ida_segment_iomap.png
ida_segments.png
After doing a re-analyze of the code, all direct(!) referenced into that map are shown as "unknw_..." addresses:
ida_iomap_unknown_addresses.png
Next i named the references to their real names according to the datasheet of the processor:
ida_iomap_named_addresses.png
Now i could easily use the backref-lookup of IDA to find a lot of interesting stuff handling CAN:
ida_xref_can_d_mcr.png
As CAN D is connected to MS-CAN of car i prefix each of those functions with "mscan_..." so it's easier to sort them while reading the code:
ida_mscan_subs.png
Identify CAN functions

Now let's go through each function to identify what they are for to give them reasonable names.

1.) mscan_27C6

This looks like an init-function, setting all the base parameters:

Code: Select all

void *mscan_27C6()
{
  char v0; // r1
  unsigned int v1; // r0
  unsigned int v2; // r0
  __int16 *v3; // r1
  void *result; // r0
  __int16 v5; // r1

  byte_40000D68 = 0;
  word_40000D6E = -1;
  v0 = CAN_D_CTRL_R3;
  CAN_D_CTRL_R3 = v0 | 0x80;
  CAN_D_MCR_R1 &= 0x7FFFu;
  CAN_D_MCR_R1 |= 0x200u;
  while ( CAN_D_MCR_R1 & 0x200 )
    ;
  CAN_D_MCR_R1 |= 0x4000u;
  CAN_D_MCR_R1 |= 0x1000u;
  while ( !(CAN_D_MCR_R1 & 0x100) )
    ;
  CAN_D_CTRL_R3 = byte_40000810 & 0x3F;
  CAN_D_CTRL_R4 = byte_40000811 & 0xBF;
  CAN_D_CTRL_R4 = byte_40000811 & 0xBF | 0x40;
  CAN_D_CTRL_R2 = byte_4000080F;
  CAN_D_CTRL_R1 = byte_4000080E;
  CAN_D_MCR_R4 = 16;
  v1 = 0;
  do
  {
    CAN_D_MB0_CTRL[8 * v1] = 0;
    v1 = (v1 + 1) & 0xFF;
  }
  while ( v1 <= 0xF );
  CAN_D_MB0_CTRL[0] |= 0x800u;
  v2 = 13;
  do
  {
    v3 = &CAN_D_MB0_CTRL[8 * v2];
    *v3 = 1024;
    v3[2] = word_4000080C[13 - v2 + 3];
    v2 = (v2 + 255) & 0xFF;
  }
  while ( v2 > 0xB );
  result = &CAN_D_MCR_R1;
  CAN_D_RXGMASK_R1 = 0x1FFF;
  CAN_D_RXGMASK_R3 = (signed int)&CAN_D_MCR_R1 >> 26;
  CAN_D_IMASK = 0;
  CAN_D_IFLAG = (signed int)&CAN_D_MCR_R1 >> 26;
  v5 = CAN_D_MCR_R1;
  CAN_D_MCR_R1 = v5 | word_4000080C[0] & 0x4CC;
  CAN_D_MCR_R1 &= 0xEFFFu;
  CAN_D_MCR_R1 &= 0xBFFFu;
  return result;
}
...to be continued...

Re: UDS-Handler in Convers+ Firmware

Posted: 06 Mar 2023, 18:40
by Go4IT
Locating UDS functions

There is a handler function checking the second byte of an UDS request payload, which is the service identifier (SID):
uds_handleRequest.png
Inside this big switch/case block we find one handling SID 0x2E "Read Data By Identifier" that calls this subroutine to parse and prepare the data requested:
uds_readByIdentifier.png
The routine is acting on the two "Data Identifier" (DID) bytes which follows the SID. It is copying some data from the eeprom or the firmware image into some return buffer. It also set's a length and response-code.

The routine is checking for several DIDs. DID 0xF101 is explicitly answered with NRC out-of-range, as any other unmatched also. Maybe there was a special handler for it in a debug-version of the firmware or for other cars. Orherwise the handling make not much sense.

Nut now we know excactly what DIDs could be used, but also know an interesting hook to inject an own handler. We could change the jump-address for 0xF101 to our own handler which might provide other data, e.g. the whole eeprom contents or such...

...to be continued ...

Re: UDS-Handler in Convers+ Firmware

Posted: 06 Mar 2023, 19:54
by Go4IT
Locating Security Key algorithm

It looks like the firmware builds a 3 byte random data SEED using the current value of an internal timer (here the CAN timer is used). It sends this 3 bytes to back to the tester and keeps the values in memory during the session (beware of the timeout!).

Now the tester calculate a KEY out of the SEED bytes and the secret algorithm and send those 3 KEY bytes back to the ECU.

The ECU then does se same calculation out of the previously stored SEED bytes (purly random data) and compares it's own result with the one send from the tester. If both match, the tester knows the algo and is trusted.

This is the routine generating the SEED (which simply consists of 3 random number bytes):

Code: Select all

void uds_generateSeed()
{
  int v0; // r0
  unsigned int v1; // r1
  int v2; // r2

  v0 = uds_msg;
  v1 = ((unsigned int)*(unsigned __int8 *)(uds_msg + 1) + 1) >> 1;
  byte_400017FA = v1;
  if ( v1 == 1 )
  {
    if ( byte_400017F8 != 2 || byte_400017F9 != 1 )
    {
      byte_400017F8 = 1;
      byte_400017F9 = 1;
      random();                                 // generate 3 byte random data
      v2 = uds_msg;                             // copy SEED into send-buffer
      *(_BYTE *)(uds_msg + 2) = seedByte1[0];
      *(_BYTE *)(v2 + 3) = seedByte2;
      *(_BYTE *)(v2 + 4) = seedByte3;
      uds_responseLength = 4;
    }
    else
    {
      *(_BYTE *)(uds_msg + 2) = 0;              // return invalid seed
      *(_BYTE *)(v0 + 3) = 0;
      *(_BYTE *)(v0 + 4) = 0;
      uds_responseLength = 4;
    }
  }
  else
  {
    sub_3726();
  }
  JUMPOUT(&loc_36D6);
}
Maybe someone is smart enough to get the random algo out of this pseudo-code:

Code: Select all

// This function generates 3 random bytes in RAM at 0x400017F0-0x400017F2
unsigned int random()
{
  unsigned int v0; // r4
  int v1; // r0
  unsigned int v2; // r1
  unsigned int result; // r0
  unsigned int v4; // r1
  unsigned int v5; // r1

  v0 = 0;
  do
  {
    v1 = mscan_returnTimerR3Address();
    v2 = 0;
    do
    {
      v1 = (3 * v1 + 29) & 0xFFFF;
      v2 = (v2 + 1) & 0xFF;
    }
    while ( v2 < 5 );
    seedByte3 = BYTE2(v1);
    seedByte2 = v1;
    sub_4538();
    result = mscan_returnTimerR3Address();
    v4 = 0;
    do
    {
      result = (29 * result + 3) & 0xFFFF;
      v4 = (v4 + 1) & 0xFF;
    }
    while ( v4 < 5 );
    v5 = (unsigned __int8)seedByte2 ^ (result >> 8);
    seedByte2 ^= BYTE2(result);
    result = (unsigned __int8)result;
    seedByte1[0] = result;
    v0 = (v0 + 1) & 0xFF;
    if ( (_BYTE)result )
      break;
    if ( v5 )
      break;
    result = (unsigned __int8)seedByte3;
    if ( seedByte3 )
      break;
    if ( v0 >= 5 )                              // simulated random
    {
      seedByte1[0] = 3;
      seedByte2 = 0x1D;
      result = 0x65;
      seedByte3 = 0x65;
    }
  }
  while ( v0 < 5 );
  return result;
}
It stores 3 random bytes at base address 0x400017F0

I guess this is the key calculating algorithm

Code: Select all

// Calculate KEY from SEED using secret algorithm
int __fastcall uds_calcKey(_BYTE *a1)
{
  int v1; // r4
  int v2; // r6
  unsigned int v3; // r5
  signed int v4; // r0
  unsigned __int8 *v5; // r0
  unsigned int v6; // r2
  unsigned int v7; // r2
  unsigned int v8; // r1
  int v9; // r1
  char v10; // r0
  char v11; // r2
  int result; // r0
  _BYTE *v13; // [sp+0h] [bp-18h]

  v13 = a1;
  v1 = 1;
  v2 = 0;
  byte_400017F3 = 8;
  byte_400017F4 = 0x30;
  byte_400017F5 = 0x61;
  byte_400017F6 = 0x55;
  byte_400017F7 = 0xAAu;
  byte_400017ED = 0xA9u;
  byte_400017EE = 0x41;
  byte_400017EF = 0xC5u;
  v3 = 0;
  do
  {
    sub_4538();
    v4 = 1;
    if ( !((unsigned __int8)seedByte1[v2] & (unsigned __int8)v1) )
      v4 = 0;
    v5 = (unsigned __int8 *)(v4 ^ byte_400017ED & 1);
    if ( v1 == 128 )
    {
      v1 = 1;
      v2 = (v2 + 1) & 0xFF;
    }
    else
    {
      v1 = 2 * v1 & 0xFF;
    }
    v6 = (unsigned int)(unsigned __int8)byte_400017ED >> 1;
    byte_400017ED = (unsigned __int8)byte_400017ED >> 1;
    if ( (unsigned __int8)byte_400017EE << 31 )
      byte_400017ED = v6 | 0x80;
    v7 = (unsigned int)(unsigned __int8)byte_400017EE >> 1;
    byte_400017EE = (unsigned __int8)byte_400017EE >> 1;
    if ( (unsigned __int8)byte_400017EF << 31 )
      byte_400017EE = v7 | 0x80;
    v8 = ((unsigned int)(unsigned __int8)byte_400017EF >> 1) | v5[0x4615];
    byte_400017ED ^= v5[0x461B];
    byte_400017EE ^= v5[0x4619];
    byte_400017EF = v8 ^ v5[0x4617];
    v3 = (v3 + 1) & 0xFF;
  }
  while ( v3 < 0x40 );
  v9 = (unsigned __int8)byte_400017ED;
  v10 = byte_400017EE;
  *v13 = ((unsigned __int8)byte_400017ED >> 4) | 16 * byte_400017EE;
  v11 = byte_400017EF;
  v13[1] = v10 & 0xF0 | ((unsigned __int8)byte_400017EF >> 4);
  result = v11 & 0xF | 16 * v9;
  v13[2] = result;
  return result;
}

Re: UDS-Handler in Convers+ Firmware

Posted: 09 Mar 2023, 07:33
by Go4IT
You may also find disassemblings like this equation in the parenthesis:

Code: Select all

STRB            R1, [R5,#(byte_0x400017F2 - 0x400017F0)]
This is a disassembler optimization which looks strange to me.

"STRB" is a load operation. The value referenced by the base-address found in R5 and an index given afterwards into the register R1. As it is a STRB (B stands for Byte) it will only fetch a single byte from the resulting address and store it into the lowest bits of the 32-bit register R1, all higher bits will be padded with '0'. The hash "#" in front of the index-value denotes that there is an immediate value given, but the disassembler builds this special form for it instead of pointing out a number: "(seedByte3 - 0x400017F0)". "byte_0x400017F2" is the name of a memory-location, here 0x400017F2. So what come out of this sub-calculation is: 0x400017F2 - 0x400017F0 = 2.

So in fact the code is:

Code: Select all

STRB            R1, [R5,#2]
which is much more readable and understandable in my opinion.
I haven't found an option in IDA Pro to change this, maybe you?

You also often find so called "nullsub" functions like this:

Code: Select all

nullsub_4538
BX              PC
IDA Pro is smart enough to understand that this function is only returning to the caller without doing anything. The "BX" branch-exit to the value of the PC-register is like a "return;" in C, it does nothing. Those fragments are traces to additional, mostly debug code removed from the resulting release build.

Using the "Proximity Browser" function of IDA you get an idea how it works:
uds_handler_secutiryaccess.png
A central UDS-message handler detects the SecurityAccess SID (0x27) and depending on the following DID (1 = Request SEED, 2 = Send KEY) it processes the data and give feedback through an answer put the data into the msgPtr.

Re: UDS-Handler in Convers+ Firmware

Posted: 09 Mar 2023, 09:01
by Go4IT
Hi, i've digged into that random generator function i found.

The full code of SEED generation

Now to the main random-generator. It calculates a 3 byte random sequence (i named the first byte of the memorylocation "randomSeedSequence") used for the challenge send to the tester in an UDS-SecurityAccess flow. Again the disassembled code first (i've added line-numbers in front of the commands, removed nullsubs and changed the names of the loc_... into some more readable ones):

Code: Select all

generateRandomSeed:
1   PUSH            {R4-R6,LR}
2   LDR             R5, =randomSeedSequence  ; pointer to the first of 3 byte sequence

3   MOVS            R4, #0
loop1_31BA:

4   BL              mscan_getTimerValue  ; function returns a 16-bit random value in R0
5   MOVS            R1, #0
loop2_31C0:
6   LSLS            R3, R0, #1
7   ADDS            R0, R3, R0
8   ADDS            R0, #29
9   LSLS            R0, R0, #16
10  LSRS            R0, R0, #16
11  ADDS            R1, #1
12  LSLS            R1, R1, #24
13  LSRS            R1, R1, #24
14  CMP             R1, #5
15  BCC             loop2_31C0

16  LSRS            R1, R0, #8
17  STRB            R1, [R5,#2]
18  STRB            R0, [R5,#1]

19  BL              mscan_getTimerValue  ; function returns a 16-bit random value in R0
20  MOVS            R1, #0
loop3_31E4:
21  MOVS            R3, #29
22  MULS            R0, R3
23  ADDS            R0, #3
24  LSLS            R0, R0, #16
25  LSRS            R0, R0, #16
26  ADDS            R1, #1
27  LSLS            R1, R1, #24
28  LSRS            R1, R1, #24
29  CMP             R1, #5
30  BCC             loop3_31E4

31  LDRB            R1, [R5,#1]
32  LSRS            R2, R0, #8
33  EORS            R1, R2
34  STRB            R1, [R5,#1]
35  LSLS            R0, R0, #24
36  LSRS            R0, R0, #24
37  STRB            R0, [R5]

38  ADDS            R4, #1
39  LSLS            R4, R4, #24
40  LSRS            R4, R4, #24
41  CMP             R0, #0
42  BNE             generateRandomSeed_exit

43  CMP             R1, #0
44  BNE             generateRandomSeed_exit

45  LDRB            R0, [R5,#2]
46  MOVS            R1, R5
47  CMP             R0, #0
48  BNE             generateRandomSeed_exit
  
49  CMP             R4, #5
50  BCC             loc_322C

51  MOVS            R0, #3
52  STRB            R0, [R1]
53  MOVS            R0, #0x1D
54  STRB            R0, [R1,#1]
55  MOVS            R0, #0x65
56  STRB            R0, [R1,#2]

loc_322C:
57  CMP             R4, #5
58  BCC             loop1_31BA

generateRandomSeed_exit:
59  POP             {R4-R6}
60  POP             {R3}
61  BX              R3
Generating random values

The "generateRandomSeed" function needs to get a random number. The firmware provides this by reading the current value of the FlexCAN-D-Port timer. By the datasheet this is a freerunning 16-bit timer, clocked by the configured baudrate speed of the CAN bus. Means, everytime one read the timer-register 0xFC0A000A he get's a random 16-bit value in the lower half-word of the 32-bit register.

The ARM code of this function is:

Code: Select all

mscan_getTimerValue:
LDR             R0, =FC0A0000     ; FlexCAN D Base Register (datasheet 23.5.2.1)
LDRH            R0, [R0,#0x0A]    ; Read 0xFC0A000A = TIMER (datasheet 23.5.2.3)
BX              LR                ; R0 = 0x0000TTTT
It could be translated to "c" like this:

Code: Select all

uint16_t mscan_getTimerValue()
{
  uint32_t *ptr = 0xFC0A000A;
  return &ptr & 0xFFFF;
}
Now let's analyze what this code is doing

In line 2, register R5 is set with a pointer to the start of the randomSeedSequence. This is what the function does as a side-effect.

The first crypto-loop

Code: Select all

4   BL              mscan_getTimerValue  ; function returns a 16-bit random value in R0

05  MOVS            R1, #0  ; init loop counter with 0
loop2_31C0:

06  LSLS            R3, R0, #1  ; R3 = R0 * 2
07  ADDS            R0, R3, R0  ; R0 = R0 + R3
08  ADDS            R0, #29  ; R0 = R0 + 29
09  LSLS            R0, R0, #16
10  LSRS            R0, R0, #16

; increase loop counter by 1
11  ADDS            R1, #1
12  LSLS            R1, R1, #24
13  LSRS            R1, R1, #24

; compare loop counter is below 5
14  CMP             R1, #5
15  BCC             loop2_31C0
The first loop in line 5-15 take the 16-bit random number in R0 (delivered from the timervalue subfunction called in line 4) and do some calculation with it.

R1 is a loop counter (set to 0 in line 5) which get's increased in line 11. Line 12+13 looks odd but in fact they just ensure the counter value stays at 8 bit. Shifting a value in a 32-bit register by 24 bits to the left and back to the right again is nothing else than setting all higher bits to zero. It's like in C a " <value> & 0xFF".

The loop itself should only iterate 5 times. As the loop counter is initialized with 0, it should break if counter reaches value 5. This is done with CMP in line 14 and BCC (Branch if Carry is Clear) in line 15. The CMP compares the loop counter value in R1 with 5. This sets or clears some flags in the processor. It sound strange but on ARM the Zero-Flag AND the Carry-Flag is set when two values match. The BCC only jumps to the loop address if the carry-flag is not set, so if the counter reaches 5 it will passed by.
In C this would look like:

Code: Select all

for (uint8_t loop1=0; loop1<5; loop1++) { ... }
Lines 6-10 calculates the timer value. Line 6 shift the value 1 bit to the left, which is mathematically the same als multiply it by two.
The result is added to the timer value in line 7. Line 8 adds a fixed value of 29 to the result. And finally line 9+10 do the left-shift/right-shift shuffle to mask out the higher 24 bits so that only a 8-bit value remains.
In C this is:

Code: Select all

uint16_t rnd;
rnd = mscan_getTimerValue();
...
rnd = ((rnd  * 2) + rnd + 29) & 0xFF;
Lines 16-18 store the so calculated value to the sequence pointed by R5. Line 16 will mask the high-byte of the random-word and stores it at randomSeedSequence[2], whereas line 18 stores the low-byte of the random-word into randomSeedSequence[1].

So the full loop code above would be:

Code: Select all

uint16_t rnd;
rnd = mscan_getTimerValue();
for (uint8_t loop1=0; loop1<5; loop1++) {
  rnd = ((rnd * 2) + rnd + 29) & 0xFFFF;
}
randomSeedSequence[1] = rnd & 0xFF;
randomSeedSequence[2] = (rnd >> 8) & 0xFF;
the second crypto-loop

The loop in lines 19-30 does exact the same thing as in the first loop, only the calculation and target bytes differ. It fetches a random word from the timer and iterates 5 times over this calculation in line 21-25:

Code: Select all

21  MOVS            R3, #29
22  MULS            R0, R3
23  ADDS            R0, #3
24  LSLS            R0, R0, #16
25  LSRS            R0, R0, #16
Line 21+22 multiplies the random value by "29", line 23 adds "3" on the result and line 24+25 masks out the high-word bits so only a 16-bit value remain.

The rest of the code there is to calculate a little more with the former results:

Code: Select all

31  LDRB            R1, [R5,#1]
32  LSRS            R2, R0, #8
33  EORS            R1, R2
34  STRB            R1, [R5,#1]

35  LSLS            R0, R0, #24
36  LSRS            R0, R0, #24
37  STRB            R0, [R5]
In 31 it loads the second byte (index #1) of the randomSeedSequence into R1.
Line 32 loads R2 with the high-byte of the timer-word.
Line 33 makes an XOR of both values (this is a disassembler short for "EORS R1, R2, R1", means R1 = R1 XOR R2).
In line 34 it stores the value back into the sequence.
Line 35+36 masks out the low-byte of the timer-word and stores it in line 37 as the first byte of the sequence.

In C that would be:

Code: Select all

rnd = mscan_getTimerValue();
for (uint8_t loop2=0; loop2<5; loop2++) {
  rnd = ((rnd * 29) + 3) & 0xFFFF;
}
randomSeedSequence[0] = rnd & 0xFF;
randomSeedSequence[1] = randomSeedSequence[1] ^ (rnd >> 8);
The exceptions

What now follows in the code can be described as safety and fallback methods, which ensures that there is a valid SEED sequence generated and the calling code can rely on that without error checking.

Under normal circumstances the work is done now, but for safety reasons the developers wanted to ensure that the outcome does not contain three zero bytes (0x00 0x00 0x00) as a SEED sequence. Because of the random values and various calculations it is very unlikely for this to happen - but in case of this they simply start over the whole process again. This is done for 5 times and for the un-un-un-unlikely scenario that all those iterations result in three null bytes, there is a fixed fallback sequence 0x00, 0x1D, 0x65 build in.

This is how it looks in disassembler:

Code: Select all

31  LDRB            R1, [R5,#1]
...
37  STRB            R0, [R5]

38  ADDS            R4, #1
39  LSLS            R4, R4, #24
40  LSRS            R4, R4, #24
41  CMP             R0, #0
42  BNE             generateRandomSeed_exit
Increase the loop-counter value. Then check if R0 (contains the high-byte of the last generated random word) is zero. If not, we are done and can exit and return. This means at least 2 bytes must not be zero, one could be zero in the resulting sequence.

This also follows for the low-byte of the random word:

Code: Select all

43  CMP             R1, #0
44  BNE             generateRandomSeed_exit
And even if the first two bytes of the random sequence where 0x00, we can also return with it as long as the last byte is something else:

Code: Select all

45  LDRB            R0, [R5,#2]
46  MOVS            R1, R5  ; <- this is not relevant for the branch.
47  CMP             R0, #0
48  BNE             generateRandomSeed_exit
If we haven't break-off the loop above, the code generated an null-byte sequence. If that happens for 5 times, then use a fixed default sequence instead:

Code: Select all

49  CMP             R4, #5
50  BCC             loc_322C
51  MOVS            R0, #3
52  STRB            R0, [R1]
53  MOVS            R0, #0x1D
54  STRB            R0, [R1,#1]
55  MOVS            R0, #0x65
56  STRB            R0, [R1,#2]
loc_322C:
...
At least ensure we only iterate 5 times:

Code: Select all

loc_322C:
57  CMP             R4, #5
58  BCC             loop1_31BA
This can be put in C as:

Code: Select all

// try to generate a valid 3 byte random seed (valid means that there must be at least one non-zero byte in it)
for (uint8_t n=0; n<5; n++)
{
  ...
  if (randomSeedSequence[0] || randomSeedSequence[1] || randomSeedSequence[2]) {
    break;
  }
}
// fallback to a default value of no sequence could be generated
if ( ! randomSeedSequence[0] && ! randomSeedSequence[1] && ! randomSeedSequence[2]) {
  randomSeedSequence = { 0x03, 0x1D, 0x65 };
}
Putting it all together...

The full code of generating a seed in C looks like this:

Code: Select all

/**
 * Create a sequence of 3 random bytes
 */
void createRandomSeed(uint8_t *seed)
{
    uint16_t rnd;

    // try to generate a valid 3 byte random seed for max. 5 times
    for (uint8_t n=0; n<5; n++)
    {
        // first calculation
        rnd = mscan_getTimerValue();
        for (uint8_t i=0; i<5; i++) {
            rnd = ((rnd * 2) + rnd + 29) & 0xFFFF;
        }
        seed[1] = rnd & 0xFF;
        seed[2] = (rnd >> 8) & 0xFF;

        // second calculation
        rnd = mscan_getTimerValue();
        for (uint8_t i=0; i<5; i++) {
          rnd = ((rnd * 29) + 3) & 0xFFFF;
        }
        seed[0] = rnd & 0xFF;
        seed[1] = seed[1] ^ (rnd >> 8);

        // if there is at least one non-zero byte in the sequence, where done!
        if (seed[0] || seed[1] || seed[2]) {
            break;
        }
    }

    // fallback to a default value of no sequence could be generated
    if ( ! seed[0] && ! seed[1] && ! seed[2]) {
      seed[0] = 0x03;
      seed[1] = 0x1D;
      seed[2] = 0x65;
    }
}
You can test this code by creating a C source this this, appended with the function above:

Code: Select all

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>

uint16_t timerValue();
void     createRandomSeed(uint8_t *);

int main()
{
    uint8_t seed[3];
    time_t t;

    // init random generator
    srand((unsigned) time(&t));

    // create the seed bytes
    createRandomSeed(seed);

    printf("Random SEED: %02x %02x %02x\n", seed[0], seed[1], seed[2]);
    return 0;
}

/**
 * Return a 16 bit random value
 */
uint16_t mscan_getTimerValue()
{
    //uint32_t *ptr = 0xFC0A000A;
    //return &ptr & 0xFFFF;
    return rand() % 0x10000; // simulation of FlexCAN timer
}
It will run an give you a nice 3 byte random sequence every time.

Conclusions

Now, this is not thaaat advanced but it shows how reverse engineering can be done. It also shows us how the firmware is doing it and that it really creates a RANDOM seed, not seeds of a fixed sequence out of an array. This way it is also clear that the security access function does not care which are the random bytes, nor how they where created. To in fact you could choose any other way to simulate that. An so we could also patch the firmware to simply use the same random seed sequence every time ;-)

Re: UDS-Handler in Convers+ Firmware

Posted: 09 Mar 2023, 18:52
by Go4IT
The next routine i go into is the one that calls the above random seed generator. This can easily be found in IDA with the backref list function. There is only one other function which calls the generator. Let's see if we find out what it's doing. Disassembled code first:

Code: Select all

sub_3726:
 1 LDR             R1, =byte_400017F8
 2 MOVS            R0, #0
 3 STRB            R0, [R1]
 4 LDR             R1, =byte_400017F9
 5 MOVS            R0, #0xFF
 6 STRB            R0, [R1]
 7 BX              LR

loc_36D6:
 1 POP             {R4-R6}
 2 POP             {R3}
 3 BX              R3

uds_generateSeed:
 1 PUSH            {R4-R6,LR}
 2 LDR             R4, =uds_msg
 3 LDR             R2, =byte_400017FA
 4 LDR             R0, [R4]
 5 LDRB            R1, [R0,#1]
 6 ADDS            R1, #1
 7 LSRS            R1, R1, #1
 8 STRB            R1, [R2]
 9 CMP             R1, #1
10 BEQ             loc_3750
11 BL              sub_3726
12 MOVS            R0, #6

loc_374E:
13 B               loc_36D6

loc_3750:
14 LDR             R2, =byte_400017F8
15 MOVS            R6, #4
16 LDRB            R1, [R2]
17 LDR             R5, =uds_responseLength
18 LDR             R3, =byte_400017F9
19 CMP             R1, #2
20 BNE             loc_3772
21 LDRB            R1, [R3]
22 CMP             R1, #1
23 BNE             loc_3772
24 MOVS            R1, #0
25 STRB            R1, [R0,#2]
26 STRB            R1, [R0,#3]
27 STRB            R1, [R0,#4]
28 STRH            R6, [R5]
29 MOVS            R0, #4
30 B               loc_374E

loc_3772:
31 MOVS            R0, #1
32 STRB            R0, [R2]
33 STRB            R0, [R3]
34 BL              generateRandomSeed ; This function generates 3 random bytes in RAM at 0x400017F0-0x400017F2
35 LDR             R0, =randomSeedSequence
36 LDR             R2, [R4]
37 LDRB            R1, [R0]
38 STRB            R1, [R2,#2]
39 LDRB            R1, [R0,#(byte_400017F1 - 0x400017F0)]
40 STRB            R1, [R2,#3]
41 LDRB            R0, [R0,#(byte_400017F2 - 0x400017F0)]
42 STRB            R0, [R2,#4]
43 STRH            R6, [R5]
44 MOVS            R0, #3
45 B               loc_374E

Re: UDS-Handler in Convers+ Firmware

Posted: 12 Mar 2023, 14:41
by Go4IT
No let's pick the final enemy:

The secret key calc algorithm

This is what IDA make:

Code: Select all

v1 = 1;
  v2 = 0;
  uds_calcKey_1 = 8;
  uds_calcKey_2 = 0x30;
  uds_calcKey_3 = 0x61;
  uds_calcKey_4 = 0x55;
  uds_calcKey_5 = 0xAAu;
  uds_calcKey_h1 = 0xA9u;
  uds_calcKey_h2 = 0x41;
  uds_calcKey_h3 = 0xC5u;
  v3 = 0;
  do
  {
    nullsub_4538();
    v4 = 1;
    if ( !((unsigned __int8)randomSeedSequence[v2] & (unsigned __int8)v1) )
      v4 = 0;
    v5 = (unsigned __int8 *)(v4 ^ uds_calcKey_h1 & 1);
    if ( v1 == 128 )
    {
      v1 = 1;
      v2 = (v2 + 1) & 0xFF;
    }
    else
    {
      v1 = 2 * v1 & 0xFF;
    }
    v6 = (unsigned int)(unsigned __int8)uds_calcKey_h1 >> 1;
    uds_calcKey_h1 = (unsigned __int8)uds_calcKey_h1 >> 1;
    if ( (unsigned __int8)uds_calcKey_h2 << 31 )
      uds_calcKey_h1 = v6 | 0x80;
    v7 = (unsigned int)(unsigned __int8)uds_calcKey_h2 >> 1;
    uds_calcKey_h2 = (unsigned __int8)uds_calcKey_h2 >> 1;
    if ( (unsigned __int8)uds_calcKey_h3 << 31 )
      uds_calcKey_h2 = v7 | 0x80;
    v8 = ((unsigned int)(unsigned __int8)uds_calcKey_h3 >> 1) | v5[0x4615];
    uds_calcKey_h1 ^= v5[0x461B];
    uds_calcKey_h2 ^= v5[0x4619];
    uds_calcKey_h3 = v8 ^ v5[0x4617];
    v3 = (v3 + 1) & 0xFF;
  }
  while ( v3 < 0x40 );
  v9 = (unsigned __int8)uds_calcKey_h1;
  v10 = uds_calcKey_h2;
  *v13 = ((unsigned __int8)uds_calcKey_h1 >> 4) | 16 * uds_calcKey_h2;
  v11 = uds_calcKey_h3;
  v13[1] = v10 & 0xF0 | ((unsigned __int8)uds_calcKey_h3 >> 4);
  result = v11 & 0xF | 16 * v9;
  v13[2] = result;
  return result;
}
This is the machine code, i'd like to disassemble myself:

Code: Select all

uds_calcKey
    PUSH            {R6,R7,LR}
    PUSH            {R0,R4,R5}
    MOVS            R4, #1
    MOVS            R6, #0
    LDR             R1, =randomSeedSequence
    MOVS            R0, #8
    STRB            R0, [R1,#(uds_calcKey_1 - 0x400017F0)]
    MOVS            R0, #0x30
    STRB            R0, [R1,#(uds_calcKey_2 - 0x400017F0)]
    MOVS            R0, #0x61
    STRB            R0, [R1,#(uds_calcKey_3 - 0x400017F0)]
    MOVS            R0, #0x55
    STRB            R0, [R1,#(uds_calcKey_4 - 0x400017F0)]
    MOVS            R0, #0xAA
    STRB            R0, [R1,#(uds_calcKey_5 - 0x400017F0)]
    MOVS            R0, #0xA9
    SUBS            R3, R1, #3
    STRB            R0, [R3]
    MOVS            R0, #0x41
    STRB            R0, [R3,#(uds_calcKey_h2 - 0x400017ED)]
    MOVS            R0, #0xC5
    STRB            R0, [R3,#(uds_calcKey_h3 - 0x400017ED)]
    MOVS            R5, #0
loc_3264:
    BL              nullsub_4538
    LDR             R1, =randomSeedSequence
    MOVS            R2, #1
    LDRB            R1, [R1,R6]
    MOVS            R0, R2
    ANDS            R1, R4
    BNE             loc_3276
    MOVS            R0, #0
loc_3276:
    LDR             R3, =randomSeedSequence
    SUBS            R3, #3
    LDRB            R1, [R3]
    ANDS            R2, R1
    EORS            R0, R2
    CMP             R4, #0x80
    BNE             loc_328E
    MOVS            R4, #1
    ADDS            R6, #1
    LSLS            R6, R6, #0x18
    LSRS            R6, R6, #0x18
    B               loc_3292
; ---------------------------------------------------------------------------
loc_328E:
    LSLS            R2, R4, #25
    LSRS            R4, R2, #24
loc_3292:
    LSRS            R2, R1, #1
    STRB            R2, [R3]
    LDRB            R1, [R3,#(uds_calcKey_h2 - 0x400017ED)]
    LSLS            R7, R1, #31
    BEQ             loc_32A2
    MOVS            R7, #0x80
    ORRS            R2, R7
    STRB            R2, [R3]
loc_32A2:
    LSRS            R2, R1, #1
    STRB            R2, [R3,#(uds_calcKey_h2 - 0x400017ED)]
    LDRB            R1, [R3,#(uds_calcKey_h3 - 0x400017ED)]
    LSLS            R7, R1, #31
    BEQ             loc_32B2
    MOVS            R7, #0x80
    ORRS            R2, R7
    STRB            R2, [R3,#(uds_calcKey_h2 - 0x400017ED)]
loc_32B2:
    LDR             R2, =0x4615
    LSRS            R1, R1, #1
    LDRB            R2, [R2,R0]
    ORRS            R1, R2
    LDR             R7, =0x4615
    LDRB            R2, [R3]
    ADDS            R7, #6
    LDRB            R7, [R7,R0]
    EORS            R2, R7
    STRB            R2, [R3]
    LDR             R7, =0x4615
    LDRB            R2, [R3,#(uds_calcKey_h2 - 0x400017ED)]
    ADDS            R7, #4
    LDRB            R7, [R7,R0]
    EORS            R2, R7
    STRB            R2, [R3,#(uds_calcKey_h2 - 0x400017ED)]
    LDR             R2, =0x4615
    ADDS            R2, #2
    LDRB            R0, [R2,R0]
    EORS            R1, R0
    STRB            R1, [R3,#(uds_calcKey_h3 - 0x400017ED)]
    ADDS            R5, #1
    LSLS            R5, R5, #0x18
    LSRS            R5, R5, #0x18
    CMP             R5, #0x40
    BCC             loc_3264
    LDR             R3, =randomSeedSequence
    SUBS            R3, #3
    LDRB            R1, [R3]
    LSRS            R2, R1, #4
    LDRB            R0, [R3,#(uds_calcKey_h2 - 0x400017ED)]
    LSLS            R4, R0, #4
    ORRS            R2, R4
    LDR             R4, [SP,#0x18+var_18]
    STRB            R2, [R4]
    LDRB            R2, [R3,#(uds_calcKey_h3 - 0x400017ED)]
    LSRS            R3, R2, #4
    MOVS            R4, #0xF0
    ANDS            R0, R4
    LDR             R4, [SP,#0x18+var_18]
    ORRS            R0, R3
    STRB            R0, [R4,#1]
    LSLS            R0, R2, #0x1C
    LSRS            R0, R0, #0x1C
    LDR             R4, [SP,#0x18+var_18]
    LSLS            R1, R1, #4
    ORRS            R0, R1
    STRB            R0, [R4,#2]
    POP             {R3-R7}
    POP             {R3}
    BX              R3
The resulting code in C:

Code: Select all

// uds_seed it a pointer to bytes send to tester
void calcKey(uint8_t *uds_seed)
{
  uint8_t uds_keyV[5] = { 0x08, 0x30, 0x61, 0x55, 0xAA };
  uint8_t uds_keyH[3] = { 0xA9, 0x41, 0xC5 };
  uint8_t R6 = 0;
  uint8_t R4 = 1;

  int i = 0;
  do
  {
    R2 = 1;
    if ( ! uds_seed[R6] & R4) {
      R0 = 0;
    } else {
      R0 = 1;
    }
    R0 = R0 ^ (uds_keyH[0] & 1);
    if (R4 == 0x80) {
      R4 = 1;
      R6 = (R6 + 1) & FF;
    } else {
      R2 = R4 << 24;
      R4 = R2 >> 24;
    }
    R2 = R1 >> 1;
    uds_seed[0] = R2;
    R1 =uds_seed[1];
    if ( (R1 & 0x01) != 0 ) {
      uds_seed[0] = R2 | 0x80;
    }
    R2 = R1 >> 1;
    uds_seed[1] = R2
    R1 = uds_seed[2];
    if ( (R1 & 0x1) != 0 ) {
      uds_seed[1] = R2 | 0x80;
    }

    R2 = 0x4615;
    R1 = R1 >> 1; // R1 = R1 / 2
    R2 = R2[R0];
    R1 = R1 | R2;
    
    R7 = 0x4615;
    R2 = uds_seed[0];
    R7 = R7 + 0x06;
    R7 = R7[R0];
    R2 = R2 ^ R7;
    uds_seed[0] = R2;

    R7 = 0x4615;
    R2 = uds_seed[1];
    R7 = R7 + 0x04;
    R7 = R7[R0];
    R2 = R2 ^ R7;
    uds_seed[1] = R2;

    R2 = 0x4615;
    R2 = R2 + 2;  
    R0 = R2[R0];
    R1 = R1 ^ R0;
    uds_seed[2] = R1;

    i++;
  }
  while (i < 0x40);

  R1 = uds_keyH[0];
  R2 = R1 >> 4;
  R0 = uds_keyH[1];
  R4 = R0 << 4;
  R2 = R2 | R4;
  R4 = &SP; // load R4 with pointer at SP (var_18 - 18)
  *R4 = R2;

  R2 = uds_keyH[2];
  R3 = R2 >> 4;
  R4 = 0xF0;
  R0 = R0 & R4;
  R4 = &SP;
  R0 = R0 | R3;
  R4[1] = R0;
  R0 = R2 & 0x0F;

  R4 = &SP;
  R1 = R1 << 4;
  R0 = R1 | R1;
  R4[2] = R0;
  return;
}
00004615                 DCB    0
00004616                 DCB 0x80
00004617                 DCB    0
00004618                 DCB 0x10
00004619                 DCB    0
0000461A                 DCB 0x90
0000461B                 DCB    0
0000461C                 DCB 0x28 ; (
0000461D                 DCB    4
0000461E                 DCB    0
0000461F                 DCB    0
00004620                 DCB    0
00004621                 DCB 0x50 ; P
00004622                 DCB    0
00004623                 DCB 0x83