Skip to content

Commit 7260a30

Browse files
committed
add gctf 2021 comlink
1 parent e1b4766 commit 7260a30

File tree

3 files changed

+250
-0
lines changed

3 files changed

+250
-0
lines changed

2021-07-17-google-ctf/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Google CTF 2021
2+
3+
### Table of contents
4+
5+
* [comlink (hardware)](comlink)
+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# Google CTF - Comlink (394pt / 7 solves)
2+
3+
> We have captured a spy. They were carrying this device with them. It seems to be some kind of Z80-based processor connected to an antenna for wireless communications. We also managed to record the last message they sent but unfortunately it seems to be encrypted. According to our research it seems like the device has an AES hardware peripheral for more efficient encryption. You need to help us recover the message. We have extracted the firmware running on the device and you can also program the device with your own firmware to figure out how it works. I heard that a security researcher at ACME Corporation found some bugs in the hardware but we haven't managed to get hold of them for details and we need this solved now! Good luck!
4+
5+
## 1. Initial plan
6+
7+
We are provided with 2 files and a netcat connection:
8+
- `captured_transmission.dat` - clearly contained some raw bytes, we attempted to decode it as a radio transmission, but based on results, entropy and file sized we concluded that it's encrypted raw output from the device
9+
- `firmware.ihx` - dumped firmware from the device in Intel HEX format.
10+
- netcat connection where we can upload our own custom firmware in Intel HEX format
11+
12+
The description mentions a hardware bug, which is the key to this challenge. So our plan was to reverse the firmware, find the hardware bug and decrypt the message.
13+
14+
## 2. Reversing the firmware
15+
16+
After converting Intel HEX to binary firmware using hex2bin.py from z80asm, we used ghidra to reverse the binary:
17+
18+
We can see that entrypoint calls `init_hex_chars` and `main`.
19+
```
20+
start
21+
ram:0100 31 00 00 LD SP,0x0
22+
ram:0103 cd 46 05 CALL init_hex_chars
23+
ram:0106 cd ca 02 CALL main
24+
ram:0109 c3 04 02 JP infinite_loop
25+
```
26+
27+
`init_hex_chars` was a function which copied a buffer of hex characters. We believe it was planned to be used as part of the challenge, as firmware doesn't use this buffer at all. `main` was quite big, but it mostly performed operation of xoring input buffer with static IV buffer of `XXXXXXXXXXXXXXXX`, so skipping to the interesting parts:
28+
29+
```
30+
aes_interface:
31+
ram:02a4 c1 POP BC
32+
ram:02a5 e1 POP HL
33+
ram:02a6 e5 PUSH HL
34+
ram:02a7 c5 PUSH BC
35+
ram:02a8 11 10 80 LD DE,0x8010
36+
ram:02ab 01 10 00 LD BC,0x10
37+
ram:02ae ed b0 LDIR
38+
ram:02b0 db 30 IN A,(0x30)
39+
ram:02b2 4f LD C,A
40+
ram:02b3 cb c1 SET 0x0,C
41+
ram:02b5 79 LD A,C
42+
ram:02b6 d3 30 OUT (0x30),A
43+
wait_bit
44+
ram:02b8 db 30 IN A,(0x30)
45+
ram:02ba 0f RRCA
46+
ram:02bb 38 fb JR C,wait_bit
47+
ram:02bd c1 POP BC
48+
ram:02be d1 POP DE
49+
ram:02bf d5 PUSH DE
50+
ram:02c0 c5 PUSH BC
51+
ram:02c1 21 20 80 LD HL,0x8020
52+
ram:02c4 01 10 00 LD BC,0x10
53+
ram:02c7 ed b0 LDIR
54+
ram:02c9 c9 RET
55+
```
56+
57+
Encryption key is nowhere to be found in the firmware, so it has to be embedded in the AES device. Memory region from `0x8000` to `0x8100` are mapped to respective ports:
58+
- port 10 (`0x8010`) - AES input buffer of 16 bytes
59+
- port 20 (`0x8020`) - AES output buffer of 16 bytes
60+
- port 30 (`0x8030`) - AES control bit, used for both starting AES and polling the status
61+
- `0x8100` - was used in firmware, however we concluded that modifying this value doesn't change the result of AES operation
62+
63+
## 3. Writing custom firmware
64+
65+
For that we have used [z80asm](https://www.nongnu.org/z80asm/). We started with printing back our own buffer to confirm our reversing results:
66+
67+
```asm
68+
ld b, 0x10
69+
ld hl, input
70+
call send_buf
71+
infi:
72+
jr infi
73+
74+
; e = byte
75+
send_byte:
76+
in a, (1)
77+
rrca
78+
jr c, send_byte
79+
80+
ld a, e
81+
out (0), a
82+
83+
in a, (1)
84+
or 1
85+
out (1), a
86+
ret
87+
88+
; b = count
89+
; hl = ptr
90+
send_buf:
91+
ld e, (hl)
92+
inc hl
93+
call send_byte
94+
djnz send_buf
95+
ret
96+
97+
input: db "AAAAAAAAAAAAAAAA"
98+
```
99+
100+
And later decided to use the AES (skipping utility functions from above):
101+
102+
```asm
103+
; send input bytes to AES device
104+
ld bc, 0x10
105+
ld de, 0x8010
106+
ld hl, input
107+
ldir
108+
109+
; start encryption
110+
ld a, 0x01
111+
out (0x30), a
112+
113+
; wait for encryption end
114+
wait:
115+
in a,(0x30)
116+
rrca
117+
jr c, wait
118+
119+
ld hl, 0x8020
120+
ld b, 0x10
121+
call send_buf
122+
123+
infi:
124+
jr infi
125+
126+
input: db "AAAAAAAAAAAAAAAA"
127+
```
128+
129+
Which returned our encrypted buffer.
130+
131+
## 4. Finding the bug
132+
133+
We had a few ideas:
134+
- description of the challenge says *z80 based*, so maybe there's a bug in one of the obscure instructions?
135+
- AES module has a bug and doesn't perform *real* AES, but one with a bug, so maybe fewer rounds?
136+
- there might be a bug in communication between the devices?
137+
138+
It took us some time to confirm / refute those ideas, but while testing communication issues, we attempted this payload:
139+
140+
```asm
141+
;call encrypt
142+
ld hl, input
143+
ld de, 0x8010
144+
ld bc, 0x10
145+
ldir
146+
147+
ld a, 0x01
148+
ld b, 0xff
149+
wait:
150+
out (0x30), a
151+
djnz wait
152+
153+
ld hl, 0x8020
154+
ld de, 0x9000
155+
ld bc, 0x10
156+
ldir
157+
158+
ld hl, 0x8020
159+
ld de, 0x9010
160+
ld bc, 0x10
161+
ldir
162+
163+
ld hl, 0x9000
164+
ld b, 0x20
165+
call send_buf
166+
167+
infi:
168+
jr infi
169+
```
170+
171+
It performs AES, while spamming port `0x30` with the value `0x01`, so it attempts over and over to start encryption process. After that it copies the buffer twice and sends both copies to us.
172+
173+
```
174+
[+] Opening connection to comlink.2021.ctfcompetition.com on port 1337: Done
175+
[*] 00000000 2a 45 a9 67 6d 00 47 16 0c 72 69 b2 cf 4a c3 f1 │*E·g│m·G·│·ri·│·J··│
176+
00000010 8c f0 46 67 6d 00 47 16 0c 72 69 b2 cf 4a c3 f1 │··Fg│m·G·│·ri·│·J··│
177+
```
178+
179+
As you can see, first 3 bytes differ between outputs! We were quite baffled. What did it mean? We tried counting how many cycles it takes to perform encryption, but then realised the entire buffer differs, but only for a short time. By starting the read process with different offset we could read entire value of this "modified" buffer.
180+
181+
```asm
182+
;call encrypt
183+
ld hl, input
184+
ld de, 0x8010
185+
ld bc, 0x10
186+
ldir
187+
188+
ld hl, 0x8020
189+
call attack
190+
191+
ld hl, 0x8024
192+
call attack
193+
194+
ld hl, 0x8028
195+
call attack
196+
197+
ld hl, 0x802c
198+
call attack
199+
200+
infi:
201+
jr infi
202+
203+
attack:
204+
ld de, 0x9000
205+
ld bc, 0x4
206+
207+
ld a, 0x01
208+
out (0x30), a
209+
out (0x30), a
210+
211+
ldir
212+
213+
ld hl, 0x9000
214+
ld b, 0x4
215+
jr send_buf
216+
217+
input: db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
218+
```
219+
220+
And the output:
221+
222+
```
223+
[+] Opening connection to comlink.2021.ctfcompetition.com on port 1337: Done
224+
[*] 00000000 1d 05 ef e8 63 c3 d9 92 a8 f1 7b ce 93 47 59 5b │····│c···│··{·│·GY[│
225+
```
226+
227+
## 5. Putting pieces together
228+
229+
We deduced, that perhaps the AES device handles interrupts incorrectly and exposes to us internal buffer when we perform multiple writes to port `0x30`. This lead us to believe, that if we can read the state of internal buffer after first round - which is xoring the input buffer with key - we could then recover the key. Since we used inbut buffer of null bytes - we already had the key! All that we needed to do is perform the decryption:
230+
231+
```py
232+
from Crypto.Cipher import AES
233+
234+
a = AES.new(key=bytes.fromhex("1d05efe863c3d992a8f17bce9347595b"), mode=AES.MODE_CBC, iv=b"X"*16)
235+
with open("captured_transmission.dat", "rb") as f:
236+
print(a.decrypt(f.read()))
237+
```
238+
239+
Output:
240+
```
241+
This is agent 1337 reporting back to base. I have completed the mission but I am being pursued by enemy operatives. They are closing in on me and I suspect the safe-house has been compromised. I managed to steal the codes to the mainframe and sending it over now: CTF{HAVE_YOU_EVER_SEEN_A_Z80_CPU_WITH_AN_AES_PERIPHERAL}. If you do not hear from me again, assume the worst. Agent out!
242+
```
243+
244+
Final flag: `CTF{HAVE_YOU_EVER_SEEN_A_Z80_CPU_WITH_AN_AES_PERIPHERAL}`

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# CTF writeups from P4 Team
22

33
## 2021
4+
* [2021.07.17 **Google CTF 2021**(8th place/379 teams)](2021-07-17-google-ctf)
45
* [2021.05.28 **Pwn2Win CTF 2021**(9th place/720 teams)](2021-05-28-pwn2win)
56

67
## 2020

0 commit comments

Comments
 (0)