Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pid>/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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ 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.
- **scripts/verify.sh** - E2E test. Baseline (cheats succeed) vs protected (cheats blocked). Machine-generated results.

## 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

Expand Down
1 change: 1 addition & 0 deletions daemon/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ SRCS := main.c \
sig_loader.c \
integrity.c \
self_protect.c \
debugger_detect.c \
policy.c \
scanner.c \
heartbeat.c
Expand Down
71 changes: 71 additions & 0 deletions daemon/debugger_detect.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* debugger_detect.c - TracerPid polling for debugger detection
*
* Parses /proc/<pid>/status for the TracerPid field to detect
* attached debuggers. Complements eBPF LSM ptrace hooks.
*/

#include <stdio.h>
#include <string.h>

#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;
}
53 changes: 53 additions & 0 deletions daemon/debugger_detect.h
Original file line number Diff line number Diff line change
@@ -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/<pid>/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 <sys/types.h>

/* 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/<pid>/status
* @pid: Process to check
*
* Pure function. Opens /proc/<pid>/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 */
35 changes: 34 additions & 1 deletion daemon/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "policy.h"
#include "scanner.h"
#include "self_protect.h"
#include "debugger_detect.h"
#include "sig_loader.h"

/* -------------------------------------------------------------------------
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions scripts/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ preflight() {
| grep -o '"accountId" *: *"[^"]*"' | cut -d'"' -f4 || echo "local")

cat > "${OUT_DIR}/summary.txt" <<HEADER
# Owlbear E2E Verification Report (v1.1.0)
# Owlbear E2E Verification Report (v1.2.0)
# Generated: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Host: $(uname -n)
# Kernel: $(uname -r)
Expand Down Expand Up @@ -917,7 +917,7 @@ FOOTER
main() {
echo ""
echo -e "${BOLD}================================================${NC}"
echo -e "${BOLD} Owlbear E2E Verification (v1.1.0)${NC}"
echo -e "${BOLD} Owlbear E2E Verification (v1.2.0)${NC}"
echo -e "${BOLD} Evidence Package Builder${NC}"
echo -e "${BOLD}================================================${NC}"
echo ""
Expand Down
8 changes: 6 additions & 2 deletions tests/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ TEST_BINS := test_events \
test_sig_loader \
test_event_pipeline \
test_integrity \
test_self_protect
test_self_protect \
test_debugger_detect

.PHONY: all unit integration clean

Expand Down Expand Up @@ -72,6 +73,9 @@ test_integrity: test_harness.o test_integrity.o $(DAEMON_DIR)/integrity.o
test_self_protect: test_harness.o test_self_protect.o $(DAEMON_DIR)/self_protect.o
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

test_debugger_detect: test_harness.o test_debugger_detect.o $(DAEMON_DIR)/debugger_detect.o
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)

# Daemon objects needed by tests
$(DAEMON_DIR)/%.o: $(DAEMON_DIR)/%.c
$(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $<
Expand All @@ -92,7 +96,7 @@ clean:
$(RM) $(DAEMON_DIR)/policy.o $(DAEMON_DIR)/scanner.o $(DAEMON_DIR)/heartbeat.o
$(RM) $(DAEMON_DIR)/bpf_event_convert.o $(DAEMON_DIR)/sig_loader.o
$(RM) $(DAEMON_DIR)/event_pipeline.o $(DAEMON_DIR)/integrity.o
$(RM) $(DAEMON_DIR)/self_protect.o
$(RM) $(DAEMON_DIR)/self_protect.o $(DAEMON_DIR)/debugger_detect.o
$(RM) $(DAEMON_DIR)/*.d

-include $(wildcard *.d)
Binary file added tests/test_debugger_detect
Binary file not shown.
79 changes: 79 additions & 0 deletions tests/test_debugger_detect.c
Original file line number Diff line number Diff line change
@@ -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 <string.h>
#include <sys/types.h>
#include <unistd.h>

#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;
}
Loading