fold/tests/integration/test_binary_rop.cpp

85 lines
3.1 KiB
C++

// 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 <catch2/catch_test_macros.hpp>
#include <cstdlib>
#include <filesystem>
#include <string>
#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<RopGadgetRaw>& 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
}