Skip to content

Instantly share code, notes, and snippets.

@four0four
Last active May 12, 2024 04:20
Show Gist options
  • Save four0four/4cbf05ac491fa39da5c192e6ac0977c7 to your computer and use it in GitHub Desktop.
Save four0four/4cbf05ac491fa39da5c192e6ac0977c7 to your computer and use it in GitHub Desktop.
Zynq BootROM Secrets - UART loader

Zynq BootROM Secrets: UART loader

Recently I acquired (md5: ADF639AFE9855EE86C8FAAD216C970D9) the Zynq bootrom, and during the reversing process uncovered some interesting secrets, one of which is an as-of-yet undocumented UART loader. As documented the Zynq bootrom will load from NOR/NAND/SPI flashes, eMMC/SDIO-based storage (unfortunately) not USB, or anything else more complex.

Not sure why Xilinx didn't document this. In my brief testing it is super unreliable if you just spit everything at once - they reset the RX/TX paths during the process, so timing is critical, but that might be the janky meter-long ftdi cable. You can change the baudrate during the process, but I was too lazy to do the math.

Here's the disassembly that made me look twice (that, and checks for the MIO boot_mode[2:0] that weren't specified in the docs :)):

ROM:0000A220 BL              uart_init
ROM:0000A224 MOV             r_buff, #0x1030
ROM:0000A228 MOV             R1, #'X' ; data
ROM:0000A22C MOVT            r_buff, #0xE000 ; void *
ROM:0000A230 MOV             R4, #0x488
ROM:0000A234 BL              dsb_write
ROM:0000A238 MOV             r_buff, #FIFO ; void *
ROM:0000A240 MOV             R1, #'L' ; data
ROM:0000A244 BL              dsb_write
ROM:0000A248 MOV             r_buff, #FIFO ; void *
ROM:0000A250 MOV             R1, #'N' ; data
ROM:0000A254 BL              dsb_write
ROM:0000A258 MOV             r_buff, #FIFO ; void *
ROM:0000A260 MOV             R1, #'X' ; data
ROM:0000A264 BL              dsb_write`

Sure enough, if we set the correct MIO straps (boot_mode[2:0] = 0b011, if you'd like to try), the bootloader prompts us! Settings are 115200 8N1, port is MIO49 (RxD) and MIO48 (TxD). My Zybo had these broken out to the usb-uart interface, my Cora did not. The miner board I had sitting around had them broken out to the UART header. The latter was the only one to sufficiently break out the boot_mode strapping pins, fwiw.

Protocol is simple, all ints are LE encoded and 4 bytes:

zynq: XLNX-ZYNQ

you: BAUD[BAUDGEN value][baud_divider_reg0 value][image length][checksum][image data]

The checksum may be set to 0 to skip - it's just an additive 32 bit sum of the image data, but often enough some random corruption will cause this to fail

I don't understand the whole BAUD bit - since the ROM loops and tries this a couple times, perhaps there was an attempt to autobaud at one point that didn't make it into the rev I have.

The really handy part is the bootrom actually will print error messages over UART too (even when not using this mode :))!

code description
0x200A probably a checksum error, the logic is a bit fucky and tries to find another multiboot header, which flows to this error
0x2501 didn't handshake within 128 attempts
0x2502 Size error (hilariously, there's a signing bug here, and you can pass this check with a size < 0. unfortunately this shortcuts the actual copy loop. no cool 0day here, sorry).
0x2503 UART checksum error (set to 0 to bypass)

Attached is a super simple bit of python you can use to experiment with this! Just give it a Zynq image as the first arg (the same thing you build with bootgen, or place on an sdcard, etc).

#!/bin/env python3
import serial
import time
import sys
if len(sys.argv) < 2:
print("gimme a file")
sys.exit(-1)
binfile = sys.argv[1]
img = open(binfile, 'rb').read()
baudgen = 0x11
reg0 = 0x6
def chksum(data):
chk = 0
for d in data:
chk += d
return chk
def dbgwrite(ser, data):
print(str(data))
ser.write(data)
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'
#size = 0xFFFFFFFE # :<
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)
print("bootrom sez: " + str(ser.read(ser.in_waiting)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment