ImaginaryCTF 2022 - wired
Challenge Description
We found this video on social media, and suspect that it is being used to transmit some information. Can you figure out what the message is?
Overview
The video file shows an Arduino Uno connected to eight LEDs and a device labeled as a hardware RNG module. The program.hex file is an ihex file, presumably the program being run by the Arduino shown in the video.
From there, we can guess that the Arduino is somehow encoding the characters of the flag and transmitting them to the LEDs.
Simulating the Arduino
I used simavr to run the program.hex file. The board_simduino
program in the examples
directory of this project is effectively a simulated Arduino that can be debugged using gdb.
This writeup of an AVR challenge from a couple years ago was a really helpful reference for me, and I ended up using more or less the same setup.
To debug the program, I did the following:
- Start gdb using
avr-gdb
and target the simulated Arduino with the commandtarget remote localhost:1234
. - Start simduino with
./simduino.elf -d
. If gdb is running, it should stop the simduino program at the entry point. - In gdb, allow the simduino program to continue. At this point, program.hex can be uploaded to simduino using the command
avrdude -p m328p -c arduino -P /tmp/simavr-uart0 -U flash:w:program.hex
.
I should note that GDB doesn’t handle breakpoint addresses correctly. GDB interprets all addresses as being in data memory, so it puts breakpoints in the wrong place - for example, if you try to set a breakpoint at 0x100
, GDB will put the breakpoint at 0x800100
.
The workaround for this is to define all breakpoints in terms of an offset to the program counter. When I first started the program, I defined a variable $a
that was equal to the program counter at that time (in my case, it was 0x7800
). To set a breakpoint at 0x100
, I could then use the command break *($a-0x7800+0x100)
.
Disassembly
The Arduino Uno is built on the ATmega328 processor, so it uses the AVR instruction set. This means we can disassemble the program.hex file using the command avr-objdump -m avr -d program.hex
. (Ghidra also supports the AVR architecture, but it numbers the memory addresses differently from GDB, which was inconvenient. Because of this, I found the disassembly from avr-objdump
easier to work with most of the time.)
0: 0c 94 5c 00 jmp 0xb8 ; 0xb8
4: 0c 94 79 00 jmp 0xf2 ; 0xf2
8: 0c 94 79 00 jmp 0xf2 ; 0xf2
c: 0c 94 79 00 jmp 0xf2 ; 0xf2
10: 0c 94 79 00 jmp 0xf2 ; 0xf2
...
We can see that the program starts with a large table of jump instructions, presumably interrupt vectors. The jmp 0xb8
instruction at address 0 tells us that 0xb8
is the actual starting address of the program.
The hardware RNG module
3e2: 90 92 7c 00 sts 0x007C, r9 ; 0x80007c
3e6: 80 91 7a 00 lds r24, 0x007A ; 0x80007a
3ea: 80 64 ori r24, 0x40 ; 64
3ec: 80 93 7a 00 sts 0x007A, r24 ; 0x80007a
3f0: 80 91 7a 00 lds r24, 0x007A ; 0x80007a
3f4: 86 fd sbrc r24, 6
3f6: fc cf rjmp .-8 ; 0x3f0
3f8: 90 91 78 00 lds r25, 0x0078 ; 0x800078
3fc: 80 91 79 00 lds r24, 0x0079 ; 0x800079
At address 3e2, we can see several accesses of the addresses0x78
, 0x79
, and 0x7a
. This doesn’t seem like much at first, but a look at this include file for the ATmega328 processor reveals that these addresses are actually memory mapped to registers containing information related to the processor’s analog-to-digital converter.
In other words, when the program reads from these addresses, it’s probably actually reading from the A0
pin of the Arduino, where the RNG module is connected. The result of this read is stored in r24
.
;load flag character into r18
414: 29 91 ld r18, Y+
416: fe 01 movw r30, r28
418: 31 97 sbiw r30, 0x01 ; 1
;XOR flag with random number from A0
41a: 28 27 eor r18, r24
;store XOR result
41c: 20 83 st Z, r18
After the read from pin A0
, the result is then XORed with a character of the flag.
Generating the XOR key
At first I thought each character of the flag was XORed with the same value, but it’s actually a little more complicated than that. The XOR key for each character is generated based on the key used for the previous character.
We can see that the XOR key is generated using the following steps:
41e: 9c 01 movw r18, r24
420: 21 70 andi r18, 0x01 ; 1
422: 33 27 eor r19, r19
First, the LSB of the previous XOR key is stored in r18
.
424: b6 95 lsr r27
426: a7 95 ror r26
428: 97 95 ror r25
42a: 87 95 ror r24
Then, the previous XOR key is shifted right. This makes it look like the XOR key is stored in r27:r26:r25:r24
, but only r25
and r24
are actually used. I found that it made the most sense to treat the XOR key as a 16-bit value stored in r25:r24
.
42c: 23 2b or r18, r19
42e: 11 f0 breq .+4 ; 0x434
430: 2d ea ldi r18, 0xAD ; 173
432: 92 27 eor r25, r18
434: ec 16 cp r14, r28
436: fd 06 cpc r15, r29
438: 69 f7 brne .-38 ; 0x414
Finally, we check whether the LSB of the previous XOR key was 1 or 0. If it was 1, we XOR r25
(the high byte of the XOR key) with 0xAD
.
I replicated this key generation function with the following Python function:
def shift_key(prev_key):
new_key = prev_key >> 1
if(prev_key & 1 == 1):
new_key = new_key ^ 0xAD00
return new_key
The LED Pinout
Now that we know how the flag is encoded in memory, we need to figure out how to interpret the values shown on the LEDs.
470: 61 e0 ldi r22, 0x01 ; 1
472: 82 e0 ldi r24, 0x02 ; 2
474: 0e 94 7b 00 call 0xf6 ; 0xf6
I noticed that the function at 0xf6
was getting called a lot, and that it always took 2 parameters: a value between 2 and 9 in r24
, and a value of either 1 or 0 in r22
.
Looking at the function at 0xf6
, it’s hard to tell exactly what’s happening, but it looks like the function sets or clears a single bit from a register, then stores the result somewhere. From there, I guessed that the function turns a given output pin on or off. Since there are 8 connected pins and 8 possible values passed in through r24
, it’s safe to assume that r24
stores the number of the pin to be modified, and r22
stores whether the pin should be set or cleared.
At that point, I needed to figure out which pins corresponded to which LEDs. Fortunately, before the flag is transmitted, a function is called that turns on the LED for each pin in order, so we can match the pin numbers to the LEDs. (I didn’t notice that pin 9 came before pins 2 through 8 at first, leading to several very painful hours of trying to figure out why none of the values made any sense.)
The Encoded Flag
;bit 0
460: 80 fe sbrs r8, 0
462: 04 c0 rjmp .+8 ; 0x46c
464: 61 e0 ldi r22, 0x01 ; 1
466: 89 e0 ldi r24, 0x09 ; 9
468: 0e 94 7b 00 call 0xf6 ; 0xf6
;bit 1
46c: 81 fc sbrc r8, 1
46e: 04 c0 rjmp .+8 ; 0x478
470: 61 e0 ldi r22, 0x01 ; 1
472: 82 e0 ldi r24, 0x02 ; 2
474: 0e 94 7b 00 call 0xf6 ; 0xf6
;bit 2
478: 82 fe sbrs r8, 2
47a: 04 c0 rjmp .+8 ; 0x484
47c: 61 e0 ldi r22, 0x01 ; 1
47e: 83 e0 ldi r24, 0x03 ; 3
480: 0e 94 7b 00 call 0xf6 ; 0xf6
...
The actual transmission of the flag takes place in these lines. A character of the encoded flag is stored in r8
, and each bit of this character determines which LEDs should be on.
However, we need to be careful: notice that some bits of the flag character are checked with the sbrs
(Skip if Bit in Register Set) instruction, and others are checked with sbrc
(Skip if Bit in Register Clear). An sbrs
instruction followed by an rjmp
instruction means that the program takes the jump if the bit being checked was 0. Similarly, sbrc
followed by rjmp
takes the jump if the bit being checked was 1.
Effectively, this means that sometimes a lit LED corresponds to a value of 1 and an unlit LED corresponds to a value of 0, but sometimes it’s the other way around. Bits 1,3,4, and 7 are checked with sbrc
, so their LEDs are on if that bit of the flag character was 0. The others are checked with sbrs
, so their LEDs are on if the bit was 1.
When recording the values from the video, I treated a lit LED as a 1 and an unlit LED as a 0, meaning that I had to negate bits 1, 3, 4, and 7 before attempting to decode the flag.
def negate_bits(bits, which_to_negate):
new_bits = 0
for i in range(8):
if i in which_to_negate:
new_bits |= (~bits & (1 << i))
else:
new_bits |= bits & (1 << i)
return new_bits
Decoding
At this point, we have all the information we need to decode the video. For each combination of LEDs shown in the video, we need to do the following:
- Write the combination of LEDs as an 8-bit value, using pin 9 as the LSB and pins 2-8 for the following pins. Treat a lit LED as a 1 and an unlit LED as a 0.
- Negate bits 1, 3, 4, and 7.
- XOR the result with the corresponding XOR key in the sequence.
Since we know the first character of the flag is i
, we have all the information we need to generate the sequence of XOR keys, since each XOR key in the sequence is uniquely determined by the previous key. By XORing i
with 0b01010110
, the first value shown in the video, we find that the first XOR key in the sequence is 0xa5
. From there, we can calculate all subsequent keys.
The full solve script is given below:
#Negate bits from the input
def negate_bits(bits, which):
new_bits = 0
for i in range(8):
if i in which:
new_bits |= (~bits & (1 << i))
else:
new_bits |= bits & (1 << i)
return new_bits
#Generate a new XOR key from the previous key
def shift_key(prev_key):
new_key = prev_key >> 1
if(prev_key & 1 == 1):
new_key = new_key ^ 0xAD00
return new_key
#Generate as many successive XOR keys as we need
def list_keys(key, num_times):
keyList = []
keyList.append(key)
curr_key = key
for i in range(num_times):
keyList.append(shift_key(curr_key))
curr_key = shift_key(curr_key)
return keyList
#XOR each character of the encoded flag
#with the lower 8 bits of the corresponding XOR key
def generate_flag(init_val, flag_vals):
s = ""
theKeys = list_keys(init_val, len(flag_vals))
for i in range(len(flag_vals)):
s += chr(flag_vals[i]^(theKeys[i] & 0xff))
return s
flag_vals = [0b01010110, 0b10101011, 0b01000111, 0b10101000, 0b11001011, 0b01111000, 0b00110101, 0b00010110, 0b10011010, 0b11000111, 0b01011001, 0b00100110, 0b10010011, 0b01001110, 0b10011100, 0b00001111, 0b11111101, 0b10000011, 0b01101101, 0b00101101]
new_flag_vals = []
for i in flag_vals: new_flag_vals.append(negate_bits(i, [1,3,4,7]))
print(generate_flag(0xa5, new_flag_vals))
This successfully prints the flag: ictf{weird_rng_912b}
Originally posted at https://hackmd.io/@clairelevin/ryuqxzQ2q.