UDM Pro emulation
Introduction#
I and a couple of others wanted to attempt competing in the well-known Pwn2Own competition [1], and where better to start than in the SOHO smashup. In this category two devices have to be pwned; a router and a consumer device such as a NAS. The router is exposed only through its WAN interface, but the consumer interface is directly connected to the router and any of its services exposed to the LAN can be attacked if the router is pwned first.
The hardest part of this challenge seems the router, so this is where we started as well. We first checked which of the devices in scope have firmware available for download, attempted to unpack the downloaded firmware images, and ran EMBA [2] to get some preliminary analysis done. That’s when the real work started however, how do you find the full attack surface when only the WAN side of the router is exposed? Assuming the device uses a firewall that blocks traffic coming in from the WAN interface, this should fall in roughly three categories:
- UDP services listening on all interfaces: even if the firewall blocks traffic from the WAN interface going to this service, in some cases it’s possible to spoof the IP address on a single UDP packet, which would then be passed on to the service. Any vulnerability or exploit requiring communication back-and-forth is likely to fail with this attack vector.
- Administration service: Some routers expose a service by default that allows for example your ISP to monitor the status of the router. This is intended to help in debugging any problems you might have with your internet connection, but is an attack vector as well.
- Programs making calls to the outside world: Outgoing connections are generally not blocked by the firewall, and depending on the circumstances these can be intercepted and used as an attack vector. For example, a script checking for updates over an insecure connection could receive a custom firmware image with our backdoor in it.
Statically finding as many functionalities that fall in these categories is difficult, and buying an assortment of devices to test them all is expensive, so we would like to emulate the firmware we found online. In an ideal scenario we can find some vulnerabilities in this way and only need to test the exploit on one actual device. This blog post describes the process of emulating the Ubiquiti Dream Machine Pro firmware.
Unpacked firmware image#
The firmware for the UDM Pro can be downloaded from the site of Ubiquiti itself [3]. In the initial stage of this research version 4.1.13 was downloaded. This was unpacked using Binwalk:
binwalk -Me b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin
Binwalk found the following signatures in the image:
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL HEXADECIMAL DESCRIPTION
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
336245 0x52175 PKCS DER hash, SHA256
528780 0x8118C Device tree blob (DTB), version: 17, CPU ID: 0, total size: 25117 bytes
557452 0x8818C Device tree blob (DTB), version: 17, CPU ID: 0, total size: 23817 bytes
586124 0x8F18C Device tree blob (DTB), version: 17, CPU ID: 0, total size: 25396 bytes
614796 0x9618C Device tree blob (DTB), version: 17, CPU ID: 0, total size: 26048 bytes
643468 0x9D18C Device tree blob (DTB), version: 17, CPU ID: 0, total size: 26428 bytes
672140 0xA418C Device tree blob (DTB), version: 17, CPU ID: 0, total size: 26345 bytes
1218172 0x12967C CRC32 polynomial table, little endian
1228537 0x12BEF9 PKCS DER hash, SHA1
1228761 0x12BFD9 PKCS DER hash, SHA256
1229241 0x12C1B9 PKCS DER hash, SHA256
1229596 0x12C31C U-Boot version string: 2015.07-alpine_db-2.21-HAL (Jul 30 2024 - 13:04:14 +0800)
1342204 0x147AFC Device tree blob (DTB), version: 17, CPU ID: 0, total size: 1772 bytes
1470056 0x166E68 Device tree blob (DTB), version: 17, CPU ID: 0, total size: 14553682 bytes
16023802 0xF480FA SquashFS file system, little endian, version: 4.0, compression: zstd, inode count: 46094, block size: 262144, image size: 761319517 bytes, created: 2024-12-24 15:05:15
777347386 0x2E55613A ELF binary, 64-bit executable, ARM 64-bit for System-V (Unix), little endian
781166650 0x2E8FA83A SHA256 hash constants, little endian
The interesting parts of the output are the fact it identified a Device Tree Blob (DTB), a U-Boot image, and a file system. In the output from the extraction the DTB and the file system have been unpacked. However, the U-Boot image was not extracted successfully, and notably a kernel image is missing. Given the other components found in the image file, a kernel image would have made sense. Hence, we also tried unpacking the image using Unblob:
unblob b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin
╭──────────────────────────────────────────────────────────────────────────────────────────────── unblob (25.5.26) ──────────────────────────────────────────────────────────────────────────────────────────────╮
│ Output path: [...]/ubiquiti/b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin_extract │
│ Extracted files: 468 │
│ Extracted directories: 57 │
│ Extracted links: 49 │
│ Extraction directory size: 799.47 MB │
╰────────────────────────────────────────────────────────────────────────────────────────────────── Summary ───────────────────────────────────────────────────────────────────────────────────────────────────╯
Chunks distribution
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━┳━━━━━━━━┓
┃ Chunk type ┃ Size ┃ Ratio ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━╇━━━━━━━━┩
│ SQUASHFS_V4_LE │ 726.05 MB │ 90.91% │
│ ELF64 │ 26.21 MB │ 3.28% │
│ CPIO_PORTABLE_ASCII │ 19.28 MB │ 2.41% │
│ GZIP │ 13.85 MB │ 1.73% │
│ UNKNOWN │ 13.27 MB │ 1.66% │
│ ZSTD │ 388.00 B │ 0.00% │
└─────────────────────┴───────────┴────────┘
Chunk identification ratio: 98.34%
Encountered errors
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Severity ┃ Name ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Severity.WARNING │ ExtractCommandFailedReport │
└──────────────────┴────────────────────────────┘
That seems promising. Aside from the file system it also found a CPIO archive and a GZipped file. And indeed, when running file to further trying to identify the extracted GZip archive, it turns out to be our missing kernel:
file 1470272-7260772.gzip_extract/gzip.uncompressed
1470272-7260772.gzip_extract/gzip.uncompressed: Linux kernel ARM64 boot executable Image, little-endian, 4K pages
The CPIO archive is an initramfs, which can help tremendously when emulating the firmware:
file 7314056-16021055.gzip_extract/mkinitramfs-MAIN_heRQG1
7314056-16021055.gzip_extract/mkinitramfs-MAIN_heRQG1: ASCII cpio archive (SVR4 with no CRC)
With the artifacts retrieved using unblob we’re ready for a first attempt at emulating the firmware.
First emulation attempt#
In the initial search for information about the target we found a blog post [4] describing earlier attempts at emulating the UDM Pro. Repeating what they tried with the files we acquired in the first step gives the following command:
qemu-system-aarch64 -machine virt \
-cpu max \
-m 4G \
-kernel 1470272-7260772.gzip_extract/gzip.uncompressed \
-no-reboot -nographic \
-initrd 7314056-16021055.gzip_extract/mkinitramfs-MAIN_heRQG1
Just as described in the blog post, this provides no output whatsoever. As a next step they built a custom kernel which resolved this issue, but they did not provide any more information about which kernel they built.
debugging kernel#
Further googling yielded a repository based on the actual kernel used in the UDM Pro [5]. The repository mostly intends to use the built kernel on an actual device, rather then use it for emulation, so some changes were in order. I used the stock flavour branch to get closer to the kernel actually used on the UDM Pro at the time of testing. This did not initially work for me, due to a seemingly buggy macro EXPORT_SYMBOL_GPL in include/linux/export.h. After updating this to the newer version in the main branch the macro was working and a kernel could be built with make olddefconfig, export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- LOCALVERSION= and make -j $(nproc). Just building a kernel does not really work for our purposes however, as we’re trying to use it in QEMU. This means it needs support for the virtio driver. We also want to make sure it can print its logs to a serial console in QEMU. I ended up with the following list, which probably ended up with some options which were not necessary but also did not cause any issues:
CONFIG_CMDLINE_FORCE=y
CONFIG_CONSOLE_LOGLEVEL_DEFAULT=8
CONFIG_CONSOLE_LOGLEVEL_QUIET=8
CONFIG_MESSAGE_LOGLEVEL_DEFAULT=7
CONFIG_DYNAMIC_DEBUG=y
CONFIG_EMBEDDED=y
CONFIG_DEBUG_KERNEL=y
CONFIG_PACKET_DIAG=y
CONFIG_SCSI_LOGGING=y
CONFIG_SCSI_VIRTIO=y
CONFIG_VT_HW_CONSOLE_BINDING=y
CONFIG_SERIAL_AMBA_PL011=y
CONFIG_SERIAL_AMBA_PL011_CONSOLE=y
CONFIG_VIRTIO_CONSOLE=y
CONFIG_HW_RANDOM_VIRTIO=y
CONFIG_HW_RANDOM=y
CONFIG_VIRTIO_MMIO=y
CONFIG_VIRTIO_NET=y
CONFIG_VIRTIO_PCI=y
CONFIG_VIRTIO_INPUT=y
CONFIG_VIRT_DRIVERS=y
CONFIG_VIRTIO_MEM=y
CONFIG_NLMON=y
CONFIG_PCIEPORT=y
CONFIG_AHCI=y
CONFIG_VIRTIO_MMIO_CMDLINE_DEVICES=y
These options could be added in .github/config/config.local.udm and are processed into one deduplicated file with the following command:
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- LOCALVERSION= make olddefconfig
The kernel can then be built as follows:
make -j $(nproc)
After building the kernel with these options it can be found in arch/arm64/boot/Image.gz, which we run with QEMU as follows:
qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 4G \
-kernel Image.gz \
-no-reboot -nographic \
-initrd mkinitramfs-MAIN_heRQG1
This finally gives some output:
[ 0.000000] Booting Linux on physical CPU 0x0000000000 [0x411fd070]
[ 0.000000] Linux version 4.19.152-ui-alpine (root@2849382949fe) (gcc version 7.5.0 (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04)) #1 SMP Wed Aug 20 09:37:00 UTC 2025
[ 0.000000] Machine model: linux,dummy-virt
[ 0.000000] efi: Getting EFI parameters from FDT:
[ 0.000000] efi: UEFI not found.
[ 0.000000] On node 0 totalpages: 1048576
[ 0.000000] DMA32 zone: 12288 pages used for memmap
[ 0.000000] DMA32 zone: 0 pages reserved
[ 0.000000] DMA32 zone: 786432 pages, LIFO batch:63
[ 0.000000] Normal zone: 4096 pages used for memmap
[ 0.000000] Normal zone: 262144 pages, LIFO batch:63
[ 0.000000] psci: probing for conduit method from DT.
[ 0.000000] psci: PSCIv1.1 detected in firmware.
[ 0.000000] psci: Using standard PSCI v0.2 function IDs
[ 0.000000] psci: Trusted OS migration not required
[ 0.000000] psci: SMC Calling Convention v1.0
[ 0.000000] random: get_random_bytes called from start_kernel+0xac/0x3f4 with crng_init=0
[ 0.000000] percpu: Embedded 21 pages/cpu s45464 r8192 d32360 u86016
[ 0.000000] pcpu-alloc: s45464 r8192 d32360 u86016 alloc=21*4096
[ 0.000000] pcpu-alloc: [0] 0
[ 0.000000] Detected PIPT I-cache on CPU0
[ 0.000000] CPU features: enabling workaround for ARM erratum 832075
[ 0.000000] ARM_SMCCC_ARCH_WORKAROUND_1 missing from firmware
[ 0.000000] CPU features: enabling workaround for EL2 vector hardening
[ 0.000000] Built 1 zonelists, mobility grouping on. Total pages: 1032192
[ 0.000000] Kernel command line:
[ 0.000000] Dentry cache hash table entries: 524288 (order: 10, 4194304 bytes)
[ 0.000000] Inode-cache hash table entries: 262144 (order: 9, 2097152 bytes)
[ 0.000000] software IO TLB: mapped [mem 0xfbfff000-0xfffff000] (64MB)
[ 0.000000] Memory: 4022080K/4194304K available (9084K kernel code, 884K rwdata, 2624K rodata, 576K init, 321K bss, 172224K reserved, 0K cma-reserved)
[ 0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[ 0.000000] rcu: Hierarchical RCU implementation.
[ 0.000000] rcu: RCU event tracing is enabled.
[ 0.000000] rcu: RCU restricting CPUs from NR_CPUS=4 to nr_cpu_ids=1.
[ 0.000000] rcu: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=1
[ 0.000000] NR_IRQS: 64, nr_irqs: 64, preallocated irqs: 0
[ 0.000000] GICv2m: range[mem 0x08020000-0x08020fff], SPI[80:143]
[ 0.000000] arch_timer: cp15 timer(s) running at 62.50MHz (virt).
[ 0.000000] clocksource: arch_sys_counter: mask: 0xffffffffffffff max_cycles: 0x1cd42e208c, max_idle_ns: 881590405314 ns
[ 0.000232] sched_clock: 56 bits at 62MHz, resolution 16ns, wraps every 4398046511096ns
[ 0.005982] Console: colour dummy device 80x25
[ 0.007414] console [tty0] enabled
[ 0.008612] Calibrating delay loop (skipped), value calculated using timer frequency.. 125.00 BogoMIPS (lpj=250000)
[ 0.008769] pid_max: default: 32768 minimum: 301
[ 0.010450] Mount-cache hash table entries: 8192 (order: 4, 65536 bytes)
[ 0.010549] Mountpoint-cache hash table entries: 8192 (order: 4, 65536 bytes)
[ 0.039304] /cpus/cpu-map: empty cluster
[ 0.045874] ASID allocator initialised with 32768 entries
[ 0.046722] rcu: Hierarchical SRCU implementation.
[ 0.053472] EFI services will not be available.
[ 0.054553] smp: Bringing up secondary CPUs ...
[ 0.054645] smp: Brought up 1 node, 1 CPU
[ 0.054685] SMP: Total of 1 processors activated.
[ 0.054758] CPU features: detected: 32-bit EL0 Support
[ 0.056096] CPU: All CPU(s) started at EL1
[ 0.056355] alternatives: patching kernel code
[ 0.073300] devtmpfs: initialized
[ 0.083560] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[ 0.083754] futex hash table entries: 256 (order: 2, 16384 bytes)
[ 0.088269] DMI not present or invalid.
[ 0.092711] NET: Registered protocol family 16
[ 0.098546] cpuidle: using governor menu
[ 0.102352] DMA: preallocated 256 KiB pool for atomic allocations
[ 0.103024] Initializing Peripheral Bus System - PBS
[ 0.103103] pbs entry was not found in device-tree
[ 0.103149] Serial: AMBA PL011 UART driver
[ 0.123407] 9000000.pl011: ttyAMA0 at MMIO 0x9000000 (irq = 39, base_baud = 0) is a PL011 rev1
[ 0.134695] console [ttyAMA0] enabled
[ 0.159191] HugeTLB registered 2.00 MiB page size, pre-allocated 0 pages
[ 0.164950] vgaarb: loaded
[ 0.166098] SCSI subsystem initialized
[ 0.167094] libata version 3.00 loaded.
[ 0.168456] usbcore: registered new interface driver usbfs
[ 0.168866] usbcore: registered new interface driver hub
[ 0.169222] usbcore: registered new device driver usb
[ 0.169992] pps_core: LinuxPPS API ver. 1 registered
[ 0.170165] pps_core: Software ver. 5.3.6 - Copyright 2005-2007 Rodolfo Giometti <giometti@linux.it>
[ 0.170493] PTP clock support registered
[ 0.171576] Advanced Linux Sound Architecture Driver Initialized.
[ 0.181410] Bluetooth: Core ver 2.22
[ 0.181714] NET: Registered protocol family 31
[ 0.181877] Bluetooth: HCI device and connection manager initialized
[ 0.182244] Bluetooth: HCI socket layer initialized
[ 0.182483] Bluetooth: L2CAP socket layer initialized
[ 0.182932] Bluetooth: SCO socket layer initialized
[ 0.191356] clocksource: Switched to clocksource arch_sys_counter
[ 0.217145] NET: Registered protocol family 2
[ 0.224000] tcp_listen_portaddr_hash hash table entries: 2048 (order: 3, 32768 bytes)
[ 0.224562] TCP established hash table entries: 32768 (order: 6, 262144 bytes)
[ 0.225286] TCP bind hash table entries: 32768 (order: 7, 524288 bytes)
[ 0.226074] TCP: Hash tables configured (established 32768 bind 32768)
[ 0.228034] UDP hash table entries: 2048 (order: 4, 65536 bytes)
[ 0.228545] UDP-Lite hash table entries: 2048 (order: 4, 65536 bytes)
[ 0.230626] NET: Registered protocol family 1
[ 0.232906] PCI: CLS 0 bytes, default 64
[ 0.238063] Unpacking initramfs...
[ 0.380690] Freeing initrd memory: 19736K
[ 0.385535] Initialise system trusted keyrings
[ 0.387399] workingset: timestamp_bits=46 max_order=20 bucket_order=0
[ 0.399584] squashfs: version 4.0 (2009/01/31) Phillip Lougher
[ 1.443831] Key type asymmetric registered
[ 1.444121] Asymmetric key parser 'x509' registered
[ 1.444502] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 249)
[ 1.444902] io scheduler noop registered
[ 1.445079] io scheduler deadline registered
[ 1.445552] io scheduler cfq registered (default)
[ 1.445740] io scheduler mq-deadline registered
[ 1.445877] io scheduler kyber registered
[ 1.450725] pl061_gpio 9030000.pl061: PL061 GPIO chip @0x0000000009030000 registered
[ 1.453589] al_dma: Annapurna Labs DMA Driver 0.01
[ 1.462901] Serial: 8250/16550 driver, 4 ports, IRQ sharing enabled
[ 1.466948] cacheinfo: Unable to detect cache hierarchy for CPU 0
[ 1.487345] loop: module loaded
[ 1.496525] libphy: Fixed MDIO Bus: probed
[ 1.497030] tun: Universal TUN/TAP device driver, 1.6
[ 1.498039] al_eth_drv: Initializing Peripheral Bus System (PBS) resources
[ 1.498674] al_eth_drv: PBS entry was not found in device-tree
[ 1.499473] ehci_hcd: USB 2.0 'Enhanced' Host Controller (EHCI) Driver
[ 1.499915] ehci-pci: EHCI PCI platform driver
[ 1.500352] ehci-platform: EHCI generic platform driver
[ 1.500841] ohci_hcd: USB 1.1 'Open' Host Controller (OHCI) Driver
[ 1.501206] ohci-pci: OHCI PCI platform driver
[ 1.501727] uhci_hcd: USB Universal Host Controller Interface driver
[ 1.502716] usbcore: registered new interface driver cdc_acm
[ 1.502959] cdc_acm: USB Abstract Control Model driver for USB modems and ISDN adapters
[ 1.503485] usbcore: registered new interface driver usb-storage
[ 1.504206] usbcore: registered new interface driver cp210x
[ 1.504786] usbserial: USB Serial support registered for cp210x
[ 1.506462] i2c /dev entries driver
[ 1.507662] Bluetooth: HCI UART driver ver 2.3
[ 1.508069] Bluetooth: HCI UART protocol H4 registered
[ 1.508270] Bluetooth: HCI UART protocol BCSP registered
[ 1.510681] usbcore: registered new interface driver usbhid
[ 1.510871] usbhid: USB HID core driver
[ 1.512702] usbcore: registered new interface driver snd-usb-audio
[ 1.519833] xt_time: kernel timezone is -0000
[ 1.520619] IPVS: Registered protocols ()
[ 1.521010] IPVS: Connection hash table configured (size=4096, memory=64Kbytes)
[ 1.523391] IPVS: ipvs loaded.
[ 1.524029] gre: GRE over IPv4 demultiplexor driver
[ 1.524267] ip_gre: GRE over IPv4 tunneling driver
[ 1.528886] IPv4 over IPsec tunneling driver
[ 1.535455] NET: Registered protocol family 10
[ 1.546849] Segment Routing with IPv6
[ 1.549548] sit: IPv6, IPv4 and MPLS over IPv4 tunneling driver
[ 1.552030] NET: Registered protocol family 17
[ 1.552796] bridge: filtering via arp/ip/ip6tables is no longer available by default. Update your scripts to load br_netfilter if you need this.
[ 1.553869] Bluetooth: RFCOMM socket layer initialized
[ 1.554245] Bluetooth: RFCOMM ver 1.11
[ 1.554460] Bluetooth: BNEP (Ethernet Emulation) ver 1.3
[ 1.554708] Bluetooth: BNEP socket layer initialized
[ 1.554929] Bluetooth: HIDP (Human Interface Emulation) ver 1.2
[ 1.555189] Bluetooth: HIDP socket layer initialized
[ 1.555377] 8021q: 802.1Q VLAN Support v1.8
[ 1.559842] registered taskstats version 1
[ 1.560071] Loading compiled-in X.509 certificates
[ 1.589580] Key type encrypted registered
[ 1.599947] input: gpio-keys as /devices/platform/gpio-keys/input/input0
[ 1.602694] hctosys: unable to open rtc device (rtc0)
[ 1.606102] cfg80211: Loading compiled-in X.509 certificates for regulatory database
[ 1.738247] cfg80211: Loaded X.509 cert 'sforshee: 00b28ddf47aef9cea7'
[ 1.738955] ALSA device list:
[ 1.739072] No soundcards found.
[ 1.744353] platform regulatory.0: Direct firmware load for regulatory.db failed with error -2
[ 1.744937] cfg80211: failed to load regulatory.db
[ 1.748425] uart-pl011 9000000.pl011: no DMA platform data
[ 1.768293] Freeing unused kernel memory: 576K
[ 1.775520] Run /init as init process
Loading, please wait...
Starting version 247.3-7+deb11u6
^CBegin: Loading essential drivers ... [ 3.431052] ui_hdd_pwrctl: loading out-of-tree module taints kernel.
[ 3.434916] ui_hdd_pwrctl: Unknown symbol ui_hdd_pwrctl_blink_set (err -2)
[ 3.437391] ui_hdd_pwrctl: Unknown symbol ui_hdd_pwrctl_blink_set (err -2)
modprobe: can't load module ui-hdd-pwrctl (extra/ui-hdd-pwrctl.ko): unknown symbol in module, or unknown parameter
[ 3.451120] loop: exports duplicate symbol loop_register_transfer (owned by kernel)
[ 3.453715] loop: exports duplicate symbol loop_register_transfer (owned by kernel)
modprobe: can't load module loop (kernel/drivers/block/loop.ko): invalid module format
[ 3.463776] xxhash: exports duplicate symbol xxh32 (owned by kernel)
[ 3.464506] xxhash: exports duplicate symbol xxh32 (owned by kernel)
modprobe: can't load module xxhash (kernel/lib/xxhash.ko): invalid module format
modprobe: module linear not found in modules.dep
modprobe: module multipath not found in modules.dep
modprobe: module raid0 not found in modules.dep
modprobe: module raid1 not found in modules.dep
modprobe: module raid456 not found in modules.dep
modprobe: module raid5 not found in modules.dep
modprobe: module raid6 not found in modules.dep
modprobe: module raid10 not found in modules.dep
modprobe: module efivars not found in modules.dep
done.
Begin: Running /scripts/init-premount ... done.
Begin: Mounting root file system ...
Ubiquiti Debian OS initialization...
Begin: Waiting device /dev/mtdblock5 ... 30 ...
29 ...
[...]
1 ...
timed out waiting /dev/mtdblock5
BusyBox v1.30.1 (Debian 1:1.30.1-6+deb11u1) built-in shell (ash)
Enter 'help' for a list of built-in commands.
(initramfs)
Unpacking the image the right way#
While we now could interact with the emulated kernel, it doesn’t have any of the components from the firmware image we’re trying to emulate, only the kernel we just built. To be able to boot more of the firmware itself, we took a look at the initramfs. Unpacking this file yields a small file system used during startup, including some references to the mtdblock5 we are missing:
$ rg mtdblock5
board-define
4:CONFIGDEV=/dev/mtdblock5
scripts/product-override
7:CONFIGDEV="/dev/mtdblock5"
A script scripts/ubnt uses these values in commands which attempt to mount some partitions, which are in turn used in the init script in the root directory of the initramfs. The initramfs also contains various other scripts, among which /usr/sbin/fwupdate, which updates the device firmware with a new image. Since this accepts the same image as we’re trying to analyze it must contain more information about the image format. The scripts extracts various components from the firmware image using a function in /usr/sbin/ubnt-tools called fwextract. From the update script the following partitions can be found:
- kernel
- rootfs
- preloader
- atf
- uboot
Looking for any of these terms in the firmware image itself shows that these are tagged with FILE, e.g. FILErootfs. We can easily check for other occurrences as follows:
$ rg -Uao 'FILE[a-z]+' b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin
1:FILEuboot
6495:FILEkernel
45980:FILErootfs
282333:FILElg
577632:FILEo
2872083:FILEupdater
While lg and o might be false positives, updater looks promising.
We can run the ubnt-tools binary using QEMU’s user mode, from the initramfs root directory, e.g.:
qemu-aarch64-static -cpu cortex-a57 -L $(pwd) usr/sbin/fwextract -t rootfs -o test b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin
Note that unpacking the firmware does not always preserve file permissions, and in our case ubnt-tools was not marked as executable, so a chmod +x usr/sbin/ubnt-tools might be required. The binary also seems to perform some checks to verify it’s not being emulated. I patched those out:
$ diff <(aarch64-none-linux-gnu-objdump -D fwextract) <(aarch64-none-linux-gnu-objdump -D ubnt-tools)
2c2
< fwextract: file format elf64-littleaarch64
---
> ubnt-tools: file format elf64-littleaarch64
19848c19848
< 14dcc: d503201f nop
---
> 14dcc: b4000377 cbz x23, 14e38 <MD5_Final@@Base+0x39f4>
20154c20154
< 15294: d503201f nop
---
> 15294: b4002fc1 cbz x1, 1588c <MD5_Final@@Base+0x4448>
Afterwards we can use it to extract the partitions in the intended way:
$ qemu-aarch64-static -cpu cortex-a57 -L $(pwd) usr/sbin/fwextract -kt rootfs -o rootfs.bin b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin
WARN: Firmware file: 'b012-UDMPRO-4.1.13-4bc426ae-2619-4f5f-8d19-7502798be61a.bin'
WARN: Creating partition data file: rootfs.bin
Repeating the command gives us the following files:
$ file rootfs.bin
rootfs.bin: Squashfs filesystem, little endian, version 4.0, zstd compressed, 761319517 bytes, 46094 inodes, blocksize: 262144 bytes, created: Tue Dec 24 15:05:15 2024
$ file kernel.bin
kernel.bin: Device Tree Blob version 17, size=14553682, boot CPU=0, string block size=157, DT structure block size=14551880
$ file uboot.bin
uboot.bin: data
$ file updater.bin
updater.bin: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=MfgmSpD0MFJ2WLVTDXWD/_A2q3T6_MqUbEFrgCGar/MtIrxQouqjTvv_VaKK9A/k7mWl_hDkFnu_KF2rBxn, stripped
The kernel actually contains a FIT image:
$ mkimage -l kernel.bin
Image contains unit addresses @, this will break signing
FIT description: AL324 UniFi Dream Machine Pro FIT image
Created: Tue Dec 24 16:05:15 2024
Image 0 (kernel@1)
Description: AL324 UDMPRO
Created: Tue Dec 24 16:05:15 2024
Type: Kernel Image
Compression: gzip compressed
Data Size: 5790500 Bytes = 5654.79 KiB = 5.52 MiB
Architecture: AArch64
OS: Linux
Load Address: 0x04080000
Entry Point: 0x04080000
Hash algo: sha1
Hash value: 1a052671e28dd5497706a8784d61448dda291b59
Sign algo: sha1,rsa2048:udm_al324
Sign value: ccfb6519ce7a56b12e53432faca8d90c513391ca497b48ec9af3061a6bf628fac15669e6ff2848289f05c049aafe9ac7d1806bdff10ab024935024b37814d9de1f33c5d111629b2fa90b666d522943b8f464e80d696f29691e7b587a54d990edce795a45dffa253b71ff8c3a0d38b72797aed1e12ef4034fc08880839c6c0f4f8409384fbe07f95fd31b76cfe4afd7e55fb25d8ebe8112a45e0514a745eaf0ddad51cab51bac45da7c9212d1efb459a571db2103dc968fa18d3214f96d191ba8ada57a4645acd66f0504f77448052eef56620cd883085fad519c1c3a91360c3bd81ecbf5a7bc2c15cae4b657a6f8c264c7363716f1487f6303c0330dd1f41193
Timestamp: Tue Dec 24 16:05:19 2024
Image 1 (fdt@1)
Description: al324-ubnt-udmpro-10g
Created: Tue Dec 24 16:05:15 2024
Type: Flat Device Tree
Compression: uncompressed
Data Size: 25396 Bytes = 24.80 KiB = 0.02 MiB
Architecture: AArch64
Load Address: 0x04078000
Hash algo: sha1
Hash value: a7a2758200e6f2b9657e2f1104d4ddf3fc24d865
Sign algo: sha1,rsa2048:udm_al324
Sign value: 5379277240e245d09223499348d20684819351393fe06006f50fd499f6b7b22ea1b87868bfa9271207a5eccf13424c84b2dc2f1723a6eba731616a4c190c922a93a8fdedc4f170ea8fcd63ea99cf4357a283bb6a4335afd21430cba3b7e4d939a0709f5bca85a2e58174ce40afd7605f672753b860dc94dd20757eae99721bb7ed9e5fa622b3fd88afa3351b865b4d6f10b3975ce56fee902081c6bb8ab7976dc4d0cbba90610e2ebf469e75095780b8e544b72d3b90a8ae03e4179f6fb23cae0661af0e16cb10dce1d90459b3a21b8c3e89c8e7e347e6840767aac469a0e54f50765a9ad83bf75bf7f60a59a5b1930b1b72121f348c4e548b945986f089e819
Timestamp: Tue Dec 24 16:05:20 2024
Image 2 (fdt@2)
Description: al324-ubnt-udmpro-10g-v2
Created: Tue Dec 24 16:05:15 2024
Type: Flat Device Tree
Compression: uncompressed
Data Size: 26048 Bytes = 25.44 KiB = 0.02 MiB
Architecture: AArch64
Load Address: 0x04078000
Hash algo: sha1
Hash value: 911fb3ca61eb099a18229071742ca2d480a68668
Sign algo: sha1,rsa2048:udm_al324
Sign value: 86d42e9dc0a5cacdbb2d3ebebbc1853a659b618da1bd023ad14545b15c24c3020de506b5d777214254ff83dd2379c9fd80fcdc46db1f6047eb8cde0369020b61cc4c7b7f029c41c684c3515bb135418ea82e5dca10baba53c0b29fdf13e21f2fef8f26946bb9038d7c6b6204a5c60a56edd76485f8f0768bdefd6c532a3bdc48792ad8998594eecf12597c4291946ff3957810af6a4c626efa81aef9de723bb31147595b0cce42b6e90673cdb8ee1f432e2621e7093ee18d29884d015ffb3cab192d63fb7bbb45ab700b9cd00eba07772c4ad12a37ecc31970aff520e4a81d4200930b8e448dcc83ec05508d0c2d21e755527447f5b32c346477abfde0af4925
Timestamp: Tue Dec 24 16:05:21 2024
Image 3 (ramdisk@1)
Description: ramdisk
Created: Tue Dec 24 16:05:15 2024
Type: RAMDisk Image
Compression: gzip compressed
Data Size: 8706999 Bytes = 8502.93 KiB = 8.30 MiB
Architecture: AArch64
OS: Linux
Load Address: unavailable
Entry Point: unavailable
Hash algo: sha1
Hash value: 1164edb5fd12c7ceea16d6488c040967b3b72d39
Sign algo: sha1,rsa2048:udm_al324
Sign value: c9f59895137d9c3a9788959f11143e6332bbfeaceb711ae68cd5f4e82d45657f409f7cd068706be67b4f345fd791d5a09bae39c339643ed2228106cf3d0e0f956d66aaeb6672d036f2e49cff01f307f94b19755bd4e13af52c1bd3085f3644bb223c3f98cb7fe6ca6716af9016a9b1dc1b6e0cc14936d2c55790de0f23230747ff5a73184d6a6a1213db1709a0819d64eaf34913d0ec5492ec74164f0bbf5cdfaf8d682bc3fc5749455920dd53f7be9677c35c3b0c5764db2fbbe0d1e12561627bf57bbe82ae8f0929cf9864539c7292a1c5a7cc4b18b996170abb2077cdd3b10a5939a8047439ca734f90c8c01389ccd18f6b8f01a16b10403a6ac26a370be2
Timestamp: Tue Dec 24 16:05:22 2024
Default Configuration: 'udmpro@1'
Configuration 0 (udmpro@1)
Description: UDMPRO 10G v1 configuration
Kernel: kernel@1
Init Ramdisk: ramdisk@1
FDT: fdt@1
Hash algo: sha1
Hash value: unavailable
Configuration 1 (udmpro@2)
Description: UDMPRO 10G v2 configuration
Kernel: kernel@1
Init Ramdisk: ramdisk@1
FDT: fdt@2
Hash algo: sha1
Hash value: unavailable
At an offset of 216 bytes we can find the magic bytes for GZIP, 1F08. We can extract the compressed kernel as follows [6]:
$ dd if=kernel.bin of=kernel.gz bs=1 skip=216 count=5790500
5790500+0 records in
5790500+0 records out
5790500 bytes (5.8 MB, 5.5 MiB) copied, 11.4056 s, 508 kB/s
$ gunzip kernel.gz
$ file kernel
kernel: Linux kernel ARM64 boot executable Image, little-endian, 4K pages
However, when running QEMU with this kernel we don’t get any output, so we opted to use our own kernel instead.
Creating the right image structure#
After getting two ways of obtaining a valid kernel, of which one gave some debugging output, we still did not have a way of booting the image. The initramfs expects certain devices which hold the rest of the firmware, so we need to obtain these from the firmware image. The scripts which were identified in the previous section also dealt with mounting these, and so provided ample information on the required structure of the devices. The missing device is mounted as follows:
mount_configdev() {
[ -b "${CONFIGDEV}" ] || ui_panic "${CONFIGDEV} not found"
repair_or_mkfs ${CONFIGDEV}
mkdir_mount_p -t ext4 -o sync ${CONFIGDEV} ${CONFIGDIR}
}
From this we know it is an ext4 image, which is mounted from whatever is configured in CONFIGDEV, to the directory CONFIGDIR. If we grep for the directory variable we get some ideas of what this is used for:
$ rg CONFIGDIR
usr/sbin/reset2defaults
4:CONFIGDIR=/mnt/.config
13:[ -d "${CONFIGDIR}" ] || mkdir -p ${CONFIGDIR}
15:if ! mount -t ext4 ${CONFIGDEV} ${CONFIGDIR}; then
17: mount -t ext4 ${CONFIGDEV} ${CONFIGDIR} || exit 1
24:touch ${CONFIGDIR}/.reset-to-defaults
28:umount ${CONFIGDIR}
scripts/ubnt
28:CONFIGDIR="/config"
29:FLAG_FACTORY_RESET=${CONFIGDIR}/.factory-reset
30:FLAG_NET_UPGRADE=${CONFIGDIR}/.network-upgrade
31:FLAG_R2DEF=${CONFIGDIR}/.reset-to-defaults
32:FLAG_UPGRADE_BOOT=${CONFIGDIR}/.upgrade-bootup
33:FLAG_RESET_BOOT=${CONFIGDIR}/.reset-bootup
34:FLAG_RECOVER_BOOT=${CONFIGDIR}/.recover-bootup
37:CONFIG_VERSION_FILE=${CONFIGDIR}/version
146: mkdir_mount_p -t ext4 -o sync ${CONFIGDEV} ${CONFIGDIR}
334: umount ${CONFIGDIR}
At the very least it seems like this expects a file called ‘version’ in the directory. In the original extracted firmware we can find a file which is a likely candidate:
$ find . -type f -name version
./usr/lib/version
./usr/share/perl/5.32.1/unicore/version
$ cat ./usr/lib/version
UDMPRO.al324.v4.1.13.1abc0d9.241224.2256
Emulating an actual mtdblock with QEMU is difficult, so we also patched the initramfs to use a different value for CONFIGDEV:
$ diff <(xxd mkinitramfs-MAIN_DQZkE3) <(xxd mkinitramfs-MAIN_DQZkE3-patched)
27,29c27,29
< 000001a0: 6576 2f62 6f6f 7433 0a43 4f4e 4649 4744 ev/boot3.CONFIGD
< 000001b0: 4556 3d2f 6465 762f 6d74 6462 6c6f 636b EV=/dev/mtdblock
< 000001c0: 350a 4d41 494e 5f49 4e54 4552 4641 4345 5.MAIN_INTERFACE
---
> 000001a0: 6576 2f62 6f6f 7433 0a23 4c4f 4c0a 434f ev/boot3.#LOL.CO
> 000001b0: 4e46 4947 4445 563d 2f64 6576 2f76 6461 NFIGDEV=/dev/vda
> 000001c0: 310a 4d41 494e 5f49 4e54 4552 4641 4345 1.MAIN_INTERFACE
We then create the ‘block device’ as follows:
$ qemu-img create mtdblock.img 1M
Formatting 'mtdblock.img', fmt=raw size=1048576
$ parted mtdblock.img -s -- mklabel msdos
$ parted mtdblock.img -s -- mkpart primary 0 100%
Warning: The resulting partition is not properly aligned for best performance: 1s % 2048s != 0s
$ sudo kpartx -v -a mtdblock.img
add map loop1p1 (252:1): 0 2047 linear 7:1 1
$ sudo mkfs.ext4 /dev/mapper/loop1p1
mke2fs 1.47.1 (20-May-2024)
Filesystem too small for a journal
Discarding device blocks: done
Creating filesystem with 1020 1k blocks and 128 inodes
Allocating group tables: done
Writing inode tables: done
Writing superblocks and filesystem accounting information: done
$ sudo mount /dev/mapper/loop1p1 /mnt/disk/
$ sudo cp ../b012-UDMPRO-4.1.13/usr/lib/version /mnt/disk/
$ sudo umount /mnt/disk
$ sudo kpartx -d -v mtdblock.img
del devmap : loop1p1
loop deleted : /dev/loop1
Lastly, we use it in QEMU:
$ qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 4G \
-kernel Image.gz \
-no-reboot -nographic \
-initrd mkinitramfs-MAIN_DQZkE3-patched \
-drive file=mtdblock.img,if=none,id=disk0,format=raw \
-device virtio-blk-device,drive=disk0
[...]
Ubiquiti Debian OS initialization...
Begin: Waiting device /dev/vda1 ... done.
Begin: Waiting device /dev/disk/by-partlabel/overlay ... 5 ...
4 ...
3 ...
2 ...
1 ...
done.
filesystem on the /dev/vda1 is in a consistent state
[ 7.849804] EXT4-fs (vda1): mounted filesystem without journal. Opts: (null)
Begin: Mounting /dev/disk/by-partlabel/overlay to /mnt/.rwfs ... Recovering userdev
fsck.ext4 -pf /dev/disk/by-partlabel/overlay exits with 8
fsck.ext4 -nf /dev/disk/by-partlabel/overlay exits with 8
fsck.ext4 -yf /dev/disk/by-partlabel/overlay exits with 8
fsck.ext4 -nf /dev/disk/by-partlabel/overlay exits with 8
/dev/disk/by-partlabel/overlay repair failed, try mount
mount: mounting /dev/disk/by-partlabel/overlay on /tmp/.repair_fs_device_try_mount failed: No such file or directory
/dev/disk/by-partlabel/overlay is corrupted, reformat it
Begin: Waiting device /dev/disk/by-partlabel/overlay ... 5 ...
4 ...
3 ...
2 ...
1 ...
timed out waiting /dev/disk/by-partlabel/overlay
That’s some progress at least. We’re now looking for a partition called ‘overlay’. Fortunately, there are some scripts in the initramfs which deal with this:
$ rg overlay mkinitramfs-MAIN_DQZkE3_extract/
mkinitramfs-MAIN_DQZkE3_extract/usr/lib/modules/4.19.152-ui-alpine/modules.builtin
13:kernel/fs/overlayfs/overlay.ko
mkinitramfs-MAIN_DQZkE3_extract/scripts/ubnt
149:overlay_cleanup() {
154: log_begin_msg "Cleaning up overlay filesystem"
234: do_overlay_cleanup=0
326: do_overlay_cleanup=1
349: do_overlay_cleanup=1
353: if [ ${do_overlay_cleanup} -ne 0 ]; then
354: overlay_cleanup ${MNT_RWFS}
359: log_begin_msg "Setting up overlay filesystem"
365: -t overlay \
367: overlayfs-root ${rootmnt}
370: log_begin_msg "Moving mountpoints to overlay filesystem"
mkinitramfs-MAIN_DQZkE3_extract/scripts/product-override
6:USERDEV="/dev/disk/by-partlabel/overlay"
21: mkpart overlay 10684416S 100%
mkinitramfs-MAIN_DQZkE3_extract/usr/lib/aarch64-linux-gnu/libmount.so.1.1.0: binary file matches (found "\0" byte around offset 7)
Of particular interest is the script mkinitramfs-MAIN_DQZkE3_extract/scripts/product-override, as it seems to build the partition we’re looking for, among other things:
fcd_init_layout() {
wait_device /dev/boot
/sbin/parted -s -- /dev/boot \
mklabel gpt \
mkpart boot 2048S 133119S \
mkpart recovery 133120S 198655S \
mkpart root 198656S 4392959S \
mkpart log 4392960S 6490111S \
mkpart persistent 6490112S 10684415S \
mkpart overlay 10684416S 100%
mkfs_p ${KERNELDEV}
# NOTE: ${KERNELRDEV} uses block image here, please don't format it
mkfs_p ${BOOTDEV}
mkfs_p ${LOGDEV}
mkfs_p ${PERSISTDEV}
mkfs_p ${USERDEV}
}
Based on this script we create our own file with the expected structure and files, including the rootfs and kernel image we recovered earlier:
parted mmcblk.img -s -- \
mklabel gpt \
mkpart boot 2048S 133119S \
mkpart recovery 133120S 198655S \
mkpart root 198656S 4392959S \
mkpart log 4392960S 6490111S \
mkpart persistent 6490112S 10684415S \
mkpart overlay 10684416S 100%
sudo kpartx -v -a mmcblk.img
sudo mkfs.ext4 /dev/mapper/loop0p1
sudo tune2fs -O ^orphan_file /dev/mapper/loop0p1
sudo mkfs.ext4 /dev/mapper/loop0p2
sudo tune2fs -O ^orphan_file /dev/mapper/loop0p2
sudo mkfs.ext4 /dev/mapper/loop0p3
sudo tune2fs -O ^orphan_file /dev/mapper/loop0p3
sudo mkfs.ext4 /dev/mapper/loop0p4
sudo tune2fs -O ^orphan_file /dev/mapper/loop0p4
sudo mkfs.ext4 /dev/mapper/loop0p5
sudo tune2fs -O ^orphan_file /dev/mapper/loop0p5
sudo mkfs.ext4 /dev/mapper/loop0p6
sudo tune2fs -O ^orphan_file /dev/mapper/loop0p6
sudo mount /dev/mapper/loop0p1 /mnt/disk
sudo cp uImage /mnt/disk/
sudo umount /mnt/disk
sudo mount /dev/mapper/loop0p3 /mnt/disk
sudo cp rootfs /mnt/disk/{rootfs,rootfs.bkp}
sudo umount /mnt/disk
sudo kpartx -d -v mmcblk.img
Later on in the testing it was practical to be able to make adjustments to the rootfs as well, which could be done as follows:
LOOP_DEV="$(sudo kpartx -v -a mmcblk.img | rg -o 'loop[0-9]' | uniq)"
sudo mount /dev/mapper/${LOOP_DEV}p3 /mnt/disk/
sudo mount /mnt/disk/rootfs /mnt/disk2/
mkdir /tmp/{work,upper,target}
sudo mount --type="overlay" --options="lowerdir=/mnt/disk2,upperdir=/tmp/upper,workdir=/tmp/work" --source="overlay" --target="/tmp/target"
# make changes to the file system in the directory '/tmp/target' here
sudo mksquashfs /tmp/target rootfs-patched -noappend
sudo umount /tmp/target
sudo umount /mnt/disk2
sudo cp rootfs-patched /mnt/disk/rootfs
sudo umount /mnt/disk
sudo kpartx -v -d mmcblk.img
We then try again with QEMU with the new image:
$ qemu-system-aarch64 \
-M virt \
-cpu cortex-a57 \
-m 4G \
-kernel Image.gz \
-no-reboot -nographic \
-initrd mkinitramfs-MAIN_DQZkE3-patched \
-drive file=mtdblock.img,if=none,id=disk0,format=raw \
-device virtio-blk-device,drive=disk0 \
-drive file=mmcblk.img,if=none,id=disk1,format=raw \
-device virtio-blk-device,drive=disk1
[...]
Ubiquiti Debian OS initialization...
Begin: Waiting device /dev/vda1 ... done.
Begin: Waiting device /dev/disk/by-partlabel/overlay ... done.
[ 4.240802] random: fast init done
filesystem on the /dev/vda1 is in a consistent state
[...]
[ 10.827971] PPP generic driver version 2.4.2
[ 10.828561] Unable to handle kernel paging request at virtual address 8080202000000097
[ 10.828907] Mem abort info:
[ 10.829016] ESR = 0x96000004
[ 10.829131] Exception class = DABT (current EL), IL = 32 bits
[ 10.829338] SET = 0, FnV = 0
[ 10.829449] EA = 0, S1PTW = 0
[ 10.829603] Data abort info:
[ 10.829703] ISV = 0, ISS = 0x00000004
[ 10.829851] CM = 0, WnR = 0
[ 10.830227] [8080202000000097] address between user and kernel address ranges
[ 10.830642] Internal error: Oops: 96000004 [#1] SMP
[ 10.830932] Modules linked in: ppp_generic(+) slhc
[ 10.831322] Process systemd-modules (pid: 322, stack limit = 0x(____ptrval____))
[ 10.831793] CPU: 0 PID: 322 Comm: systemd-modules Tainted: G O 4.19.152-ui-alpine #1
[ 10.832033] Hardware name: linux,dummy-virt (DT)
[ 10.832317] pstate: 80000005 (Nzcv daif -PAN -UAO)
[ 10.832770] pc : ppp_init_net+0x18/0x74 [ppp_generic]
[ 10.832963] lr : ops_init+0x84/0x130
[ 10.833105] sp : ffffff80094b3a70
[ 10.833227] x29: ffffff80094b3a70 x28: 0000000000000013
[ 10.833437] x27: 0000000000000100 x26: ffffffc0f7114200
[ 10.833581] x25: ffffff8008d14000 x24: ffffff8008c99000
[ 10.833724] x23: ffffff80094b3b28 x22: ffffff8008d14890
[ 10.833920] x21: ffffff8008d14980 x20: ffffffc0f7114480
[ 10.834105] x19: ffffff80009ae060 x18: ffffffffffffffff
[ 10.834275] x17: 0000000000000000 x16: 0000000000000000
[ 10.834482] x15: ffffff8008c99d88 x14: ffffffc0f822d000
[ 10.834666] x13: ffffffc0f876c300 x12: ffffffc0f876c200
[ 10.834818] x11: ffffffc0f98add00 x10: ffffffc0f98aab80
[ 10.835081] x9 : ffffffc0f98ada00 x8 : ffffffc0f8218a80
[ 10.835236] x7 : ffffffc0f81dde00 x6 : ffffffc0f732ca90
[ 10.835423] x5 : 0000000000000040 x4 : 0000000000000008
[ 10.835582] x3 : 0000000000000001 x2 : 0000000000002710
[ 10.835785] x1 : 0000000000000012 x0 : 8080202000000007
[ 10.836103] Call trace:
[ 10.836254] ppp_init_net+0x18/0x74 [ppp_generic]
[ 10.836447] ops_init+0x84/0x130
[ 10.836567] register_pernet_operations+0xf8/0x200
[ 10.836714] register_pernet_device+0x38/0x78
[ 10.836899] ppp_init+0x2c/0x1000 [ppp_generic]
[ 10.837040] do_one_initcall+0x5c/0x178
[ 10.837161] do_init_module+0x58/0x1a0
[ 10.837286] load_module+0x1f04/0x22a0
[ 10.837403] __se_sys_finit_module+0xd0/0x100
[ 10.837521] __arm64_sys_finit_module+0x18/0x20
[ 10.837641] el0_svc_handler+0xc0/0x1a0
[ 10.837762] el0_svc+0x8/0xc4
[ 10.838047] Code: 910003fd b9410821 f9000bf3 f948b800 (f8615813)
[ 10.838591] ---[ end trace 98eb8021bf8615e9 ]---
[ 10.838998] Kernel panic - not syncing: Fatal exception
[ 10.839466] Kernel Offset: disabled
[ 10.839733] CPU features: 0x0,20006002
[ 10.840045] Memory Limit: none
[ 10.840385] Rebooting in 3 seconds..
[ 10.940881] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000008
[ 10.941330] Mem abort info:
[ 10.941480] ESR = 0x96000005
[ 10.941619] Exception class = DABT (current EL), IL = 32 bits
[ 10.941910] SET = 0, FnV = 0
[ 10.942042] EA = 0, S1PTW = 0
[ 10.942173] Data abort info:
[ 10.942303] ISV = 0, ISS = 0x00000005
[ 10.942509] CM = 0, WnR = 0
[ 10.942706] user pgtable: 4k pages, 39-bit VAs, pgdp = (____ptrval____)
[ 10.943006] [0000000000000008] pgd=0000000000000000, pud=0000000000000000
While the partitions work, we now run into a kernel panic…
After some debugging it seems that some of the kernel modules contained in the rootfs don’t play well with the kernel we built.
Kernel modules#
Since some kernel modules won’t load with the kernel we built we could try to disable them, as long as they are not relevant to the functions we’re trying to test. A short search in the rootfs for ppp_generic yields a seemingly custom config file:
$ rg ppp_generic
etc/modules-load.d/udapi-server.conf:ppp_generic
[...]
We tried removing the file, rebuilding the squashFS, and then emulating it with QEMU again. This actually works, and we’re greeted with a login prompt:
Debian GNU/Linux 11 UDMPRO ttyAMA0
UDMPRO login:
Future work#
Given that the basic emulation works, we can use it to look for vulnerabilities in the UDM Pro without requiring the device itself. A good first step would be to add networking functionality to the emulation, as this would allow a more accurate view of what is exposed.
References#
- https://www.zerodayinitiative.com/blog/2025/7/30/pwn2own-returns-to-ireland-with-a-one-million-dollar-whatsapp-target
- https://github.com/e-m-b-a/emba
- https://www.ui.com/download/software/udm-pro
- https://emulatedbox.wordpress.com/2024/12/12/emulating-ubiquity-dream-machine-firmware-booting-into-user-space/
- https://github.com/fabianishere/udm-kernel
- https://www.techpository.com/linux-unpacking-and-repacking-u-boot-uimage-files/