Car (Key) Hacking (Not Really)

Here is the key fob for my 2000 Toyota Camry (possibly the best car ever made):

In this post, we’ll be decoding (not decrypting) how the fob interacts with the car.

Step Zero: Recon

Let’s kick things off by popping open the case and taking a look at the circuit board:

As we can see, the only chip on the board (aside from the RF IC) is a Microchip HCS361. Lucky for us, the datasheet for this part is easily available online: http://ww1.microchip.com/downloads/en/DeviceDoc/40146F.pdf.

We can see that the IC is a KeeLoq Code Hopping Encoder. KeeLoq is a block cipher that uses 32-bit blocks and a 64-bit key. Since we won’t be breaking it, I won’t go into the details of KeeLoq in this post.

Taking a look at the pinout of the chip, we see that pin 6 is the DATA pin:

Looking at the trace on the PCB, we can see correctly that it is fed as an input into the RF transmitter:

Step One: Getting a Trace

So first things first, let’s solder a wire onto the pin (and one onto GND) to see what sort of signal is being transmitted on DATA :

In order to write the Arduino sketch to read in the data, it’s helpful to look at the datasheet and see if it gives any clues as to what the signal might look like.

We can see that there are a few different transmission modes. Ignoring different possible baud rates and duty cycles, there are two main types of bit encodings used.

The first is straight PWM. For a review of PWM, you can take a look at my clap-controlled TV remote blog post.

The second is variable PWM or VPWM. This is similar to PWM except instead of modulating the duty cycle, we modulate the pulse length depending on the previous and current bits.

From Figure 9–8, we can see that the transmission will continuously alternate between high and low logic levels and the length of the pulse will be modulated as previously described.

Taking a look at the datasheet, we see that the the chip can run on anywhere from 2.0–6.6V. Using a multimeter to measure the voltage on the VDD pin, we see that it is indeed running on 6.6V. Therefore, we shouldn’t directly read from the DATA pin on the Arduino because a voltage of 6.6V could damage the pins meant to handle a max of 5V.

What we can do is remove the battery and run the chip on the Arduino’s 5V power supply. This way, there will be a 5V logical high level on the DATA pin and we will not damage the Arduino. (To do this, just solder another wire onto the VDD pin and connect to Arduino 5V out. I forgot to take a picture of this.)

Writing the Arduino Sketch

Let’s whip up a quick Arduino sketch to just do an analog read of the data pin using free-running ADC interrupts (see the clap-controlled remote post for info on Arduino fast ADC):

#define FOB_PIN A0void setup() {
Serial.begin(115200);
pinMode(FOB_PIN, INPUT);
// Setup ADC on A0
ADCSRA = (1<<ADEN) | (1<<ADATE) | (1<<ADPS2);
ADCSRB = 0;
ADMUX = 1<<REFS0;
// Start conversions
ADCSRA |= 1<<ADIE;
ADCSRA |= 1<<ADSC;
}
ISR(ADC_Vect) {
Serial.print(micros());
Serial.print(", ");
Serial.print(ADCL | (ADCH << 8));
Serial.println(";");
}
void loop() {}

Looking at the trace produced, we see a promising looking waveform (it looks like multiple transmissions are made on each button press):

However, zooming in on the first transmission:

we can see pulses that look too long to be valid. This is because Arduino ADC is slow. We are sampling slower than the Nyquist rate of the signal so we are getting aliasing.

Rethinking the sketch, I realized that it’s stupid to use the Arduino analog pins as input because the DATA pin is outputting 5V logical high. We can just perform a digitalRead!

We can remove all the ADC code and just add:

#define FOB_PIN 3...void loop() {
Serial.print(micros());
Serial.print(", ");
Serial.print(digitalRead(FOB_PIN);
Serial.println(";");
}

Now, looking at the trace…

It looks like we’re running into the exact same issue. Why is that?

The answer comes down to the serial port. Even running with a baud rate of 115200, the serial port is slow compared to the 16MHz clock frequency of the Arduino. Since we are performing multiple writes to the serial port on every loop, our effective clock frequency is way lower than 16MHz. Therefore, we’re still getting the aliasing that we saw before.

How do we solve this?

The answer is two-fold.

First, we will use pin change interrupts to sample the DATA pin. Pin change interrupts fire every time the logical level on a pin or set of pins changes. This way, we are not continually polling the pin when we don’t need to, we only get notified when a new sample is put onto the wire.

Second, we will use a circular buffer to store the samples generated in the interrupt.

In the main loop, we will write the generated samples to the serial port. Those who have taken an intro operating systems class will recognize this as a classic producer/consumer scenario.

We’ll also make some further optimizations. The first optimization stems from the fact that the pin’s value alternates on every pin change interrupt. Therefore, we don’t need to keep a buffer of samples and print them over serial, we just need to keep track of the timestamp of the change and calculate the level based on how many changes occurred before.

Another small optimization is that we don’t care about the absolute timestamp of each pin change; we only care about the duration of each pulse. Therefore, we can change the data type of our buffer from a long to an int and just keep track of the delta from the last timestamp.

#define NSAMPLES 256
#define FOB_PIN 3
unsigned int timeDiffs[NSAMPLES];
unsigned long lastTime = 0, currTime;
unsigned char prodIdx = 0, consIdx = 0;void setup() {
Serial.begin(115200);
pinMode(FOB_PIN, INPUT);
cli();
PCICR |= 4; // turn on PC interrupts for port d
PCMSK2 = 1 << fobPin; // enable PC interrupts for pin 3
sei();
}
ISR(PCINT2_vect) {
if (prodIdx - consIdx == NSAMPLES) {
Serial.println("dropped sample!");
PCICR ^= 4; // disable PC interrupts
return;
}
currTime = micros();
timeDiffs[prodIdx] = currTime - lastTime;
lastTime = currTime;

++prodIdx;
}
void loop() {
if (!(prodIdx - consIdx)) // wait until there are produced samples
return;
Serial.println(timeDiffs[consIdx]);
++consIdx;
}

Now, the main loop only prints to the console when there is a new sample generated or multiple samples buffered.

Also note that I am using a char overflow trick to make the buffer circular. Since the buffer has 256 elements and the producer and consumer indices are chars, if we continually increment them, they will overflow from 255 -> 0 and hence access the buffer in a circular manner. This way, we don’t have to do any modular arithmetic.

Now if we record a trace of the signal and plot it, we can see that the chip is using VPWM since some pulses are twice as long as others:

Now that we can reliably capture a transmission from the chip, we need to figure out what the transmission means. Referring back to the datasheet, we can see that there are two possible formats of the transmission:

Both formats share most of the same fields. The lower 32 bits are the KeeLoq ciphertext. The upper 32 bits consist of the serial number, some flags, and the CRC. The only difference is whether or not the button status is transmitted in the clear.

To determine which format is being used, we’ll capture two transmissions: one when pressing lock and one when pressing unlock. If the plaintext portion of the transmission changes, we know that we are using the first format since the button status will be different for lock and unlock. If it stays the same, we know we are using the second format and can parse the 32-bit extended serial number.

Taking a look at the plots of the two traces,

we can see that the last (upper) 32 bits are the same and therefore we are using the second format. It’s a little hard to see now but we’ll confirm this later.

Interpreting the Code Word

We can now write some code to read in the waveform and parse the different portions of the transmission.

To read the waveform, we iterate over the time differences and interpret the bit as a 1 or 0 depending on its position and whether it is 200us or 400us:

We can then parse the code word as described in the datasheet:

Finally, we can calculate the CRC and assert that we got a correct codeword.

The CRC is calculated based off the following equation given in the datasheet:

And here is the code for the calculation:

We can confirm that our processing is correct since the decoded serial numbers are the same for the lock and unlock codeword and their calculated CRC’s match the transmitted CRC:

The Same Thing But Now With RF

Now that we can obtain the code word using physically invasive methods, let’s see if we can do the same thing using just the RF output of the fob.

For this portion of the project, we’ll be using the radio noob’s favorite SDR, the RTL-SDR. Let’s use the GQRX’s rtl_sdr command line tool to read the raw IQ data for a few code words.

Before we continue, a short explanation of IQ data is in order. IQ data is the output format of (any?) SDR. It represents each sample with a real (I) and imaginary (Q) component.

I, the in-phase component is the portion of the sinusoid described by that sample that is in phase with the time of the sample.

Q, the quadrature component is named as such because it represents the sinusoid that is pi/2 (2*pi/4 hence quad-rature) out of phase with the sinusoid described by the I component.

They are real and imaginary components because Euler’s identity states that e^(i*theta) = cos(theta) + i*sin(theta) and since sin(theta) = cos(theta+pi/2), we can represent in phase and quadrature sinusoids with a complex number.

I realize that probably didn’t make much sense so if anyone has a more elegant/accurate explanation, please comment on this post.

Analyzing the Signal

Anyways, for each sample output by the SDR, we get a complex number. We can then perform a FFT on the signal and analyze the waveform of the carrier frequency. Looking at the FCC ID for this fob (GQ43VT12T), we can see that like every other rolling code transmitter, it’s carrier frequency is 315MHz.

Let’s confirm this by looking at the spectrogram and spectral density:

The peak power is a little off but that could just be an artifact of our FFT size.

Now just because the microcontroller drives the RF transmitter with a VPWM signal doesn’t necessarily mean that the RF signal uses VPWM as well. Therefore, we’ll have to take a closer look at one of the packets and see what type of modulation is used:

It looks like the RF signal is indeed using VPWM because some of the pulses look twice as long as others. In addition, there is no consistent duty cycle so the signal can’t be straight PWM.

Demodulating the Signal

Now someone who’s experienced with radio would probably use GNURadio and construct a flow graph to perform the demodulation. However, after a multi-hour nightmare of trying to set up GNURadio on my Mac, I decided to give up and roll my own.

This took some trial and error to figure out but I’ll now walk through the final result.

We start with our trimmed signal:

Then we perform a sliding window FFT on the signal. This is followed by computing the power of the FFT bin that is closest to our carrier frequency:

As a result, we get a sequence of noisy samples:

Which we can threshold to get a clean pulse train:

Finally, we can perform the VPWM decoding:

After demodulation and decoding, we can parse the code word:

We can see that we get the same serial number as before and the CRC is correct so we can assume that we decoded the signal correctly.

Trying (and failing) to Get the Key

It’s cool and all to be able to capture and replay codewords but what I really want to do is obtain the key so I can:

  1. Decrypt the codeword to get the rolling code counter
  2. Clone the fob

As far as I know, there have been no practical attacks against the KeeLoq cipher using just obtained ciphertexts.

However, there is a power analysis attack that can reliably infer the key with a few hundred power traces: https://eprint.iacr.org/2008/058.pdf.

I briefly tried this attack with the Arduino but the 77KHz ADC is far too slow to perform this attack in a reasonable number of traces.

I currently have plans to construct a cheap circuit that can perform 20Msps traces. Hopefully, I will be able to build this and write a blog post on it in a few months. However, if someone in the Columbus, OH area has an oscilloscope or ChipWhisperer that they want to lend me, that would greatly speed up the process.

Thanks for reading and let me know what you think!

Computer science enthusiast

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store