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
and
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:
The system is powered by a
CTF-8051micro-controller, probably based on Intel 8051.The firmware is read from a
CTF-5593DEEPROM.- 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
FlagROMdevice 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 weather
station to read the content of FlagROM.
Firmware source code
Let’s now read 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:
1 2 3 4 5 6 7 8const 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 99
i2c status: transaction completed / ready
? r 119 128
i2c status: transaction completed / ready
37 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 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 we
understand that this error message is due to the port_to_int8 function
returning -1:
| |
str_to_uint8 cannot return -1, i.e. this error message is triggered by
is_port_allowed returning -1:
| |
This function checks the port I2C address against ALLOWED_I2C whitelist, but
it has a flaw: it does not check that it reached the end of the port string
when reaching the end of an item in ALLOWED_I2C.
The following Python script exploits this vulnerability to find all I2C devices:
| |
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 0
i2c status: transaction completed / ready
? r 101025 128
i2c status: transaction completed / ready
2 0 6 2 4 228 117 129 48 18 8 134 229 130 96 3
2 0 3 121 0 233 68 0 96 27 122 0 144 10 2 120
1 117 160 2 228 147 242 163 8 184 0 2 5 160 217 244
218 242 117 160 255 228 120 255 246 216 253 120 0 232 68 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 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:
| |
We use cpu_rec and confirm that the 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:
| |
During EEPROM programming, bits can only be cleared: it is impossible to
replace a 0 by a 1.
We choose to put this piece of assembly code at 0x0a02 offset as it
is filled with 0xFF. To run this code, we replace the data and code
before with 0x00 which is a NOP instruction.
Let’s implement this in Python:
| |
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?}.
