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 installlinux-tools-common+linux-tools-generic——the former is just a wrapper, the real binary is in thelinux-tools-$(uname -r)kernel companion package); older Debian (buster and earlier) has no standalone package, compiletools/bpf/bpftoolfrom 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 commandssudo(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.
-
Locate the carrier(what form the sample is in determines the subsequent methods):
- libbpf-style BPF ELF:
file prog.oshows eBPF;readelf -S prog.o/llvm-readelf -Sview sections —.textand sections named after attach points (such askprobe/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 xlatedas 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 listlists all loaded progs (id/type/name/tag/map_ids/btf_id), checking against the sample's characteristics (name/tag/load time)
- libbpf-style BPF ELF:
-
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(appendlinumto 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;opcodesoption 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
- Static:
-
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
.mapssection (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
- Runtime:
-
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
- ELF side: the section name is the attach target (
-
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_MAPPERis 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
- helper: in xlated
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 displayscall unknown#<号>(displayed asbpf_unspec#0when kptr_restrict=2); Cause — helper IDs are stable numbers in the Linux BPF UAPI, and existingBPF_FUNC_*values are not renumbered with kernel versions (___BPF_FUNC_MAPPERexplicitly 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 displayingcall unknown#Njust 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: useinclude/uapi/linux/bpf.h's___BPF_FUNC_MAPPERto verify ID→name; for callability usesudo bpftool feature probeto 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 ascall pc+X#<子程序符号名>relative calls; tail call targets are not in this prog (hidden in the prog_array map, xlated only showsbpf_tail_call); Countermeasure — treat xlated as the execution truth, and use static disassembly only as semantic reference; use thelinumoption 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.BTFis present, usebpftool btf dump file prog.oto export types, and on the vmlinux side usebpftool btf dump file /sys/kernel/btf/vmlinuxComparison 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 listtake 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.extwith 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