Chest was one of the reverse-engineering challenge of the European Cyber Week 2021 challenges. I’m the author of that AVR challenge and will detail here my solution.
The provided file chest.hex
file is in Intel HEX format.
1$ cat chest.hex
2:100000000C9434000C9449000C9449000C94490061
3:100010000C9449000C9449000C9449000C9449003C
4:100020000C9449000C9449000C9449000C9449002C
5:100030000C9449000C9449000C9449000C9449001C
6:100040000C9449000C9449000C9449000C9449000C
7:100050000C9449000C9449000C9449000C944900FC
8:100060000C9449000C94490011241FBECFEFD8E036
9:10007000DEBFCDBF11E0A0E0B1E0E8EAF1E002C0F0
10:1000800005900D92A632B107D9F70E94CA000C94D0
11:10009000D2000C9400001092C5008093C40088E147
12:1000A0008093C10086E08093C20008959091C000C3
13:1000B00095FFFCCF8093C60008950F931F93CF93B5
14:1000C000DF93EC018C01060F111DC017D10721F041
15:1000D00089910E945600F9CFDF91CF911F910F9126
16:1000E00008958091C00087FFFCCF8091C6000895DD
17:1000F0000F931F93CF93DF93EC018C01060F111D1B
18:10010000C017D10721F00E9471008993F9CFDF91C8
19:10011000CF911F910F910895CF93DF93CDB7DEB7A5
20:100120002B970FB6F894DEBF0FBECDBF8BE0EBE18F
21:10013000F1E0DE01119601900D928A95E1F76BE0F6
22:10014000CE0101960E945D000E9471002B960FB6B1
23:10015000F894DEBF0FBECDBFDF91CF910895FF921F
24:100160000F931F93CF93DF93F82EC0E0D1E00CE103
25:1001700011E0888184508F250E94560022960C172A
26:100180001D07B9F78AE0DF91CF911F910F91FF9082
27:100190000C94560087E60E944B000E948C000E943F
28:1001A000AF00FBCFF894FFCF4F5551505D62795DA2
29:1001B00073546F5770424355717A3E3842454378C5
30:0E01C000773300456E746572206B65790A0016
31:00000001FF
The Intel HEX is a transitional file format for microcontrollers, (E)PROMs or other devices. The documentation states that HEXs can be converted to binary files and programmed into a configuration device.
1$ objcopy -I ihex chest.hex -O binary chest.bin ; xxd chest.bin
200000000: 0c94 3400 0c94 4900 0c94 4900 0c94 4900 ..4...I...I...I.
300000010: 0c94 4900 0c94 4900 0c94 4900 0c94 4900 ..I...I...I...I.
400000020: 0c94 4900 0c94 4900 0c94 4900 0c94 4900 ..I...I...I...I.
500000030: 0c94 4900 0c94 4900 0c94 4900 0c94 4900 ..I...I...I...I.
600000040: 0c94 4900 0c94 4900 0c94 4900 0c94 4900 ..I...I...I...I.
700000050: 0c94 4900 0c94 4900 0c94 4900 0c94 4900 ..I...I...I...I.
800000060: 0c94 4900 0c94 4900 1124 1fbe cfef d8e0 ..I...I..$......
900000070: debf cdbf 11e0 a0e0 b1e0 e8ea f1e0 02c0 ................
1000000080: 0590 0d92 a632 b107 d9f7 0e94 ca00 0c94 .....2..........
1100000090: d200 0c94 0000 1092 c500 8093 c400 88e1 ................
12000000a0: 8093 c100 86e0 8093 c200 0895 9091 c000 ................
13000000b0: 95ff fccf 8093 c600 0895 0f93 1f93 cf93 ................
14000000c0: df93 ec01 8c01 060f 111d c017 d107 21f0 ..............!.
15000000d0: 8991 0e94 5600 f9cf df91 cf91 1f91 0f91 ....V...........
16000000e0: 0895 8091 c000 87ff fccf 8091 c600 0895 ................
17000000f0: 0f93 1f93 cf93 df93 ec01 8c01 060f 111d ................
1800000100: c017 d107 21f0 0e94 7100 8993 f9cf df91 ....!...q.......
1900000110: cf91 1f91 0f91 0895 cf93 df93 cdb7 deb7 ................
2000000120: 2b97 0fb6 f894 debf 0fbe cdbf 8be0 ebe1 +...............
2100000130: f1e0 de01 1196 0190 0d92 8a95 e1f7 6be0 ..............k.
2200000140: ce01 0196 0e94 5d00 0e94 7100 2b96 0fb6 ......]...q.+...
2300000150: f894 debf 0fbe cdbf df91 cf91 0895 ff92 ................
2400000160: 0f93 1f93 cf93 df93 f82e c0e0 d1e0 0ce1 ................
2500000170: 11e0 8881 8450 8f25 0e94 5600 2296 0c17 .....P.%..V."...
2600000180: 1d07 b9f7 8ae0 df91 cf91 1f91 0f91 ff90 ................
2700000190: 0c94 5600 87e6 0e94 4b00 0e94 8c00 0e94 ..V.....K.......
28000001a0: af00 fbcf f894 ffcf 4f55 5150 5d62 795d ........OUQP]by]
29000001b0: 7354 6f57 7042 4355 717a 3e38 4245 4378 sToWpBCUqz>8BECx
30000001c0: 7733 0045 6e74 6572 206b 6579 0a00 w3.Enter key..
Note that we can also use the online tool matrixstorm to do this.
Now that we have our binary, we now need to identify for which architecture it was compiled.
1file chest.bin
2chest.bin: data
Well, our beloved friend file
didn’t even recognized the file format. At that point, we have several options to discover the architecture:
- compile a sample project for many architectures and clustering the outputs using correlation techniques like binary diffing in the hope of identifying the correct architecture
- try disassembling for many architectures in the hope of discovering the right code.
- googling the HEX
The Googling technique is definetly the fastest and the easiest. It gives us a lot of results concerning AVR. Let’s give the avr-objdump
disassembler a try:
1$ avr-objdump -m avr -D chest.hex
2
300000000 <.sec1>:
4 0: 0c 94 34 00 jmp 0x68 ; 0x68
5 4: 0c 94 49 00 jmp 0x92 ; 0x92
6 8: 0c 94 49 00 jmp 0x92 ; 0x92
7 c: 0c 94 49 00 jmp 0x92 ; 0x92
8 10: 0c 94 49 00 jmp 0x92 ; 0x92
9 14: 0c 94 49 00 jmp 0x92 ; 0x92
10 18: 0c 94 49 00 jmp 0x92 ; 0x92
11 1c: 0c 94 49 00 jmp 0x92 ; 0x92
12 20: 0c 94 49 00 jmp 0x92 ; 0x92
13 24: 0c 94 49 00 jmp 0x92 ; 0x92
14 28: 0c 94 49 00 jmp 0x92 ; 0x92
15 2c: 0c 94 49 00 jmp 0x92 ; 0x92
16 30: 0c 94 49 00 jmp 0x92 ; 0x92
17 34: 0c 94 49 00 jmp 0x92 ; 0x92
18 38: 0c 94 49 00 jmp 0x92 ; 0x92
19 3c: 0c 94 49 00 jmp 0x92 ; 0x92
20 40: 0c 94 49 00 jmp 0x92 ; 0x92
21 44: 0c 94 49 00 jmp 0x92 ; 0x92
22 48: 0c 94 49 00 jmp 0x92 ; 0x92
23 4c: 0c 94 49 00 jmp 0x92 ; 0x92
24 50: 0c 94 49 00 jmp 0x92 ; 0x92
25 54: 0c 94 49 00 jmp 0x92 ; 0x92
26 58: 0c 94 49 00 jmp 0x92 ; 0x92
27 5c: 0c 94 49 00 jmp 0x92 ; 0x92
28 60: 0c 94 49 00 jmp 0x92 ; 0x92
29 64: 0c 94 49 00 jmp 0x92 ; 0x92
30 68: 11 24 eor r1, r1
31
32[...]
33
34 92: 0c 94 00 00 jmp 0 ; 0x0
35
36[...]
If we don’t pay attention to the last bytes which are encoding the strings, it looks like valid code. At the beginning, we can read a 26-entries vector table.
We know that this code is targeting an Atmel AVR microcontroller, but which one? Here is a list of the most common ones, and here is a matrix listing some of their available features.
Again, we have multiple options to discover the correct microcontroller:
- compile samples with all the different MCU types supported by (let’s say)
avr-gcc
and again, use some correlation techniques against our dump - searching more infos about what we already know, like the interrupt vectors
- googling the code
Again, the googling technique is the fastest and the less painful. We can quickly find a dump like ours that is targeting an ATmega328P.
We can proceed with a static analysis using the AVR instruction set and the ATMega328P datasheet:
1 __vectors:
2 0: 0c 94 34 00 jmp 0x68 ; 0x68 ; RESET ; __ctors_end
3 4: 0c 94 49 00 jmp 0x92 ; 0x92 ; INT0 ; __bad_interrupt
4 8: 0c 94 49 00 jmp 0x92 ; 0x92 ; INT1
5 c: 0c 94 49 00 jmp 0x92 ; 0x92 ; PCINT0
6 10: 0c 94 49 00 jmp 0x92 ; 0x92 ; PCINT1
7 14: 0c 94 49 00 jmp 0x92 ; 0x92 ; PCINT2
8 18: 0c 94 49 00 jmp 0x92 ; 0x92 ; WDT
9 1c: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER2 COMPA
10 20: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER2 COMPB
11 24: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER2 OVF
12 28: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER1 CAPT
13 2c: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER1 COMPA
14 30: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER1 COMPB
15 34: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER1 OVF
16 38: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER0 COMPA
17 3c: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER0 COMPB
18 40: 0c 94 49 00 jmp 0x92 ; 0x92 ; TIMER0 OVF
19 44: 0c 94 49 00 jmp 0x92 ; 0x92 ; SPI,STC
20 48: 0c 94 49 00 jmp 0x92 ; 0x92 ; USART,RX
21 4c: 0c 94 49 00 jmp 0x92 ; 0x92 ; USART,UDRE
22 50: 0c 94 49 00 jmp 0x92 ; 0x92 ; USART,TX
23 54: 0c 94 49 00 jmp 0x92 ; 0x92 ; ADC
24 58: 0c 94 49 00 jmp 0x92 ; 0x92 ; EE READY
25 5c: 0c 94 49 00 jmp 0x92 ; 0x92 ; ANALOG COMP
26 60: 0c 94 49 00 jmp 0x92 ; 0x92 ; TWI
27 64: 0c 94 49 00 jmp 0x92 ; 0x92 ; SPM READY
28
29 __ctors_end:
30 68: 11 24 eor r1, r1 ; r1 = 0
31 6a: 1f be out 0x3f, r1 ; clear 0xf3 (SREG)
32 ; Initialize stack pointer at 0x08ff
33 6c: cf ef ldi r28, 0xFF
34 6e: d8 e0 ldi r29, 0x08
35 70: de bf out 0x3e, r29 ; 0x3e is the high portion of the stack pointer
36 72: cd bf out 0x3d, r28 ; 0x3d is the low portion of the stack pointer
37
38 __do_copy_data:
39 74: 11 e0 ldi r17, 0x01
40 76: a0 e0 ldi r26, 0x00
41 78: b1 e0 ldi r27, 0x01
42 7a: e8 ea ldi r30, 0xA8 ; low portion of the data at 0x01A8
43 7c: f1 e0 ldi r31, 0x01 ; high portion of the data at 0x01A8
44 7e: 02 c0 rjmp .+4 ; 0x84
45 80: 05 90 lpm r0, Z+
46 82: 0d 92 st X+, r0
47 84: a6 32 cpi r26, 0x26 ; size of the data section
48 86: b1 07 cpc r27, r17
49 88: d9 f7 brne .-10 ; 0x80
50
51 8a: 0e 94 ca 00 call 0x194 ; __main
52 8e: 0c 94 d2 00 jmp 0x1a4 ; __exit
53
54 __bad_interrupt:
55 92: 0c 94 00 00 jmp 0
56
57 __usart_init: ; from avr/iom328p.h
58 96: 10 92 c5 00 sts 0x00C5, r1 ; UBRR0H or _SFR_MEM8(0xC5)
59 9a: 80 93 c4 00 sts 0x00C4, r24 ; UBRR0L
60 9e: 88 e1 ldi r24, 0x18 ; 24 or 00011000b
61 a0: 80 93 c1 00 sts 0x00C1, r24 ; UCSR0B
62 a4: 86 e0 ldi r24, 0x06 ; 06 or 00000110b
63 a6: 80 93 c2 00 sts 0x00C2, r24 ; UCSR0C
64 aa: 08 95 ret
65
66 __usart_transmit_byte: ; while (!(UCSR0A & (1 << UDRE0));
67 ac: 90 91 c0 00 lds r25, 0x00C0 ; r25 = UCSR0A
68 b0: 95 ff sbrs r25, 5 ; skip if bit 5 (UDRE0) in r25 is set
69 b2: fc cf rjmp .-8 ; loop
70 b4: 80 93 c6 00 sts 0x00C6, r24 ; UDR0 = 24
71 b8: 08 95 ret
72
73 __usart_transmit_bytes:
74 ba: 0f 93 push r16
75 bc: 1f 93 push r17
76 be: cf 93 push r28
77 c0: df 93 push r29
78 c2: ec 01 movw r28, r24
79 c4: 8c 01 movw r16, r24
80 c6: 06 0f add r16, r22
81 c8: 11 1d adc r17, r1
82 ca: c0 17 cp r28, r16
83 cc: d1 07 cpc r29, r17
84 ce: 21 f0 breq .+8 ; 0xd8
85 d0: 89 91 ld r24, Y+ ; load indirect from data space using index Y (post inc)
86 d2: 0e 94 56 00 call 0xac ; __usart_transmit_byte(r24)
87 d6: f9 cf rjmp .-14 ; 0xca
88 d8: df 91 pop r29
89 da: cf 91 pop r28
90 dc: 1f 91 pop r17
91 de: 0f 91 pop r16
92 e0: 08 95 ret
93
94 __usart_receive_byte: ; while (!(UCSR0A & (1 << RXC0)));
95 e2: 80 91 c0 00 lds r24, 0x00C0 ; r24 = UCSR0A
96 e6: 87 ff sbrs r24, 7 ; skip if bit 7 (RXC0) in r24 is set
97 e8: fc cf rjmp .-8 ; loop
98 ea: 80 91 c6 00 lds r24, 0x00C6 ; UDR0
99 ee: 08 95 ret
100
101 __usart_receive_bytes:
102 f0: 0f 93 push r16
103 f2: 1f 93 push r17
104 f4: cf 93 push r28
105 f6: df 93 push r29
106 f8: ec 01 movw r28, r24
107 fa: 8c 01 movw r16, r24
108 fc: 06 0f add r16, r22
109 fe: 11 1d adc r17, r1
110 100: c0 17 cp r28, r16
111 102: d1 07 cpc r29, r17
112 104: 21 f0 breq .+8
113 106: 0e 94 71 00 call 0xe2 ; __usart_receive_byte
114 10a: 89 93 st Y+, r24
115 10c: f9 cf rjmp .-14
116 10e: df 91 pop r29
117 110: cf 91 pop r28
118 112: 1f 91 pop r17
119 114: 0f 91 pop r16
120 116: 08 95 ret
121
122 __display_prompt:
123 118: cf 93 push r28
124 11a: df 93 push r29
125 11c: cd b7 in r28, 0x3d ; save SP low
126 11e: de b7 in r29, 0x3e ; save SP high
127 120: 2b 97 sbiw r28, 0x0b ; r28 -= strlen(prompt) + 1
128 122: 0f b6 in r0, 0x3f ; save SREG
129 124: f8 94 cli ; clear interrupts
130 126: de bf out 0x3e, r29 ; restore SP high
131 128: 0f be out 0x3f, r0 ; restore SREG
132 12a: cd bf out 0x3d, r28 ; restore SP low
133 12c: 8b e0 ldi r24, 0x0B ; strlen(_prompt) + 1
134 12e: eb e1 ldi r30, 0x1B
135 130: f1 e0 ldi r31, 0x01
136 132: de 01 movw r26, r28
137 134: 11 96 adiw r26, 0x01 ; 1
138 136: 01 90 ld r0, Z+ <----,
139 138: 0d 92 st X+, r0 |
140 13a: 8a 95 dec r24 |
141 13c: e1 f7 brne .-8 -----'
142 13e: 6b e0 ldi r22, 0x0B ; strlen(_prompt) + 1
143 140: ce 01 movw r24, r28
144 142: 01 96 adiw r24, 0x01 ; inc r24 ptr
145 144: 0e 94 5d 00 call 0xba ; __usart_transmit_bytes(r24, r22)
146 148: 0e 94 71 00 call 0xe2 ; r24 = __usart_receive_byte()
147 14c: 2b 96 adiw r28, 0x0b ; move r28 at the end of _prompt
148 14e: 0f b6 in r0, 0x3f ; save SREG
149 150: f8 94 cli
150 152: de bf out 0x3e, r29 ; restore SP high (not touched)
151 154: 0f be out 0x3f, r0 ; restore SREG
152 156: cd bf out 0x3d, r28 ; restore SP low (end of _prompt, beginning of _flag)
153 158: df 91 pop r29
154 15a: cf 91 pop r28
155 15c: 08 95 ret
156
157 __decode:
158 15e: ff 92 push r15
159 160: 0f 93 push r16
160 162: 1f 93 push r17
161 164: cf 93 push r28
162 166: df 93 push r29
163 168: f8 2e mov r15, r24
164 16a: c0 e0 ldi r28, 0x00
165 16c: d1 e0 ldi r29, 0x01
166 16e: 0c e1 ldi r16, 0x1C
167 170: 11 e0 ldi r17, 0x01
168 172: 88 81 ld r24, Y
169 174: 84 50 subi r24, 0x04 ; acc -= 4
170 176: 8f 25 eor r24, r15 ; acc ^= user input key
171 178: 0e 94 56 00 call 0xac ; __usart_transmit_byte
172 17c: 22 96 adiw r28, 0x02 ; i += 2
173 17e: 0c 17 cp r16, r28
174 180: 1d 07 cpc r17, r29
175 182: b9 f7 brne .-18 ; loop
176 184: 8a e0 ldi r24, 0x0A ; r24 = '\n'
177 186: df 91 pop r29
178 188: cf 91 pop r28
179 18a: 1f 91 pop r17
180 18c: 0f 91 pop r16
181 18e: ff 90 pop r15
182 190: 0c 94 56 00 jmp 0xac ; __usart_transmit_byte('\n')
183
184 __main:
185 194: 87 e6 ldi r24, 0x67 ; UBRR
186 196: 0e 94 4b 00 call 0x96 ; __usart_init(UBRR)
187 19a: 0e 94 8c 00 call 0x118 ; while __decode(__display_prompt())
188 19e: 0e 94 af 00 call 0x15e ;
189 1a2: fb cf rjmp .-10
190
191 __exit:
192 1a4: f8 94 cli
193
194 __stop:
195 1a6: ff cf rjmp .-2 ; 0x1a6
196
197 __flag: ; OUQP]by]sToWpBCUqz>8BECxw3
198 1a8: 4f 55 51 50 5d 62 79 5d 73 54 6f 57 70 42 43 55 71 7a 3e 38 42 45 43 78 77 33
199
200 __prompt: ; Enter key
201 1c3: 45 6e 74 65 72 20 6b 65 79 0a 00
Perfect, there is a decoding routine at 0x15e
for the encoded flag at 0x1a8
. This routine needs a xor key, which is received using a serial line. We can pursue the static analysis by bruteforcing the key. This python script will do the job:
1encoded = "OUQP]by]sToWpBCUqz>8BECxw3"
2rot_key = 4
3for xor_key in range(0xFF):
4 decoded = "".join(
5 [
6 chr((ord(char) - rot_key) ^ xor_key)
7 for i, char in enumerate(encoded)
8 if not i % 2
9 ]
10 )
11 if decoded.casefold().startswith("ecw{"):
12 print(f"{xor_key:#04x}: {decoded}")
1$ python3 decode.py
20x0e ECW{aeb1c401}
Another approach was to emulate the MCU and bruteforce the key like so:
1$ qemu-system-avr -S -s -nographic -serial tcp::5678,server=on,wait=off -machine uno -bios chest.bin
2$ printf "\x0e" | nc localhost 5678
3ECW{aeb1c401}
I hope you enjoyed it as much as I do, see you next year!