Skip to content

Commit 0d1d8d4

Browse files
committed
feat(sources): add execve, ptrace, sensitive_file_open BPF checks
sensitive_file_open uses compile-time unrolled byte comparison to avoid BPF loops — compatible with all kernel versions.
1 parent 2189c3a commit 0d1d8d4

2 files changed

Lines changed: 155 additions & 0 deletions

File tree

sources/predefined_programs.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,111 @@ def module_load() -> str:
182182
"""
183183

184184

185+
def execve() -> str:
186+
"""Trace all execve syscalls (process execution)."""
187+
return """
188+
#include <uapi/linux/ptrace.h>
189+
#include <linux/sched.h>
190+
191+
#define TASK_COMM_LEN 16
192+
#define PAYLOAD_LEN 256
193+
194+
struct event_t {
195+
u64 ts;
196+
u32 pid;
197+
char comm[TASK_COMM_LEN];
198+
char payload[PAYLOAD_LEN];
199+
};
200+
201+
BPF_PERF_OUTPUT(events);
202+
203+
TRACEPOINT_PROBE(syscalls, sys_enter_execve) {
204+
struct event_t ev = {};
205+
ev.ts = bpf_ktime_get_ns();
206+
ev.pid = bpf_get_current_pid_tgid() >> 32;
207+
bpf_get_current_comm(&ev.comm, sizeof(ev.comm));
208+
bpf_probe_read_user_str(ev.payload, sizeof(ev.payload), args->filename);
209+
events.perf_submit(args, &ev, sizeof(ev));
210+
return 0;
211+
}
212+
"""
213+
214+
215+
def ptrace() -> str:
216+
"""Trace ptrace calls via security_ptrace_access_check (process injection / debugging)."""
217+
return """
218+
#include <uapi/linux/ptrace.h>
219+
#include <linux/sched.h>
220+
221+
#define TASK_COMM_LEN 16
222+
#define PAYLOAD_LEN 256
223+
224+
struct event_t {
225+
u64 ts;
226+
u32 pid;
227+
char comm[TASK_COMM_LEN];
228+
char payload[PAYLOAD_LEN];
229+
};
230+
231+
BPF_PERF_OUTPUT(events);
232+
233+
int kprobe__security_ptrace_access_check(struct pt_regs *ctx, struct task_struct *child, unsigned int mode) {
234+
struct event_t ev = {};
235+
ev.ts = bpf_ktime_get_ns();
236+
ev.pid = bpf_get_current_pid_tgid() >> 32;
237+
bpf_get_current_comm(&ev.comm, sizeof(ev.comm));
238+
bpf_probe_read_kernel_str(ev.payload, sizeof(ev.payload), child->comm);
239+
events.perf_submit(ctx, &ev, sizeof(ev));
240+
return 0;
241+
}
242+
"""
243+
244+
245+
def sensitive_file_open(path: str) -> str:
246+
"""Trace opens of a specific absolute file path (e.g. /etc/shadow).
247+
248+
Uses compile-time unrolled byte comparison — no BPF loops, works on all kernel versions.
249+
"""
250+
if not path.startswith("/"):
251+
raise ValueError(f"path must be absolute: {path!r}")
252+
path_bytes = path.encode("utf-8")
253+
if len(path_bytes) > 255:
254+
raise ValueError(f"path too long (max 255 bytes): {path!r}")
255+
comparisons = " ||\n ".join(
256+
f"fname[{i}] != {b}" for i, b in enumerate(path_bytes)
257+
) + f" ||\n fname[{len(path_bytes)}] != 0"
258+
return f"""
259+
#include <uapi/linux/ptrace.h>
260+
#include <linux/sched.h>
261+
262+
#define TASK_COMM_LEN 16
263+
#define PAYLOAD_LEN 256
264+
265+
struct event_t {{
266+
u64 ts;
267+
u32 pid;
268+
char comm[TASK_COMM_LEN];
269+
char payload[PAYLOAD_LEN];
270+
}};
271+
272+
BPF_PERF_OUTPUT(events);
273+
274+
TRACEPOINT_PROBE(syscalls, sys_enter_openat) {{
275+
char fname[PAYLOAD_LEN];
276+
bpf_probe_read_user_str(fname, sizeof(fname), args->filename);
277+
if ({comparisons}) return 0;
278+
279+
struct event_t ev = {{}};
280+
ev.ts = bpf_ktime_get_ns();
281+
ev.pid = bpf_get_current_pid_tgid() >> 32;
282+
bpf_get_current_comm(&ev.comm, sizeof(ev.comm));
283+
__builtin_memcpy(ev.payload, fname, sizeof(ev.payload));
284+
events.perf_submit(args, &ev, sizeof(ev));
285+
return 0;
286+
}}
287+
"""
288+
289+
185290
def ip_host(addr: str) -> str:
186291
"""Trace TCP connections to or from the given IPv4 address (dotted decimal)."""
187292
octets = [int(o) for o in addr.split(".")]

tests/unit/test_predefined_programs.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22

33
from sources.predefined_programs import (
44
commit_creds,
5+
execve,
56
icmp,
67
ip_host,
78
module_load,
9+
ptrace,
10+
sensitive_file_open,
811
suid_exec,
912
tcp_port,
1013
udp_port,
@@ -75,3 +78,50 @@ def test_module_load_returns_string() -> None:
7578

7679
def test_module_load_is_deterministic() -> None:
7780
assert module_load() == module_load()
81+
82+
83+
# --- execve ---
84+
85+
86+
def test_execve_returns_string() -> None:
87+
assert isinstance(execve(), str)
88+
89+
90+
def test_execve_is_deterministic() -> None:
91+
assert execve() == execve()
92+
93+
94+
# --- ptrace ---
95+
96+
97+
def test_ptrace_returns_string() -> None:
98+
assert isinstance(ptrace(), str)
99+
100+
101+
def test_ptrace_is_deterministic() -> None:
102+
assert ptrace() == ptrace()
103+
104+
105+
# --- sensitive_file_open ---
106+
107+
108+
def test_sensitive_file_open_returns_string() -> None:
109+
assert isinstance(sensitive_file_open("/etc/shadow"), str)
110+
111+
112+
def test_sensitive_file_open_different_paths_differ() -> None:
113+
assert sensitive_file_open("/etc/shadow") != sensitive_file_open("/etc/passwd")
114+
115+
116+
def test_sensitive_file_open_is_deterministic() -> None:
117+
assert sensitive_file_open("/etc/shadow") == sensitive_file_open("/etc/shadow")
118+
119+
120+
def test_sensitive_file_open_rejects_relative_path() -> None:
121+
with pytest.raises(ValueError):
122+
sensitive_file_open("etc/shadow")
123+
124+
125+
def test_sensitive_file_open_rejects_too_long_path() -> None:
126+
with pytest.raises(ValueError):
127+
sensitive_file_open("/" + "a" * 255)

0 commit comments

Comments
 (0)