|
| 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}` |
0 commit comments