From a75d5cd0e2efe67c59b68a4b2cfcb0aad44b9dc3 Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 15 Mar 2026 05:51:09 +0000 Subject: [PATCH] TracePID polling for debugger detection --- CHANGELOG.md | 9 ++++ README.md | 6 +-- daemon/Makefile | 1 + daemon/debugger_detect.c | 71 +++++++++++++++++++++++++++++++ daemon/debugger_detect.h | 53 +++++++++++++++++++++++ daemon/main.c | 35 +++++++++++++++- scripts/verify.sh | 4 +- tests/Makefile | 8 +++- tests/test_debugger_detect | Bin 0 -> 17184 bytes tests/test_debugger_detect.c | 79 +++++++++++++++++++++++++++++++++++ 10 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 daemon/debugger_detect.c create mode 100644 daemon/debugger_detect.h create mode 100755 tests/test_debugger_detect create mode 100644 tests/test_debugger_detect.c 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" <
?(e+{e9I-iHK-5Qn34Mpfz*_%Z5^fDDYs*>Fb7fPJ2zcp%#=`yL1pXvR^)#ZZ!_ENm}>mE>HP*)LM7f; zU`KiRqD6*z3&$%r4hK2^LOH#vCLU{8R96#^%#FtqnO$>tEnYNtQJpWH^v%~zpmurj zPv_LSjoTI6bQ8vDRFxg3F8)Y%JWyy%#GJVdaZKI%;LomLH$IjjbLT?tvL41NT-w}k$jGWyiEQvUzE3?3fDpbF1LnsnWMVn)3E1KH2VRby2h;9ov#G?q?+?q^qLqbNiq$2WAi8HwZ zGt@Bs=V3~i{0~>Vp-1NrmFr`%v2=Ak44$wTAAKCB`*8Igu4g94a4HN_PjLMwT3)IC z0d*=e-^x7K!GqVv^Qqu|#;g3=ObIUavnvcv*BYHOQhFV@T=OK$J8*QsF8vOCyp1X~ z;J~HtB<^6G&THrOzu$q=d?Dq41K0OC794coX1*|jLk^r`NIC4lB^M++;=t(|l=7kj zA0;4Yw*%)}tCpT};A0&6ryck>2R_6OWnd@+Lm3##z)%K;GVrI*z=z{+{Uq3Vp(>d3 z{IW)=;NEVlvT!=s`Fhm}9jvhMPQZof>v0@E#ji-eg(Q8yDijJ$2XvhlMtwgYtkZ&} z@3p}?Es*+tJXoiNOW!ktby~3W{mo#V7Ak$;8m!X-r7tyDr-eyh!(g2jBz;>3>$DK* zTV>Y^)5oFpP2;DmQM&zw>HB=e^#@(``&{*etKQ_Q-|eb@)m7iz?;!uxr_;NMm8*1gKtW@{OPJFglF^>{@mQkYJZ*K9M^H ztce)fkMTBvhIVA3cl?wN(mTNv`Xjae!b&2uE(f#y!Q&TK1dk643s#&Ap1EvY4+n3s zgQ`NW$)niQ_;)NLvC7Qe9_(E946(uN`_`CXZrQV_G23B>lhK>s2a{}1K7oPH1}@|R7lH?a*+74=Con*b59Yux4GdU!26Nl{Q5&ew zo(^V@ADu+PE>F!^YQjly%QV;XB3b2kz-y0ZIkf@RoTm<4XLm)`GaJmCC!8&KRU2Hy ziRg}xrWVS^zZGVP`Xj7tP5f6h$MJhVr})R_Nc^9G)A64NK7A&6%LWGKbqBiw{m1U8rD{)pA1>Ey;N@K4 zWj!k07?o3$E@Y(}qjJjHgi+~6?G%p+PAtmG6L=0VS$R%#+^U~`Mp-#LTUzxLINhqp z!1TQZTN-T||I>(cR<|Mtere#WwH}d9Bhp!pgv(ryzxJ>DuQ}=m|ld*!x43;7h&@HH0wnz&tZ@V3UdncjOoxXG{<57@F{iZ-@Yti-Ug?`ybcCl z1|q@PCb^a;|1y+|QeSOOy`xC-;7NbgldjIRW{GYCbvmdyZp}TqHT4K>I=up%Zp|Vv zMUiV=gPvC~&AvuspZx_5d>7_%su4d~p9Jsb0`GpooYDJZwC%f?t4+?H(Hyt(jZY|N ze>Y27`2;xK$^&5BIol6qN6WiBdyK363JB;#(^XFxrahibI8w=ZR)W($7lX+^g3jeU z3&CPu^unrfq_ixlId0i(_&k3r~F>kSbep*G*Ip)9ofn$EIQ5w_VI_2Mk9N0%1^R&;Tn9Vqvm|p{@V{QhMe^$rb z0G4CES|$DbYt3=Y)e`e*Tb_2xZ!&!h0yF2%Rbj%ho`Im5U=R8rc9P>Kc=hGt` zKEEL`f3H!>ZO$oQYGPi}nCF(;DduP#P0Y_{aLh|!@~`TcAAseU$FvpsoUJ*|?Qj2! z`}veDzriW*fjsB=A^haVK1s)kPvAJ~O!l?xJMi_8Q};=&+Yb3%&O26hF#EtjHghT1 z`M`k6jChe|jPo^gM;QbmM92eC?7uqmiWMBCK z?wPK6p=a;k1EkWa#cIze9j>n*E^?2NMX+n{5s>VeVAsU6*C4zfBK!2te{5?&ehTuS znP3Ve$nHH%9y(tkqB2d!_jk!b*WP|$M=`yDAN!~_e20291BQFMGfP+_*x5ZQ*!37S z6MB}dXO;mYq1-u8Vfh9-a(-Gc_m~cALL8&Xk{sVfErREV&sQ7>=1j`~fBX>!Os4@i zAezsw*EdVd=NBMxq!Ei53``!QlD9{t9%;|#6x@6V4a^2U>wMrd)y_jUhnF~nS=VVz z|8O*51aO+bHM0G)?B?MZyEjkh^{igM+beb`4V9q`3}s*_179Em75sIuC>@PAA|o%KPXP=!hUdC-}lvwx4957j|iKxbSk z6!wE20zC}+b=+%Cf!>8BOCFS#OI3LH@-dcbGx?(teRHN#v3rZE*i~I|)0h!e2cS#% z&GwtPM0-~O;r7)I~2 zr2WG_sTBWl$Xl^s*ivL)Z`(ft`O}cE10A$qZny6R;3u&@ixt`L(Dnokm7xp_Wnd@+ zLm3##z)%K;GBA{Z|34X!-=E3v&1eZoMXyCc$LJakyqLy!D_y#8GW15S%kSD4wtufS zndS2PHd^mep_hMDKDt~;5N~Mh%yd%@)EX&PP9qbs#cNASFF{R&UIt!br z7XBRb-vm<-@I8YxSCHI z_TlP-f?J2ik$wmI2q)Uh3KKv}>C-q@s%mwb_am^n@-nVmKgwOexLjwVUk|22O;*)@ zgYapof2Uia{!GbrBl`D)p|~9`|NDVgsEPLPOGW=VFs0i6&su-7n&dZ{`ei%V{g1S>x1X1Qd#_Yi{|bCs349ObzY?n#_kCw;W6GN6OJb8ixFLpju+0iU z+R8LF;kEMMhJujQ8p3vkM3iYg-)ekt4p^(i*hy zAr=a!QsMRx-v6fBRZ}Y58VyAZj4L5_bx5cFS$#LoNv0dM!DDBPGQ-%-6fP9ue?K{ zwOdwh2!sL~*MzXWW6jq#uG~<+8Y#u*q);Hp(%_mcDzt9>rd2D~hc>NUyEU*av~A_8 z^#NG8H>&t$=Lt5J@U|2iqJ1Y<-)vH}8OFB5)|KQ=oG`_oF7!z2%{8_n1t_Jdlf!#! zij+gqNZ1PVZkl2VopR`2@va?P1)byVJvKza&3>FBNN)xzLU0ivsM%Aan`_2y@Oufu z7Vo+VMbgPoOE?js9Xa)zU>b=fLK%FYP`DmwyHGT6D9eE4S47EM|04yYj$!%jQwZ8BOb zSJls@8AGeLgwrj`7imu*grQc-NbZZK()hSkR0%1cXbVpoEfIK=n(<*K8e<_mLWNsnaAwSrZso(Y)kpKA!fpOZq_lMH=B4?5N znqaiH}33(2S!~KdwDO6nml9z}RW8{H$7QsKgNm zVkdQaPA5#_%X+_>?H7w8Q@8!K9G}%~ZL#Mx+Bmh9ij&H@VqHH2h0^ nnSVzDleQa6bNoxXF`-;^2_>$P(lb|S`?X7q#$=a)tE&DRMUeIa literal 0 HcmV?d00001 diff --git a/tests/test_debugger_detect.c b/tests/test_debugger_detect.c new file mode 100644 index 0000000..98a8b78 --- /dev/null +++ b/tests/test_debugger_detect.c @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_debugger_detect.c - Tests for TracerPid debugger detection + * + * Tests the pure TracerPid parsing function and the stateful + * debugger detection context. All tests run without a debugger + * attached to the test process. + */ + +#include +#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; +}