85 lines
3.1 KiB
C++
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
|
|
}
|