fold/tests/unit/test_rop_gadgets.cpp
trvon a5488f210e
feat: attach ROP/exploit gadgets to binary buffer-overflow findings
Turn a detected binary stack-overflow into actionable exploit material:
each buffer-overflow finding now carries candidate exploitation primitives
discovered from the same binary.

- rop_gadgets module (fold::binary, r2-gated): classifyGadgets() is a pure,
  unit-tested classifier over (addr, opcode-text) gadget pairs into reg-control
  / syscall / stack-pivot / control-flow classes, arch-aware (AArch64 + x86-64);
  discoverExploitPrimitives() enumerates via r2 /Rj, keeps top-N per class, and
  adds "win" targets (jumpable high-value func/stub addresses — system/execve/
  puts/printf/... — resolved by name via extractCallTargetNames).
- r2_backend: extractRopGadgets wraps r2 /Rj (flattened opcode text), bounded by
  search.maxhits to keep large static binaries fast (~0.5s on a 900KB binary).
- report: ExploitContext gains a GadgetRef list; BinaryBBObserver::onBufferCopy
  fills input_channel (the sink) + buffer_offset (recovered capacity) +
  controlled_value; scanBinaryWithPcode attaches the gadget inventory once when
  any binary-buffer-overflow finding exists (no cost on clean binaries, graceful
  if r2 is absent). Serialized in JSON (write + read round-trip) and SARIF.

Scope: supply classified primitives, not auto-assembled chains; attached only
to buffer-overflow findings (no standalone subcommand). Opt-in security tooling
for authorized CTF/pentest use.

Validated: libbof_overflow BOF findings now carry exploit_context.gadgets
(reg-control x0-setup sequences, stack-pivots, printf win target) +
input_channel/buffer_offset. Unit: test_rop_gadgets (8 cases). meson test 75/75.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 08:58:16 -04:00

68 lines
2.8 KiB
C++

// Unit tests for the ROP gadget classifier (classifyGadgets). Pure — feeds
// synthetic (addr, opcode-text) gadget pairs and asserts the assigned class,
// with no r2 dependency (mirrors the scanInstructionsFor* disasm-scanner tests).
#include <catch2/catch_test_macros.hpp>
#include <cstdint>
#include <string>
#include <utility>
#include <vector>
#include "fold/binary/rop_gadgets.hpp"
namespace {
using fold::binary::classifyGadgets;
using fold::binary::Gadget;
using fold::binary::GadgetClass;
GadgetClass classOne(const std::string& text, const std::string& arch) {
std::vector<std::pair<std::uint64_t, std::string>> raw = {{0x1000, text}};
return classifyGadgets(raw, arch).front().cls;
}
} // namespace
TEST_CASE("rop: aarch64 epilogue ldp x29,x30 is register control", "[rop]") {
REQUIRE(classOne("ldp x29, x30, [sp], 0x10 ; ret", "arm") == GadgetClass::RegControl);
REQUIRE(classOne("mov x0, x19 ; ret", "arm") == GadgetClass::RegControl);
}
TEST_CASE("rop: aarch64 svc is a syscall gadget", "[rop]") {
REQUIRE(classOne("mov x8, 0x5d ; svc 0 ; ret", "arm") == GadgetClass::Syscall);
}
TEST_CASE("rop: explicit sp write is a stack pivot", "[rop]") {
REQUIRE(classOne("add sp, sp, 0x40 ; ret", "arm") == GadgetClass::StackPivot);
REQUIRE(classOne("mov sp, x29 ; ret", "arm") == GadgetClass::StackPivot);
}
TEST_CASE("rop: aarch64 indirect branch is control flow", "[rop]") {
REQUIRE(classOne("blr x8", "arm") == GadgetClass::ControlFlow);
REQUIRE(classOne("br x16", "arm") == GadgetClass::ControlFlow);
}
TEST_CASE("rop: x86-64 pop reg is register control, syscall is syscall", "[rop]") {
REQUIRE(classOne("pop rdi ; ret", "x86") == GadgetClass::RegControl);
REQUIRE(classOne("pop rsi ; pop r15 ; ret", "x86") == GadgetClass::RegControl);
REQUIRE(classOne("syscall", "x86") == GadgetClass::Syscall);
REQUIRE(classOne("leave ; ret", "x86") == GadgetClass::StackPivot);
}
TEST_CASE("rop: a bare ret is not a useful primitive", "[rop]") {
REQUIRE(classOne("ret", "arm") == GadgetClass::Other);
REQUIRE(classOne("nop ; ret", "arm") == GadgetClass::Other);
}
TEST_CASE("rop: syscall outranks a register load in the same gadget", "[rop]") {
// mov x8 (syscall number) then svc — the svc dominates the classification.
REQUIRE(classOne("mov x8, 0xdd ; mov x0, x19 ; svc 0", "arm") == GadgetClass::Syscall);
}
TEST_CASE("rop: classifyGadgets preserves address and text", "[rop]") {
std::vector<std::pair<std::uint64_t, std::string>> raw = {{0x401234, "pop rdi ; ret"},
{0x401240, "ret"}};
auto out = classifyGadgets(raw, "x86");
REQUIRE(out.size() == 2);
REQUIRE(out[0].addr == 0x401234);
REQUIRE(out[0].text == "pop rdi ; ret");
REQUIRE(out[0].cls == GadgetClass::RegControl);
REQUIRE(out[1].cls == GadgetClass::Other);
}