This is Part-2 of a 3-Part Series. Check out Part-1 here and Part-3 here.
This is the second in a series of three posts in which I break down the creation of a unique key fob badge for the 2024 Car Hacking Village (CHV). Part 1 is an overview of the project and the major components; I recommend you begin there. In this post, I’ll discuss some of the software aspects and the reasoning behind certain decisions.
This blog covers several high-level subjects, including UHF and LF transmissions, receiving signals, modulation schemes like OOK/ASK, basic signal processing within a microcontroller, and the C programming language. While this post isn’t meant to be a tutorial on any specific technology, protocol, or component, if you’re interested, it’s definitely worthwhile exploring the various topics I’ll touch on here.
Let’s dive in. As mentioned in Part 1, the key fob transmits using UHF and receives using LF. It also supports CAN and features two physical touch sensor buttons. In addition to the discussion that follows, there are intentional bugs in the software, so I recommend checking it out for a closer look.
UHF transmissions
The raw format of a typical UHF frame doesn’t play nicely with standard serial communication, which includes start and stop bits. This made it crucial to choose an interface that matched the physical signaling requirements for transmitting UHF packets.
In Part 1, I mentioned using the MICRF113 UHF transmitter, which operates with OOK (on-off keying). The MICRF113 transmits whatever speed or state is sent to the DATA signal. This meant I needed to send a simple bit stream from the microcontroller to the MICRF113, without start or stop bits or gaps between bytes. For this I chose a Serial Peripheral Interface (SPI).
SPI is a synchronous serial communication protocol used for short-distance communication, primarily in embedded systems. It uses a controller/peripheral architecture with a single controller. The typical physical signals used in SPI include:
Clock (SCLK): Synchronizes the data transmission.
Data Out, or Peripheral In Controller Out (PICO): The line for sending data from the controller to the peripheral.
Data In, or Peripheral Out Controller In (POCI): The line for sending data from the peripheral to the controller.
Chip Select (CS): Selects the peripheral device.
In my case, I only needed the Data Out line to send the bit stream to the MICRF113. This setup allowed for a continuous stream of data without the interruptions caused by start or stop bits, making it ideal for UHF transmission.
Figure 1, courtesy of SparkFun, illustrates the typical physical signals used in SPI:
For more detailed information on SPI, you can refer to the SparkFun SPI Tutorial.
By choosing SPI, I ensured that the physical aspect of signaling aligned perfectly with how I wanted the UHF packet to be transmitted, resulting in a more efficient and reliable communication setup.
One of the great things about most microcontrollers is the flexibility to assign physical pins to specific functions. In this case, we’re dealing with the signals SCK, OUT, IN, and CS.
I took a bit of a creative approach here. Instead of assigning pins to SCK, IN, or CS, I left them unallocated. This means that whenever I write to the SPI bus, the only physical signal coming from the microcontroller is via the OUT pin. This pin toggles high and low according to the bit stream I send out.
This “hacky” method simplifies the setup and ensures that the data stream is transmitted exactly as needed, without any unnecessary signals getting in the way. It’s a neat trick that leverages the microcontroller’s flexibility to achieve a clean and efficient communication setup.
The frame format of the UHF packet is structured as follows: {pre-amble}{frame counter}{length of payload}{variable length data}.
- Pre-amble: This is a sequence of bits sent at the beginning of the packet to help the receiver synchronize with the incoming data stream.
- Frame counter: This is a value that increments with each packet sent, helping to keep track of the sequence of packets.
In the code snippet below, you’ll see the pre-amble being set on line 73 as hex values 55, 55, 54. As a bitstream, this translates to 0101 0101 0101 0101 0101 0100. Notice the two zeros at the end, which are crucial for synchronization.
On line 76, the frame counter is added to the byte stream, although it’s calculated earlier on line 65. This counter ensures each packet is uniquely identifiable. Next, on line 79, the length of the payload is appended to the byte stream. This tells the receiver how much data to expect.
Finally, on line 82 through 86, the payload itself is added byte by byte to the output array. This is where the actual data is transmitted. Or is it? This code represents a simple obfuscation. How would you go about recovering the original payload data if you received this UHF transmission?
Take a close look at the stack-defined buffer in this function. Can you spot the calculation error? It’s a great exercise to understand not only how these elements come together to form a complete UHF packet, but also how easily security vulnerabilities can occur in code.
POST (Power On Self Test)
It’s important to remember that this badge was created for the DEFCON 2024 security conference, and the amazing team at the CHV had to program these badges before they were sold. But how could they ensure the badges were working correctly? The simple answer: a Power On Self Test (POST).
Two key functions of the badge are tested in the code snippet below. First, on line 124, the output on Port B5 is turned on. This signal, as mentioned in the previous blog, is directly connected to the LED on the board via a resistor. This LED serves as a general status indicator.
On line 126, there’s a remark indicating that the loop will run 8 times with a delay, transmitting a test message each time. The test message, defined on line 129, is the word “TESTING”. Immediately following this, on line 130 the send function transmits the test packet. Keep in mind, the send function processes the payload, so you won’t see the actual “TESTING” message being directly transmitted.
Lines 133 through 136 introduce a delay. After the delay, on line 139 the Port B5 output is XORed with itself, meaning it will change state with each loop iteration. This results in the LED flashing while the test UHF transmissions are performed. Finally, after the self-test, on line 146 the LED is turned off by setting the state of Port B5 to 0.
This POST ensures that the badge is functioning correctly and could be tested by the CHV team before it reaches the hands of DEFCON attendees, providing a reliable and engaging experience for all participants.
Main program code
Now we get to the heart of the code. I saved this part for last because it’s more involved (well, nearly last; there is one final aspect, but I will get to that). This isn’t the most elegant piece of code, but as I explained in Part 1, time was a big constraint on the project. The code works, but it was written in a matter of days and definitely has room for improvement.
The main part of the code polls the ADC (line 152), which is connected via an internal op-amp to the LF antenna. The ADC is configured to sample around 250,000 samples per second. Sometimes, due to the nature of the code—like checking button states—the actual sampling rate drops below that, but it’s still high enough to capture the various energized states of the LF antenna.
The LF signal is alternating current (AC), meaning it alternates above and below zero. Components in the schematic shift this offset so the actual received signal at the microcontroller is always within the range of 0V to 3.3V.
On line 157, this AC offset is added back into the sample so that the LF signal is now measured as positive and negative amplitude, and the value is stored in lf_coil_val. Figure 4, courtesy of Wikipedia, shows an example AC signal on the left. The middle schematic is a full-wave bridge rectifier, and the image on the right is the rectified signal.
This section of the code is crucial for ensuring accurate signal processing, and it demonstrates the complexity and slightly novel approach involved in the project.
How does this apply to our LF signal? If you recall from the previous blog, the LF signal at the antenna looks much like the left image. The reason there’s no bridge rectifier on the PCB is that the signal we’re dealing with is often too small for a bridge rectifier to handle effectively. Instead, I’ve implemented an equivalent in software after amplifying the signal using an onboard op-amp.
A rectifier is an electrical device that converts AC, which periodically reverses direction, to direct current (DC), which flows in only one direction. This process is essential for detecting the amplitude of the LF signal.
To detect the amplitude, which is ultimately what we need, the first step is to rectify the signal on line 158 by multiplying it by itself. This transforms the signal into the image on the right side of Figure 4. From a signal processing perspective, the actual signal is the square of that, so the peaks are much higher.
Remember that all I want to do is determine a high state and a low state, which form the basis of receiving 1s and 0s. This software-based approach allows us to accurately process even the smallest signals, ensuring reliable reception.
Line 161 provides a simple IIR averaging filter to the amplitude data we now have following line 158. The variable ‘delta_lf_coil_val’ now contains a filtered signal strength value, which we must convert into binary. Line 163 compares the variable to our threshold value. If it is higher than this, we have a 1, so count the number of cycles in which it remains a 1. The more cycles, the more symbols of 1 we have. Each symbol is many cycles wide, which is how the code determines how many symbols of 1 are present.
If the filtered signal strength variable ‘delta_lf_coil_val’ is less than the threshold, we have a 0. Again, count the number of cycles in which the value stays below the threshold. On every change of state from 1 to 0 and 0 to 1, the code calculates how many of the changed symbols are present. These counts of number of cycles per state are stored on line 171 in the code above, and 188 in the code below.
To detect the end of a frame, the code on line 194 looks for a large number of 0 symbols. If sufficient 0 symbols are present, the code can continue with the checks, form a message, and ultimately send a UHF frame on line 225.
Line 224 builds the payload; however, prior to that, line 216 through 220 are where the various counts are converted into the actual 1’s and 0’s.
What this code essentially does is receive an LF frame. If it passes some basic checks, it then transmits the LF frame using an obfuscation algorithm and a secret value. This code is a simplified version of how some key fob systems work. It’s designed for some fun capture-the-flag opportunities and as a platform for building cool systems. However, it also demonstrates some basic principles used in passive entry and passive start systems.
Now imagine replacing the obfuscation algorithm with a proper encryption algorithm and a pre-shared key known only to the key fob and the vehicle. You’d have a simple yet effective way to determine which key fob is nearby. Extrapolate this to a real-world scenario, and you can start to see the basics behind these technologies.
This code serves as a great starting point for understanding and experimenting with the fundamental concepts of secure communication in key fob systems. It’s a blend of practical application and a bit of playful exploration, perfect for anyone looking to dive into the world of passive entry and start systems.
Touch Buttons
The last aspect of this code I’ll cover here is that for the touch buttons. In Part 1 I discussed the touch buttons implemented using two circular PCB traces. A tiny amount of electricity flows across your skin when you touch them, and the microcontroller detects that electricity, turning it into an on or an off.
To keep the main code running as fast as possible, the button testing code runs periodically. When the code does run, it checks the AN3 analogue input on line 245, it applies a simple filter on line 246, and if the filtered value is above the dynamic threshold on line 249, it runs the transmit code, where is builds a packet with the payload of “UNLOCK”. This word forms one of the CTF challenges. Remember, the send function obfuscates the payload, so you would have to first work out the obfuscation algorithm and the secret value in order to recover the flags sent by the key fob.
I hope this was interesting and informative. What I love about this sort of project is solving a problem using a generic microcontroller. To produce these in volume, the design would have to change, but for the purpose of a fun platform the approach really works, providing a means to interact with systems that are usually not so accessible.
That brings us to the end of the second of our three-part look at IOActive’s CHV key fob badge. In Part 3, I’ll address how to interact with the badge using your computer by means of a software-defined radio.