DEF CON 30 Car Hacking Village CTF challenge
DRAMATIS PERSONAE
:
AAAAAchilles, a CTF player, ready to capture the flags.
Ben Gardiner, the challenge designer, planning for anything but.
SCENE
: Car Hacking Village. Ben
is sitting at a table. On the table is a piece of paper.
ENTER AAAAAchilles
:
AAAAAchilles reads the words on the paper aloud
AAAAAchilles: “Challenge name Just Decode It 2. Here is a mystery ECU in a red case. It is sending on its CAN interface a flag! The rules are:
- you may connect to the grey/red and grey/white FLRY wires
- you may request a power cycle and an organizer will cycle the USB A connector
- DO NOT disconnect the connector/harness on the ECU
Hint: read the spec, understand the spec”
The mystery ECU looks like this:
AAAAAchilles: Hey so what’s this?
Ben: It’s a CTF challenge, Just Decode It 2 on the scoreboard.
A: How do I connect up to it?
Ben holds up a twisted pair of FLRY wires
A: Do you have an adapter?
B: Nope. But here are some lever nuts for the wires.
AAAAAchilles
returns with a CAN adapter and connects wires up to it
A: I captured some CAN frames. It seems to repeat the same sequence over and over again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
can0 1CEBFF29 [8] 03 74 72 79 3A 5E 20 65
can0 1CEBFF29 [8] 04 41 72 4C 6C 65 72 7E
can0 1CEBFF29 [8] 05 73 31 7C 5B 65 7D 00
can0 1CEBFF29 [8] 20 22 00 05 FF A9 F1 00
can0 1CEBFF29 [8] 01 46 7C 41 47 7B 2B 6F
can0 1CEBFF29 [8] 02 5F 2E 7C 5E 37 45 7E
can0 1CEBFF29 [8] 03 74 72 79 3A 5E 20 65
can0 1CEBFF29 [8] 04 41 72 4C 6C 65 72 7E
can0 1CEBFF29 [8] 05 73 31 7C 5B 65 7D 00
can0 1CEBFF29 [8] 20 22 00 05 FF A9 F1 00
can0 1CEBFF29 [8] 01 46 7C 41 47 7B 2B 6F
can0 1CEBFF29 [8] 02 5F 2E 7C 5E 37 45 7E
can0 1CEBFF29 [8] 03 74 72 79 3A 5E 20 65
can0 1CEBFF29 [8] 04 41 72 4C 6C 65 72 7E
can0 1CEBFF29 [8] 05 73 31 7C 5B 65 7D 00
can0 1CEBFF29 [8] 20 22 00 05 FF A9 F1 00
can0 1CEBFF29 [8] 01 46 7C 41 47 7B 2B 6F
can0 1CEBFF29 [8] 02 5F 2E 7C 5E 37 45 7E
can0 1CEBFF29 [8] 03 74 72 79 3A 5E 20 65
can0 1CEBFF29 [8] 04 41 72 4C 6C 65 72 7E
can0 1CEBFF29 [8] 05 73 31 7C 5B 65 7D 00
B: Yeah! Nice!
A: It’s J1939. Where can I get the database to decode these PGNs?
B: I like the way you think. But the challenge is solvable with candump can0 -a
.
A: OK so the ASCII in the frames says F|AG{+o_.|^7E~try:^ eArLler~s1|[e}
I tried but the flag doesn’t work.
B: Sorry that’s not the flag. It’s a hint. Can you read it?
A: I can try… but WTF is up with all the weird leet-speak lettering? Are you trying to be cool?
Ben laughs out loud.
B: No. That was actually really hard to pick all those characters. It’ll make sense later - if you solve it.
A: OK So the hint says “Too late. Try earlier slice.”
B: Correct!
A: Can you reboot it for me?
B: Yup.
A: Can you reboot it for me again?
B: Sure!
A: Can you reboot it for me again please?
B: I can reboot it a hundred times for you but it’s not going to make a difference to your capture. Did you get all the hints on the label?
A: I can’t decode the big data matrix.
B: Oh yeah. That hint is dodgy in most decode tools. I put the decode in text below it.
A: It’s all 2’s (and some 0’s).
B: 😐 … It’s a hint.
A: OK I switched all the 2’s to 1’s and then decoded the binary it said ‘Why decode this? Decode the signal.’
B: Yep!
A: 😐
B: 😐
A: 😐
B: Did you get all the hints on the label?
A: I can’t decode the QR code.
B: Try different software.
A: Hey I decoded the QR code and it said January USA.
B: Nice! Yep, that’s a strong hint.
A: So the scoreboard hint says ‘Read the spec’ What spec?
B: CAN 2.0B
Ben laughs out loud.
Time passes. AAAAAchilles returns and connects up a logic analyzer to the wires
A: I connected a logic analyzer up to the wires and did a capture. This looks like CAN FD… there’s a lot of higher speed transitions than the bitrate. But the CAN decoder in sigrok chokes on it and I’m not receiving any CAN FD frames.
B: Look at you! Very nice. It does look like CAN FD. But it’s not. Did you get the hint?
A: 😑 January USA
B: Yes. That is a strong hint.
A: OK so the ECU is repeating the same frames over and over and I’ve captured traffic during power on and it is sending the same frames then too. There is no earlier slice.
B: Or is there?
A: OK so the ECU always repeats what it sends, even on power up. But there is an ‘earlier slice’ to see the flag… And my logic analyzer capture showed lots of high speed transitions faster than the bitrate… Is this a Janus frame challenge, based on the research by Dr. Ken Tindell at Canis Labs? Is that what all the 2s were about (because Janus had two faces)? OMG ‘January USA’ when abbreviated is JanUS! You were hitting us over the head with it this whole time!
B: Indeed I was. That is very astute of you to pick up on all of that. Yes, it is a Janus frame challenge… Now go read up on Dr. Tindell’s blog and solve it!
A: I got it!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[:~] $ sudo ip link set can0 type can bitrate 500000 sample-point 0.50
[:~] $ sudo ip link set up can0
[:~] $ candump can0 -a
can0 1CEBFF29 [8] 05 40 52 4C 59 21 7D 00 '.@RLY!}.'
can0 1CEBFF29 [8] 20 22 00 05 FF A9 F1 00 ' "......'
can0 1CEBFF29 [8] 01 46 7C 41 47 7B 28 48 '.F|AG{(H'
can0 1CEBFF29 [8] 02 56 5F 5C 7E 67 45 74 '.V_\~gEt'
can0 1CEBFF29 [8] 03 7C 6C 2B 5C 6E 49 63 '.|l+\nIc'
can0 1CEBFF29 [8] 04 45 7E 5E 6E 64 3B 65 '.E~^nd;e'
can0 1CEBFF29 [8] 05 40 52 4C 59 21 7D 00 '.@RLY!}.'
can0 1CEBFF29 [8] 20 22 00 05 FF A9 F1 00 ' "......'
can0 1CEBFF29 [8] 01 46 7C 41 47 7B 28 48 '.F|AG{(H'
can0 1CEBFF29 [8] 02 56 5F 5C 7E 67 45 74 '.V_\~gEt'
can0 1CEBFF29 [8] 03 7C 6C 2B 5C 6E 49 63 '.|l+\nIc'
can0 1CEBFF29 [8] 04 45 7E 5E 6E 64 3B 65 '.E~^nd;e'
can0 1CEBFF29 [8] 05 40 52 4C 59 21 7D 00 '.@RLY!}.'
B: Awesome! So yeah the Janus pairs of frames being emitted were tricky to find: they had to both be ASCII and valid Janus frame pairs which made the search harder. We made a script to look for lots of alternative ‘leet’ spellings which is why the flags are written so weird:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
import itertools
import sys
from canhack.canframe import *
from binascii import *
def janus_mismatch(bitseq_a, bitseq_b):
# Check 1: both frames must be the same length
if len(bitseq_a) != len(bitseq_b):
return 0
# Check 2: if 10 then must be the same until back in sync
synced = True
for i in range(len(bitseq_a)):
if synced:
if bitseq_a[i] == '1' and bitseq_b[i] == '0':
synced = False
else:
if bitseq_a[i] != bitseq_b[i]:
return i
if bitseq_a[i] == '1':
synced = True
return -1
seed_transport_payload_pairs = [
(b'\x01F|AG{(H',
b'\x01F|AG{+o'),
(b'\x02V_\\~gEt',
b'\x02_.|^7E~'),
(b'\x03|l+\\nIc',
b'\x03try:^ e'),
(b'\x04E~^nd;e',
b'\x04ArLler~'),
(b'\x05@RLY!}\x00',
b'\x05s1|[e}\x00')
]
ID_A=0x73a
ID_B=0x3ff29
def janus_data_mismatch(first, second):
f_a = CANFrame(id_a=ID_A, id_b=ID_B, ide=True, data=first)
f_b = CANFrame(id_a=ID_A, id_b=ID_B, ide=True, data=second)
bitseq_a = f_a.bitseq()
bitseq_b = f_b.bitseq()
m = janus_mismatch(bitseq_a, bitseq_b)
#if m == -1:
# print("FOUND JANUS PAIR: %s and %s" % (bytes(first), bytes(second)))
#if m != -1:
# print("tried %s and %s" % (bytes(first), bytes(second)))
return m
def switch_case_of_byte(val):
s = chr(val)
if s.isupper():
s = s.lower()
elif s.islower():
s = s.upper()
return s.encode('utf-8')[0]
def all_alternate_bytes(val):
all_alts = [
[b'a', b'A', b'4', b'@', b'^', b'&'],
[b'b', b'B', b'8'],
[b'c', b'C', b'(', b'['],
[b'd', b'D', b')', b']'],
[b'e', b'E', b'3'],
[b'f', b'F', b'='],
[b'g', b'G', b'6'],
[b'h', b'H', b'4'],
[b'i', b'I', b'1', b'!', b'|'],
[b'j', b'J', b'i', b'I', b'1', b'!'],
[b'k', b'K'],
[b'l', b'L', b'1', b'|'],
[b'm', b'M'],
[b'n', b'N'],
[b'o', b'O', b'0', b'#'],
[b'p', b'P', b'9'],
[b'q', b'Q'],
[b'r', b'R'],
[b's', b'S', b'5', b'$', b'z', b'Z', b'2'],
[b't', b'T', b'7', b'+'],
[b'u', b'U', b'v', b'V'],
[b'v', b'V', b'w', b'W', b'u', b'U'],
[b'w', b'W', b'v', b'V'],
[b'x', b'X', b'%', b'*'],
[b'y', b'Y', b'i'],
[b'z', b'Z', b'2', b's', b'S', b'5', b'$'],
[b' ', b'_', b'-', b'~', b'.', b','],
[b'-', b'/', b'\\', b'|', b'~', b':', b';', b'.', b',', b'_'],
[b'_', b'/', b'\\', b'|', b'~', b':', b';', b'.', b',', b'-'],
[b'~', b'_', b'/', b'\\', b'|', b':', b';', b'.', b',', b'-'],
[b'=', b'_', b'~', b'-', b':'],
[b':', b'=', b'_', b'~', b'-', b';', b' ', b'/', b'\\', b'|'],
[b';', b'=', b'_', b'~', b'-', b':', b' ', b'/', b'\\', b'|'],
[b'.', b'/', b'\\', b'|', b'~', b':', b';', b',', b'_'],
[b',', b'.', b'/', b'\\', b'|', b'~', b':', b';', b'.', b'_'],
[b'/', b'-', b'\\', b'|', b'~', b':', b';', b'.', b',', b'_'],
[b'\\', b'-', b'/', b'|', b'~', b':', b';', b'.', b',', b'_'],
[b"'", b'"', b'`'],
[b'"', b"'", b'`'],
[b'`', b"'", b'"'],
[b'1', b'l', b'|', b'I'],
[b'2', b'z', b'Z'],
[b'3', b'E'],
[b'4', b'A'],
[b'5', b's', b'S'],
[b'6', b'G'],
[b'7', b'T'],
[b'8', b'B'],
[b'9', b'P'],
[b'0', b'o', b'O', b'#']
]
alts = [chr(val)]
was_upper = chr(val).isupper()
val = ord(chr(val).lower())
for alt in all_alts:
if val == ord(alt[0]):
alts = alt
if was_upper:
alts = alts.copy()
for i in range(len(alts)):
alts[i] = chr(switch_case_of_byte(ord(alts[i])))
break
return alts
def mutate_search(i_save, m_save, first_save, second_save):
targets = [(2, 0), (2, 1), (1, 0), (1, 1)]
for l in range(1, len(targets)):
for combo in itertools.combinations(targets, l):
first = first_save.copy()
second = second_save.copy()
if len(combo) == 1:
if combo[0][0] == 1:
thingy = first
else:
thingy = second
alts = all_alternate_bytes(thingy[combo[0][1] + i_save])
for alt in alts:
thingy[combo[0][1] + i_save] = ord(alt)
m = janus_data_mismatch(first, second)
if m == -1:
return first, second
if m > m_save:
if i_save + 1 >= len(first) - 1:
continue
test1, test2 = mutate_search(i_save + 1, m, first, second)
if test1 is None:
continue
return test1, test2
elif len(combo) == 2:
if combo[0][0] == 1:
thing1 = first
else:
thing1 = second
thing1_alts = all_alternate_bytes(thing1[combo[0][1] + i_save])
if combo[1][0] == 1:
thing2 = first
else:
thing2 = second
thing2_alts = all_alternate_bytes(thing2[combo[1][1] + i_save])
for thing1_alt in thing1_alts:
for thing2_alt in thing2_alts:
thing1[combo[0][1] + i_save] = ord(thing1_alt)
thing2[combo[1][1] + i_save] = ord(thing2_alt)
m = janus_data_mismatch(first, second)
if m == -1:
return first, second
if m > m_save:
if i_save + 1 >= len(first) - 1:
continue
test1, test2 = mutate_search(i_save + 1, m, first, second)
if test1 is None:
continue
return test1, test2
else:
# FIXME: skip for now until I can figure out how to use itertools to reuse the above tests
continue
sys.stderr.write("%d" % i_save)
return None, None
def find_match(first, second):
first = bytearray(first)
second = bytearray(second)
i = janus_data_mismatch(first, second)
if i == -1:
return first, second
first = bytearray([first[0]]) + first[1:]
second = bytearray([second[0]]) + second[1:]
return mutate_search(1, 1, first, second)
SENTINEL = b'\x00\x01\x02\x03\x04\x05\x06\x07'
check1, check2 = find_match(SENTINEL, SENTINEL)
if check1 is None:
print("BIG FAIL")
sys.exit(1)
for seed_pair in seed_transport_payload_pairs:
first, second = find_match(seed_pair[0], seed_pair[1])
if first is None:
print("\n impossible pair:")
print("%s and %s" % (seed_pair[0], seed_pair[1]))
else:
print("\n JANUS PAIR!!:")
print("hexlify('%s'), hexlify('%s')" % (hexlify(first).decode('utf-8'), hexlify(second).decode('utf-8')))
print("%s and %s for:" % (bytes(first), bytes(second)))
print("%s and %s" % (seed_pair[0], seed_pair[1]))
AAAAAchilles
is already gone flagging other flags
B: … put the result on a CANPico running the CANHack toolkit setup to send the Janus pairs on boot and then I shoved the whole thing inside a little red ECU shell!