</>PatchNote
Back to posts

2024-11-30

Power-based Feedback Fuzzing of IoT

SecurityFuzzingIoTLSTMSide-Channel

For my capstone project, a 3-person team designed a fuzzer that uses power consumption as a feedback signal for IoT routers. The idea is simple: on a black-box IoT device where instrumenting for coverage is hard, instead of execution instrumentation we watch the power the device draws (a side channel) and use it as fuzzing feedback to answer "did this input just exercise a new code path?"

My part was the step that quantizes the power data into a bitmap and turns it into a coverage feedback signal — and, to be upfront, this is exactly where I failed. Here is how it went and why.

Why power

Routers and similar IoT devices have limited resources, so they rarely ship strong security, and incidents like the Mirai botnet show how badly that can scale. Fuzzing is the go-to automation for finding bugs, but these devices are usually black boxes with no source and no symbols.

  • AFL-style coverage-guided fuzzing assumes binary instrumentation.
  • Emulating firmware under QEMU often fails on peripheral/NVRAM dependencies.
  • So on-device HTTP fuzzing usually stays black-box random fuzzing with no coverage feedback.

The project's hypothesis was to use power consumption as a proxy for coverage. If a different code path runs, the CPU/peripheral power waveform should differ too — catch that difference and you might detect "new behavior" without any instrumentation.

Prior work (Side-Channel Aware Fuzzing, Powertrace-based Fuzzing of CAN, etc.) had used side channels for fuzzing, but we found no prior case applying power-based fuzzing to a real IoT router.

First: triggering a 1-Day with existing HTTP fuzzers

Before building anything, we checked whether a known (1-Day) vulnerability could actually be triggered on real hardware using existing fuzzers. The target was CVE-2022-24355 in the httpd of a TP-Link TL-WR940N. What we tried and where it fell short:

FuzzerLimitation
wfuzzDoesn't add the vulnerable header by default; default wordlist lacks the trigger path
http-fuzzerNo template auto-generation — you must hand-write the vulnerable header into the template
httpfuzzNo automatic header add/remove — the vulnerable header must be specified manually

In every case the trigger path /loginFs/passwd was missing from the default wordlist, so nothing fired unless we supplied a custom wordlist and headers. The bottleneck was clearly the quality of the input (what you send) — which set the first design direction for our fuzzer.

Fuzzer design ① — raising input quality with Filtered Strings

Instead of random input, we harvested paths that actually exist in the firmware as seeds. We ran strings over the httpd binary to extract URI candidates and used a blacklist to drop noise.

import os
PATH = "~/httpd"
with open("blacklist.txt", "r") as bl:
    blacklist = set(line.strip() for line in bl)

stream = os.popen('strings {} | grep -E "/[a-zA-Z0-9_/.-]+"'.format(PATH))
with open("filtered_strings.txt", "w") as f:
    for line in stream:
        for part in line.split():
            if part.startswith("/") and all(item not in part for item in blacklist):
                f.write(part + "\n")

Real-world snags showed up fast:

  • Even meaningless URIs returned 200, because invalid paths redirect to /. → We dumped the invalid responses with Burp Suite and automated a preprocess step that classifies valid vs. invalid paths by response pattern.
  • Handling format strings (%s, %d) mixed into paths, and * inside a path.
  • Splitting strings before/after / separately to widen combinatorial variety.

Analyzing httpd, we also learned it uses a custom library rather than libcgic and manages aliases via httpAliasConfAdd. The payoff of this part was a low-cost methodology for extracting valid inputs straight from firmware.

Fuzzer design ② — mutator and template auto-generation

To diversify seeds, we implemented an insert/delete/modify mutator that dynamically mutates the headers, cookies, and body of HTTP requests. Request generation itself is built on http-fuzzer (Go text/template), but to kill the biggest weakness of the existing tools — hand-written templates — we bolted on automatic template generation from the filtered strings.

The full pipeline:

Seed ─┬─ firmware strings
      ├─ Burp Suite logs
      └─ known HTTP headers (72)
        │
   Preprocess ── validate paths (check HTTP response) → extract valid paths
        │
   Auto-generate template → Mutation / Generation
        │
   Monitor ── check HTTP response → store valid inputs

Power-based request detection (LSTM) — this part worked

Power waveforms were captured with a Monsoon High Voltage Power Monitor (HVPM). Wired as supply → monitor → router, the monitor powers the router; at 9V the TP-Link WR840N ran normally.

We trained an LSTM on the power time series to separate IDLE vs. Request states. Against a plain RNN, the LSTM's predicted waveform tracked the measured one far more closely. The key idea was to use the prediction error (MSE) of a model trained only on IDLE as an anomaly signal.

  • Normal (IDLE) prediction MSE: 0.196 × 1e-05
  • Anomalous (request in progress) MSE: 2.307 × 1e-05 → a ~10× jump

Error spikes when a request arrives and settles back down when it returns to IDLE, so continuous event tracking worked too. In other words, the fact that "a request came in and the router did something" is detectable from power alone. This is the part that actually worked.

The limitation — can quantization/bitmaps produce coverage?

My part came next. Beyond "a request arrived," we needed to quantize the waveform and turn it into a coverage signal saying "this input exercised a new code path" to close the fuzzer's feedback loop. Borrowing AFL's edge-coverage bitmap, I quantized the waveform into bins to build a bitmap, treating newly filled bits as "coverage increase."

The problem was that power resolution wasn't fine enough to capture code-path differences.

  • IDLE vs. Request separated cleanly, but the subtle path differences between different requests didn't separate reliably in the waveform — noise (the value jittered even at rest) buried the signal.
  • Applying an AFL-style bitmap as-is gave no reliable threshold for deciding whether coverage had grown.
  • Decisively, I lost a lot of time on the live-measurement API setup. Monsoon's monsoon Python module couldn't grab a USB backend (usb.core.NoBackendError: No backend available), even blue-screening on Windows, and I only got it attached by working around it with USB Arbitration under VMware Ubuntu. The schedule slipped because of the time spent on this.

In the end, power → coverage quantization failed, and we couldn't fully integrate power-based feedback into the fuzzer. Accepting the failure, I focused on analyzing why it failed (resolution, noise, measurement reliability) and leaving a direction for improvement.

CVE-2022-24355 — the fuzzer did trigger the crash

We couldn't close the feedback loop, but our fuzzer (auto-generated templates from filtered strings + mutation) did trigger the CVE-2022-24355 crash. The bug is in a function called by httpd's httpRpmFs(). The decompiled core loop:

sVar3 = strlen(param_1);
pcVar2 = param_1 + sVar3;
do {                        // walk backward until a '.' is found
    pcVar8 = pcVar2;
    pcVar2 = pcVar8 + -1;
} while (*pcVar8 != '.');    // with no extension ('.'), it runs past the buffer

If the input path has no ., this do-while reads backward past the string boundary, and the later loop copying the extension in uppercase overruns a stack buffer, crashing. Since each crash killed the router, we automated testing with a smart plug for remote reboots.

Retrospective — what worked, what didn't

  • Worked: a firmware-string methodology for raising input quality, a template-auto-generating fuzzer, LSTM-based request detection (sensing a request from power alone), and a 1-Day PoC crash.
  • Didn't: the feedback loop that quantizes the power waveform into code coverage. Power resolution and measurement noise were the fundamental limitation.

It stings that my part was the one that failed, but it was a hands-on way to find where the "use a side channel as coverage on a black-box device" idea breaks down. Reusing AFL's bitmap verbatim isn't enough; power signals need a different quantization / denoising approach — the clearest lesson this project left behind.

Targets: TP-Link TL-WR940N / TL-WR840N · Instrument: Monsoon HVPM · Model: LSTM (TensorFlow/Keras)