
I mainly focus on Hardware challenges, those challenge is very interesting.
CAN BUS log challenge
The CAN bus log is here: attached.
lost-in-madras
I recently visited Madras… and I got carjacked T.T
Luckily, I had a wireless OBD adapter connected to my car through which I was able to obtain the CAN log for the whole journey. It’s so unfortunate that I can’t make sense of the log.
- To confirm that it is mine, find the VIN for my car.
- Also, can you help me find where my car ended up?
Example flag: bi0s{1FTFW1R6XBFB08616_Eiffel Tower}
The task is clear: find the VIN and the latest location of the car. The log is too large, and it’s structure is:
+-----------------+----------+-------------+----------------+
| CAN Interface | CAN-ID | MSG Length | MSG |
+-----------------+----------+-------------+----------------+
Finding the VIN
Since the VIN is a 17-character alphanumeric string, it contains information about the vehicle such as the model, country, year… I write a string_dumper
for this task.
import sys
def get_all_CANID(data):
can_id_arr = []
for i in data:
temp = i.strip().split(" ")
can_id = int(temp[1].strip(), 16)
if can_id not in can_id_arr:
can_id_arr.append(can_id)
return can_id_arr
def get_message_by_can(data, can_id):
count = 0
for i in data:
temp = i.strip().split(" ")
can = int(temp[1], 16)
if can == can_id:
count += 1
size = int(temp[2].replace("[","").replace("]",""))
msg = temp[-1].split(" ")
dump_string = ""
for j in msg:
char = int(j, 16)
if char > 0x20 and char < 0x7f:
dump_string += chr(char)
else:
dump_string += "."
if size == 8:
print(i + "\t" + dump_string)
elif size == 7:
print(i + "\t\t" + dump_string)
elif size >= 5 and size < 7:
print(i + "\t\t\t" + dump_string)
elif size == 4:
print(i + "\t\t\t\t" + dump_string)
elif size == 3:
print(i + "\t\t\t\t\t" + dump_string)
elif size < 3:
print(i + "\t\t\t\t\t\t" + dump_string)
print("CAN-ID 0x%x - Total: %d" % (can_id,count))
print("")
with open("canlog.txt", "r") as file:
data = []
for line in file:
data.append(line.strip())
can_id_arr = get_all_CANID(data)
can_id_arr.sort()
if len(sys.argv) == 1:
print("[+] Dump all !!")
for i in can_id_arr:
print("0x%x" % i)
get_message_by_can(data, i)
else:
can_id = int(sys.argv[1], 16)
print("[+] Dump specific CAN-ID 0x%x" % (can_id))
get_message_by_can(data, can_id)
The author said that he used OBD-2
to dump this log. So I started looking for documentation about PIDs and CAN-IDs. The document stated:
- CAN-ID
0x7DF
: Broadcast ID, meaning all ECUs will listen and respond if the request is relevant to them. - CAN-ID
0x7E8
to0x7EF
: Response ID for the Engine Control Module (ECM), other modules may respond with0x7E9
to0x7EF
.
But in this log, we don’t see any CAN-IDs similar. So maybe it used a custom CAN (Oh man !! I hate this). The CAN-IDs 7xx
look quite interesting. As far as I’ve analyzed, I’ve realized the following:
- CAN-ID
0x733
: The request. - CAN-ID
0x73B
: The response.
Just aim the 0x73B
we found some interesting string.
vcan0 73B [8] 10 14 62 F1 90 31 46 4D ..b..1FM
vcan0 73B [8] 21 48 4B 37 44 38 32 42 !HK7D82B
vcan0 73B [8] 22 47 41 33 34 39 35 34 "GA34954
Seem like we found the VIN 1FMHK7D82BGA34954
. Look good right !! (you can look for the request contains F1 90
then pair it with the response).
NGL: first few hours I though this challenge was about some Honda car because the JHM
string. 💀
Finding the lastest location
We got the info of the car, we need to search for some DBC (CAN database) to translates “raw” CAN data into meaningful signals (like RPM, temperature,…). So I googled for Ford 2011 DBC
, it led me to this project.
We need to know about structure of a DBC File:
Component | Description |
---|---|
NS_ | Namespace for the messages and signals in a DBC file |
BS_ | Bus Speed (such as 500 kbit) |
BO_ | Message definition (CAN ID, name, size, sender) |
SG_ | Signal definition (name, position, length, byte order, scaling, etc.) |
BU_ | Node (ECU/module) definitions |
CM_ | Comments |
VAL_ | Enumerated values for signals (e.g., 0 = OFF, 1 = ON) |
BA_ | Attributes (metadata for messages, signals, nodes, etc.) |
There are some BO_
in the ford_cgea1_2_bodycan_2011.dbc
about GPS data:
BO_ 1125 GPS_Data_Nav_1: 8 XXX
SG_ GpsHsphLattSth_D_Actl : 25|2@0+ (1,0) [0|0] "" XXX
SG_ GpsHsphLongEast_D_Actl : 9|2@0+ (1,0) [0|0] "" XXX
SG_ GPS_Longitude_Minutes : 46|6@0+ (1,0) [0|0] "Minutes" XXX
SG_ GPS_Longitude_Min_dec : 55|14@0+ (0.0001,0) [0|0] "Minutes" XXX
SG_ GPS_Longitude_Degrees : 39|9@0+ (1,-179.0) [0|0] "Degrees" XXX
SG_ GPS_Latitude_Minutes : 15|6@0+ (1,0) [0|0] "Minutes" XXX
SG_ GPS_Latitude_Min_dec : 23|14@0+ (0.0001,0) [0|0] "Minutes" XXX
SG_ GPS_Latitude_Degrees : 7|8@0+ (1,-89.0) [0|0] "Degrees" XXX
BO_ 1126 GPS_Data_Nav_2: 8 XXX
SG_ Gps_B_Falt : 2|1@0+ (1,0) [0|0] "" XXX
SG_ GpsUtcYr_No_Actl : 55|5@0+ (1,1.0) [0|0] "Year" XXX
SG_ GpsUtcMnth_No_Actl : 47|4@0+ (1,1.0) [0|0] "Month" XXX
SG_ GpsUtcDay_No_Actl : 37|5@0+ (1,1.0) [0|0] "Day" XXX
SG_ GPS_UTC_seconds : 23|6@0+ (1,0) [0|0] "seconds" XXX
SG_ GPS_UTC_minutes : 15|6@0+ (1,0) [0|0] "Minutes" XXX
SG_ GPS_UTC_hours : 7|5@0+ (1,0) [0|0] "Hours" XXX
SG_ GPS_Pdop : 31|5@0+ (0.2,0) [0|0] "" XXX
SG_ GPS_Compass_direction : 26|4@0+ (1,0) [0|0] "" XXX
SG_ GPS_Actual_vs_Infer_pos : 38|1@0+ (1,0) [0|0] "" XXX
BO_ 1127 GPS_Data_Nav_3: 8 XXX
SG_ GPS_Vdop : 63|5@0+ (0.2,0) [0|0] "" XXX
SG_ GPS_Speed : 47|8@0+ (1,0) [0|0] "MPH" XXX
SG_ GPS_Sat_num_in_view : 7|5@0+ (1,0) [0|0] "" XXX
SG_ GPS_MSL_altitude : 15|12@0+ (10.0,-20460.0) [0|0] "feet" XXX
SG_ GPS_Heading : 31|16@0+ (0.01,0) [0|0] "Degrees" XXX
SG_ GPS_Hdop : 55|5@0+ (0.2,0) [0|0] "" XXX
SG_ GPS_dimension : 2|3@0+ (1,0) [0|0] "" XXX
BO_ 1144 GPS_Data_Nav_4: 8 XXX
SG_ VehPos_L_Est : 39|32@0+ (0.01,0) [0|0] "meter" XXX
SG_ VehHead_W_Actl : 23|16@0+ (0.01,-327.68) [0|0] "degrees/second" XXX
SG_ VehHead_An_Est : 7|16@0+ (0.01,0) [0|0] "degrees" XXX
The number followed by BO_
is the CAN-ID. In the log we have 0x465 (1125)
, it should be our GPS data. We still need to understand what signal will be sent. Lets examine few first line of 0x465
CAN-ID in the DBC file.
- BO_ 1125 GPS_Data_Nav_1: 8 XXX:
BO_
: Message Syntax.1125
: CAN-ID (dec).GPS_Data_Nav_1
: Name.8
: Length (Bytes).
- SG_ GpsHsphLattSth_D_Actl : 25|2@0+ (1,0) [0|0] "" XXX:
SG_
: Signal Syntax.GpsHsphLattSth_D_Actl
: Name.25|2
: Bit start | Length.@0
: Byte order,@0
for big-endian/Motorola,@1
for little-endian/Intel.+
: Unsigned,-
for signed.(1,0)
: (scale,offset), the values are used in the physical value linear equation.[0|0]
: [min|max], can be set to [0|0] - not defined.""
: Unit (such as “KPH”, “Degrees”,…) - None in this case.XXX
: Receiver node name.
Okay now we need to decode the CAN data for some GPS data. Lets examine the lastest 0x465
data:
vcan2 465 [8] 66 0D F4 48 1A 0E DD 00 f..H....
binary:
01100110 00001101 11110100 01001000 00011010 00001110 11011101 00000000
0 7 15 23 31 39 47 55
Write a script to extract those bit. Remember to use the (scale, offset)
, the formula is:
Then we calculate the latitude
and longitude
with this formula:
But it’s weird, very weird. I can’t get the correct lat/long, the result I got when I followed the bit offset in DBC file is (-81.96282, -164.24957)
. So maybe our DBC is wrong or something else ?? (this is the part that hold me).
After the CTF end, some ppl share about the correct DBC. So I try again !! This time it gives (13.07606, -126.85976)
, point to somewhere in the middle of the ocean.
Hmm thats weird, but we got the latitude
almost correct (Madras - (13.0843° N, 80.2705° E)
). I start guessing the bit position, we have 9 data:
vcan2 465 [8] 66 16 1A 08 1A 2B AE 00 f....+..
vcan2 465 [8] 66 15 06 08 1A 29 20 00 f....)..
vcan2 465 [8] 66 14 4C 08 1A 21 8B 00 f.L..!..
vcan2 465 [8] 66 12 55 88 1A 11 EC 00 f.U.....
vcan2 465 [8] 66 12 35 A8 1A 0A 00 00 f.5.....
vcan2 465 [8] 66 11 8E 68 1A 0F E2 00 f..h....
vcan2 465 [8] 66 10 D2 28 1A 09 7F 00 f..(....
vcan2 465 [8] 66 10 32 C8 1A 06 82 00 f.2.....
vcan2 465 [8] 66 0D F4 48 1A 0E DD 00 f..H....
CAN-ID 0x465 - Total: 9
You can see the first value 66
doesn’t change, because the car just moving arround Madras therefore the degrees won’t be changed, right ?? So I think, the first byte would be our GPS_Latitude_Degrees
. To extract the GPS_Latitude_Minutes
we simply take the GPS_Latitude_Degrees
bit offset, add it with 8 - the size of GPS_Latitude_Degrees
, do the same for GPS_Latitude_Min_dec
. To sum up:
- 0 -> 8:
GPS_Latitude_Degrees
- 8 -> 14:
GPS_Latitude_Minutes
- 14 -> 28:
GPS_Latitude_Min_dec
+------------------------+------------------------+-----------------------+---
| GPS_Latitude_Degrees | GPS_Latitude_Minutes | GPS_Latitude_Min_dec |...
+------------------------+------------------------+-----------------------+---
| 8-bytes | 6-bytes | 14-bytes |...
+------------------------+------------------------+-----------------------+---
0 8 14 28
For Longitude, you can see the X8 1A
won’t change, so I use the same way to extract it:
- 28 -> 37:
GPS_Longitude_Degrees
- 37 -> 43:
GPS_Longitude_Minutes
- 43 -> 57:
GPS_Longitude_Min_dec
---+--------------------------+-------------------------+------------------------+---
...| GPS_Longitutde_Degrees | GPS_Longitude_Minutes | GPS_Longitude_Min_dec |...
---+--------------------------+-------------------------+------------------------+---
...| 9-bytes | 6-bytes | 14-bytes |...
---+--------------------------+-------------------------+------------------------+---
28 37 43 57
The result:
[+] GPS_LAT_DEG = 13.00000
[+] GPS_LAT_MIN = 3.00000
[+] GPS_LAT_MIN_DEC = 0.80040
[+] GPS_LON_DEG = 80.00000
[+] GPS_LON_MIN = 16.00000
[+] GPS_LON_MIN_DEC = 0.76100
[+] Result: 13.06334, 80.279350
Script:
import binascii
GPS_lat_deg_bit_off = 0
GPS_lat_min_dec_bit_off = 14
GPS_lat_min_bit_off = 8
GPS_lon_deg_bit_off = 28
GPS_lon_min_dec_bit_off = 43
GPS_lon_min_bit_off = 37
def extract_bit(data, pos, size):
bin_data = ""
for i in data:
bin_data += format(i, "08b")
out = int(bin_data[pos : pos+size],2)
return out
def decode_gps(data):
gps_latitude_deg = extract_bit(data, GPS_lat_deg_bit_off, 8)
lat_deg = (1 * gps_latitude_deg) + (-89)
gps_latitude_min = extract_bit(data, GPS_lat_min_bit_off, 6)
gps_latitude_min = (1 * gps_latitude_min) + 0
gps_latitude_min_dec = extract_bit(data, GPS_lat_min_dec_bit_off, 14)
gps_latitude_min_dec = (0.0001 * gps_latitude_min_dec) + 0
gps_longitude_deg = extract_bit(data, GPS_lon_deg_bit_off, 9)
lon_deg = (1 * gps_longitude_deg) + (-179)
gps_longitude_min = extract_bit(data, GPS_lon_min_bit_off, 6)
gps_longitude_min = (1 * gps_longitude_min) + 0
gps_longitude_min_dec = extract_bit(data, GPS_lon_min_dec_bit_off, 14)
gps_longitude_min_dec = (0.0001 * gps_longitude_min_dec) + 0
latitude = lat_deg + (gps_latitude_min + gps_latitude_min_dec) / 60.0
longitude = lon_deg + (gps_longitude_min + gps_longitude_min_dec) /60.0
latitude = round(latitude, 5)
longitude = round(longitude, 5)
print("[+] GPS_LAT_DEG = %.5f" % lat_deg)
print("[+] GPS_LAT_MIN = %.5f" % gps_latitude_min)
print("[+] GPS_LAT_MIN_DEC = %.5f" % gps_latitude_min_dec)
print("[+] GPS_LON_DEG = %.5f" % lon_deg)
print("[+] GPS_LON_MIN = %.5f" % gps_longitude_min)
print("[+] GPS_LON_MIN_DEC = %.5f" % gps_longitude_min_dec)
print("[+] Result: %f, %f" % (latitude, longitude))
data = "660DF4481A0EDD00"
data = binascii.unhexlify(data)
decode_gps(data)
Flag: bi0s{1FMHK7D82BGA34954_M. A. Chidambaram Stadium}
found-in-madras
Thanks to you, I found my car
Just to be safe I want to check if the thief went above the speed limit or not. Get me the highest speed achieved by the car during the theft.
Seems like he was trying to gain access to Security level 15. Can you get me the SEED and KEY for the approved access?
(Based on the same CAN log as lost-in-madras)
Example flag: bi0s{12.23kph_DEADBEEF_CAFEBABE}
This challenge ask us to find the answer for 2 question:
- The Security level 15
SEED
andKEY
. - Top speed the car achieved.
The SEED and KEY
A UDS (Unified Diagnostic Services) request seed is part of a security access mechanism in the UDS protocol. This is typically part of SecurityAccess service (0x27).
First, the client will send a request for SEED
to the ECU, then the ECU responds with a SEED
. Client use the SEED
to gen a KEY
and send it back to the ECU. If ECU accept the KEY
(KEY
is good/correct), ECU send back the accept code and grants access to protected services.
So in this case, the process should look like this:
- Step 1: Client use
0x733
to send a request forSEED
. The data should beXX 27 15 ...
because we useSecurity level 15
. - Step 2: ECU return a
SEED
after the request packet. The data should beXX 67 15 <SEED>
. - Step 3: Client calculate the key and send back to the ECU. Data will be
XX 27 16 <KEY>
, 2nd-bytes isSecurity level + 1
. - Step 4: ECU accept the key, and send back something like
XX 67 16 00 00 00 ...
.
We need to pair the requests and responses, then we find out when the ECU accepts the key.
1:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 76 95 40 60 00 .g.v.@`.
vcan0 733 [8] 06 27 16 A1 4B 92 4C 00 .'..K.L.
2:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 03 62 07 92 00 .g..b...
vcan0 733 [8] 06 27 16 9B 11 10 2C 00 .'....,.
3:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 32 84 95 07 00 .g.2....
vcan0 733 [8] 06 27 16 B8 92 39 35 00 .'...95.
4:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 72 72 22 00 00 .g.rr"..
vcan0 733 [8] 06 27 16 09 CA A2 11 00 .'......
5:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 35 77 94 86 00 .g.5w...
vcan0 733 [8] 06 27 16 CA 43 AB BE 00 .'..C...
vcan0 73B [8] 02 67 16 00 00 00 00 00 .g......
6:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 44 78 58 31 00 .g.DxX1.
vcan0 733 [8] 06 27 16 40 8C F0 99 00 .'.@....
7:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 15 13 01 36 00 .g....6.
vcan0 733 [8] 06 27 16 B8 9A 35 06 00 .'...5..
8:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 11 59 98 53 00 .g..Y.S.
vcan0 733 [8] 06 27 16 A8 81 2B 47 00 .'...+G.
9:
vcan0 733 [8] 02 27 15 00 00 00 00 00 .'......
vcan0 73B [8] 06 67 15 71 68 43 89 00 .g.qhC..
vcan0 733 [8] 06 27 16 23 BC AD 69 00 .'.#..i.
Only the 5th has the accept packet, so SEED
is 35779486
and KEY
is CA43ABBE
.
Finding top speed
Based on the ford_cgea1_2_bodycan_2011.dbc
we know the CAN-ID for Engine_Data is 1059 (0x423)
. The detail:
BO_ 1059 Engine_Data_MS: 8 XXX
...
SG_ VEH_SPD : 7|16@0+ (0.01,-100.0) [0|0] "KPH" XXX
SG_ ENG_SPD : 23|16@0+ (0.25,0) [0|0] "RPM" XXX
SG_ Fuel_Level_State_UB : 37|1@0+ (1,0) [0|0] "" XXX
Lets extract bit from 7->23
for the vehicle speed. But we have too many line of data, we need an efficient way to decode it. Lets me introduce you, the cantools
.
We load the DBC file in, and use cantools
to decode the message. Example script to decode the message:
import cantools
dbc_string = """
VERSION ""
NS_ :
NS_DESC_
BS_:
BU_: XXX
BO_ 1059 Engine_Data_MS: 8 XXX
SG_ Res_UreaLvlLo_B_Dsply_UB : 35|1@0+ (1,0) [0|0] "" XXX
SG_ Res_UreaLvlLo_B_Dsply : 36|1@0+ (1,0) [0|0] "" XXX
SG_ Fuel_Level_State : 47|2@0+ (1,0) [0|0] "" XXX
SG_ AwdOffRoadMode_D_Stats_UB : 55|1@0+ (1,0) [0|0] "" XXX
SG_ AwdRnge_D_Actl_UB : 42|1@0+ (1,0) [0|0] "" XXX
SG_ RearDiffLckLamp_D_Rq_UB : 32|1@0+ (1,0) [0|0] "" XXX
SG_ AwdOffRoadMode_D_Stats : 41|2@0+ (1,0) [0|0] "" XXX
SG_ AwdRnge_D_Actl : 45|3@0+ (1,0) [0|0] "" XXX
SG_ RearDiffLckLamp_D_Rq : 34|2@0+ (1,0) [0|0] "" XXX
SG_ VEH_SPD : 7|16@0+ (0.01,-100.0) [0|0] "KPH" XXX
SG_ ENG_SPD : 23|16@0+ (0.25,0) [0|0] "RPM" XXX
SG_ Fuel_Level_State_UB : 37|1@0+ (1,0) [0|0] "" XXX
"""
db = cantools.database.load_string(dbc_string)
for message in db.messages:
print("Loaded message: %s (ID: %d)" % (message.name, message.frame_id))
data = bytes([0xF7, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) # just raw data
decoded = db.decode_message(1059, data)
print("Decoded Message:", decoded)
I tried few data, but the speed is very insane (above 300+ km/h probably ilegal in every country XD). So maybe the DBC file is wrong again and we need to modify it. I use the same approach (guessing bit pos) I mentioned above. Here is few data of 0x423
:
vcan1 423 [5] 22 3E 00 00 00 ">...
vcan1 423 [5] 2D 3E 00 00 00 ->...
vcan1 423 [5] 38 3E 00 00 00 8>...
vcan1 423 [5] 43 3E 00 00 00 C>...
vcan1 423 [5] 4F 3E 00 00 00 O>...
vcan1 423 [5] 5A 3E 00 00 00 Z>...
You can see the 3E
is consistent, why ?? Because in the high speed CAN bus (500 kbps or 1 Mbps) the common refresh times is usually 10ms - 50ms
. Therefore, in short time the speed will slowly increase or decrease like xx.01 -> xx.05
or xx.55 -> xx.38
. The xx
doesn’t change.
The formula (just to merge 2-bytes
into a WORD
) to get the speed is:
Then we apply the (scale, offset)
:
Seem like the 2nd-byte is the , and the 1st-byte is the . So I modified the DBC, here is the final DBC I used.
BO_ 1059 Engine_Data_MS: 5 XXX
SG_ VEH_SPD : 0|16@1+ (0.01,-100.0) [0|0] "KPH" XXX
The VEH_SPD
is modifed, start at 0
, size is 16
and using @1+
for little-endian instead of @0+
.
At this point, I think the author is messing with us, he was doing something with the bit/byte-order.
import cantools
dbc_string = """
VERSION ""
NS_ :
NS_DESC_
BS_:
BU_: XXX
BO_ 1059 Engine_Data_MS: 5 XXX
SG_ VEH_SPD : 0|16@1+ (0.01,-100.0) [0|0] "KPH" XXX
"""
db = cantools.database.load_string(dbc_string)
for message in db.messages:
print("Loaded message: %s (ID: %d)" % (message.name, message.frame_id))
data_arr = []
with open("423_filter.txt", "r") as file:
file_data = file.readlines()[1:-2]
for line in file_data:
data = line.strip().split(" ")[-1].split("\t")[0]
data = data.split(" ")
temp_arr = []
for i in data:
temp_arr.append(int(i, 16))
data_arr.append(bytes(temp_arr))
max = 0
for i in data_arr:
decoded = db.decode_message(1059, i)
print("[+] Speed: %f" % decoded["VEH_SPD"])
if max < decoded["VEH_SPD"]:
max = decoded["VEH_SPD"]
print("[+] Got max speed: %.2f" % max)
Flag: bi0s{65.13kph_35779486_CA43ABBE}
I Will explain about the Bit-field and we dont need to use the guessing approach. There are two more challengs too. To be continued…