feat(agent,engine): ptrace-attach + kernel-module-load probes for code-injection/kernel-tamper (JEF-318) - #282
Merged
thejefflarson merged 4 commits intoAug 1, 2026
Conversation
…e-injection/kernel-tamper (JEF-318) Falco fires critical on ptrace attach (process injection) and kernel-module load (init_module/finit_module); no agent hook covered security_ptrace_access_check or module load before this. Adds two fentry probes (Retire-Falco G2) + engine-side "tamper-now" corroboration, entry-scoped and foothold-gated (never blanket), and default-on (no PROTECTOR_*_ENABLE flag — detection is on by default; shadow already makes it inert of action). AGENT (agent/protector-agent-ebpf/src/main.rs): - fentry on security_ptrace_access_check(child, mode), filtered in-kernel to mode & PTRACE_MODE_ATTACH so the constant PTRACE_MODE_READ checks /proc/<pid>/... triggers never reach the ring. Further deduped per attacking pid (PTRACE_SEEN, mirroring CREDENTIAL_READ_SEEN's JEF-320 ring-DoS lesson) since process_vm_readv/ writev also hits this hook and a legitimate chatty caller (a debugger) can invoke it in a loop. - fentry on security_kernel_load_data(id, contents), filtered to id == LOADING_MODULE. load_module() calls this hook on BOTH init_module (in-memory buffer) and finit_module (fd, which first reaches security_kernel_read_file) before any parsing, so one probe covers both syscalls. Rare in normal workloads — no dedup needed. - DECISION: neither probe touches a single vmlinux struct offset. ptrace's `mode` and module-load's `id`/`contents` are plain scalar args (not pointers), and the ptrace target task_struct's pid is deliberately NOT read — struct task_struct is enormous and its layout is far more volatile across kernel configs/versions than the already-ON-NODE-PENDING JEF-317 linux_binprm/inode offsets, so it was judged too fragile a read for a field the corroboration predicate doesn't need (the attacking pid alone, from the existing EventHeader, is enough). This keeps this probe's on-node risk profile lower than JEF-317's. - Both new KIND_PTRACE_ATTACH/KIND_MODULE_LOAD events reuse the shared EventHeader verbatim as their wire body (no new struct) — the occurrence, attributed by pid/cgroup, is the whole fact. WIRE (agent/common, behavior): two new KIND_* constants; two new fieldless Behavior variants (PtraceAttach, ModuleLoad) — pure kernel facts, no classification on the wire type (JEF-113). ENGINE (engine/src/engine/reason/proof/corroborate.rs): ptrace_attach_on_foothold and module_load_on_foothold, mirroring privilege_escalation_on_foothold's shape — corroborate a PrivilegeEscalation-tactic objective ONLY on a proven internet-facing foothold entry. Neither behavior is ever routed into the flat corroborates() blanket arm (both stay `false` there, like PrivilegeChange) — a debugger, strace, a supervisor ptrace-attaching its own child, or a legitimate driver-loading DaemonSet are all ordinary on non-foothold pods. DECISION NEEDED (flagged, not blocking): ATT&CK tags ptrace/process-injection (T1055) Defense Evasion + Privilege Escalation, but this repo's Tactic enum has no DefenseEvasion variant. Both new shapes land on PrivilegeEscalation, mirroring privilege_escalation_on_foothold's own T1611/T1098.006 precedent. A dedicated DefenseEvasion tactic is a reasonable follow-up if a future shape needs the distinction. ON-NODE-PENDING (docs/ebpf-testing-on-nodes.md loop): confirm fentry attaches to both new hooks on both fleet arches; confirm enum kernel_load_data_id's LOADING_MODULE == 2 via `bpftool btf dump ... format c | grep -A8 'enum kernel_load_data_id'` (a plain integer compare, not verifier-checked, so a wrong value would misclassify silently rather than reject loud); functional smoke test (ptrace-attach + insmod on a real pod) to confirm both classifiers fire end-to-end. Tests: engine unit tests for both entry-scoped predicates (positive/negative/ foothold-gate/tactic-gate/cross-shape-non-interference), behavior wire round-trip + fingerprint/summary/variant-label tests, agent decode tests for the header-only wire shape. cargo fmt clean, clippy -D warnings clean (engine/behavior workspace, agent workspace, and the protector-agent-ebpf crate via `cargo check`/`cargo clippy` — direct rustc compiles clean; the bpf-linker LINK step fails locally with "unable to find LLVM shared lib", the known local-toolchain gap, not a code issue). Closes JEF-318 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
thejefflarson
enabled auto-merge (squash)
July 28, 2026 02:05
…probe-for-code-injection-kernel-tamper
…probe-for-code-injection-kernel-tamper
…probe-for-code-injection-kernel-tamper
thejefflarson
deleted the
thejefflarson/jef-318-retire-falco-g2-agent-probe-for-code-injection-kernel-tamper
branch
August 1, 2026 19:31
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Falco fires critical on ptrace attach (process injection) and kernel-module load
(
init_module/finit_module); no agent hook coveredsecurity_ptrace_access_checkor module load before this. Adds two fentry probes (Retire-Falco G2 parity) plus
engine-side "tamper-now" corroboration, entry-scoped and foothold-gated (never
blanket) — feeds F3's classifier (JEF-309). Detection is on by default — no
PROTECTOR_*_ENABLEflag (per the just-codified repo convention); shadow alreadymakes it inert of action. Shadow-gated throughout (ADR-0014): only sets
corroborated, never actuates.The fentry mechanism (both probes, zero vmlinux struct offsets)
security_ptrace_access_check(child, mode), filteredin-kernel to
mode & PTRACE_MODE_ATTACHso the constantPTRACE_MODE_READchecksevery
/proc/<pid>/…access triggers never reach the ring. Further deduped perattacking pid (
PTRACE_SEEN, mirroringCREDENTIAL_READ_SEEN's JEF-320 ring-DoSlesson) —
process_vm_readv/writevalso hit this hook, and a legitimate chattycaller (a debugger) can invoke it in a tight loop.
security_kernel_load_data(id, contents),filtered to
id == LOADING_MODULE.load_module()calls this hook on bothinit_module(in-memory buffer) andfinit_module(fd — which first reachessecurity_kernel_read_file) before any parsing, so one probe covers bothsyscalls. Module loads are rare in normal container workloads — no dedup gate
needed.
fentry, notlsm/*—bpfis not in the fleet's active LSM list(confirmed on-node this session), so
lsm/programs would never attach; fentry onthe
security_*function works regardless, same as every other probe in this file.vmlinuxstruct field. Both hooks'arguments (
mode,id,contents) are plain scalars, not pointers — and theptrace target
task_struct's pid is deliberately not read:struct task_structis enormous and its layout is far more volatile across kernelconfigs/versions than the already-ON-NODE-PENDING JEF-317
linux_binprm/inodeoffsets, so it was judged too fragile a read for a field the corroboration
predicate below doesn't need — the attacking pid alone (already on every event's
EventHeader) is enough to scope the signal to the foothold entry. This keepsthis ticket's on-node risk profile materially lower than JEF-317's — no new
struct-offset verifier risk at all.
KIND_PTRACE_ATTACH/KIND_MODULE_LOADring events reuse the sharedEventHeaderverbatim as their entire wire body (no new struct) — theoccurrence, attributed by the header's pid/cgroup, is the whole fact.
Wire (pure data, JEF-113)
agent/common: two newKIND_*constants (7, 8).behavior: two new fieldlessBehaviorvariants —PtraceAttach,ModuleLoad. No classification lives on thewire type; the agent emits only the raw kernel occurrence.
Engine (
engine/src/engine/reason/proof/corroborate.rs)ptrace_attach_on_footholdandmodule_load_on_foothold, mirroringprivilege_escalation_on_foothold's exact shape: corroborate aPrivilegeEscalation-tactic objective only on a proven internet-facing foothold
entry. Neither behavior is ever routed into the flat
corroborates()blanket arm(both stay
falsethere, likePrivilegeChange) — a debugger,strace, asupervisor ptrace-attaching its own child, or a legitimate driver-loading DaemonSet
(a CNI/CSI plugin, a kernel-module operator) are all ordinary on non-foothold pods.
DECISION NEEDED (flagged for the architect, not blocking): ATT&CK tags
ptrace/process-injection (T1055) Defense Evasion + Privilege Escalation, but
this repo's
Tacticenum has noDefenseEvasionvariant. Both new shapes land onPrivilegeEscalation, mirroringprivilege_escalation_on_foothold's ownT1611/T1098.006 precedent (kernel-module load is at least as defensible a fit —
full kernel-mode code execution from a container is genuinely a host-escape/
privilege-escalation event). A dedicated
DefenseEvasiontactic variant is areasonable follow-up if a future shape needs the distinction — not resolving it
here to stay in scope.
ON-NODE-PENDING (loud, per docs/ebpf-testing-on-nodes.md — done post-merge via the merge-behind-shadow roll + SSH)
fentryattaches tosecurity_ptrace_access_checkandsecurity_kernel_load_dataon both fleet arches (7.0.0 raspi arm64 +generic amd64) — expected yes (every other probe in this file already attaches
this way), but unconfirmed off-node.
enum kernel_load_data_id'sLOADING_MODULE == 2viabpftool btf dump … format c | grep -A8 'enum kernel_load_data_id'on both arches. This is aplain integer compare, not verifier-checked (unlike a struct offset) — a
wrong value would misclassify silently rather than reject loud, so this is the
one item here that fails quietly if wrong.
PTRACE_MODE_ATTACH == 0x02matches the running kernel'sinclude/linux/ptrace.h(stable across kernel versions in upstream Linux, butunconfirmed on this fleet).
ptrace(PTRACE_ATTACH)(orgdb -p) +insmod/modprobeon a real pod, confirm both classifiers fire end-to-end (probe attach → decode →
Behavior::PtraceAttach/ModuleLoad→ engine corroboration on a foothold entry).PTRACE_SEENdedup gate's real-world hit rate — isprocess_vm_readv/writevactually chatty enough on the fleet to need it, orwas this conservative-by-default?
This PR is merge-behind-shadow: shadow-gated (ADR-0014), and since these are
brand-new probes attaching to previously-unhooked LSM functions, on-node load/attach
validation is a genuine open question the way JEF-317's inode-offset fields were —
unlike JEF-317's Route A follow-up, though, neither new probe here touches a
vmlinux struct at all, so the verifier-rejection risk specifically is lower.
Test plan
matching tactic corroborates), negative (non-foothold entry does not),
tactic-gate regression (an unrelated objective does not corroborate even on
the foothold), and cross-shape non-interference (the other new behavior /
an ordinary exec does not falsely trigger either shape) —
corroborate_ptrace_tests.rs,corroborate_module_load_tests.rs.{"kind": "..."}taggedshape,
summary()/fingerprint_key()/variant_label(),is_alert() == false, fullRuntimeObservationround-trip —behavior/src/tests.rs.Behaviorfor both new kinds, plus a truncated-event regression —observer_ebpf_tests.rs.cargo fmt --allclean (engine/behavior workspace + agent workspace + theprotector-agent-ebpfcrate).cargo clippy --workspace --all-targets -- -D warningsclean on theengine/behavior workspace and the agent (userspace) workspace;
cargo clippy -- -D warningsclean onprotector-agent-ebpf.cargo testgreen: 960 engine/behavior tests, 45 + 7 agent-userspace tests(all pre-existing plus new).
protector-agent-ebpf(the kernel-side crate carrying all the new probecode) compiles cleanly via
cargo check/cargo rustc— rustc succeeds; thebpf-linkerLINK step fails locally with "unable to find LLVM shared lib",the documented local-toolchain gap (docs/ebpf-testing-on-nodes.md), not a
code issue. CI's
agent.ymlebpfjob (self-hosted Linux, baked-inbpf-linker) is the real gate for the link step.
cargo test --features ebpffor the agent crate has no CI runner today —agent.ymlonly runs
cargo build --features ebpf(no test), andrust.ymldoesn'ttouch the agent at all. This means
observer_ebpf_tests.rs— includingtests that predate this PR (exec/file-write decode, etc.) — has never
actually executed in CI. Flagging as a follow-up gap, not fixing here
(out of this ticket's scope).
Closes JEF-318
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP