Ben Gardiner
Ben Gardiner Senior Cybersecurity Research Engineer contractor at the National Motor Freight Traffic Association

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:

  1. you may connect to the grey/red and grey/white FLRY wires
  2. you may request a power cycle and an organizer will cycle the USB A connector
  3. DO NOT disconnect the connector/harness on the ECU

Hint: read the spec, understand the spec”

The mystery ECU looks like this:

CAN controller board

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?

alt_text

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.

alt_text

alt_text

alt_text

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!

alt_text

comments powered by Disqus