diff --git a/CHANGELOG.md b/CHANGELOG.md index 72fb27e..88d111e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-03-15 + +### Added +- **WP1b: TracerPid polling for debugger detection** (`daemon/debugger_detect.c`): polls `/proc//status` for non-zero `TracerPid` every 5s in the watchdog loop. Detects debuggers that attached before daemon start or via methods eBPF hooks don't cover. Emits `OWL_EVENT_PTRACE_ATTEMPT` with `source=OWL_SRC_DAEMON` on 0-to-nonzero state transition. 6 unit tests (TDD). + +### Changed +- `scripts/verify.sh`: version bumped to v1.2.0 +- `README.md`: updated status (101 tests, 11 suites), added TracerPid mention, removed stale "no anti-debug" limitation + ## [1.1.0] - 2026-03-15 ### Added diff --git a/README.md b/README.md index 08b44a3..a3777a6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Runs on Graviton3 (c7g.large), Ubuntu 24.04, kernel 6.17. - **kernel/** - loadable module. Kprobes on ptrace, /proc/pid/mem, process_vm_readv/writev, mmap, module load/unload. ARM64 system register monitoring. Chardev for event delivery. - **ebpf/** - BPF LSM hooks returning -EPERM (ptrace_access_check, file_open, file_mprotect). Tracepoints. Kprobe on do_init_module. Ring buffer to userspace. -- **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. CRC32 code integrity. Self-protection watchdog. +- **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. CRC32 code integrity. Self-protection watchdog. TracerPid debugger detection. - **game/** - ncurses test target. Mutable state, function pointers, exported address. - **cheats/** - 8 attack programs: process_vm_readv, /proc/pid/mem, ptrace read, ptrace write, process_vm_writev, LD_PRELOAD, mprotect injection, debug registers. - **platform/** - Lambda + API Gateway + DynamoDB telemetry receiver. @@ -18,9 +18,9 @@ Runs on Graviton3 (c7g.large), Ubuntu 24.04, kernel 6.17. ## Status -v1.0.0. 95 unit tests, 10 suites. 30/31 E2E pass on Graviton3. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, process_vm_writev. Module can't be unloaded while daemon runs. +v1.2.0. 101 unit tests, 11 suites. 30/31 E2E pass on Graviton3. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. -Prototype limitations: linear signature scan, CRC32 not cryptographic, no fleet management, no anti-debug beyond prctl. +Prototype limitations: linear signature scan, CRC32 not cryptographic, no fleet management. ## Getting started diff --git a/daemon/Makefile b/daemon/Makefile index 1f2014b..d4a820b 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -22,6 +22,7 @@ SRCS := main.c \ sig_loader.c \ integrity.c \ self_protect.c \ + debugger_detect.c \ policy.c \ scanner.c \ heartbeat.c diff --git a/daemon/debugger_detect.c b/daemon/debugger_detect.c new file mode 100644 index 0000000..0cf2919 --- /dev/null +++ b/daemon/debugger_detect.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * debugger_detect.c - TracerPid polling for debugger detection + * + * Parses /proc//status for the TracerPid field to detect + * attached debuggers. Complements eBPF LSM ptrace hooks. + */ + +#include +#include + +#include "debugger_detect.h" + +int owl_debugger_detect_init(struct owl_debugger_detect *dd, pid_t target) +{ + if (!dd) + return -1; + + memset(dd, 0, sizeof(*dd)); + dd->target_pid = target; + dd->last_tracer = 0; + + return 0; +} + +int owl_check_tracer_pid(pid_t pid) +{ + char path[64]; + char line[256]; + FILE *fp; + int tracer = -1; + + if (pid <= 0) + return -1; + + snprintf(path, sizeof(path), "/proc/%d/status", (int)pid); + + fp = fopen(path, "r"); + if (!fp) + return -1; + + while (fgets(line, sizeof(line), fp)) { + if (strncmp(line, "TracerPid:", 10) == 0) { + if (sscanf(line + 10, "%d", &tracer) != 1) + tracer = -1; + break; + } + } + + fclose(fp); + return tracer; +} + +int owl_debugger_detect_check(struct owl_debugger_detect *dd) +{ + if (!dd) + return -1; + + int tracer = owl_check_tracer_pid(dd->target_pid); + if (tracer < 0) + return -1; + + int result = 0; + + /* Detect 0 -> nonzero transition (debugger newly attached) */ + if (tracer != 0 && dd->last_tracer == 0) + result = 0x01; + + dd->last_tracer = tracer; + return result; +} diff --git a/daemon/debugger_detect.h b/daemon/debugger_detect.h new file mode 100644 index 0000000..7e9eab5 --- /dev/null +++ b/daemon/debugger_detect.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * debugger_detect.h - TracerPid polling for debugger detection + * + * Complements eBPF LSM ptrace hooks by polling /proc//status + * for a non-zero TracerPid. Catches debuggers that attached before + * the daemon started or via methods the eBPF hook doesn't cover. + */ + +#ifndef OWLBEAR_DEBUGGER_DETECT_H +#define OWLBEAR_DEBUGGER_DETECT_H + +#include + +/* Debugger detection state */ +struct owl_debugger_detect { + pid_t target_pid; + pid_t last_tracer; /* 0 = none last check */ +}; + +/** + * owl_debugger_detect_init - Initialize debugger detection context + * @dd: Detection context + * @target: PID to monitor + * + * Returns 0 on success, -1 on null context. + */ +int owl_debugger_detect_init(struct owl_debugger_detect *dd, pid_t target); + +/** + * owl_check_tracer_pid - Read TracerPid from /proc//status + * @pid: Process to check + * + * Pure function. Opens /proc//status, parses TracerPid field. + * Returns the tracer PID (0 = no tracer), or -1 on error. + */ +int owl_check_tracer_pid(pid_t pid); + +/** + * owl_debugger_detect_check - Stateful debugger detection check + * @dd: Detection context + * + * Calls owl_check_tracer_pid() on the target, detects 0->nonzero + * transitions (debugger newly attached). + * + * Returns: + * 0x01 — tracer newly detected (state transition) + * 0x00 — no change (still no tracer, or tracer already known) + * -1 — error (null context or proc read failure) + */ +int owl_debugger_detect_check(struct owl_debugger_detect *dd); + +#endif /* OWLBEAR_DEBUGGER_DETECT_H */ diff --git a/daemon/main.c b/daemon/main.c index bc5881d..904a1de 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -31,6 +31,7 @@ #include "policy.h" #include "scanner.h" #include "self_protect.h" +#include "debugger_detect.h" #include "sig_loader.h" /* ------------------------------------------------------------------------- @@ -322,6 +323,7 @@ static int event_loop(int dev_fd, struct owl_bpf_ctx *bpf, struct owl_pipeline *pipeline, struct owl_integrity *integrity, struct owl_self_protect *selfprot, + struct owl_debugger_detect *dbg_detect, FILE *log_file) { int epfd; @@ -504,6 +506,33 @@ static int event_loop(int dev_fd, struct owl_bpf_ctx *bpf, if (log_file) print_event(&be, log_file); } + + /* TracerPid debugger detection */ + if (dbg_detect) { + int dbg_result = owl_debugger_detect_check(dbg_detect); + if (dbg_result & 0x01) { + printf("owlbeard: [ALERT] debugger attached (TracerPid=%d)!\n", + dbg_detect->last_tracer); + + struct owlbear_event de; + struct timespec ts; + memset(&de, 0, sizeof(de)); + clock_gettime(CLOCK_MONOTONIC, &ts); + de.timestamp_ns = (uint64_t)ts.tv_sec * 1000000000ULL + + (uint64_t)ts.tv_nsec; + de.event_type = OWL_EVENT_PTRACE_ATTEMPT; + de.severity = OWL_SEV_CRITICAL; + de.source = OWL_SRC_DAEMON; + de.pid = (uint32_t)dbg_detect->last_tracer; + de.target_pid = (uint32_t)pipeline->target_pid; + de.payload.memory.caller_pid = (uint32_t)dbg_detect->last_tracer; + + print_event(&de, stdout); + if (log_file) + print_event(&de, log_file); + owl_pipeline_process(pipeline, &de); + } + } } } @@ -650,6 +679,7 @@ int main(int argc, char *argv[]) struct owl_pipeline pipeline; struct owl_integrity integrity; struct owl_self_protect selfprot; + struct owl_debugger_detect dbg_detect; struct owl_bpf_ctx *bpf = NULL; if (parse_args(argc, argv, &cfg) < 0) @@ -738,13 +768,16 @@ int main(int argc, char *argv[]) /* Initialize self-protection */ owl_selfprotect_init(&selfprot, dev_fd, owl_bpf_ringbuf_fd(bpf)); + /* Initialize debugger detection */ + owl_debugger_detect_init(&dbg_detect, cfg.target_pid); + printf("owlbeard: ready (pid=%d, target=%d, mode=%s)\n", getpid(), cfg.target_pid, cfg.enforce ? "enforce" : "observe"); /* Run the event loop */ ret = event_loop(dev_fd, bpf, &pipeline, &integrity, &selfprot, - log_file) == 0 ? EXIT_SUCCESS : EXIT_FAILURE; + &dbg_detect, log_file) == 0 ? EXIT_SUCCESS : EXIT_FAILURE; printf("owlbeard: shutting down (events=%u, blocks=%u, kills=%u, sigs=%u)\n", pipeline.events_processed, pipeline.actions_block, diff --git a/scripts/verify.sh b/scripts/verify.sh index 004d021..f373c50 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -297,7 +297,7 @@ preflight() { | grep -o '"accountId" *: *"[^"]*"' | cut -d'"' -f4 || echo "local") cat > "${OUT_DIR}/summary.txt" <
+#include +#include + +#include "test_harness.h" +#include "debugger_detect.h" + +/* ------------------------------------------------------------------------- + * Pure function: owl_check_tracer_pid + * ----------------------------------------------------------------------- */ + +TEST(check_tracer_pid_self) { + /* No debugger attached to the test process */ + int tracer = owl_check_tracer_pid(getpid()); + ASSERT_EQ(tracer, 0); +} + +TEST(check_tracer_pid_zero) { + /* PID 0 is invalid — should return error */ + int tracer = owl_check_tracer_pid(0); + ASSERT_EQ(tracer, -1); +} + +TEST(check_tracer_pid_nonexistent) { + /* No such process — should return error */ + int tracer = owl_check_tracer_pid(999999999); + ASSERT_EQ(tracer, -1); +} + +/* ------------------------------------------------------------------------- + * Stateful context: owl_debugger_detect_init / _check + * ----------------------------------------------------------------------- */ + +TEST(detect_init_sets_target) { + struct owl_debugger_detect dd; + int ret = owl_debugger_detect_init(&dd, 12345); + ASSERT_EQ(ret, 0); + ASSERT_EQ(dd.target_pid, 12345); + ASSERT_EQ(dd.last_tracer, 0); +} + +TEST(detect_check_null_returns_error) { + ASSERT_EQ(owl_debugger_detect_check(NULL), -1); +} + +TEST(detect_check_no_debugger) { + struct owl_debugger_detect dd; + owl_debugger_detect_init(&dd, getpid()); + int result = owl_debugger_detect_check(&dd); + ASSERT_EQ(result, 0); +} + +/* ------------------------------------------------------------------------- + * Runner + * ----------------------------------------------------------------------- */ + +int main(void) +{ + printf("=== Owlbear Debugger Detection Tests ===\n"); + + RUN_TEST(check_tracer_pid_self); + RUN_TEST(check_tracer_pid_zero); + RUN_TEST(check_tracer_pid_nonexistent); + RUN_TEST(detect_init_sets_target); + RUN_TEST(detect_check_null_returns_error); + RUN_TEST(detect_check_no_debugger); + + TEST_SUMMARY(); + return test_failures; +}