DH.J
🗺️

⛳️ CTF

Practical Car Hacking CTF Teaser

Practical Car Hacking CTF Teaser - Write up

Donghyeon Jeong··221 min read

Volkswagen CAN Checksum (250pts)

The goal of this challenge is to compute the correct one byte checksum XX for the CAN message with payload XX0f0300. The flag is of the format CTF{XX}.

VW uses the Autosar CRC8 8H2F checksum. Before computing the CRC, the payload is extended by a "secret" byte based on the Arbitration ID. Part of this challenge is figuring out what this "secret" byte is based on some traffic logged from the car. For some messages the "secret" value depends on the value of the counter, but that is not the case for this message.

Example code to generate the CRC:

python

import crcmod

crc = crcmod.mkCrcFun(
    poly=0x100 + polynomial,
    initCrc=initial_value
    rev=False,
)

data = payload + secret_byte
checksum = crc(data) ^ 0xff

The following 15 messages were captured from the car. Note the first byte is the checksum of the message, then second byte contains a counter.

plain text

0 74000300
1 c1010300
2 31020300
3 84030300
4 fe040300
5 4b050300
6 bb060300
7 0e070300
8 4f080300
9 fa090300
10 0a0a0300
11 bf0b0300
12 c50c0300
13 700d0300
14 800e0300

The challenge requires calculating the correct CAN checksum for Volkswagen vehicles. It then reveals that Volkswagen uses the Autosar CRC8 8H2F CRC checksum, and this information can be found in the AutoSAR documentation as shown below.

AUTOSAR_SWS_CRCLibrary.pdf

Therefore, we can determine the polynomial and initial_value from the example code, which have hexadecimal values of 2F and FF respectively.

python

import crcmod

polynomial = b'\x2f'
initial_value= 'b\xff'

crc = crcmod.mkCrcFun(
    poly=0x100 + polynomial,
    initCrc=initial_value
    rev=False,
)

Continuing on, the challenge provides CAN log messages and explains that the first byte contains the checksum while the second byte contains the counter value.

We simply need to utilize the given CAN log messages and known information.

For example, in the case of the first message:

plain text

checksum_byte = b'\x74'
payload = b'\x00\x03\x00'

However, we cannot determine the secret_byte value used in the calculation from the data variable.

We can attempt a brute force attack within the valid hexadecimal range 0xFF

python

def FindSecretByte(checksum_byte: bytes, payload: bytes):
    secret_byte = 0x00
    for i in range(0xFF+1):
        data = payload + struct.pack('B', i)
        checksum = struct.pack('B', crc(data) ^ 0xFF)
        if checksum == checksum_byte:
            secret_byte = i
            break
    return secret_byte

Once we reach this point, we can obtain the secret_key value. Now, to solve the challenge, we need to use the given value XX0f0300, where the XX field represents a valid CRC checksum value.

Since we just wrote the code to calculate the secret_byte, we can use the provided CRC generation example code.

python

import crcmod

polynomial = b'\x2f'
initial_value= 'b\xff'

crc = crcmod.mkCrcFun(
    poly=0x100 + polynomial,
    initCrc=initial_value
    rev=False,
)

data = payload + secret_byte
checksum = crc(data) ^ 0xff

I organized the necessary code and wrote a solution to solve the challenge.

python

(env) dhjeong@dhjeong-vmx:~/Desktop/icanhack/crc$ cat ck.py 
import crcmod
import struct

polynomial = 0x2F
initial_value= 0xFF

crc = crcmod.mkCrcFun(
    poly=0x100 + polynomial,
    initCrc=initial_value,
    rev=False,
)

def FindSecretByte(checksum_byte: bytes, payload: bytes):
    secret_byte = 0x00
    for i in range(0xFF+1):
        data = payload + struct.pack('B', i)
        checksum = struct.pack('B', crc(data) ^ 0xFF)
        if checksum == checksum_byte:
            secret_byte = i
            break
    return secret_byte

def CalcCRC(secret_byte: bytes, payload: bytes):
    data = payload + secret_byte
    checksum = crc(data) ^ 0xFF
    return checksum

if __name__ == "__main__":
    checksum_byte = b'\x74'
    payload  = b'\x00\x03\00'
    secret_byte = FindSecretByte(checksum_byte, payload)
    secret_byte = struct.pack('B', secret_byte)

    # Valid CRC Calcuation
    payload = b'\x0f\x03\x00'
    checksum = CalcCRC(secret_byte, payload)
    print(f"CRC Checksum: {hex(checksum)}")

Hitag2 Keyfob ID (Part 1) (500pts)

This challenge contains a recording from a Keyfob featuring a Hitag2 cipher for RKE. The keyfob transmits a message containing a plaintext keyfob ID, counter and button followed by a MAC. Attached to this challenge you will find a SDR recording of 6 presses of the unlock button.

Use URH to decode the messages from the keyfob and figure out the keyfob ID. The flag is of the form CTF{keyfob id}, e.g CTF{536c8dab}

Refer to the following papers for more information on Hitag2 and the possible message structure.

---

This is a challenge about Keyfobs, specifically focusing on RKE (Remote Keyless Entry) systems.

The attached file provided for solving the challenge contains recorded unlock button signals that include Hitag2 encryption.

To learn more about Hitag2 in detail, I conducted preliminary research through several sources.

Hitag2 has been used in remote keyless entry protocols, and recently, vulnerabilities that allow signal cloning related to Hitag2 have been published through various research papers (Usenix).

First, I used URH (Universal Radio Hacker) to analyze the provided Keyfob signals.

Catptured signals from the keyfob

From the captured signal, it can be confirmed that the short and long signals occurring while pressing the unlock button 6 times are repeated several times, consistent with what was described in the challenge.

Catptured signals from the keyfob #2

A closer examination of the captured signal reveals a binary structure where the presence of a signal is represented as 1 and the absence as 0, which corresponds to the OOK (On-Off Keying) modulation scheme.

Example: OOK(On/Off Keying) Signal
Catptured signals from the keyfob #3

In order to obtain a more intuitive view of the signal, I modified the Autodetect parameters feature and signal viewer in URH.

URH Analysis

To decode the captured signal data, I performed the following steps: Keyfob signal transmission generally employs OOK (On-Off Keying) or ASK modulation in combination with Manchester encoding, so I chose the Manchester II type in URH and conducted protocol analysis.

Hitag2 (PCF7946) Message Strcture

Generally, the Hitag2 packet structure (based on PCF7946) consists of SYNC, UID, BTN, CNTRL, KS, and CHK fields. The criterion for identifying valid data is a correct CRC checksum. I developed a small verification script for this purpose.

python

def VerifyChecksum(data: str):
    hitag2_bytes = bytes.fromhex(data)
    print(f"Bytes: {data}")

    checksum = 0
    for i in range(2, 12):
        checksum ^= hitag2_bytes[i]
    if checksum == hitag2_bytes[12]:
        return (True, data)
    else:
        return (False, data)

if __name__ == "__main__":
    pkt = [
            "ffff0200472a1f3997444ce5b8",
            "ffff0200472c0738b62cf8bea8",
            "ffff03ff8e542741fe373d1858",
            "ffff0200472a03a05773cffb3c",
            "ffff0200472a03a47f7cb05590",
            "ffff020047d407519faa51bc50",
            "ffff0200472a03acdf7f7e4c62",
            "ffff0200472a03b0577d9e6e06"
    ]
    for i, packet in enumerate(pkt):
        is_valid, data = VerifyChecksum(packet)
        if is_valid:
            print(f"Checksum OK / Data: {data}")
Verifying Checksum

I then wrote code to extract the UID values from data with valid CRC checksums.

python

"""
SYNC(16) / UID(32) / BTN(4) / CNTRL(10) / KS(32) / CHK(8)
"""

def parse_hitag2_pkt(data: str):
    if len(data) != 26:
        raise ValueError(f"Invalid packet length...")

    try:
        hitag2_bytes = bytes.fromhex(data)
    except ValueError:
        raise ValueError(f"{data} is not hex byte")
    hitag2_bits = ''.join(format(byte, '08b') for byte in hitag2_bytes)
    
    # Parse UID
    uid = int(hitag2_bits[16:48], 2)
    print(f"UID: {uid:08X}")
    
if __name__ == "__main__":
 parse_hitag2_pkt("ffff0200472a03b0577d9e6e06")
image

The challenge asked for the Keyfob ID, so extracting the UID value is all that's needed.

Hitag2 Keyfob ID (Part 2) (1000pts)

This challenge contains a recording from a Keyfob featuring a Hitag2 cipher for RKE. The keyfob transmits a message containing a plaintext keyfob ID, counter and button followed by a MAC. Attached to this challenge you will find a SDR recording of 6 presses of the unlock button.

Use URH to decode the messages from the keyfob and figure out the keyfob ID, button and keystream. Use this to crack the (equivalent) key that's inside the keyfob. The flag is of the form CTF{key}, e.g. CTF{1d81e7e1a6fe}

---

This challenge follows on from Part 1 of the previous problem, using the same Keyfob signal. The difference here is that we need to actually determine the valid Key value of the Keyfob.

In order to crack the Key value, several additional fields must be extracted from the captured Keyfob signal. We will employ the hitag2crack tool as suggested by the challenge

Figure: ht2crack5opencl

To use the tool, it appears to require UID, nR, aR, nR2, and aR2 values. The challenge also mentions two pairs of IV and Keystream.

According to known papers, nR is referred to as IV, which is formed by concatenating the CNTRH field (0:18), CNTRL (10bit), and BTN (4bit), where CNTRH is at the MSB and BTN is at the LSB.

aR is actually the Keystream, and the cracking code indicates it will be inverted. It is simply 0x7FFFFFF XOR.

Based on known information, calculating the nR value is expected to follow this formula:

nR = CNTRH(18bit) + CNTRL(10bit) + BTN(4bit)

= (CNTRH << 14) | (CNTRL << 4) | BTN

Here, we assume the CNTRH field is 0. In practice, Hitag2 CNTRH values are often uninitialized or set to 0, and the cracking tool used here also operates under this assumption.

Therefore, for nR calculation, we use the following formula:

python

cntrh = 0x00000
nr = (cntrh << 14) | (cntrl << 4) | btn

The aR computation is straightforward. It involves extracting the 32-bit ks field from valid packets and applying an XOR operation. ks ^ 0xFFFFFFFF

python

ar = ks ^ 0xFFFFFFFF

I combined these elements to create the following code, which simply builds upon the previous UID extraction code.

python

btn = int(hitag2_bits[48:52], 2)
cntrl = int(hitag2_bits[52:62], 2)
ks = int(hitag2_bits[62:94], 2)
cntrh = 0x00000
nR = (cntrh << 14) | (cntrl << 4) | btn
aR = ks ^ 0xFFFFFFFF
print(f"UID: {uid:08X} / BTN: {btn:01X} / CNTRL: {cntrl:03X} / KS: {ks:08X}")
print(f"nR: {nR:08X} / aR: {aR:08X}")
image

I processed all valid packets from the captured Keyfob signal to extract UID, BTN, CNTRL, and KS fields from packets with valid checksums, and calculated nR (IV) and aR (Keystream) values using several mathematical formulas.

The only thing left now is to crack the key with the tool.

nr = 00000CE0, ar = EA209864

nr2 = 00000E91, ar = CA141C76

These two pairs are used in combination.

Successfully Cracking Key

AVTP Video (1000pts)

Attached to this challenge you will find a pcap captured from a G30 BMW. This capture was taken using a TAP on the automotive ethernet connection between the BDC (e.g. the gateway/BCM) and the rear view camera.

In this pcap you will first see SOME/IP traffic. When the car is put in reverse a video stream is started alongside the SOME/IP traffic. The goal of the challenge is to decode this video stream. The rear view camera is pointed at a piece of paper containing the flag.

---

image
image

Analysis of the provided pcap file using Wireshark shows SOME/IP packets as demonstrated below. Packets consisting of JPEG messages are also observable.

According to the challenge description, an Ethernet connection between the BDC and rear camera was used in a BMW vehicle model, and when the rear camera becomes active, a video stream starts together with SOME/IP communication.

That's why the packets look this way. To solve this challenge, you must first have knowledge of the AVTP protocol. Furthermore, understanding how BMW vehicles process rear camera footage would definitely be beneficial.

image
image

While analyzing the packets, I was able to discover several characteristics.

First, I found that when the video stream starts, the first and second packets contain 0x67 and 0x68 values respectively in some parts. I don't yet know what this means.


0000   03 01 a9 ee 10 75 44 0c ee 36 5e 46 81 00 a0 56
0010   22 f0 03 80 03 01 44 0c ee 36 5e 46 10 75 00 00
0020   00 00 02 00 00 01 05 59 00 00 7c 85 88 84 00 00

Additionally, once the packets containing 0x67 and 0x68 bytes conclude, there seems to be actual data stream beginning with 0x7C or 0x5C. I conducted a more detailed analysis of these bytes.


0000   03 01 a9 ee 10 75 44 0c ee 36 5e 46 81 00 a0 56
0010   22 f0 03 80 03 01 44 0c ee 36 5e 46 10 75 00 00
0020   00 00 02 00 00 01 05 59 00 00 7c 85 88 84 00 00

In practice, the packets show that JPEG stream initiation uses 0x7C85 bytes. Subsequently, until near the stream's conclusion, packets contain 0x7C05 bytes, while the final packet before stream termination displays 0x7C45 bytes.

Now let's examine another type that begins with 0x5C rather than 0x7C.


0000   03 01 a9 ee 10 75 44 0c ee 36 5e 46 81 00 a0 56
0010   22 f0 03 80 60 01 44 0c ee 36 5e 46 10 75 01 0f
0020   d0 ab 02 00 00 01 05 92 00 00 5c 81 88 88 00 10

I analyzed both byte cases, but they don't seem to match the known H.264 NAL Unit format.

For normal H.264 Stream messages, it looks like this:

00 00 00 01 67 ... / 00 00 00 01 68 ... / 00 00 00 01 65 ...

0x67: SPS (Sequence Parameter Set)

0x68: PPS (Picture Parameter Set)

0x65: Coded slice of an IDR Picture (IDR Slice)

Therefore, when a message corresponding to a 0x7C85 packet is received, it is expected that it should be modified to 0x0165 bytes. That leaves the remaining 0x5C case. Since I needed to understand what 5C means, I searched Google for related information.

H.264 includes Non-IDR slice Unit Types in addition to IDR slice. I believe that what's being used in 0x5C is likely associated with this unit type. Non-IDR slices consist of P-Slice and B-Slice, which correspond to hex values 0x61 and 0x41 respectively.

0x61: Non-IDR slice (P-slice)

0x41: Non-IDR slice (B-slice)

Based on this, the expected messages would be as follows:

00 00 00 01 67 ... / 00 00 00 01 68 ... / 00 00 00 01 41 ... (P-slice)

00 00 00 01 67 ... / 00 00 00 01 68 ... / 00 00 00 01 61 ... (B-slice)

Since it's generally indicated that P-slice Unit Type is used, for 0x5C as well, when a 0x5C81 packet is received, I will modify the bytes to 0x0141.

Using the information described above, I developed code that parses packets from the pcapng file, categorizes them, and converts them into H.264 video.

python

import struct
from scapy.all import *

def extract_h264(pcap_file, output="video.h264"):
    packets = rdpcap(pcap_file)
    data = b''
    extract_data = b''

    for i, packet in enumerate(packets):
        if packet.haslayer(Ether) and packet.haslayer(Dot1Q):
            dot1q = packet.getlayer(Dot1Q)
            if dot1q.type == 0x22F0: # AVTP Stream
                payload = bytes(dot1q.payload)
                seq = payload[2]
                stream_id = payload[4:12] # stream_id: 8byte
                stream_len = int.from_bytes(payload[20:22], byteorder='big') # stream_data_length: 2byte
                data = payload[24:24+stream_len]
                print(f"Index: {i} / Sequence: {seq} / Stream ID: {stream_id.hex()} / Length: {stream_len} bytes")

                # NAL Frame (0x67, 0x68 - Start Video)
                if data[0] == 0x67 or data[0] == 0x68:
                    data = b'\x00\x00\x00\x01' + data

                if data[0] == 0x7C: # IDR Slice
                    if data[1] == 0x85:
                        data = b'\x00\x00\x00\x01\x65' + data[2:]
                    if data[1] == 0x5 or data[1] == 0x45:
                        data = data[2:]
                
                if data[0] == 0x5C: # Non-IDR Slice
                    if data[1] == 0x81:
                        data = b'\x00\x00\x00\x01\x41' + data[2:]
                    if data[1] == 0x1 or data[1] == 0x41:
                        data = data[2:]

                extract_data += data # Valid AVTP Frame
                continue
            
            else:
                continue # Not AVTP Frame

    print(f"Saving Video ... {output}")
    with open(output, 'wb') as f:
        f.write(extract_data)
        f.close()

if __name__ == "__main__":
    extract_h264("./SOMEIP-002.pcapng")
Convert to video (ffmpeg)

After packet processing finishes, a video.h264 file is created, which can be converted to an mp4 media file with ffmpeg. (Processing time likely depends on your CPU speed.)

At last, once video conversion is complete, the flag can be verified.

(Note that the flag is reversed.)

Got Flag

References

- Autosar CRC specification:

- https://www.autosar.org/fileadmin/standards/R22-11/CP/AUTOSAR_SWS_CRCLibrary.pdf

- URH(Universal Radio Hacker) download:

- https://github.com/jopohl/urh/releases

- Introduction to hitag2

- https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final95.pdf

- Hitag2 as used in RKE, reference for message layout:

- https://www.usenix.org/system/files/conference/usenixsecurity16/sec16_paper_garcia.pdf

- Reference for cracking code, introduces "equivalent key":

- https://www.usenix.org/system/files/conference/woot18/woot18-paper-verstegen.pdf

- Hitag2 cracking code from proxmark3 . Use crack5 for CPU cracking, or crack5opencl for OpenCL based cracking:

- https://github.com/RfidResearchGroup/proxmark3/tree/master/tools/hitag2crack