Skip to content

Releases: cooparo/runtime-attacks

v0.1.0 — Shadow-stack detector + 3 attack PoCs

11 May 10:47

Choose a tag to compare

First tagged release of the AAU runtime-attacks research project.

What this is

A research repo with three things:

  1. A runtime attack detector (detector/tracer) — a user-space shadow call stack on AArch64 Linux, built on ptrace. It enforces the call/return contract: every ret must return to the address recorded by its matching bl/blr. Any deviation is flagged as [!!! ATTACK DETECTED] and the tracee is killed.
  2. Three attack PoCs — small vulnerable C programs paired with auto-generating pwntools-based exploits, used to exercise the detector and document its coverage.
  3. 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 matrix

Dev 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.