Modern attacks on routers and other embedded devices rarely inject new code, because memory that holds data is marked non-executable — instead they stitch together short fragments of the program's OWN code into a malicious sequence, and firmware hands them an enormous supply of raw material, because a device links against a whole shared library such as the standard C library while using only about 5 percent of it, leaving the other 95 percent sitting in memory as free ammunition; the obvious defence is to delete the unused code, and it works on desktops where source code or debug symbols are available, but firmware ships as a stripped binary where you cannot even reliably tell where functions begin, and deciding which code a pointer might reach is undecidable in general; this paper (UT Arlington) finds a way through by refusing to solve that problem at all, exploiting a quirk of the MIPS calling convention that forces almost all address computation through one table via two dedicated registers, so watching accesses to that table reveals which code can be reached without ever resolving a pointer — removing about two-thirds of library code and up to 80 percent of the most dangerous attack fragments, at zero runtime cost.
The argument in one glance
-
Problem
Firmware ships 95 percent unused library code, and attackers use it as ammunition
Embedded devices such as home routers are now prime targets, with one report citing a 300 percent surge in attacks in a single year. The dominant technique is CODE REUSE. Because modern systems mark data memory as non-executable, attackers cannot simply inject their own instructions any more, so instead they chain together short fragments of instructions already present in the program — a technique powerful enough to build arbitrary malicious behaviour, provided enough fragments exist. Firmware supplies them abundantly. Developers link against large shared libraries such as the standard C library for convenience, so the entire library is loaded into memory even though a program actually uses only about 5 percent of it. The remaining 95 percent computes nothing and defends nothing; it simply sits there as a reservoir of attack material.
-
What people did
Delete the code the program never uses
The natural answer is DEBLOATING — analyse which library code a program can actually reach and erase the rest, shrinking the supply of usable fragments. This has become an active defence in the desktop world, and it is attractive for embedded systems in particular because firmware is shipped as a single image that is updated wholesale, so the trimming can be done once, ahead of time, with nothing left to pay at run time.
-
Why it fell short
Existing tools need source, symbols or a runtime that firmware does not have
Every existing approach depends on something firmware lacks. One prominent system needs the application's source code along with all its libraries, and imposes roughly a 20 times slowdown at load time because each application must load its own customized copy of the library. Another needs debug symbols to locate functions, and also needs section headers — but firmware images are deliberately stripped of both to save space, so on a real router binary the tool cannot even determine where functions begin. Underneath these practical obstacles lies a theoretical one. To know which code is reachable you must follow indirect calls, where the destination is computed at run time from a pointer, and deciding what a pointer may hold is undecidable in general. Existing analyses either guess and produce false positives, or give up and mark every relocatable function as possibly used, which removes almost nothing.
-
This paper's idea
Do not resolve the pointers — exploit an architectural regularity that reveals them
This paper (UT Arlington — Zhang, Ren, Lei, Ming) builds uTrimmer, and its move is to sidestep the undecidable problem rather than attack it. Shared library code must be position-independent, meaning it can be loaded at any address, which forces it to obtain addresses by reading them from one table — the GLOBAL OFFSET TABLE — rather than embedding them. So instead of asking where each pointer points, uTrimmer asks which entries of that table are read, which reveals what code could be reached WITHOUT ever resolving a target. What makes this practical is a quirk of the MIPS calling convention, and it is why the paper is titled "one size does not fit all": MIPS dedicates two specific registers to this job, one holding the table's base address and one holding the address of the function being called. On other architectures any register may be used and the analysis becomes expensive data-flow reasoning; on MIPS the pattern is written into the instruction stream and can simply be read off. Unused blocks are then overwritten with an illegal instruction, which costs nothing at run time and makes any mistake crash loudly rather than silently. The result removes about two-thirds of library code and up to 80 percent of the most dangerous attack fragments.
Why this problem is worth solving
The attack this defends against does not look like what most people picture
when they imagine software exploitation, so it is worth being precise.
A CODE-REUSE
ATTACK exploits a memory-corruption bug without injecting any new instructions.
It became the dominant technique once systems began marking data memory
NON-EXECUTABLE, which means that even if an attacker writes their own machine
code into a buffer, the processor refuses to run it. The workaround is to use
instructions already present and already executable. The attacker corrupts the
program’s control data so that it jumps to a short fragment of existing code
that ends in a return or an indirect jump, which hands control to the next
fragment, and so on. Each fragment — a GADGET — does something tiny, but chained
together they can perform arbitrary computation, which is why this is powerful
enough to fully compromise a device. The essential point for defence is that
the attack is limited by SUPPLY: it needs enough distinct fragments to assemble
the sequence it wants. Removing code the program never executes therefore
directly removes the attacker’s raw material, and it is a defence that costs
nothing at run time because the deleted code was never going to be run
The obstacle is that firmware arrives in a form that resists analysis.
A
STRIPPED BINARY has had its debug symbols and often its section headers removed
to save space, which firmware images do routinely. Without symbols there is no
list of functions and no reliable way to tell where one function ends and the
next begins, so tools that identify unused FUNCTIONS have nothing to work with.
A second complication is that shared library code must be POSITION-INDEPENDENT,
able to run correctly no matter what address it gets loaded at. It therefore
cannot contain hard-coded addresses of its own functions and data. Instead the
loader fills in a table of real addresses at load time — the GLOBAL OFFSET TABLE
— and the code reads addresses out of that table whenever it needs one. This is
normally seen as an obstacle to analysis, since it adds a level of indirection
between the code and the things it refers to. The insight of this paper is to
treat it as the opposite. Because position-independent code MUST route its
address computations through that table, watching which entries get read tells
you which code and data can be referred to, without needing to determine what
any individual pointer actually holds at run time
What people did before, and where it broke
- Source-level debloating. Compile away unused code when you build the program. Effective and well understood, but it presumes you have the source for the application and every library it uses, which nobody has for a router they wish to harden.
- Debloating with a customized per-application library. One prominent system removes unreachable code but requires source, and gives each application its own tailored copy of the library — which costs roughly 20 times slower loading and forfeits the sharing that made shared libraries worthwhile.
- Debloating binaries using debug symbols. Another works on binaries rather than source, but relies on debug symbols to locate functions and on section headers to navigate the file. Firmware is stripped of both deliberately, so the tool cannot start.
- Value-set analysis for indirect calls. The rigorous route is to reason about what values a pointer may take. It suffers from a high false-positive rate, which for debloating means over-including code and removing little.
- Marking everything relocatable as used. The safe fallback treats every function that could conceivably be referenced as reachable. It is sound and nearly useless, and it still misses the complicated cases arising from C++ virtual functions and read-only global function pointers.
- The gap. No approach could debloat stripped firmware libraries, which is precisely where the defence would matter most — devices too weak for runtime protections, shipping enormous unused code, updated as a single image. That is the gap uTrimmer fills.
How unused code is found without resolving a single pointer
Unpack the firmware, follow what the program can reach, and overwrite the rest
-
Stripped firmware defeats existing analysis
Firmware ships without debug symbols or section headers, so tools cannot locate functions, and following indirect calls to determine reachability is undecidable in general. Existing analyses either over-approximate until nothing can be removed, or produce false positives that would break the device.
-
Unpack and disassemble new
The file system is extracted from the firmware image with standard unpacking tooling, and each binary is disassembled by scanning linearly through the code, since there are no symbols to guide the process.
-
Work out which libraries serve which programs new
Collect the interfaces each application imports, together with the routines libraries run automatically at load and unload time, then build a graph of which library depends on which and sort it so that each library is trimmed only after everything that calls into it has been accounted for.
-
Trace reachability from the entry points new
For each library, build a control-flow graph starting from the interfaces that are actually imported. The hard part is code reached indirectly, and here the analysis classifies the ways an address can be loaded from the global offset table into six patterns, tracing arithmetic on loaded addresses with symbolic execution where needed.
-
Over-include wherever certainty is impossible new
When a virtual table pointer is loaded, every function in that table is marked reachable rather than trying to determine which is called. This deliberately keeps code that might be dead, trading some reduction for the guarantee that nothing live is removed.
-
Overwrite the unreachable blocks with an illegal instruction new
Rather than rewriting the binary, which would require fixing every reference to shifted code, unused basic blocks are simply overwritten in place with a byte that is not a valid instruction. Nothing moves, nothing costs anything at run time, and any mistake produces an immediate crash instead of silent misbehaviour.
The idea
Refuse the undecidable problem and find a structural regularity instead. The textbook obstacle to this whole enterprise is that determining which code an indirect call can reach requires knowing what a pointer holds, which is undecidable in general and expensive to approximate. Existing work either approximates badly, producing false positives that would brick a router, or gives up and keeps everything. uTrimmer never answers the question. It observes that shared library code must be position-independent and therefore cannot embed addresses, so it is FORCED to read them from the global offset table — which means that watching which table entries are read reveals what could be referenced, without ever determining a pointer’s value. Sidestepping a hard problem by exploiting a structural constraint that makes it unnecessary is the same dodge-rather-than-solve move covered elsewhere on this site, where a system restricts itself to read-only data so that the coherence problem simply does not arise rather than building machinery to handle it.
One size does not fit all, and that is the contribution, not an apology. The title states the thesis. What makes the analysis tractable is that the MIPS calling convention dedicates two registers to this purpose — one always holding the table’s base address, one always holding the address of the function being called — so the pattern is legible directly in the instruction stream. On other architectures any general-purpose register may be used, which turns a simple pattern match into expensive data-flow analysis, and the authors say plainly that their approach does not transfer. Rather than seeking a universal defence and achieving a weak one everywhere, they build a strong one where a local regularity permits it. Exploiting what is specifically true of your substrate instead of porting a generic technique is the same re-derive-for-the-substrate discipline covered elsewhere on this site, in the graphics-processor page whose authors borrow non-uniform memory from CPUs but rebuild its policies because bandwidth rather than latency is what binds there.
Make the failure mode loud, and let it double as the test. The final design choice looks minor and is not. Unused blocks are not deleted or relocated — which would require finding and fixing every reference to code that moved — but overwritten in place with an illegal instruction. Nothing shifts, so nothing needs updating; there is no runtime cost and no extra memory. The subtler benefit is that if the analysis was ever wrong and the program reaches erased code, the processor faults immediately and unmistakably, rather than executing something plausible and corrupting state quietly. That converts the riskiest property of the whole system into something observable, and the authors use exactly that as their correctness criterion, reporting that across all test suites no illegal-instruction fault ever occurred. Designing so that a mistake announces itself rather than hiding is the fail-loudly discipline covered elsewhere on this site wherever a system makes its own errors detectable instead of hoping they are absent.
Trimming a router’s C library
From a whole shared library to only the parts a program can reach
How uTrimmer hardens stripped MIPS firmware. Numbers from the paper's evaluation.
Step 1 of 5
What the device ships with
A router's firmware links its programs against full shared libraries such as the compact C library used in embedded systems. Each program uses roughly 5 percent of that code. The rest is loaded into executable memory anyway, where it serves as a supply of short instruction fragments an attacker can chain together.
the firmware is a single image updated wholesale, so any trimming can be done once ahead of time with nothing to pay later the binaries are stripped of debug symbols and often section headers, so no list of functions exists
Why it works
- A code-reuse attack needs a supply of distinct instruction fragments → erasing code the program can never execute removes that supply directly → cutting system-call fragments by 79.7 percent and jump-oriented ones by 68.5 percent on the benchmark programs.
- Position-independent code cannot embed its own addresses and must read them from one table → observing which table entries are read reveals what can be referenced → so reachability is determined without ever resolving what a pointer holds.
- MIPS dedicates one register to the table base and one to the callee address → the access pattern is legible directly in the instruction stream → turning what would be expensive data-flow analysis elsewhere into a pattern the tool can simply read off.
- Erased blocks are overwritten in place rather than removed → no code moves, so no references need updating → giving zero runtime overhead and zero extra memory, unlike approaches that hand each application a customized library.
- Overwriting uses an invalid instruction → any reachable block wrongly erased faults immediately and visibly → so the analysis’ central risk becomes directly testable, and the test suites report no such fault.
What it costs (trade-offs and scope)
- It is deliberately architecture-specific and does not generalise. The whole method rests on the MIPS convention of dedicating registers to the address table. The authors state that other architectures allow any register for the same job, and that disassembly difficulties there make the approach not directly transferable — the title concedes this openly.
- Soundness is not claimed. The authors say plainly that in binary analysis theoretical guarantees are undermined by the toolchain, and their analysis relies on empirically learned patterns and heuristic symbolic execution. Confidence comes from testing, not proof.
- Conservatism costs reduction. Where certainty is impossible the analysis over-includes, marking every function in a virtual table reachable when its pointer is loaded, and handling read-only global function pointers cautiously. That caution is the main reason a gap to static linking remains.
- Some constructs are out of reach. Libraries loaded by name at run time cannot be handled when the name is computed dynamically, and deliberately obfuscated code would defeat the control-flow reconstruction — though the authors argue obfuscation is rare on devices too slow to afford its cost.
- Analysis is slow, though offline. A single program takes 1.1 to 6.2 hours depending on how many libraries it pulls in, and the full router image took 21.7 hours. This is paid once by whoever builds the firmware, but it is not interactive.
The evaluation
uTrimmer was built on the angr binary analysis framework and applied to three kinds of target: the integer benchmarks of SPEC CPU2017, real firmware applications including Apache, BusyBox and OpenSSL, and a complete commercial router image — a TP-Link Archer A10 containing 25 shared libraries and 75 applications. Correctness was assessed by running the official test suites for each program, with runtime library search paths customized so that the tools doing the testing did not accidentally use the trimmed libraries. Security was measured by counting the attack fragments remaining, and the amount removed was compared against static linking as the ideal.
Removing unused library code from firmware
| Property | Source-level debloating | Symbol-based binary debloating | uTrimmer |
|---|---|---|---|
| What it needs | |||
| Handling indirect calls | |||
| Runtime cost | |||
| Gap to static linking | |||
| Works on a real router |
Check yourself
Check your understanding
1. Determining which code an indirect pointer can reach is undecidable in general, yet uTrimmer needs exactly that to know what is safe to delete. Predict how it proceeds.
2. The paper is titled "one size does not fit all" and its method works only on MIPS. Predict why that is presented as a contribution rather than a weakness.
3. Rather than deleting or relocating unused code, uTrimmer overwrites it in place with an invalid instruction. Predict the two distinct benefits.
4. The paper reports its code reduction as a gap against static linking rather than as a bare percentage. Predict why that framing is more informative.
Sources
- One size does not fit all: security hardening of MIPS embedded systems via static binary debloating for shared libraries (ACM DL, DOI 10.1145/3503222.3507768) — ASPLOS 2022; full text read for this explainer.
- Artifact (Zenodo) — the released tool, which earned ACM Artifacts Available, Functional and Reproduced badges.
- Author project page — paper listing and related materials.