| """ |
| dataset_builder.py β Build a labelled gadget dataset from exploit scripts. |
| |
| Usage |
| ----- |
| from lcsajdump.ml.dataset_builder import build_dataset, THESIS_SAMPLES |
| X, y, groups, meta = build_dataset(THESIS_SAMPLES) |
| |
| # Or from the command line: |
| python -m lcsajdump.ml.dataset_builder --out dataset.csv |
| |
| Positive label (y=1): gadget address was used in an exploit script. |
| Negative label (y=0): gadget was found by lcsajdump but not used. |
| |
| Groups: per-binary group sizes for LambdaRank (LightGBM/XGBoost). |
| """ |
|
|
| from __future__ import annotations |
|
|
| import ast |
| import json |
| import os |
| import re |
| import subprocess |
| import sys |
| from pathlib import Path |
| from typing import Optional |
|
|
| from lcsajdump.ml.features import ( |
| extract_features, |
| FEATURE_NAMES, |
| FEATURE_NAMES_WITH_LM, |
| ARCH_PROFILES, |
| ) |
|
|
| |
|
|
| _HEX_RE = re.compile(r"\b(0x[0-9a-fA-F]{3,12})\b") |
|
|
| |
| _ELF_SYM_RE = re.compile( |
| r'elf\.(?:symbols?|sym)\s*\[[\'"]([\w]+)[\'"]\]', |
| re.IGNORECASE, |
| ) |
|
|
| |
| _EXE_SYM_RE = re.compile( |
| r'(?:exe|binary)\.(?:symbols?|sym)\s*\[[\'"]([\w]+)[\'"]\]', |
| re.IGNORECASE, |
| ) |
|
|
|
|
| def _resolve_elf_symbols(binary_path: str, symbol_names: set) -> dict[str, int]: |
| """ |
| Resolve ELF symbol names to addresses using pyelftools. |
| Returns {name: address} for found symbols. |
| """ |
| if not symbol_names: |
| return {} |
| try: |
| from elftools.elf.elffile import ELFFile |
|
|
| result = {} |
| with open(binary_path, "rb") as f: |
| elf = ELFFile(f) |
| symtab = elf.get_section_by_name(".symtab") or elf.get_section_by_name( |
| ".dynsym" |
| ) |
| if symtab: |
| for sym in symtab.iter_symbols(): |
| if sym.name in symbol_names and sym["st_value"]: |
| result[sym.name] = sym["st_value"] |
| return result |
| except Exception as e: |
| print( |
| f"[dataset_builder] ELF symbol resolution failed for {binary_path}: {e}", |
| file=sys.stderr, |
| ) |
| return {} |
|
|
|
|
| def extract_gadget_addresses(exploit_path: str, binary_path: str) -> set[int]: |
| """ |
| Parse an exploit script and return the set of gadget addresses it uses. |
| |
| Handles: |
| - Direct hex literals: pivot = 0x40100f |
| - ELF symbol references: elf.symbols['gadget_set_x0'] / exe.sym['name'] |
| - pwntools flat({offset: addr, ...}) patterns |
| """ |
| source = Path(exploit_path).read_text(errors="replace") |
|
|
| addresses: set[int] = set() |
|
|
| |
| for m in _HEX_RE.finditer(source): |
| try: |
| addresses.add(int(m.group(1), 16)) |
| except ValueError: |
| pass |
|
|
| |
| syms = set() |
| for m in _ELF_SYM_RE.finditer(source): |
| syms.add(m.group(1)) |
| for m in _EXE_SYM_RE.finditer(source): |
| syms.add(m.group(1)) |
|
|
| if syms and binary_path and os.path.exists(binary_path): |
| resolved = _resolve_elf_symbols(binary_path, syms) |
| addresses.update(resolved.values()) |
|
|
| return addresses |
|
|
|
|
| |
|
|
|
|
| def _run_lcsajdump( |
| binary_path: str, |
| arch: str = "auto", |
| depth: int = 20, |
| darkness: int = 5, |
| instructions: int = 15, |
| all_exec: bool = False, |
| ) -> dict: |
| """Run lcsajdump CLI and return parsed JSON output.""" |
| cmd = [ |
| sys.executable, |
| "-m", |
| "lcsajdump.cli", |
| binary_path, |
| "--json", |
| "--limit", |
| "999999", |
| "--depth", |
| str(depth), |
| "--darkness", |
| str(darkness), |
| "--instructions", |
| str(instructions), |
| "--algo", |
| ] |
| if arch != "auto": |
| cmd += ["--arch", arch] |
| if all_exec: |
| cmd += ["--all-exec"] |
|
|
| result = subprocess.run(cmd, capture_output=True, text=True) |
| if result.returncode != 0: |
| raise RuntimeError( |
| f"lcsajdump CLI failed (exit {result.returncode}) for {binary_path}:\n" |
| f"{result.stderr[-1000:]}" |
| ) |
|
|
| stdout = result.stdout |
| start = stdout.find("{") |
| if start == -1: |
| raise RuntimeError(f"No JSON in lcsajdump output for {binary_path}") |
| return json.loads(stdout[start:]) |
|
|
|
|
| |
|
|
|
|
| def _gadgets_from_json(data: dict) -> list[dict]: |
| """Flatten sequential + jump_based entries from lcsajdump JSON.""" |
| gadgets = [] |
| for gtype_key, gtype_label in ( |
| ("sequential", "Sequential"), |
| ("jump_based", "Jump-Based"), |
| ): |
| for entry in data.get(gtype_key, []): |
| gadgets.append( |
| { |
| "address": int(entry["primary_address"], 16), |
| "duplicate_addresses": [ |
| int(a, 16) for a in entry.get("duplicate_addresses", []) |
| ], |
| "type": gtype_label, |
| "score": entry.get("score", 0), |
| "instructions": entry.get("instructions", []), |
| } |
| ) |
| return gadgets |
|
|
|
|
| def _gadget_text(g: dict) -> str: |
| """Flat instruction text for a gadget (for pattern matching).""" |
| return " ; ".join( |
| (i.get("mnemonic", "") + " " + i.get("op_str", "")).strip() |
| for i in g.get("instructions", []) |
| ).lower() |
|
|
|
|
| def _matches_any_pattern(gadget_text: str, patterns: list) -> bool: |
| """Return True if gadget_text matches at least one compiled regex pattern.""" |
| for pat in patterns: |
| if isinstance(pat, str): |
| if pat.lower() in gadget_text: |
| return True |
| else: |
| if pat.search(gadget_text): |
| return True |
| return False |
|
|
|
|
| def _build_rows_for_binary( |
| binary_path: str, |
| arch: str, |
| exploit_paths: list[str], |
| lcsaj_json: Optional[dict] = None, |
| depth: int = 20, |
| darkness: int = 5, |
| instructions_limit: int = 15, |
| all_exec: bool = False, |
| patterns: list = None, |
| verbose: bool = True, |
| lm=None, |
| max_gadgets: Optional[int] = None, |
| ) -> list[dict]: |
| """ |
| Build one row per gadget for a single binary. |
| |
| Positive labels come from two complementary sources (OR-combined): |
| 1. exploit_paths β hex addresses / ELF symbols extracted from real exploit scripts |
| 2. patterns β list of strings or re.Pattern objects; any gadget whose |
| instruction text matches is labelled positive. |
| Use this for ROP Emporium / challenge-specific canonical gadgets. |
| |
| Returns a list of dicts with keys = FEATURE_NAMES + ['label', 'binary', 'address', 'arch']. |
| """ |
| if verbose: |
| print( |
| f"[dataset_builder] Processing {os.path.basename(binary_path)} ({arch})..." |
| ) |
|
|
| |
| if lcsaj_json is None: |
| lcsaj_json = _run_lcsajdump( |
| binary_path, |
| arch=arch, |
| depth=depth, |
| darkness=darkness, |
| instructions=instructions_limit, |
| all_exec=all_exec, |
| ) |
|
|
| gadgets = _gadgets_from_json(lcsaj_json) |
| if not gadgets: |
| print( |
| f"[dataset_builder] WARNING: no gadgets found for {binary_path}", |
| file=sys.stderr, |
| ) |
| return [] |
|
|
| |
| all_lcsaj_addrs: set[int] = set() |
| for g in gadgets: |
| all_lcsaj_addrs.add(g["address"]) |
| all_lcsaj_addrs.update(g["duplicate_addresses"]) |
|
|
| |
| used_addrs: set[int] = set() |
| for exp_path in exploit_paths: |
| if not os.path.exists(exp_path): |
| continue |
| found = extract_gadget_addresses(exp_path, binary_path) |
| hits = found & all_lcsaj_addrs |
| if verbose: |
| print( |
| f" {os.path.basename(exp_path)}: " |
| f"{len(found)} hex literals β {len(hits)} match lcsajdump gadgets" |
| ) |
| used_addrs |= hits |
|
|
| |
| pattern_matched = 0 |
| compiled_patterns = patterns or [] |
|
|
| |
| |
| rows = [] |
| for g in gadgets: |
| g_size = sum(i.get("size", 4) for i in g.get("instructions", [])) if g.get("instructions") else 15 |
|
|
| feats = extract_features( |
| instructions=g["instructions"], |
| arch=arch, |
| gadget_type=g["type"], |
| heuristic_score=g["score"], |
| address=g["address"], |
| gadget_pool=all_lcsaj_addrs, |
| lm=lm, |
| binary_path=binary_path, |
| gadget_size=g_size |
| ) |
| by_addr = g["address"] in used_addrs or any( |
| a in used_addrs for a in g["duplicate_addresses"] |
| ) |
| by_pattern = bool( |
| compiled_patterns |
| and _matches_any_pattern(_gadget_text(g), compiled_patterns) |
| ) |
| label = 1 if (by_addr or by_pattern) else 0 |
| if by_pattern and not by_addr: |
| pattern_matched += 1 |
|
|
| row = dict(feats) |
| row["label"] = label |
| |
| _parent = os.path.basename(os.path.dirname(binary_path)) |
| _base = os.path.basename(binary_path) |
| row["binary"] = f"{_parent}/{_base}" if _parent else _base |
| row["address"] = g["address"] |
| row["arch"] = arch |
| |
| row["pattern_match"] = int(by_pattern and not by_addr) |
| row["exploit_verified"] = int(by_addr) |
| rows.append(row) |
|
|
| |
| |
| |
| if max_gadgets is not None and len(rows) > max_gadgets: |
| import random |
|
|
| pos_rows = [r for r in rows if r["label"] == 1] |
| neg_rows = [r for r in rows if r["label"] == 0] |
| rng = random.Random(42) |
|
|
| |
| n_pos_target = max(1, min(len(pos_rows), int(max_gadgets * 0.40))) |
| n_neg_target = max_gadgets - n_pos_target |
| n_neg_target = max(1, min(len(neg_rows), n_neg_target)) |
|
|
| |
| |
| exploit_rows = [r for r in pos_rows if r.get("exploit_verified", 0)] |
| pattern_rows = [r for r in pos_rows if not r.get("exploit_verified", 0)] |
|
|
| |
| n_exploit = len(exploit_rows) |
| n_pattern_target = max(0, n_pos_target - n_exploit) |
|
|
| if len(pattern_rows) > n_pattern_target: |
| pattern_rows.sort(key=lambda r: -r.get("heuristic_score", 0)) |
| pattern_rows = pattern_rows[:n_pattern_target] |
|
|
| pos_rows = exploit_rows + pattern_rows |
|
|
| neg_rows = ( |
| rng.sample(neg_rows, n_neg_target) |
| if len(neg_rows) > n_neg_target |
| else neg_rows |
| ) |
| rows = pos_rows + neg_rows |
| rng.shuffle(rows) |
|
|
| |
| |
| |
| if rows: |
| ret_count = sum(1 for r in rows if r.get("is_ret_terminated", 0)) |
| majority_ret = 1 if ret_count / len(rows) > 0.5 else 0 |
| for r in rows: |
| r["majority_term_is_ret"] = majority_ret |
|
|
| pos = sum(r["label"] for r in rows) |
| if verbose: |
| src = f" ({pattern_matched} from patterns)" if pattern_matched else "" |
| print( |
| f" β {len(rows)} gadgets, {pos} positive ({pos / max(len(rows), 1) * 100:.1f}%){src}" |
| ) |
|
|
| return rows |
|
|
|
|
| |
|
|
|
|
| def build_dataset( |
| samples: list[dict], |
| verbose: bool = True, |
| lm=None, |
| ) -> tuple: |
| """ |
| Build the full dataset from a list of sample descriptors. |
| |
| Parameters |
| ---------- |
| samples : list of dict, each with keys: |
| 'binary' : str β path to the ELF binary |
| 'arch' : str β 'x86_64', 'arm64', or 'riscv64' |
| 'exploits' : list[str] β paths to exploit scripts |
| 'depth' : int (optional) β lcsajdump depth, default 20 |
| 'darkness' : int (optional) β lcsajdump darkness, default 5 |
| 'json_cache': str (optional) β path to pre-computed lcsajdump JSON |
| lm : InstructionLM, optional |
| If provided, appends LM embedding features (lm_emb_0β¦15) to each row. |
| |
| Returns |
| ------- |
| X : list[dict] β feature rows (use pd.DataFrame(X) for training) |
| y : list[int] β binary labels (1 = used in exploit, 0 = not used) |
| groups : list[int] β group sizes for LambdaRank (one per binary) |
| meta : list[dict] β {binary, arch, address} per row (for diagnostics) |
| """ |
| all_rows: list[dict] = [] |
| groups: list[int] = [] |
|
|
| for sample in samples: |
| binary = sample["binary"] |
| arch = sample["arch"] |
| exploits = sample.get("exploits", []) |
|
|
| |
| _arch_sp = ( |
| ARCH_PROFILES.get(arch, {}).get("search_params", {}) |
| if ARCH_PROFILES |
| else {} |
| ) |
| depth = sample.get("depth", _arch_sp.get("d", 20)) |
| darkness = sample.get("darkness", _arch_sp.get("darkness", 5)) |
| instr = sample.get("instructions", _arch_sp.get("i", 100)) |
|
|
| json_cache = sample.get("json_cache") |
| all_exec = sample.get("all_exec", False) |
| lcsaj_json = None |
| if json_cache and os.path.exists(json_cache): |
| with open(json_cache) as f: |
| lcsaj_json = json.load(f) |
| if verbose: |
| print(f"[dataset_builder] Using cached JSON: {json_cache}") |
|
|
| rows = _build_rows_for_binary( |
| binary_path=binary, |
| arch=arch, |
| exploit_paths=exploits, |
| lcsaj_json=lcsaj_json, |
| depth=depth, |
| darkness=darkness, |
| instructions_limit=instr, |
| all_exec=all_exec, |
| patterns=sample.get("patterns"), |
| verbose=verbose, |
| lm=lm, |
| max_gadgets=sample.get("max_gadgets"), |
| ) |
| if rows: |
| all_rows.extend(rows) |
| groups.append(len(rows)) |
|
|
| feat_names = FEATURE_NAMES_WITH_LM if lm is not None else FEATURE_NAMES |
| X = [{k: r[k] for k in feat_names} for r in all_rows] |
| y = [r["label"] for r in all_rows] |
| meta = [ |
| { |
| "binary": r["binary"], |
| "arch": r["arch"], |
| "address": r["address"], |
| "binary_id": r["arch"] + "::" + r["binary"], |
| } |
| for r in all_rows |
| ] |
|
|
| if verbose: |
| total_pos = sum(y) |
| print( |
| f"\n[dataset_builder] Total: {len(X)} samples, " |
| f"{total_pos} positive ({total_pos / max(len(X), 1) * 100:.1f}%), " |
| f"{len(groups)} groups [features={len(feat_names)}]" |
| ) |
|
|
| return X, y, groups, meta |
|
|
|
|
| def save_csv(X: list[dict], y: list[int], meta: list[dict], path: str): |
| """Write the dataset to a CSV file for inspection.""" |
| import csv |
|
|
| rows = [] |
| for feat, label, m in zip(X, y, meta): |
| row = { |
| "binary_id": m["binary_id"], |
| "binary": m["binary"], |
| "arch": m["arch"], |
| "address": hex(m["address"]), |
| "label": label, |
| |
| "exploit_verified": feat.get("exploit_verified", 0), |
| "pattern_match": feat.get("pattern_match", 0), |
| } |
| row.update(feat) |
| rows.append(row) |
|
|
| |
| feat_keys = list(X[0].keys()) if X else FEATURE_NAMES |
| fieldnames = [ |
| "binary_id", |
| "binary", |
| "arch", |
| "address", |
| "label", |
| "exploit_verified", |
| "pattern_match", |
| ] + feat_keys |
| with open(path, "w", newline="") as f: |
| writer = csv.DictWriter(f, fieldnames=fieldnames) |
| writer.writeheader() |
| writer.writerows(rows) |
| print(f"[dataset_builder] Saved {len(rows)} rows to {path}") |
|
|
|
|
| |
| |
|
|
| _THESIS = Path("/home/chris/thesis_ctfs/src_ctf") |
|
|
| |
| _ARM64_PATTERNS = [ |
| |
| "ldp x19, x20, [sp", |
| "ldp x21, x22, [sp", |
| "ldp x23, x24, [sp", |
| "ldp x25, x26, [sp", |
| "ldp x29, x30, [sp", |
| "ldr x30, [sp", |
| "ldr x29, [sp", |
| |
| "ldr x0, [sp", |
| "ldr x1, [sp", |
| "ldr x19, [sp", |
| |
| "blr x0", |
| "blr x1", |
| "blr x2", |
| "blr x3", |
| "blr x19", |
| "blr x20", |
| "blr x21", |
| "blr x22", |
| "br x0", |
| "br x1", |
| "br x2", |
| "br x3", |
| |
| "ldr x3, [x20", |
| "ldr x0, [x19", |
| |
| "svc #0", |
| ] |
|
|
| |
| |
| |
| |
| import re as _re |
|
|
| _RISCV_PATTERNS = [ |
| |
| |
| _re.compile(r"\bld\s+ra,\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| _re.compile(r"\blw\s+ra,\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| |
| _re.compile(r"\bc\.ldsp\s+ra,\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| _re.compile(r"\bc\.lwsp\s+ra,\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| _re.compile(r"\bc\.ldsp\s+a[0-7],\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| _re.compile(r"\bc\.lwsp\s+a[0-7],\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| |
| _re.compile(r"\bld\s+a[0-7],\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| _re.compile(r"\blw\s+a[0-7],\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| |
| _re.compile(r"\bld\s+a7,\s*(?:0x)?[0-9a-f]+\(sp\)", _re.IGNORECASE), |
| |
| "jr ra", |
| "jalr zero, 0(ra)", |
| _re.compile(r"\bjalr\s+zero,\s*\d+\(ra\)"), |
| |
| "ecall", |
| |
| _re.compile(r"\bli\s+a7,\s*\d+"), |
| _re.compile(r"\bli\s+a0,\s*\d+"), |
| ] |
|
|
| |
| |
| _X86_COMMON_THESIS = [ |
| "pop rdi", |
| "pop rsi", |
| "pop rdx", |
| "pop rax", |
| "pop rbx", |
| "pop rcx", |
| "syscall", |
| ] |
|
|
| |
| |
| _JOP_PATTERNS = [ |
| "jmp qword ptr [rdi", |
| "jmp qword ptr [rcx", |
| "jmp qword ptr [rdx", |
| "jmp qword ptr [rax", |
| "jmp qword ptr [rsp", |
| "jmp qword ptr [rbx", |
| "jmp qword ptr [rsi", |
| ] |
|
|
| |
| |
| _JOP_X86_PATTERNS = [ |
| |
| "jmp qword ptr [rdi", |
| "jmp qword ptr [rcx", |
| "jmp qword ptr [rdx", |
| "jmp qword ptr [rax", |
| "jmp qword ptr [rbx", |
| "jmp qword ptr [rsp", |
| "jmp qword ptr [rsi", |
| "jmp qword ptr [r8", |
| "jmp qword ptr [r9", |
| "jmp qword ptr [r10", |
| "jmp qword ptr [r11", |
| |
| "jmp qword ptr [rdi + rax*8", |
| "jmp qword ptr [rdx + rcx*8", |
| "jmp qword ptr [rdi + rsi*8", |
| "jmp qword ptr [rax + rcx*8", |
| |
| "call qword ptr [rdi", |
| "call qword ptr [rsi", |
| "call qword ptr [rbx", |
| "call qword ptr [rcx", |
| "call qword ptr [r12", |
| "call qword ptr [r13", |
| "call qword ptr [rax", |
| "call qword ptr [r8", |
| "call qword ptr [r9", |
| |
| "call qword ptr [r12 + rbx*8", |
| "call qword ptr [rbx + rbp*8", |
| "call qword ptr [rdi + rax*8", |
| "call qword ptr [rsi + rbx*8", |
| "call qword ptr [rax + rbx*8", |
| "call qword ptr [rax + rcx*8", |
| ] |
|
|
| ONLINE_CTF_SAMPLES = [ |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/mra/mra", |
| "arch": "arm64", |
| "exploits": [ |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/mra/exploit.py" |
| ], |
| "patterns": _ARM64_PATTERNS, |
| }, |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/baby_arm/pwn", |
| "arch": "arm64", |
| "exploits": [ |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/baby_arm/solve.py" |
| ], |
| "patterns": _ARM64_PATTERNS, |
| }, |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/easy_pwn/02-overwrite-ret", |
| "arch": "arm64", |
| "exploits": [ |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/easy_pwn/02-overwrite-ret.py" |
| ], |
| "patterns": _ARM64_PATTERNS, |
| }, |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/easy_pwn/06-system-rop", |
| "arch": "arm64", |
| "exploits": [ |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/easy_pwn/06-system-rop.py" |
| ], |
| "patterns": _ARM64_PATTERNS, |
| }, |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/easy_pwn/07-execve-rop", |
| "arch": "arm64", |
| "exploits": [ |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/arm64_extra/easy_pwn/07-execve-rop.py" |
| ], |
| "patterns": _ARM64_PATTERNS, |
| }, |
| ] |
|
|
| |
| |
| |
| |
| |
|
|
| _ROPE = Path("/home/chris/Desktop/lcsajdump-debug/rop_emporium_bins") |
|
|
| |
| _X86_COMMON = [ |
| "pop rdi", |
| "pop rsi", |
| "pop rdx", |
| "pop rax", |
| "pop rbx ; pop rbp", |
| ] |
|
|
| |
| _X86_PIVOT = [ |
| "xchg rax, rsp", |
| "xchg rsp, rax", |
| "xchg rbx, rsp", |
| "xchg rsp, rbx", |
| "mov rsp, rax", |
| "mov rsp, rbx", |
| "mov rsp, rcx", |
| "mov rsp, rdx", |
| "leave", |
| "pop rsp", |
| "add rsp,", |
| "sub rsp,", |
| ] |
|
|
| |
|
|
| _X86_32_COMMON = [ |
| "pop eax", |
| "pop ebx", |
| "pop ecx", |
| "pop edx", |
| "pop ebp", |
| "pop edi", |
| "int 0x80", |
| "ret", |
| ] |
|
|
| _X86_32_PIVOT = [ |
| "xchg eax, esp", |
| "xchg esp, eax", |
| "xchg ebx, esp", |
| "xchg esp, ebx", |
| "mov esp, eax", |
| "mov esp, ebx", |
| "mov esp, ecx", |
| "mov esp, edx", |
| "leave", |
| "pop esp", |
| "add esp,", |
| "sub esp,", |
| ] |
|
|
| ROP_EMPORIUM_SAMPLES = [ |
| |
| |
| { |
| "binary": str(_ROPE / "ret2win/ret2win"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "ret2win/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + ["ret2win"], |
| }, |
| { |
| "binary": str(_ROPE / "split/split"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "split/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON, |
| }, |
| { |
| "binary": str(_ROPE / "callme/callme"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "callme/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + ["pop rdi ; pop rsi ; pop rdx"], |
| }, |
| { |
| "binary": str(_ROPE / "write4/write4"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "write4/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + ["pop r14 ; pop r15", "mov qword ptr [r14], r15"], |
| }, |
| { |
| "binary": str(_ROPE / "badchars/badchars"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "badchars/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + ["pop r12 ; pop r13", "mov qword ptr [r13], r12", "xor byte ptr [r15], r14b"], |
| }, |
| { |
| "binary": str(_ROPE / "fluff/fluff"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "fluff/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + ["xlatb", "bextr", "stosb", "pop rdx ; pop rcx"], |
| }, |
| { |
| "binary": str(_ROPE / "pivot/pivot"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "pivot/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + _X86_PIVOT + ["pop rax", "xchg rsp, rax", "mov rax, qword ptr [rax]", "add rax, rbp"], |
| }, |
| { |
| "binary": str(_ROPE / "ret2csu/ret2csu"), |
| "arch": "x86_64", |
| "exploits": [str(_ROPE / "ret2csu/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_COMMON + [ |
| "pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15", |
| "mov rdx, r15", |
| "mov rsi, r14", |
| "call qword ptr [r12", |
| ], |
| }, |
| |
| { |
| "binary": str(_ROPE / "ret2win32/ret2win32"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "ret2win32/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON + ["ret2win"], |
| }, |
| { |
| "binary": str(_ROPE / "split32/split32"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "split32/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON, |
| }, |
| { |
| "binary": str(_ROPE / "callme32/callme32"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "callme32/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON + ["pop ebx ; pop esi ; pop edi ; pop ebp"], |
| }, |
| { |
| "binary": str(_ROPE / "write432/write432"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "write432/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON + ["mov dword ptr [edi], ebp", "pop edi ; pop ebp"], |
| }, |
| { |
| "binary": str(_ROPE / "badchars32/badchars32"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "badchars32/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON + ["mov dword ptr [edi], esi", "xor byte ptr [ebp", "pop ebx ; pop esi ; pop edi ; pop ebp"], |
| }, |
| { |
| "binary": str(_ROPE / "fluff32/fluff32"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "fluff32/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON + ["pext", "xchg byte ptr [ecx], dl", "bswap ecx", "pop ecx"], |
| }, |
| { |
| "binary": str(_ROPE / "pivot32/pivot32"), |
| "arch": "x86_32", |
| "exploits": [str(_ROPE / "pivot32/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_32_COMMON + _X86_32_PIVOT + ["pop eax", "xchg esp, eax", "mov eax, dword ptr [eax]", "add eax, ebx"], |
| }, |
| ] |
|
|
| |
| |
| |
| |
|
|
| _CTFTIME = Path("/home/chris/Desktop/lcsajdump-debug/ctf_binaries") |
|
|
| |
| _X86_HEAP_COMMON = [ |
| "pop rdi", |
| "pop rsi", |
| "pop rdx", |
| "pop rax", |
| "pop rbp", |
| "ret", |
| "syscall", |
| "int 0x80", |
| ] |
|
|
| |
| |
| CTFTIME_SAMPLES = [ |
| |
| { |
| "binary": str(_CTFTIME / "lactf2025/eepy/vuln"), |
| "arch": "x86_64", |
| "exploits": [str(_CTFTIME / "lactf2025/eepy/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_HEAP_COMMON + _X86_COMMON + _X86_PIVOT, |
| }, |
| |
| { |
| "binary": str(_CTFTIME / "lactf2025/minceraft/chall"), |
| "arch": "x86_64", |
| "exploits": [str(_CTFTIME / "lactf2025/minceraft/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_HEAP_COMMON + _X86_COMMON, |
| }, |
| |
| { |
| "binary": str(_CTFTIME / "lactf2025/state-change/chall"), |
| "arch": "x86_64", |
| "exploits": [str(_CTFTIME / "lactf2025/state-change/solve.py")], |
| "all_exec": True, |
| "patterns": _X86_HEAP_COMMON + _X86_COMMON + _X86_PIVOT, |
| }, |
| ] |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| _JOP_COP_DIR = Path( |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/pivot_jop_cop/bins" |
| ) |
|
|
| |
| _ARM64_JOP_PATTERNS = _ARM64_PATTERNS + [ |
| "blr x8", |
| "blr x9", |
| "blr x10", |
| "blr x11", |
| "blr x16", |
| "blr x17", |
| "br x8", |
| "br x9", |
| "br x16", |
| "br x17", |
| "ldr x9, [x0]", |
| "ldr x9, [x1]", |
| "ldr x16, [x0]", |
| "ldr x17, [x0]", |
| ] |
|
|
| |
| _RISCV_JOP_PATTERNS = list(_RISCV_PATTERNS) + [ |
| "jr a0", |
| "jr a1", |
| "jr a2", |
| "jr a3", |
| "jr t0", |
| "jr t1", |
| "jr t2", |
| "jr s1", |
| "jr s2", |
| "c.jr a", |
| "jalr zero, 0(a0)", |
| "jalr zero, 0(a1)", |
| "jalr zero, 0(a2)", |
| "jalr zero, 0(a3)", |
| "jalr ra, 0(a0)", |
| "jalr ra, 0(a1)", |
| "jalr ra, 0(t0)", |
| "jalr ra, 0(t1)", |
| ] |
|
|
| JOP_COP_SAMPLES = [ |
| |
| { |
| "binary": str(_JOP_COP_DIR / "x86_64/jop_dispatch"), |
| "arch": "x86_64", |
| "exploits": [], |
| "patterns": _JOP_X86_PATTERNS + _X86_COMMON, |
| "max_gadgets": 600, |
| }, |
| |
| { |
| "binary": str(_JOP_COP_DIR / "x86_64/jop_vtable"), |
| "arch": "x86_64", |
| "exploits": [], |
| "patterns": _JOP_X86_PATTERNS + _X86_COMMON, |
| "max_gadgets": 600, |
| }, |
| |
| { |
| "binary": str(_JOP_COP_DIR / "x86_64/cop_callback"), |
| "arch": "x86_64", |
| "exploits": [], |
| "patterns": _JOP_X86_PATTERNS + _X86_COMMON, |
| "max_gadgets": 600, |
| }, |
| |
| { |
| "binary": str(_JOP_COP_DIR / "x86_64/cop_ret2csu_style"), |
| "arch": "x86_64", |
| "exploits": [], |
| "patterns": _JOP_X86_PATTERNS + _X86_COMMON, |
| "max_gadgets": 600, |
| }, |
| |
| { |
| "binary": str(_JOP_COP_DIR / "arm64/jop_arm64"), |
| "arch": "arm64", |
| "exploits": [], |
| "all_exec": True, |
| "patterns": _ARM64_JOP_PATTERNS, |
| "max_gadgets": 600, |
| }, |
| |
| { |
| "binary": str(_JOP_COP_DIR / "riscv64/jop_riscv"), |
| "arch": "riscv64", |
| "exploits": [], |
| "all_exec": True, |
| "patterns": _RISCV_JOP_PATTERNS, |
| "max_gadgets": 600, |
| }, |
| ] |
|
|
| |
| |
|
|
| TESTCTF_SAMPLES = [ |
| |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/testCTFs/rop/vuln", |
| "arch": "riscv64", |
| "exploits": ["/home/chris/Desktop/lcsajdump-debug/testCTFs/rop/exploit.py"], |
| "patterns": _RISCV_JOP_PATTERNS, |
| "max_gadgets": 400, |
| }, |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/testCTFs/rop2/vuln2", |
| "arch": "riscv64", |
| "exploits": ["/home/chris/Desktop/lcsajdump-debug/testCTFs/rop2/exploit.py"], |
| "patterns": _RISCV_JOP_PATTERNS, |
| "max_gadgets": 400, |
| }, |
| |
| |
| ] |
|
|
| REAL_JOP_COP_SAMPLES = [ |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/dplastico/pwnday01/juujuu/juujuu", |
| "arch": "x86_64", |
| "exploits": [ |
| "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/dplastico/pwnday01/juujuu/xpl.py" |
| ], |
| "patterns": _JOP_X86_PATTERNS + _X86_COMMON, |
| "max_gadgets": 600, |
| }, |
| |
| { |
| "binary": "/home/chris/Desktop/lcsajdump-debug/ctf_binaries/bkerler/exploit_me/bin/exploit64", |
| "arch": "arm64", |
| "exploits": [], |
| "patterns": _ARM64_JOP_PATTERNS, |
| "max_gadgets": 600, |
| }, |
| ] |
|
|
| |
| |
| ALL_SAMPLES = ( |
| ONLINE_CTF_SAMPLES |
| + ROP_EMPORIUM_SAMPLES |
| + CTFTIME_SAMPLES |
| + TESTCTF_SAMPLES |
| + REAL_JOP_COP_SAMPLES |
| ) |
|
|
|
|
| |
|
|
| if __name__ == "__main__": |
| import argparse |
|
|
| parser = argparse.ArgumentParser(description="Build lcsajdump gadget ML dataset") |
| parser.add_argument( |
| "--out", |
| default="gadget_dataset.csv", |
| help="Output CSV path (default: gadget_dataset.csv)", |
| ) |
| parser.add_argument("--quiet", action="store_true") |
| args = parser.parse_args() |
|
|
| X, y, groups, meta = build_dataset(ALL_SAMPLES, verbose=not args.quiet) |
| save_csv(X, y, meta, args.out) |
|
|