SuperAPI AI Directory

re-ebpf

Security & auditing

View repository

Updated 10-15 21:35

About this skill

eBPF program reverse engineering and adversarial analysis (BPF-64, progs/maps, xlated)

When to use / when not to use

  • Use: program analysis at the level of the BPF-64 instruction set (8-byte fixed-length instructions, r0-r10 registers, lddw/alu32/helper calling conventions)
  • Use: three source forms — BCC (compiled at runtime, often only the kernel-side prog remains), libbpf (conventional BPF ELF, complete with .maps/.BTF), hand-written bytecode (bpf_insn arrays/precompiled blobs, no metadata)
  • Use: three purposes — trace forensics (recovering functionality of bpftrace/bcc programs), malicious eBPF (kernel residency/loading chain/evasion techniques), EDR countermeasures (bpf hook identification and countering)
  • Use: progs/maps association recovery, hook point (kprobe/tracepoint/cgroup/xdp/tc/fentry) identification, semantic mapping from helper call numbers to kernel APIs
  • Do not use: classic kernel modules (.ko drivers) — use [[re-kernel]]
  • Do not use: traditional cBPF such as seccomp/socket filters (classic BPF, instruction set differs from BPF-64, not covered by this skill)
  • Do not use: general scenarios where only the host loader binary needs to be analyzed ([[re-binary-core]]); on-chain BPF bytecode (Solana-like) has a different semantic table, use [[re-blockchain]]
  • Note: helper IDs are stable numbers of the Linux BPF UAPI (already BPF_FUNC_* values are not renumbered with kernel versions), version differences affect whether a helper exists, whether it is allowed for a specific prog_type/context, and kernel structure layout; keep the analysis environment kernel version as consistent as possible with the sample's target version (see Pitfall 3)

Tool preparation

Verify all tools before use. Loading/running BPF programs into the kernel requires root (or CAP_BPF/CAP_SYS_ADMIN); dynamic analysis of malicious samples (kernel residency/evasion types) is by default performed in an isolated virtual machine + snapshot ([[re-sandbox]], [[re-analyze/platform-tips]] highest principle); read-only queries only (prog list/dump xlated) are lower risk, but it is likewise recommended to verify kernel version consistency in a controllable environment.

bpftool —— core tool (prog/map querying and xlated disassembly)

  • Debian/Ubuntu: apt install bpftool(there is a standalone package since Debian bullseye; Ubuntu 19.10+ can also install linux-tools-common + linux-tools-generic——the former is just a wrapper, the real binary is in the linux-tools-$(uname -r) kernel companion package); older Debian (buster and earlier) has no standalone package, compile tools/bpf/bpftool from the kernel source or clone the libbpf/bpftool repository to build
  • Fedora: dnf install bpftool(official repository); Arch: pacman -S bpftool
  • Windows: no Linux kernel eBPF runtime environment (Microsoft ebpf-for-windows is an independent implementation, not a Linux kernel form); install according to the distribution inside WSL2 (Linux kernel); macOS: no kernel eBPF support, use a Linux virtual machine
  • Verify: bpftool version; add to query-type commands sudo(bpftool prog list、bpftool prog dump xlated id N、bpftool feature probe、bpftool map show/dump id N、bpftool net show、bpftool btf dump file /sys/kernel/btf/vmlinux)

llvm-objdump —— statically disassemble BPF ELF

  • Debian/Ubuntu: apt install llvm;Fedora: dnf install llvm;Arch: pacman -S llvm;macOS: brew install llvm(llvm-objdump is in /opt/homebrew/opt/llvm/bin/)
  • Verify: llvm-objdump --version; disassemble: llvm-objdump -d prog.o(ELF e_machine=EM_BPF(247) is automatically recognized, no need to specify the target); GNU objdump (binutils 2.29+ with bpf target) can also be used as an alternative
  • Note: static disassembly is the source-compiled product and differs from the execution form in the kernel (xlated) (see Pitfall 2)

bpftrace — trace forensics reconstruction (for cross-referencing when reconstructing the analyzed bpftrace/bcc program)

  • Debian/Ubuntu: apt install bpftrace;Fedora: dnf install bpftrace;Arch: pacman -S bpftrace
  • Verification: bpftrace --version;bpftrace -d -e 'probe { ... }' Output the BPF instructions it generates, used for cross-referencing the xlated dump to reconstruct script behavior

pyelftools — ELF section location scripting

  • pip install pyelftools(cross-platform)
  • Verification: python3 -c "import elftools; print(elftools.__version__)"; used to batch-extract the .text/.maps/.BTF/.rel* sections of BPF ELF

Ghidra — eBPF processor (built in since 10.3)

  • Ghidra has officially included an eBPF processor since 10.3 (the community eBPF-for-Ghidra extension has been merged upstream; older Ghidra versions need that extension installed), supporting lddw/ALU32/JMP32/BPF_ATOMIC, bpf2bpf relative calls, map relocations, and helpers with signatures in syscall form
  • Linux: official release (requires JDK) or distro packages (some repos have apt install ghidra/pacman -S ghidra);macOS: brew install --cask ghidra; Windows: official zip
  • Verification: after importing a BPF ELF, the language bar should show eBPF:LE:64:default
  • Known limitations: eBPF has no dedicated stack pointer register; the module uses r10 as SP (stack depth is always 0, and some r10-relative accesses are treated as dead code) — decompilation results need to be cross-validated against the xlated dump

LIEF — eBPF disassembly library (batch processing)

  • pip install lief(newer versions include an LLVM MC-driven eBPF disassembler,lief.parse() then disassemble by section/address)
  • Verification: python3 -c "import lief; print(lief.__version__)"
  • Suitable for scripted batch disassembly; no GUI/decompilation

Current state of dedicated reverse engineering frameworks (boundary notes)

  • The previously envisioned "bpf-gazelle-like" dedicated eBPF reverse engineering framework: after searching (2026-08), no publicly available implementation was found, and this skill does not depend on such tools; the current approach is to combine general-purpose toolchains (Ghidra processor + bpftool xlated + llvm-objdump/LIEF), and if a dedicated framework appears later it can be added

Operating steps

Execute in order, archiving the results of each step; runtime queries require root, and dynamic analysis defaults to a sandbox.

  1. Locate the carrier(what form the sample is in determines the subsequent methods):

    • libbpf-style BPF ELF: file prog.o shows eBPF;readelf -S prog.o / llvm-readelf -S view sections —.text and sections named after attach points (such as kprobe/do_sys_openat2、tracepoint/syscalls/sys_enter_openat、xdp、tc、cgroup_skb/ingress、fentry/<fn>)、.maps(BTF-defined map definitions),.BTF(type information),.BTF.ext(line info + CO-RE relocations),.rel*(relocation sections); use pyelftools when batch extraction is needed
    • BCC-style: compiled on the fly at runtime by the embedded clang, and the compilation output is usually not left on disk — on the static side only the loading script (Python/other languages) remains, while the kernel side has the finished product; forensic reconstruction mainly relies on bpftool prog dump xlated as the primary source
    • Handcrafted bytecode: a bpf_insn array or precompiled blob, with no ELF metadata and no BTF — can only be disassembled as a whole (lddw is two instructions, offset fields must be decoded manually) or dumped from the kernel
    • Runtime location: sudo bpftool prog list lists all loaded progs (id/type/name/tag/map_ids/btf_id), checking against the sample's characteristics (name/tag/load time)
  2. Disassembly (static and runtime dual perspective):

    • Static: llvm-objdump -d prog.o — the compilation output, with symbols and comments preserved, facilitating quick reading of the logic
    • Runtime: sudo bpftool prog dump xlated id <id> opcodes(append linum to show source line information) — the actual execution form after verifier processing
    • Side-by-side double comparison: the differences are exactly the verifier's rewrite points (context access rewriting, map access inlining, constant folding, etc., see pitfall 2)
    • xlated output format:序号: (opcode十六进制) 指令, e.g.:0: (b7) r0 = 0、4: (18) r1 = map[id:2]、6: (85) call bpf_tail_call#12、7: (15) if r0 == 0x0 goto pc+18;opcodes option appends the original bytes under each line
    • Instruction set essentials: fixed 8-byte length (opcode/dst:src/16-bit off/32-bit imm); r0 return value, r1-r5 pass arguments, r6-r9 callee-saved, r10 frame pointer read-only; ALU32 uses w0-w9 registers (low 32 bits of r0-r9, zero-extended);call(0x85) unified encoding—imm is the helper call number, and bpf2bpf subprogram calls are distinguished by a relative offset + src_reg marker; lddw (0x18) occupies two instructions (the second stores the high 32 bits), and src_reg distinguishes uses such as map pointer/map value address/subprogram address
  3. progs/maps association recovery:

    • Runtime: prog list 's map_ids field gives the maps referenced by the prog; in xlated (18) r1 = map[id:N](requires kallsyms to be available, see pitfall 3) → sudo bpftool map show id N(type/key_size/value_size/max_entries/pinning) → sudo bpftool map dump id N(supported by some types; prog_array can enumerate the tail-call target chain)
    • Static: recover the map definition from the BTF structure of the .maps section (type/key/value/max_entries/pinning/flags), the.rel* section records map reference points; runtime instances and static definitions are associated by name/id
    • Note the sharing relationships: multiple progs sharing one map (collected in kernel space, read in user space) is the norm; when recovering, aggregate all referencing parties by map
  4. hook point identification:

    • ELF side: the section name is the attach target (kprobe/xxx、tracepoint/syscalls/sys_enter_*、fentry/<函数>—the fentry family requires vmlinux BTF)
    • Runtime side: the prog type determines the attach category (KPROBE/TRACEPOINT/RAW_TRACEPOINT/XDP/SCHED_CLS/CGROUP_*/LSM/TRACING, etc.); the specific attach point is confirmed with sudo bpftool net show(tc/xdp and NICs),sudo bpftool cgroup show(cgroup family),sudo bpftool link show(fentry/fexit/kprobe links)
    • The prog type also determines the helper whitelist allowed by the verifier and the ctx semantics (e.g. the ctx of KPROBE is pt_regs, the ctx of tracepoint is the event structure)—the whitelist directly constrains the space for semantic recovery
  5. semantic recovery:

    • helper: in xlated call <名称>#<调用号>, the call number is resolved according to the fixed ID mapping of the Linux BPF UAPI (include/uapi/linux/bpf.h 's ___BPF_FUNC_MAPPER is explicitly numbered item by item, see pitfall 3 for the method); for the name, compare against the table at bpftool compile time or the target kernel; calling convention r1-r5 arguments, r0 return value
    • Data plane: map contents (collected data), perf ring buffer/trace_pipe output (records written out by helpers) close the loop on program behavior recovery
    • Malicious sample focus: loading chain (how the loader ELF/script + bytecode enter the kernel), persistence (pinning to /sys/fs/bpf, accompanying daemon), evasion techniques (hook target selection, fentry pre/post tampering); cross-reference kernel semantics with [[re-kernel]]
    • Output: pseudocode-level explanation (xlated instructions → function semantics) + hook point + map layout + behavioral conclusion

cross-domain combination

  • [[re-kernel]]: semantic mapping of helper call numbers to kernel APIs, cross-reference with the layout of kernel structs directly accessed in xlated
  • [[re-tracing]]: a complementary perspective of runtime syscall/function tracing—skb/kernel paths that strace cannot cover are recovered through BPF observation
  • [[re-evasion]]: EDR countermeasures — identifying and analyzing resident bpf hooks (fentry/kprobe/tracepoint/cgroup), with adversary-side technique correlation
  • [[re-sandbox]]: mandatory prerequisite for loading/running BPF samples (isolated VM + snapshot)
  • [[re-ghidra]]: Ghidra's built-in eBPF processor decompilation workflow
  • [[re-format-elf]]: the foundation for BPF ELF section parsing (.maps/.BTF/.rel* semantics)
  • [[re-malware]]: complete analysis workflow for malicious eBPF samples (this skill handles the bytecode recovery step)
  • [[re-blockchain]]: on-chain BPF bytecode such as Solana (same instruction set lineage, different helper table)
  • [[re-variant]]: instruction-level comparison of multiple versions/patches of the same program
  • Companion: [[re-binary-core]] (general analysis of host loaders), [[re-behavior]] (summarizing eBPF residency-side behavior)

Common pitfalls and traps

  • Helper sets/availability evolve with kernel versions, the IDs themselves are stable: Symptom — in xlated, the call <名>#<号> name does not match what the tool at hand displays, or bpftool displays call unknown#<号>(displayed as bpf_unspec#0when kptr_restrict=2); Cause — helper IDs are stable numbers in the Linux BPF UAPI, and existing BPF_FUNC_* values are not renumbered with kernel versions (___BPF_FUNC_MAPPER explicitly specifies the numeric value item by item, precisely to ensure ID portability in backport scenarios); version differences affect whether a helper exists, whether it is allowed for a specific prog_type/context, and whether its semantics are extended — an old bpftool displaying call unknown#N just means its name table does not recognize the newer ID, and ID drift cannot be inferred from it; name sources fall into two categories: helper names come from bpftool's compile-time table (which may be a different version from the running kernel),call pc+X# subroutine names come from the running kernel's kallsyms (requires bpf_jit_kallsyms=1, kptr_restrict=0, symbols of the form bpf_prog__); Countermeasure — parse by fixed UAPI ID mapping: use include/uapi/linux/bpf.h 's ___BPF_FUNC_MAPPER to verify ID→name; for callability use sudo bpftool feature probe to list the helpers actually supported by that kernel (by name, per prog type) and confirm; kfunc does not belong to this stable ABI; kallsyms association (/proc/sys/kernel/bpf_jit_kallsyms=1, kptr_restrict=0) affects name display, while the ID itself is governed by the UAPI
  • xlated does not correspond to source code (verifier rewriting): Symptom — the xlated instruction count/order/constants differ noticeably from the llvm-objdump results; Cause — xlated is the form after verifier processing: context access rewriting (e.g. __sk_buff changed to sk_buff direct offsets), map access inlining (helper calls replaced with direct address loads), helper inlining into kernel implementation functions (e.g. __htab_map_lookup_elem), constant folding, unreachable code removal; bpf2bpf subprograms appear as call pc+X#<子程序符号名> relative calls; tail call targets are not in this prog (hidden in the prog_array map, xlated only shows bpf_tail_call); Countermeasure — treat xlated as the execution truth, and use static disassembly only as semantic reference; use the linum option to attach source line information to xlated for alignment; expand the tail call chain hop by hop along the prog_array map's fds
  • Blind spots from missing BTF types: Symptom — hand-written bytecode/old toolchain artifacts have no .BTF, so map layout and struct offsets cannot be parsed, and everything in Ghidra is bare addresses; Cause — BTF is the sole authoritative source of type information (CO-RE relocation also depends on it); Countermeasure — infer the layout from xlated's ldimm64 targets and the offset constants of STX/ST instructions; when .BTF is present, use bpftool btf dump file prog.o to export types, and on the vmlinux side use bpftool btf dump file /sys/kernel/btf/vmlinux Comparison struct
  • map and prog separation/lifecycle: Symptom—dumping only prog without dumping map breaks the chain of behavior; after prog is unloaded, an unpinned map is destroyed, leaving no evidence at the scene; Cause—map exists independently of prog (can be shared by multiple progs, can be pinned under /sys/fs/bpf for persistence); Countermeasure—when analyzing the scene, first bpftool map show + prog list take a full snapshot, then dump each map one by one; before forensic collection, fully archive the pin directory /sys/fs/bpf
  • Unsigned library function matching: Symptom—in a stripped loader/bytecode blob, self-implemented string/hash/encoding logic has no symbols to latch onto; Cause—BPF programs have no dynamic linking, all logic is inlined within the prog; Countermeasure—identify by constant patterns (strings, magic values, table base addresses) and behavioral fingerprints (combined forms with helper call sequences); compare against homologous loader source code or common pattern libraries
  • CO-RE relocation pitfalls: Symptom—statically reading struct field offsets in the ELF does not match the target kernel's actual layout, causing incorrect semantic reconstruction; Cause—for CO-RE programs (.BTF.ext with relocation records), field offsets are calculated at load time by libbpf according to the target kernel's BTF; the offsets in the ELF are only placeholders; Countermeasure—recalculate offsets using the vmlinux BTF of the target kernel version, or directly take xlated (the final offsets after relocation) as authoritative