Releases: cooparo/runtime-attacks
v0.1.0 — Shadow-stack detector + 3 attack PoCs
First tagged release of the AAU runtime-attacks research project.
What this is
A research repo with three things:
- A runtime attack detector (
detector/tracer) — a user-space shadow call stack on AArch64 Linux, built onptrace. It enforces the call/return contract: everyretmust return to the address recorded by its matchingbl/blr. Any deviation is flagged as[!!! ATTACK DETECTED]and the tracee is killed. - Three attack PoCs — small vulnerable C programs paired with auto-generating
pwntools-based exploits, used to exercise the detector and document its coverage. - A test matrix (
tools/run_tests.py+make test) — drives detector × {benign, attack} for every attack dir and asserts on tracer exit code, stderr markers, and stdout markers.
Target: AArch64 Linux (Raspberry Pi 4/5 class hardware).
Attacks shipped
| Iter | Attack | Path | Detected? |
|---|---|---|---|
| 1 | Stack buffer overflow (direct return-address overwrite) | attacks/01-stack-bof/ |
✅ |
| 2 | Return-oriented programming (3-gadget chain) | attacks/02-rop/ |
✅ (at first hijacked ret) |
| 3a | Jump-oriented programming (blr-pivot → br x16 gadget → _exit) |
attacks/03-jop/ |
❌ — see "known gap" below |
Known gap (iter-3a)
The shadow call stack catches any control-flow hijack that subverts the call/return contract — any ret to a non-call-site fails immediately. It does not catch indirect-branch hijacks that never execute a ret. The iter-3a JOP PoC is exactly this case: an indirect call (blr Xn) is pivoted to a br x16 gadget which terminates in _exit(), so no ret ever fires and the shadow stack never sees the violation.
The test harness encodes this gap as a tracked case (03-jop :: attack (gap demo)) that is green when the attack succeeds — explicit, not hidden.
Closing this gap requires per-call-site CFG-edge validation; planned in notes/wiki/analyses/cs2-runtime-attacks-project.md as iter-3b.
Detector internals (one-paragraph summary)
Fork + PTRACE_TRACEME, set a one-shot BRK at main to skip the cost of single-stepping ld.so and __libc_start_main. From main onward, peek the next AArch64 instruction at PC and decode: bl/blr push the return site, ret pops and compares against the actual post-step PC, br is currently a no-op (the iter-3a gap). On mismatch: [!!! ATTACK DETECTED] to stderr, PTRACE_KILL, exit code 2.
How to run locally
nix develop --command $SHELL # enter the flake's dev shell
make build # builds detector/ + every attacks/*/
make test # runs the 6-case matrixDev shell ships gcc (hardening explicitly disabled so binaries are vulnerable by design), pwntools, capstone, pyelftools, gdb, and pwndbg.
CI
GitHub Actions workflow at .github/workflows/test.yml runs the full matrix on every release on an ubuntu-24.04-arm runner, using the same Nix dev shell as local development. Manual trigger available via workflow_dispatch.
What's next
Iter-3b: extend tools/build_cfg.py to emit static target sets for blr/br sites, extend tracer.c to validate the post-step PC of indirect branches against that set. Test case flips from green-when-PWNED to expected exit 2 with the standard [!!! ATTACK DETECTED] marker.
Beyond iter-3b: function reuse attacks, data-only attacks, non-control-data overflows.