Firmware triage with a saved evidence trail

Find the binaries worth opening.

The working loop is a release delta plus a named-import verify. JSON stays on stdout. Analysis never calls a model.

Official AArch64 firmware · --quick · no model calls · target not run

OPENWRT 24.10.3 → 24.10.4BPI-R3 MINI · AARCH64
COMMON ELF PATHS109

106 unchanged / 3 changed

busyboxubusdodhcpd
ubusd→3 strcpy callers→len guard

busybox and odhcpd also changed; they were not followed. Ground truth: OpenWrt advisory and upstream patch

RECORDED RUN · OPENWRT AARCH64

Two releases. One trail you can inspect.

Select a node to see the saved fact, why it changed the scope, and the command or source that supports it. The graph starts with the firmware pair, not a hand-picked crackme.

RECORDED EVIDENCE

Click a node to inspect it

10 nodes · 10 links
SELECTED05
OPEN

Changed common userland ELFs

Path and SHA-256 comparison leaves three common userland binaries. This narrows the work; it is not a vulnerability verdict.

EVIDENCEbin/busybox · sbin/ubusd · usr/sbin/odhcpd

Counts compare regular ELF files at paths present in both root filesystems. Versioned kernel-module and library paths remain outside the 109-path comparison. Default brief --extract is 64 MiB / 200 files / depth 2; the 176-ELF inventory used sandboxed unsquashfs with a higher file cap. Device nodes still need root.

WHAT THE PASS ADDED

A shorter list, with the evidence behind it.

The published advisory tells us the answer. This run tests whether the binary workflow gets to the same neighborhood without pretending an import name is a finding.

01 · REDUCE106 common ELFs were unchanged.

Matching SHA-256 values removed programs such as uhttpd from this release question.

02 · TRACEThree dynamic strcpy call sites became concrete leads.

The import was only a pivot. verify supplied caller addresses for a short decompilation pass.

03 · CHECKThe changed routine gained an empty-pattern guard.

24.10.3 reads pattern[len - 1] first. 24.10.4 checks the length first. Upstream source confirms the function and intent.

Boundary: r2b narrowed and preserved the trail. It did not discover or prove CVE-2025-62526 by itself. A 2026-09-02 Kali aarch64 replay recovered the same three 24.10.3 strcpy callers under bubblewrap. Device nodes still need root. The 176-ELF inventory used sandboxed unsquashfs with a higher file cap than default brief --extract (64 MiB / 200 files / depth 2).

DEMO · PLANNERS ON ONE BRIEFING

Empty next_argv. Four planners still ran r2b verify.

Same 24.10.3 ubusd card. Rules review kept network ahead of the strcpy capsule. Four planners were told: only uv run r2b, no raw r2. All four hit the three gold callers. Kimi and GLM had no agent CLI; they got one tool that can only exec r2b.

01 · CARDnext_argv was empty. strcpy was still in the briefing.

Process-launch auto-queue does not fire. The memory capsule and dangerous_imports do. That is the demo, not a missing feature.

02 · IN LOOPGrok, Codex, Kimi K3, and GLM all ran verify --import strcpy.

Addresses 0x4b50, 0x4b6c, 0x4ba8. No r2 axt. No Ghidra on entry.

03 · HOPRecorded decompiles missed. The CLI hop is function_addr.

Call sites and r2 0x33c4 were not Ghidra functions. A later live decompile of 0x4ba8 returns FUN_00104a3c at 0x00104a3c. Not a bake-off score.

Boundary: this is not a four-way quality ranking. It shows r2b staying in a planner loop on the firmware path that works.

RECORDED RUN · DARPA CGC I386

Briefs ranked main. next_argv was empty. Address hops hit one in nine.

Eighteen CGC service binaries, a frozen stride of nine challenges. Quick ranking was identical: entry:main. handoff.next_argv was empty. Function VAs live on handoff.regions[].addr. Explicit decompile hops from those addresses hit Palindrome's planted site once. That hop experiment is not the current auto-queue.

01 · RANKAll 18 briefs ranked only entry:main.

These services import libcgc, not strcpy. There is no PLT pivot to split patched from unpatched. next_argv stayed empty.

02 · SNIPPETThe 12-line capsule is still the prologue.

After af+pdf, Palindrome's full listing names cgc_check. The 12-line capsule may not. There is no torn c7 invalid.

03 · HOPS1 / 9 site hits from ranked region addr.

Not from auto-queued decompile. CNMP showed insert_joke as a callee. The other six frozen challenges never named the planted routine.

Boundary: ranking is not a CWE detector. Empty next_argv is valid. r2b decompile BIN ADDR is explicit. Palindrome is DARPA's easy sample. Do not treat a source-named decompile as a loop hit.

RETRIEVAL CHECK · NIST JULIET AARCH64

Region recall was zero. Good and bad ranked the same.

Seven CWEs, good and bad aarch64 ELFs, gcc -O0 -g. This is a failed retrieval check against labelled synthetic controls, not a detector score.

01 · RANK0 / 14 region recall at k=1, 3, and 5.

entry:main is the INCLUDEMAIN prologue, not the sink. imports:memory listing strcpy is not a hit.

02 · PAIRSAll seven good/bad pairs were identical.

Both twins still call strcpy, system, or free — or neither. Ranking did not split labelled good from labelled bad.

03 · VERIFYCaller recall 8 / 8, including the good twins.

verify found a non-constant libc-sink caller when asked. It abstained on CWE-190, CWE-416, and CWE-476. That is a pivot, not a CWE verdict.

Boundary: ranking is not a CWE detector. An import name is not a hit. Juliet already names the CWE and the sink. r2b did not discover these flaws.

SAME EVIDENCE, MORE REVIEW ANGLES

Width cannot add evidence.

Keep review-width as a sort of the saved regions. It is not the OpenWrt result. In the pre-fix ubusd run, width 2 adds nothing; width 3 promotes the memory/path region that leads to verify.

AArch6465,706 bytes63 r2 functions8fd16a06…e5f3
PASS 1

Triage

Which saved regions deserve the first look?

NEW AT THIS WIDTHimports:network · entry:entry0
Passes included
1
Model calls
0
Unique regions
2
imports:networkvisible at width 1
Network boundary

accept

entry:entry0visible at width 1
Program entry

entry0 @ 0x1e18

imports:memoryenters at width 3
Memory and paths

memcpy · sprintf · strcpy

One saved briefing · rules mode · top two per pass · duplicate evidence IDs collapse · target not executed

HOW THE RUN WAS BUILT

Use the tool that answers the next question.

r2b does not run every adapter on every file. It classifies the subject, records what the host can support, and keeps the result of each step with the subject hash.

FIRMWARE

Inventory and extract

Binwalk v3 found the SquashFS. unsquashfs recovered regular files. The artifact DAG kept parent and child hashes.

FIRST PASS

Map the binary

file, radare2, and Capstone supplied format, imports, functions, entry disassembly, and caller pivots.

STATIC DEPTH

Open one routine

Ghidra decompiled the selected old and new functions. angr added a bounded CFG. DWARF was checked for metadata; none was present.

RUNTIME

Opt in when needed

Frida and GEF were not run. Executing a foreign-architecture target is an explicit decision, not a side effect of “full.”

PORTABLE RESULT

The bundle keeps the run together.

The downloadable bundle contains the pre-fix ubusd analysis, brief, tool status, provenance, and rules review. Target bytes are excluded. The adjacent case JSON links it to both firmware releases.

SHA-25690544b29015b3fdd…201cbaf
r2b bundle inspect openwrt-24.10.3-ubusd-full.r2br --json

# analysis.json · briefing.json · provenance.json
# review.json · tools.json · target bytes excluded

INSTALL

Install r2b. Run a brief.

The installer creates a repo-local environment. It does not use sudo or modify system Python.

RECOMMENDED

Let the installer choose a flavor.

It checks RAM and CPU architecture, then picks core, lab, or full. You can override the choice.

git clone https://github.com/sandbornm/r2brief.git
cd r2brief
./scripts/install.sh
coreradare2 support and the small-host path. No Ghidra, angr, or web UI.

Pi, ARM, or under 8 GB

labThe regular workstation install. Add heavyweight analyzers only when a target needs them.

A normal workstation

fullAdds angr support and enables the Ghidra adapter. Ghidra still has its own local setup step.

16 GB+ x86_64

Ways to call r2b

CLI vs. harness: the CLI produces one result. A harness calls it, reads the versioned JSON, and decides what to run next. Empty next_argv is valid.

ONE RUN

CLI

r2b brief ./firmware.itb \
+  --quick --extract --json

Run a brief and inspect it. JSON stays on stdout; diagnostics stay on stderr. --quick is the product.

YOUR CODE

Python

from r2b import analyze

report = analyze("./ubusd")
for region in report.regions:
    print(region.id, region.evidence)

Call the same analysis as a library. Saving and model use remain explicit.

YOUR LOOP

Harness

r2b brief ./ubusd --quick --json > brief.json
jq '.handoff.next_argv' brief.json
# empty is valid; VAs live on handoff.regions[].addr
r2b verify ./ubusd --import strcpy --json

jq next_argv; empty is valid. Function VAs live on handoff.regions[].addr. The real ubusd follow-up is a named-import verify.

Analysis never calls a model. Optional review can use Ollama or a configured hosted provider to reorder known evidence; the original brief stays unchanged. Provider setup →