What I wanted to answer#
Part 1 left me with a few loose ends. I had a TP-Link firmware image and some partition information from its header, but I had not checked whether the component offsets lined up with the actual data. I also did not know the filesystem format or the architecture of an executable inside it. This part is where I answer those questions and get a feel for the Linux environment before looking closely at any service.
For now, this is static analysis of the extracted firmware. A startup script can tell me what might run on the router, but it cannot prove that a service was running on a device I have not booted.
Keeping the sample and output organized#
I kept the original firmware in my private firmware directory and put the generated files under analysis/part2. These variables save me from repeatedly typing that long firmware filename:
FW='/home/jonathan/research/tp-link-archer-c7-v5/firmware/Archer C7(US)_V5.0_220715/c7v5_us-up-ver1-2-1-P1[20220715-rel19099]_2022-07-15_17.44.43.bin'
PART2='/home/jonathan/research/tp-link-archer-c7-v5/analysis/part2'
mkdir -p "$PART2/logs" "$PART2/extracted"I rechecked the sample’s SHA-256 hash before extraction. It matched the one I recorded in Part 1:
sha256sum "$FW"14bab8d9cfe2062205ec6a3975d83b8dc1f770688008b8024a8bf214bfc7fcaa /home/jonathan/research/tp-link-archer-c7-v5/firmware/Archer C7(US)_V5.0_220715/c7v5_us-up-ver1-2-1-P1[20220715-rel19099]_2022-07-15_17.44.43.binI’m using Binwalk 3.1.0 here, invoked as binwalk3. The original .bin stays untouched; the extracted material and scan logs go in the Part 2 directory.
Finding the pieces of the image#
I started with a Binwalk scan and saved its output so I could come back to the exact offsets later:
binwalk3 "$FW" | tee "$PART2/logs/binwalk-scan.txt"Here are the useful signatures from that scan:
| Offset in firmware file | Binwalk identification | What it suggests |
|---|---|---|
0x5AC0 | LZMA-compressed MIPS32 uImage named u-boot image | A bootloader-related image is present. |
0x133C1 | LZMA-compressed MIPS32 uImage named MIPS OpenWrt Linux-3.3.8 | This looks like the operating-system image. |
0x11D0D2 | SquashFS 4.0, xz compression, little-endian filesystem | This is the root filesystem I want to inspect. |
0xEFCA47 | gzip-compressed data | Another compressed component, but not the root filesystem. |
Binwalk also reported a CRC32 polynomial table at 0x55B4. That is a signature inside the image, not another filesystem to extract.
One nice check against Part 1 is that the SafeLoader fwup-ptn entries for the OS image and filesystem line up with Binwalk once I account for the payload starting at 0x1014:
OS image: 0x1014 + 0x123AD = 0x133C1
Filesystem: 0x1014 + 0x11C0BE = 0x11D0D2That explains why the fwup-ptn values from Part 1 did not directly match raw offsets in the .bin. They are relative to the SafeLoader payload. The separate partition entries describe the intended flash layout, so I shouldn’t treat those as file offsets either. This checks the two component locations I care about here; it does not validate every partition entry.
Extracting the filesystem#
Next I let Binwalk extract the components into my analysis directory:
binwalk3 --extract --directory "$PART2/extracted" "$FW" | tee "$PART2/logs/binwalk-extraction.txt"It reported successful extraction of the uImages at 0x5AC0 and 0x133C1, the SquashFS at 0x11D0D2, and the gzip data at 0xEFCA47. The directory named 11D0D2 contains squashfs-root, which is the unpacked Linux filesystem. I gave it a shorter name for the rest of my notes:
ROOTFS="$PART2/extracted/$(basename "$FW").extracted/11D0D2/squashfs-root"The filesystem identification answers another Part 1 question: the root filesystem is SquashFS 4.0 with xz compression. Binwalk calls the filesystem little-endian. That is separate from the endianness of the router’s executable code, which I check below.
Walking through the root filesystem#
I started at the top level and then spent more time in the directories that actually contained programs, configuration, or web files. Here’s my map. It includes every top-level entry; the deeper branches are the ones that were useful for this pass.
squashfs-root/
├── bin/ BusyBox, shell links, and other commands
├── dev/ empty in this extracted image
├── etc/
│ ├── config/ system and service configuration
│ ├── init.d/ service startup scripts
│ ├── rc.d/ boot/shutdown links into init.d
│ ├── hotplug.d/ scripts for device/network events
│ ├── uci-defaults/ default/first-boot configuration scripts
│ ├── dropbear/ SSH service files
│ ├── preinit early boot script
│ ├── inittab init's startup instructions
│ └── rc.common shared init-script logic
├── lib/
│ ├── modules/3.3.8/ kernel modules
│ ├── preinit/ early boot helpers
│ ├── config/ UCI shell helpers
│ └── *.so* shared libraries, including uClibc
├── mnt/ empty in the extracted image
├── overlay/ empty in the extracted image
├── proc/ empty in the extracted image
├── rom/ contains a note file
├── root/ empty root-user home directory
├── sbin/ system administration programs and BusyBox links
├── sys/ empty in the extracted image
├── tmp/ contains an is_online file
├── usr/
│ ├── bin/ additional commands, including tddp
│ ├── sbin/ daemons, including uhttpd
│ └── lib/lua/ LuCI code and other Lua components
├── var -> /tmp symbolic link, not a separate directory here
└── www/
├── cgi-bin/luci LuCI CGI entry point
├── webpages/ management interface pages and assets
└── compress/ compressed web assetsThe empty directories are worth noticing, but there isn’t much to analyze inside them yet. Directories such as /proc and /sys are normally populated by a running Linux system, and /mnt and /overlay are places a booted device may use later. Their emptiness in this extracted, static copy does not tell me what they contain at runtime. The name tmp/is_online also doesn’t prove this router was online. For Part 2, the directories with the most to say are /bin, /etc, /lib, /usr, and /www.
Architecture, libraries, and what this Linux build is#
The extracted executables settle the architecture question left in Part 1. Static identification of /bin/busybox, /bin/opkg, /sbin/netifd, /usr/sbin/uhttpd, and /usr/bin/tddp reports ELF32, big-endian MIPS executables. Their ELF flags indicate MIPS32 release 2, and they request /lib/ld-uClibc.so.0 as the program interpreter. So the SquashFS format being little-endian does not mean these programs are little-endian.
The file and readelf results describe the binaries in this firmware, not the exact processor core inside physical hardware. In particular, the generic MIPS R3000 machine label that readelf prints should not be read as proof that the router has an R3000 CPU. The MIPS32r2 flags are more informative for this image.
The loader link points to uClibc 0.9.33.2 in /lib. That matters later because a dynamically linked program needs the matching loader and shared libraries if I ever try to run it outside the router. /lib/modules/3.3.8 also fits the Linux 3.3.8 version named in the extracted OS image. This looks like an OpenWrt-derived embedded Linux filesystem, but I can’t name an exact OpenWrt release from it: /etc/openwrt_release and /etc/openwrt_version still contain template placeholders. An old package-feed URL in /etc/opkg.conf isn’t reliable release identification either.
BusyBox: a lot of commands in one binary#
Before this project, I wasn’t familiar with BusyBox. Seeing how this firmware uses it helped me understand why it makes sense on a router with 16 MB of flash.
BusyBox is the most obvious piece of everyday Linux infrastructure here. Instead of shipping a separate full-sized executable for every basic command, this firmware has a roughly 403 KB /bin/busybox binary and symbolic links that call into it under different names. For example, /bin/sh and /bin/ash lead to BusyBox, as do familiar commands such as ls, cat, grep, mount, and ping. /sbin/init links to it too.
BusyBox chooses its behavior based on the name used to invoke it. So sh can give the system a shell, while init handles initialization, even though both paths end up at the same executable. Seeing telnet or tftp among its applets does not mean a Telnet or TFTP server is running. Those names identify available commands in the filesystem, not live network listeners.
Services and web files that caught my attention#
The web interface is a good example of why looking across directories helps. /etc/config/uhttpd points the web server at /www and contains HTTP/HTTPS listen settings. /www/index.html redirects to /webpages/login.html, and /www/cgi-bin/luci is a Lua entry point that loads LuCI’s CGI code. Under /usr/lib/lua/luci/controller, some controller files, including login-related code, are Lua 5.1 bytecode rather than plain-text Lua. Together, those files give me a starting map of the management interface. They do not show which requests are accepted at runtime, and the uhttpd startup script has configuration-dependent behavior, including for HTTPS.
There are also startup links for dropbear and tddp, but the links alone do not prove those services run. One detail that matters for tddp: its init script starts /usr/bin/tddp only when factory.factorymode.enable is yes. I’m keeping TDDP as a separate factory-mode lead, not treating it as a normal always-on service. The dropbear_rsa_host_key file in the extracted image is zero bytes, so its presence is not evidence of a bundled private key.
Other names such as tmpServer, cloud-client, and cloud-https are worth keeping on my list. Valuable static leads! I need to understand them a bit more before I can claim any of them are vulnerable.
What Part 2 answered, and what I want to ask in Part 3#
Part 1’s open questions now have better answers: the OS-image and filesystem offsets match the SafeLoader payload-relative entries, the root filesystem is SquashFS 4.0 with xz compression, and representative executables are 32-bit big-endian MIPS32r2 built for uClibc. I also have a rough path from BusyBox and init through configuration and startup scripts to the programs they may launch.
The next part should test the gap between present in the firmware, configured to start, and actually running. A few questions I want to carry forward:
- Which network-facing services start in a normal boot, and which require a specific mode or configuration? TDDP’s factory-mode check is one example.
- How does
uhttpdconnect the web files, LuCI controllers, and any other backend programs? Which parts are reachable before and after authentication? - Which of the vendor-integrated binaries handle input from the network, and what configuration or shared libraries do they need?
- Is one of these services practical to launch in an isolated QEMU user-mode setup, or would its device and system dependencies make that a bad first target? BusyBox may be a simpler executable to test the environment with, but it wouldn’t answer the service-behavior question by itself.
I’m leaving QEMU, debugging, and runtime testing for Part 3. For now I have an extracted filesystem and a set of specific things to verify, not a vulnerability finding.