In my last post, I poked at the firmware of a network switch. While we could learn a lot from poking around the extracted data and even gain root access, one thing missing was U-Boot console access. Let's dive a bit deeper on what's going on there in this post.
The Console
The Horaco ZX-SW82TS-L2P switch we're looking at here has one of those RJ45 Cisco-style console ports on the front. Using a suitable USB serial adapter cable, we do get read access to U-Boot logs and then, once the switch boots into Linux, we get the switch's config CLI that you also get via telnet.
During the U-Boot phase, we do see a 3 second countdown (remember the bootdelay 3 we saw in
the U-Boot environment from the last post), but no amount of
mashing Esc, Enter or any other key for that matter seems to drop us into the U-Boot prompt.
This is somewhat weird though - why would you set up a 3 second boot delay if the prompt is inaccessible anyway? So my suspicion is that there is some special key you can use to get the prompt.
Taking a closer look at LOADER.bin
In principle, we do have the actual code that is executed on the device at startup in LOADER.bin, so we should be able to determine what mechanism is present to interrupt the boot. There's a problem though: I just don't have the time and stamina anymore to just spend endless nights cracking at this, and I also never dealt with MIPS assembly.
But these days, there's another option: LLMs. So I set up a fresh VM with all the artifacts extracted in the last post. I also set up Ghidra with GhidrAssistMCP and loaded LOADER.bin and finally installed Gemini CLI.
Before getting started, I also asked the "normal" web Gemini some questions about how the
boot process in MIPS generally looks like, as I am more familiar with ARM. There I learned
that the flash is directly accessible at address 0xBFC00000 but does not have DRAM at this
point, so U-Boot generally has to bootstrap in some on-chip SRAM and setup DRAM before
continuing. This makes the disassembly look a bit weird, since the addresses referenced are all
over the place, depending whether they point to the early flash address range, the relocated one,
SRAM, etc.
To give some additional help to Gemini CLI, I also ran objdump with the 0xBFC00000 starting
location:
mips-linux-muslsf-objdump -m mips -EB -Mcpu=34kc -b binary --adjust-vma=0xBFC00000 \
-D readonly_reference_material/firmware/mtd_partitions/LOADER.bin > LOADER_BFC00000.objdump
Then finally, I started Gemini CLI and started with this prompt:
You are an expert reverse engineer. Analyze the U-Boot version in
readonly_reference_material/firmware/mtd_partitions/LOADER.bin to determine the overall
control flow, where it is relocated to, how it deviates from the sources in
readonly_reference_material/realtek_sdk/loader, how it can be interrupted during
bootdelay and what settings have to be used in ghidra to get full and correct analysis.
I've already created LOADER_BFC00000.objdump as a starting point.
Collect your insights in UBOOT.md. Be specific and go into a level of detail that allows a
skilled firmware engineer to compile and adapt a custom u-boot version. At the minimum,
your analysis should include: The boot flow w.r.t to address relocations, decompression
or similar. The steps taken to initialize DRAM. Any other hardware initialization steps.
The bootdelay logic and interrupt keys that can be used to get a uboot prompt. The steps
executed in rtk network on and how they match to code in realtek_sdk. Context information
and explanation for people unfamiliar with the particular platform.
And off it went, poking at the loaded binary in Ghidra, extracting strings, hexdumping and cross-referencing everything
with the Realtek SDK source from the ZyXEL GPL release for a similar switch Olliver Schinagl
got from ZyXEL. This helped understand the overall process better, but Gemini was somewhat fixated on the way the
bootdelay is implemented in the source code, where Hit Esc key to stop autoboot should be printed and you
can interrupt with escape and wanted to move on to further analysis of GPIOs. Since that is not the case on the real device, there must be a difference. So
I followed up with another prompt:
Before we move on to the kernel modules, revisit the bootloader. The switch hardware does
not print the message to press Esc, suggesting the code compiled for the device deviates
from the available source code. Perform in-depth reverse engineering on LOADER.bin to
determine how the actual compiled code behaves to explain the discrepancy.
And off it went again on a long stream of hexdumping, using Ghidra, etc. Just before I ran out of token quota for the day, it found something intriguing:
✦ I will check the content at file offset 0xdd6bc in LOADER.bin using od -t c to see
if it contains the string printed after the CTRL+C, z, h sequence is successfully entered.
That is interesting indeed, as I also saw a seemingly custom U-Boot command called zhaodandebug in strings output - so
maybe that's the firmware engineer's name and they used the first 2 letters to unlock the U-Boot prompt?
And indeed, if we power up the switch and hit CTRL+C, z, h in sequence when the 3 second countdown starts, we get dropped into a U-Boot prompt:
U-Boot 2011.12.(3.6.8.55120) (Jul 11 2024 - 14:44:22)
Board: RTL9300 CPU:800MHz LX:175MHz DDR:600MHz
DRAM: 512 MB
SPI-F: MXIC/C22019/MMIO32-4/ModeC 1x32 MB (plr_flash_info @ 83f9a094)
Loading 65536B env. variables from offset 0xe0000
Net: Net Initialization Skipped
No ethernet found. <-- WHEN THIS SHOWS UP, PRESS CTRL+C, z, h RAPIDLY
RTL9300# # help
? - alias for 'help'
base - print or set address offset
boota - boota - boot application image from one of dual images partition automatically
bootm - boot application image from memory
bootp - boot image via network using BOOTP/TFTP protocol
cmp - memory compare
cp - memory copy
crc32 - checksum calculation
env - environment handling commands
erase - erase FLASH memory
flerase - Erase flash partition
flinfo - print FLASH memory information
flshow - Show flash partition layout
go - start application at address 'addr'
help - print command description/usage
iminfo - print header information for application image
loadb - load binary file over serial line (kermit mode)
loads - load S-Record file over serial line
loady - load binary file over serial line (ymodem mode)
loop - infinite loop on address range
md - memory display
mm - memory modify (auto-incrementing address)
mtest - simple RAM read/write test
mw - memory write (fill)
nm - memory modify (constant address)
ping - send ICMP ECHO_REQUEST to network host
printenv- print environment variables
printsys- printsys - print system information variables
protect - enable or disable FLASH write protection
reset - Perform RESET of the CPU
reset_all- Perform whole chip RESET of the CPU
rtk - rtk - Realtek commands
run - run commands in an environment variable
saveenv - save environment variables to persistent storage
savesys - savesys - save system information variables to persistent storage
setenv - set environment variables
setsys - setsys - set system information variables
sf - SPI flash sub-system
sleep - delay execution for some time
tftpboot- boot image via network using TFTP protocol
upgrade - Upgrade loader or runtime image
usb - USB sub-system
version - print monitor, compiler and linker version
RTL9300# # help rtk
rtk - rtk - Realtek commands
Usage:
rtk rtk network on
- Enable the networking function
rtk netowkr off
- Disable the networking function
rtk testmode [mode] [port]
- Set default value for specific testing
rtk ext-devInit [deviceAddress]
- set RTL8231 MDC address
rtk ext-pinGet [pinNum]
- get external 8231 GPIO pin status
rtk ext-pinSet [pinNum] [status]
- set external 8231 GPIO pin status
rtk i2c init sw [i2c_dev_id] [sck_dev] [sck_pin] [sda_dev] [sda_pin] [8/16 access type] [chipid] [delay] [rtl8231_address (for Ext-GPIO only)] [read_type 0~1]
- create a i2c group and init
rtk i2c init hw [i2c_dev_id] [intf_id 0~1 for HW] [sda_pin] [8/16 access type] [chipid] [freq 0~3] [read_type 0~1]
- create a i2c group and init
rtk i2c read [i2c_dev_id] [reg]
rtk i2c write [i2c_dev_id] [reg] [data]
rtk pinGet [pinNum]
- get internal GPIO pin status
rtk pinSet [pinNum] [status]
- set internal GPIO pin status
rtk ledtest [port] [led_index]
- led test
rtk show hw_profile_list
- show the current all supported hw_profile list
rtk phytestmode mode port channel
- Set PHY into test mode; channel: 1=A,2=B,3=C,4=D,0=None
rtk boardid
- Get board model id
rtk boardid id
- Set board model id
rtk boardmodel
- Get board model
rtk boardmode <str>model
- Set board model
rtk 10g PORT [none | fiber10g | fiber1g | fiber100m | dac50cm | dac100cm | dac300cm] - Set 10g port media
rtk phyreg get [port] [page] [reg]
rtk phyreg set [port] [page] [reg] [data]
- Get/Set PHY register
rtk phymmd get [port] [addr] [reg]
rtk phymmd set [port] [addr] [reg] [data]
- Get/Set PHY mmd register
rtk sdsreg get [sdsId] [page] [reg]
rtk sdsreg set [sdsId] [page] [reg] [data]
- Get/Set MAC serdes register
rtk macsds set [sdsId] [sds_mode]
- Set MAC serdes mode
rtk unit get
rtk unit set [unit]
- Get/set unit ID
RTL9300# # rtk boardid
Board Model ID: <NULL>
RTL9300# # rtk boardmodel
Board Model: RTL9302D_2x8224_2XGE
I guess now I have to wait until my quota refills for more exploration, but that was surprisingly painless and productive!