// Integration coverage for ROP gadget discovery on *real* ELF binaries — the // path the unit tests (test_gadget_capstone) can only exercise on synthetic // bytes. Runs the Capstone overlapping sweep (discoverRopGadgets) over committed // static fixtures and asserts it surfaces the clean `pop reg; ret`-class gadgets // the prior r2 `/R` search missed (the discovery bottleneck this work fixed). // // Discovery is fast (r2 section dump + Capstone sweep, no `aaa`/Z3), so this is // CI-friendly. The full whole-binary BOF -> chain scan (scanBinaryWithPcode) is // minutes-long and remains a manual validation step, not an automated test. // // Only built when Capstone is enabled (the backend under test); WARN-skips when // a fixture or r2 is unavailable. #include #include #include #include #include "fold/binary/gadget_source.hpp" #include "fold/binary/r2_backend.hpp" namespace { namespace fs = std::filesystem; using namespace fold::binary; fs::path fixturesDir() { if (const char* env = std::getenv("FOLD_BINARY_FIXTURES"); env != nullptr && *env != '\0') return fs::path(env); for (const auto& cand : {fs::path("../fixtures/binary"), fs::path("tests/fixtures/binary")}) if (fs::exists(cand / "bof_x64")) return cand; return fs::path("../fixtures/binary"); } bool anyGadget(const std::vector& gs, const std::string& needle) { for (const auto& g : gs) if (g.text.find(needle) != std::string::npos) return true; return false; } } // namespace TEST_CASE("binary-rop: discovery surfaces clean pop;ret gadgets in a real x86-64 ELF", "[binary][rop][discovery]") { const auto bin = fixturesDir() / "bof_x64"; if (!fs::exists(bin)) { WARN("bof_x64 fixture missing; skipping"); return; } const auto arch = detectBinaryArch(bin.string()); if (arch.arch.empty()) { WARN("r2 unavailable / arch undetected; skipping"); return; } REQUIRE(arch.arch == "x86"); REQUIRE(arch.bits == 64); RopGadgetSearchOptions opts; // goal_regs empty -> keep all gadgets opts.max = 8192; const auto gadgets = discoverRopGadgets(bin.string(), arch, opts); REQUIRE_FALSE(gadgets.empty()); // The exact gadgets r2 `/R` truncation missed but the Capstone sweep finds. REQUIRE(anyGadget(gadgets, "pop rdi")); REQUIRE(anyGadget(gadgets, "ret")); } TEST_CASE("binary-rop: discovery finds gadgets in a real AArch64 ELF", "[binary][rop][discovery]") { const auto bin = fixturesDir() / "bof_arm64"; if (!fs::exists(bin)) { WARN("bof_arm64 fixture missing; skipping"); return; } const auto arch = detectBinaryArch(bin.string()); if (arch.arch.empty()) { WARN("r2 unavailable / arch undetected; skipping"); return; } REQUIRE(arch.arch == "arm"); REQUIRE(arch.bits == 64); RopGadgetSearchOptions opts; opts.max = 8192; const auto gadgets = discoverRopGadgets(bin.string(), arch, opts); REQUIRE_FALSE(gadgets.empty()); REQUIRE(anyGadget(gadgets, "ret")); // AArch64 gadgets are ret-terminated }