JRP Innovations

kernel

All posts tagged kernel by JRP Innovations
  • Posted on

    Reverse engineering is something I have been learning for years, most reverse engineering projects when you're learning end in confusion, destroyed devices, and breaking things to figure them out...

    I wish I started my journey with this project! By the end of the first day, I had a root shell, a full extracted firmware filesystem, the stock update image unpacked, the bootloader command list, the actual compressed kernel, several device trees, and a much better sense of what not to type into CFE, (CFE is Broadcom's network device firmware and bootloader for ARM SBCs).

    The short version is: this router is a surprisingly capable ARM Linux system wrapped in several layers of Broadcom and Netgear boot infrastructure. Though, there is also no security??

    Model caveat: My stock update image is for an RAX10 running firmware V1.0.20.174, but an early CFE boot capture identifies its board ID as RAX60 and looks for RAX60.dtb before falling back to a generic DTB. I have not yet reconciled that mismatch. For now, the claims below are about this captured BCM6755 firmware/platform combination, not necessarily every retail unit with either model number. It seems like the firmware between all RAX models may be the same with actual model-specific data passed in from CFE but that's a lofty claim to make and I'm only making it based on this weird identifier mis-match and the DTB handling behavior we will discuss later.

    Netgear RAX10 base

    Dissasembly

    I started with the obvious steps of taking it apart, there were 3 security screw holding the case together, there is a through-hole design mounting the PCB securely into the case. Once the 3 exterior screws were removed and the case cracked, I immediately noted a 4 pin header and had a suspect for serial connection. There was 3 other hardware points with potential for debug/extensibility, though no more on that for now.

    The DRAM (DDR3), the main SBC CPU package, the ethernet controller were all located under a large heatsink. The heatsink was easy to remove with 4 chunky Phillips head screws holding it down exposing EMI shielding with thermal pads for the CPU & Ethernet Controller. Fortunately the EMI shielding was pressure fit and easy to remove without severely damaging. There was an additional chip with it's own EMI shielding directly adjacent the main SBC package, (I suspect this to be radio related though I did not note down the IC label as I already identified my main points of interest for now, this will probably come back to bite me).

    Hardware packages:

    • Broadcom BCM6755A1KFEBG, SoC
    • Broadcom BCM531340KFBG, Ethernet Controller
    • Winbond W634GU6NB-12, 4Gbit(512MB) DDR3L RAM RAX10_Main_CPU_BCM6755RAX10_EthController_BCM531340RAX10_DRAM_W634GU6NB

    Seperate from the heatshield, we had the 256MB flash. * Macronix 2Gbit(256MB) NAND MX30LF2G18AC RAX10_NAND_FLASH_MX30LF2G18AC

    That means this is not a toy microcontroller router. It is a 1.5 GHz quad-core ARMv7 Linux machine with half a gigabyte of RAM, hardware packet acceleration, two PCIe Wi-Fi radios, and a slightly cursed 256 MB NAND-backed appliance filesystem; (The NAND is being managed/controlled by the host directly).

    At this point I was ready to see if what I suspected to be a serial header, really was simply a serial header. I got my multimeter and identified voltage as 3.3v, I checked to see if there was any visible voltage change from transmission drops on any of the pins, quickly I identified pin 1 for TX this way (white), I connected it to a standard serial reader after confirming pin out as p0(3.3v) P1(TX) P2(RX) P3(GND), I connected TX, RX, & GND to my Pine64 serial console. RAX10_SerialPort_UART

    I rebooted and opened a standard serial terminal over the USB COM port @ 115200 Baud (115200 8N1), this caused a flood of coherent serial output leading me to re-assemble the EMI shielding and heatsink before the device got too hot.

    The running hardware

    The bootloader identified the platform as:

    Chip ID: BCM6755_A1
    ARM Cortex A7 Quad Core: 1500MHz
    Total Memory: 536870912 bytes (512MB)
    NAND flash: 256MB
    

    The flash is SLC NAND with:

    Page size:        2048 bytes
    OOB/spare area:     64 bytes
    Erase block:    131072 bytes / 128 KiB
    ECC:            BCH-4
    

    The boot chain

    The serial console exposed the full chain:

    Early bootstrap / BTRM
    → Broadcom CFE bootloader
    → CFE reads NAND and locates vmlinux.lz
    → CFE decompresses the Linux kernel
    → CFE loads a DTB
    → CFE patches boot arguments and NVRAM into the DTB
    → Linux boots
    → Linux mounts the SquashFS root filesystem
    

    Identifying CFE lines were:

    ubi_find_file: got vmlinux.lz  size 2756881
    Decompression LZMA Image OK!
    Entry at 0x00018000
    
    cfe_fs_fetch_file: file RAX60.dtb not found
    ubi_find_file: got 947622.dtb  size 4540
    
    Setting ROOTFS: root=/dev/ubiblock0_0 ubi.mtd=0 ubi.block=0,0 rootfstype=squashfs
    CFE appending "isolcpus=3" from PROFILE to boot cmdline
    Appending CFE version to dtb
    Appending NVRAM to dtb
    

    This immediately explained something that confused me at first: the kernel is not stored in /boot, and it is not inside the root SquashFS filesystem. This brings me new questions though like why is the firmware expecting a different DTB than the one it provides? Why does it need to apppend the NVRAM? We find out later there is a mismatch between the DTB itself and the actual amount of memory installed, this is another thing that the CFE environment appears to be configuring. The verbosity of the serial output will help later though for seperating different firmware pieces with 0 guesswork.

    Now we know CFE has its own tiny file store inside the firmware UBI image for boot files. It pulls the compressed kernel and DTB from there before Linux exists. Linux then mounts a separate SquashFS volume as /.

    Storage: raw NAND is not a disk

    The router’s storage stack is:

    Raw NAND flash
    → MTD partitions
    → UBI
    → UBI volumes
    → either SquashFS or UBIFS
    

    The important part:

    mtd0 rootfs
    → UBI
    → ubi0:0
    → ubiblock0_0
    → SquashFS
    → immutable Linux root filesystem
    
    mtd2 data
    → UBI
    → ubi1:0
    → UBIFS
    → mounted writable /data
    

    This is why my initial NAND dump experiments were confusing. Raw NAND partition is not a normal disk image. It has erase blocks, bad blocks, ECC, UBI headers, wear-level metadata, and logical volumes. If a tool skips physical bad blocks while dumping, the resulting file can be perfectly useful as a stream of readable data while no longer lining up as a valid physical UBI image. This is something new to me as most of my prior successful projects with SBCs were not reverse engineered and all had more standard feeling storage stacks.

    The router reported bad blocks inside the rootfs region:

    nand_flash_read_buf(): Attempt to read bad nand block 480
    nand_flash_read_buf(): Attempt to read bad nand block 591
    nand_flash_read_buf(): Attempt to read bad nand block 593
    nand_flash_read_buf(): Attempt to read bad nand block 792
    

    That was the first major lesson:

    A “raw flash dump” is only raw if you understand exactly how the dumper handled bad blocks and OOB data.

    The easy firmware dump

    The cleanest filesystem-level target was not /dev/mtd0.

    It was:

    dd if=/dev/ubiblock0_0 bs=1M | nc 10.69.69.120 9000
    

    /dev/ubiblock0_0 is already the UBI volume presented as a block device for SquashFS. Dumping it skips the raw NAND and UBI bookkeeping layers entirely.

    The first four bytes of the resulting image were:

    68 73 71 73
     h  s  q  s
    

    That is the SquashFS magic.

    From there, extraction was straightforward:

    unsquashfs -d RAX10-rootfs-extracted RAX10-root.squashfs
    

    The extracted filesystem contains the normal Linux userspace:

    /bin
    /etc
    /lib
    /sbin
    /usr
    /www
    

    That is where the interesting Netgear shell scripts, daemon binaries, web UI pieces, configuration helpers, and Broadcom modules live.

    The official firmware image was much cleaner than expected

    Running Binwalk on the stock update file showed:

    0x00000080  UBI image
    

    The first 128 bytes are a Netgear/DNI update header. Everything after that is a normal UBI image.

    After stripping the 128-byte header and inspecting the UBI container, I found four volumes:

    rootfs_ubifs       403 erase blocks
    METADATA             1 erase block
    METADATACOPY         1 erase block
    filestruct_full.bin 26 erase blocks
    

    Despite its misleading name, rootfs_ubifs starts with:

    68 73 71 73
    hsqs
    

    It is a SquashFS image, not a UBIFS filesystem.

    The important realization was:

    rootfs_ubifs
    → SquashFS userland
    
    filestruct_full.bin
    → CFE boot artifacts:
       kernel
       device trees
       boot metadata
    

    Finding the actual kernel

    Binwalk on filestruct_full.bin found:

    0x0000001B  Device tree blob, 4540 bytes
    0x000011F9  Device tree blob, 4724 bytes
    0x0000248F  Device tree blob, 4724 bytes
    
    0x0006C3E9  LZMA compressed data
                compressed size:   2757010 bytes
                uncompressed size: 7756064 bytes
    

    That LZMA stream is the missing bootable kernel payload: vmlinux.lz.

    The CFE boot log reported vmlinux.lz as 2,756,881 bytes, while Binwalk identified a 2,757,010-byte LZMA stream. The 129-byte difference is probably file-record or container overhead, but I have not fully decoded the CFE file-store format yet.

    The kernel can be carved and decompressed with:

    dd if=img-787591378_vol-filestruct_full.bin.ubifs \
       of=vmlinux.lz \
       bs=1 \
       skip=$((0x6C3E9)) \
       count=2757010 \
       status=progress
    
    xz --format=lzma -dc vmlinux.lz > vmlinux
    file vmlinux
    

    The DTB was more revealing than expected

    The first extracted DTB decompiled successfully:

    dtc -I dtb -O dts -o dtb-0.dts dtb-0.dtb
    

    The warnings were style warnings from Broadcom’s old device-tree naming conventions, not a failed decompile.

    The DTS describes a generic Broadcom BCM947622 platform:

    model = "Broadcom BCM947622";
    compatible = "brcm,bcm947622";
    

    It includes:

    ARM Cortex-A7 CPUs
    PL011 UART at 0xFF812000
    NAND controller at 0xFF801800
    I2C
    PCIe
    SDHCI controller
    hardware RNG
    crypto accelerator
    watchdog
    thermal support
    

    The most interesting discovery was that the static DTB claims only 64 MB of RAM:

    memory {
        reg = <0x00 0x4000000>;
    };
    

    But CFE patched the live handoff DTB to 512 MB:

    /memory = 0x20000000 bytes @ 0x0
    

    CFE also injected the actual root filesystem location, NVRAM, CFE version, and the isolcpus=3 performance setting.

    So the static DTB is not the final board description. CFE turns it into one at boot.

    CFE: the bootloader has a loaded gun labeled “e”

    Interrupting boot drops into CFE. The discovery command was:

    help
    

    The reassuring commands include:

    p               Print boot line and board parameters
    cfe_version     Show CFE version
    fb              Find NAND bad blocks
    dn              Dump NAND contents including spare area
    rn              Read NAND with ECC disabled
    find            Find a string in NAND
    comp            Compare NAND blocks
    nmrp_flag_show  Show recovery flag
    meminfo         Show system memory
    

    The terrifying commands include:

    e               Erase NAND flash
    w               Write a whole image from the beginning of flash
    ws              Write an image previously loaded through Kermit/JTAG
    upgrade_cfe     Upgrade CFE
    upgrade_image   Write a complete image
    i               Erase persistent storage
    factory_default Factory reset
    

    The command name e literally means:

    Erase NAND flash
    

    Nice to know that the proprietary alternative to U-Boot is designed to be as user friendly as a .50cal rifle with a kink in the barrel.

    CFE also exposes recovery hooks:

    fw_recovery     start TFTP server
    nmrp            start NMRP client
    

    Those are likely the practical recovery path after a normal firmware brick. They are much more useful than attempting a blind raw-NAND restore.

    Security state: unsecure does not mean unrestricted

    One surprising CFE command was:

    otpcfg get mode
    

    Its result:

    SoC is in unsecure mode
    

    That is encouraging for experimentation, because the irreversible OTP secure-mode flag does not appear to be set.

    It does not prove that CFE will accept arbitrary unsigned images. CFE may still enforce a Netgear header, board ID, checksum, version rule, or signature policy.

    Also, this command should never be run casually:

    otpcfg set mode secure
    

    That sounds like an irreversible fuse operation, and I have no desire to discover exactly how irreversible.

    The router repairs its own board data

    One extracted script mirrors boarddata1 to boarddata2.

    At boot, the router logged:

    Start to handle the backup action, boarddata1 | boarddata2 ....
    Both the data of origin and backup partition are good !!
    

    The script checks both copies with dni_block_chksum. If the primary copy is valid and the backup differs or fails validation, the primary overwrites the backup. If the primary is bad and the backup is valid, the backup restores the primary.

    The notable detail is that it copies only one NAND erase block:

    nanddump ... -l 131072
    flash_erase ...
    nandwrite ...
    

    That is exactly 128 KiB: one physical erase block.

    This strongly suggests that the actual board-data object lives in the first good erase block of each 1 MB board-data partition. The remaining partition space is likely spare capacity or simply reserved by the vendor format.

    It also means these partitions are not things to casually erase or overwrite. They likely contain device-specific identity, regional settings, MAC information, and other board configuration.

    Where this leaves the project

    At the end of day one, the router is no longer a black box.

    I have:

    ✓ Root UART console
    ✓ Root Linux shell
    ✓ CFE shell
    ✓ Stock firmware container decoded
    ✓ Firmware SquashFS extracted
    ✓ Kernel location identified
    ✓ DTBs extracted and decompiled
    ✓ UBI volume structure mapped
    ✓ Boot flow understood
    ✓ A recovery path to investigate
    ✓ Several dangerous flash-writing helpers identified
    

    Here are some of the things I may look into next:

    1. Dump the runtime DTB from /sys/firmware/fdt and compare it to the static DTB.
    2. Extract and inspect the decompressed kernel.
    3. Build a tiny static ARMv7 binary and run it from the router.
    4. Map every service listening on LAN interfaces before touching vulnerability research.

    The interesting long-term idea is a hybrid system:

    Stock CFE + vendor kernel/drivers
    → custom lightweight userspace
    → eventually a custom appliance-style firmware image
    

    Ultimately since this device has it's home as a switch on my desk; the real challenge is preserving the vendor-specific pieces that make the switch, Wi-Fi radios, acceleration hardware, LEDs, watchdog, and flash layout behave.

    For now, the router remains alive, bootable, and significantly less mysterious.

  • Posted on

    The Problem

    For the last two days, I have been reconfiguring my personal network infrastructure to create a complete, secure, and coherent solution for my needs. For context, this server is running systemd Gentoo Linux with +march=native and standard stable compile flags, testing packages are not allowed globally, the kernel is manually configured, it was compiled and installed 12/31. Special care was used to ensure I selected compatible kernel options matching the Gentoo Wiki's article on Docker.

    This issue took longer than I care to admit because of a couple now glaringly obvious red herrings. I was able to emerge and install Docker without a problem, but when I attempted to run the service I received the following error:

    Dec 31 10:34:56 gentoo systemd[1]: Starting Docker Application Container Engine...
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.093036541Z" level=info msg="Starting up"
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.280360174Z" level=info msg="[graphdriver] using prior storage driver: overlay2"
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.280599062Z" level=info msg="Loading containers: start."
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.282787923Z" level=warning msg="Running modprobe bridge br_netfilter failed with message: modprobe: WARNING: Module bridge not f>
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.287999183Z" level=info msg="unable to detect if iptables supports xlock: 'iptables --wait -L -n': `modprobe: FATAL: Module ip_t>
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.340305545Z" level=info msg="stopping event stream following graceful shutdown" error="<nil>" module=libcontainerd namespace=moby
    Dec 31 10:34:56 gentoo dockerd[134836]: failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to register "bridge" driver: failed to create >
    Dec 31 10:34:56 gentoo dockerd[134836]: iptables v1.8.11 (legacy): can't initialize iptables table `nat': Table does not exist (do you need to insmod?)
    Dec 31 10:34:56 gentoo dockerd[134836]: Perhaps iptables or your kernel needs to be upgraded.
    Dec 31 10:34:56 gentoo dockerd[134836]:  (exit status 3)
    Dec 31 10:34:56 gentoo dockerd[134836]: time="2024-12-31T10:34:56.340724371Z" level=info msg="stopping event stream following graceful shutdown" error="context canceled" module=libcontainerd na>
    Dec 31 10:34:56 gentoo systemd[1]: docker.service: Main process exited, code=exited, status=1/FAILURE
    Dec 31 10:34:56 gentoo systemd[1]: docker.service: Failed with result 'exit-code'.
    

    This is where I went naive, the error there clearly indicates iptables is not working properly and the docker service can't properly register it's network configuration causing it to fail. Working through that error log from the bottom up, reviewed all the wiki entries on iptables & ip_tables, I emerged iptables again and once it was finished I attempted to run "iptables -L" which met me with another error shown below. One thing I learned through this is that ip_tables, and iptables, are two related but different things (ip_tables is a kernel module and iptables is a front-end for the module).