diff --git a/CHANGELOG.md b/CHANGELOG.md index 7153e8c..632089c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ 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). +## [2.1.0] - 2026-03-15 + +### Added +- **WP1d: LD_PRELOAD detection on process exec** (`daemon/preload_detect.c`): scans `/proc//environ` for `LD_PRELOAD` on every `PROCESS_EXEC` event. Emits `OWL_EVENT_LIB_UNEXPECTED` (0x0203, severity CRITICAL) with the preload path in the module payload. Pure scanner function + I/O wrapper, stateless one-shot per exec. +- `daemon/preload_detect.h`: public API (`owl_scan_environ_for_preload`, `owl_check_preload_env`) +- `tests/test_preload_detect.c`: 5 unit tests (TDD) — synthetic buffer scan, not-found, null buffer, null output, self-check via /proc +- `scripts/verify.sh`: E2E assertion for `ld_preload_hook` triggering `LIB_UNEXPECTED` in daemon log + +### Changed +- `daemon/event_pipeline.c`: calls `pipeline_check_preload()` on `OWL_EVENT_PROCESS_EXEC` events, emits `LIB_UNEXPECTED` if `LD_PRELOAD` found +- `daemon/Makefile`: added `preload_detect.c` to SRCS +- `tests/Makefile`: added `test_preload_detect` suite, linked `preload_detect.o` into `test_event_pipeline` +- `scripts/verify.sh`: version bumped to v2.1.0, added `ld_preload_hook.bin` to preflight checks +- `README.md`: updated status (v2.1.0, 107 tests, 12 suites), added LD_PRELOAD detection + ## [2.0.0] - 2026-03-15 ### Added diff --git a/README.md b/README.md index a679d76..a82df93 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 for /proc/pid/mem + /dev/mem + /dev/kmem, 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. TracerPid debugger detection. +- **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. CRC32 code integrity. Self-protection watchdog. TracerPid debugger detection. LD_PRELOAD environ scanning. - **game/** - ncurses test target. Mutable state, function pointers, exported address. - **cheats/** - 9 attack programs: process_vm_readv, /proc/pid/mem, /dev/mem, ptrace read, ptrace write, process_vm_writev, LD_PRELOAD, mprotect injection, debug registers. - **platform/** - Lambda + API Gateway + DynamoDB telemetry receiver. @@ -18,7 +18,7 @@ Runs on Graviton3 (c7g.large), Ubuntu 24.04, kernel 6.17. ## Status -v2.0.0. 102 unit tests, 11 suites. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, /dev/mem, /dev/kmem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. +v2.1.0. 107 unit tests, 12 suites. eBPF LSM returns EPERM on ptrace, /proc/pid/mem, /dev/mem, /dev/kmem, process_vm_writev. Module can't be unloaded while daemon runs. TracerPid polling detects debuggers attached before daemon start. LD_PRELOAD detection on exec. Prototype limitations: linear signature scan, CRC32 not cryptographic, no fleet management. diff --git a/daemon/Makefile b/daemon/Makefile index d4a820b..5d61d61 100644 --- a/daemon/Makefile +++ b/daemon/Makefile @@ -23,6 +23,7 @@ SRCS := main.c \ integrity.c \ self_protect.c \ debugger_detect.c \ + preload_detect.c \ policy.c \ scanner.c \ heartbeat.c diff --git a/daemon/event_pipeline.c b/daemon/event_pipeline.c index ea28676..3c5ad67 100644 --- a/daemon/event_pipeline.c +++ b/daemon/event_pipeline.c @@ -16,6 +16,10 @@ #include #include "event_pipeline.h" +#include "preload_detect.h" + +static void pipeline_check_preload(struct owl_pipeline *pipe, + const struct owlbear_event *exec_ev); /* ------------------------------------------------------------------------- * Initialization @@ -86,9 +90,47 @@ enum owl_policy_action owl_pipeline_process(struct owl_pipeline *pipe, break; } + /* On exec events, check for LD_PRELOAD in the new process's environ */ + if (ev->event_type == OWL_EVENT_PROCESS_EXEC) + pipeline_check_preload(pipe, ev); + return action; } +/* ------------------------------------------------------------------------- + * LD_PRELOAD detection on exec events + * ----------------------------------------------------------------------- */ + +static void pipeline_check_preload(struct owl_pipeline *pipe, + const struct owlbear_event *exec_ev) +{ + char preload_val[256]; + + if (exec_ev->pid <= 0) + return; + + if (owl_check_preload_env((pid_t)exec_ev->pid, + preload_val, sizeof(preload_val)) != 1) + return; + + struct owlbear_event ev; + struct timespec ts; + + memset(&ev, 0, sizeof(ev)); + clock_gettime(CLOCK_MONOTONIC, &ts); + ev.timestamp_ns = (uint64_t)ts.tv_sec * 1000000000ULL + + (uint64_t)ts.tv_nsec; + ev.event_type = OWL_EVENT_LIB_UNEXPECTED; + ev.severity = OWL_SEV_CRITICAL; + ev.source = OWL_SRC_DAEMON; + ev.pid = exec_ev->pid; + ev.target_pid = (uint32_t)pipe->target_pid; + strncpy(ev.payload.module.name, preload_val, + sizeof(ev.payload.module.name) - 1); + + owl_pipeline_process(pipe, &ev); +} + /* ------------------------------------------------------------------------- * Signature scanning - buffer variant (testable) * ----------------------------------------------------------------------- */ diff --git a/daemon/preload_detect.c b/daemon/preload_detect.c new file mode 100644 index 0000000..6f8aaed --- /dev/null +++ b/daemon/preload_detect.c @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * preload_detect.c - LD_PRELOAD detection via /proc//environ + * + * Scans a process's null-separated environment block for the + * LD_PRELOAD variable. Used by the event pipeline on exec events. + */ + +#include +#include +#include +#include + +#include "preload_detect.h" + +#define LD_PRELOAD_PREFIX "LD_PRELOAD=" +#define LD_PRELOAD_PREFIX_LEN 11 +#define ENVIRON_MAX_SIZE (64 * 1024) + +int owl_scan_environ_for_preload(const char *buf, size_t buf_len, + char *value_out, size_t value_len) +{ + if (!buf || buf_len == 0) + return -1; + + const char *pos = buf; + const char *end = buf + buf_len; + + while (pos < end) { + size_t entry_len = strnlen(pos, (size_t)(end - pos)); + + if (entry_len >= LD_PRELOAD_PREFIX_LEN && + strncmp(pos, LD_PRELOAD_PREFIX, LD_PRELOAD_PREFIX_LEN) == 0) { + if (value_out && value_len > 0) { + const char *val = pos + LD_PRELOAD_PREFIX_LEN; + size_t vlen = entry_len - LD_PRELOAD_PREFIX_LEN; + if (vlen >= value_len) + vlen = value_len - 1; + memcpy(value_out, val, vlen); + value_out[vlen] = '\0'; + } + return 1; + } + + pos += entry_len + 1; + } + + return 0; +} + +int owl_check_preload_env(pid_t pid, char *value_out, size_t value_len) +{ + if (pid <= 0) + return -1; + + char path[64]; + snprintf(path, sizeof(path), "/proc/%d/environ", (int)pid); + + int fd = open(path, O_RDONLY); + if (fd < 0) + return -1; + + char buf[ENVIRON_MAX_SIZE]; + ssize_t n = read(fd, buf, sizeof(buf)); + close(fd); + + if (n <= 0) + return -1; + + return owl_scan_environ_for_preload(buf, (size_t)n, value_out, value_len); +} diff --git a/daemon/preload_detect.h b/daemon/preload_detect.h new file mode 100644 index 0000000..5013e0f --- /dev/null +++ b/daemon/preload_detect.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * preload_detect.h - LD_PRELOAD detection via /proc//environ + * + * Scans a process's environment for LD_PRELOAD. Called on exec events + * to detect library injection before the dynamic linker runs. + */ + +#ifndef OWLBEAR_PRELOAD_DETECT_H +#define OWLBEAR_PRELOAD_DETECT_H + +#include +#include + +/** + * owl_scan_environ_for_preload - Scan null-separated environ buffer + * @buf: Buffer of null-separated KEY=VALUE entries + * @buf_len: Length of buffer in bytes + * @value_out: Output buffer for the LD_PRELOAD value (may be NULL) + * @value_len: Size of value_out + * + * Pure function. Walks null-separated entries looking for "LD_PRELOAD=". + * Copies the value after '=' to value_out if provided. + * + * Returns: 1 found, 0 not found, -1 error (null buf or zero len) + */ +int owl_scan_environ_for_preload(const char *buf, size_t buf_len, + char *value_out, size_t value_len); + +/** + * owl_check_preload_env - Check if a process has LD_PRELOAD set + * @pid: Process to check + * @value_out: Output buffer for the LD_PRELOAD value (may be NULL) + * @value_len: Size of value_out + * + * I/O wrapper. Reads /proc//environ (up to 64KB), calls scan. + * + * Returns: 1 found, 0 not found, -1 error + */ +int owl_check_preload_env(pid_t pid, char *value_out, size_t value_len); + +#endif /* OWLBEAR_PRELOAD_DETECT_H */ diff --git a/scripts/verify.sh b/scripts/verify.sh index 35bf17a..4c31172 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" <
/dev/null; then + assert_pass "protected/ld_preload_hook triggers LIB_UNEXPECTED detection" + elif [ -f "${phase_dir}/daemon_stdout.log" ] && \ + grep -q "LIB_UNEXPECTED" "${phase_dir}/daemon_stdout.log" 2>/dev/null; then + assert_pass "protected/ld_preload_hook triggers LIB_UNEXPECTED (stdout)" + else + assert_fail "protected/ld_preload_hook LIB_UNEXPECTED detection missing" + fi + # Check daemon log for BLOCK entries if enforce mode if [ -f "${phase_dir}/daemon.log" ]; then if grep -q "\[ENFORCE\].*\[BLOCK\]" "${phase_dir}/daemon.log" 2>/dev/null; then @@ -962,7 +980,7 @@ FOOTER main() { echo "" echo -e "${BOLD}================================================${NC}" - echo -e "${BOLD} Owlbear E2E Verification (v2.0.0)${NC}" + echo -e "${BOLD} Owlbear E2E Verification (v2.1.0)${NC}" echo -e "${BOLD} Evidence Package Builder${NC}" echo -e "${BOLD}================================================${NC}" echo "" diff --git a/tests/Makefile b/tests/Makefile index 4742156..63e1dc5 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -22,7 +22,8 @@ TEST_BINS := test_events \ test_event_pipeline \ test_integrity \ test_self_protect \ - test_debugger_detect + test_debugger_detect \ + test_preload_detect .PHONY: all unit integration clean @@ -64,7 +65,7 @@ test_bpf_loader: test_harness.o test_bpf_loader.o $(DAEMON_DIR)/bpf_event_conver test_sig_loader: test_harness.o test_sig_loader.o $(DAEMON_DIR)/sig_loader.o $(DAEMON_DIR)/scanner.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_event_pipeline: test_harness.o test_event_pipeline.o $(DAEMON_DIR)/event_pipeline.o $(DAEMON_DIR)/policy.o $(DAEMON_DIR)/scanner.o +test_event_pipeline: test_harness.o test_event_pipeline.o $(DAEMON_DIR)/event_pipeline.o $(DAEMON_DIR)/policy.o $(DAEMON_DIR)/scanner.o $(DAEMON_DIR)/preload_detect.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_integrity: test_harness.o test_integrity.o $(DAEMON_DIR)/integrity.o @@ -76,6 +77,9 @@ test_self_protect: test_harness.o test_self_protect.o $(DAEMON_DIR)/self_protect test_debugger_detect: test_harness.o test_debugger_detect.o $(DAEMON_DIR)/debugger_detect.o $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) +test_preload_detect: test_harness.o test_preload_detect.o $(DAEMON_DIR)/preload_detect.o + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + # Daemon objects needed by tests $(DAEMON_DIR)/%.o: $(DAEMON_DIR)/%.c $(CC) $(CFLAGS) $(DEPFLAGS) -c -o $@ $< @@ -96,7 +100,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 $(DAEMON_DIR)/debugger_detect.o + $(RM) $(DAEMON_DIR)/self_protect.o $(DAEMON_DIR)/debugger_detect.o $(DAEMON_DIR)/preload_detect.o $(RM) $(DAEMON_DIR)/*.d -include $(wildcard *.d) diff --git a/tests/test_event_pipeline b/tests/test_event_pipeline index e43a45a..1098ee0 100755 Binary files a/tests/test_event_pipeline and b/tests/test_event_pipeline differ diff --git a/tests/test_preload_detect b/tests/test_preload_detect new file mode 100755 index 0000000..3bce9e1 Binary files /dev/null and b/tests/test_preload_detect differ diff --git a/tests/test_preload_detect.c b/tests/test_preload_detect.c new file mode 100644 index 0000000..2c03085 --- /dev/null +++ b/tests/test_preload_detect.c @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * test_preload_detect.c - Tests for LD_PRELOAD environ detection + * + * Tests the pure environ scanning function and the I/O wrapper + * that reads /proc//environ. + */ + +#include +#include +#include + +#include "test_harness.h" +#include "preload_detect.h" + +/* ------------------------------------------------------------------------- + * Pure function: owl_scan_environ_for_preload + * ----------------------------------------------------------------------- */ + +TEST(scan_preload_found) { + /* Synthetic environ buffer with LD_PRELOAD set */ + const char buf[] = "HOME=/h\0LD_PRELOAD=/path/hook.so\0PATH=/bin\0"; + char value[256] = {0}; + + int ret = owl_scan_environ_for_preload(buf, sizeof(buf) - 1, + value, sizeof(value)); + ASSERT_EQ(ret, 1); + ASSERT_STR_EQ(value, "/path/hook.so"); +} + +TEST(scan_preload_not_found) { + const char buf[] = "HOME=/h\0PATH=/bin\0TERM=xterm\0"; + char value[256] = {0}; + + int ret = owl_scan_environ_for_preload(buf, sizeof(buf) - 1, + value, sizeof(value)); + ASSERT_EQ(ret, 0); +} + +TEST(scan_null_buffer_returns_error) { + int ret = owl_scan_environ_for_preload(NULL, 0, NULL, 0); + ASSERT_EQ(ret, -1); +} + +TEST(scan_preload_null_output) { + /* LD_PRELOAD present, but value_out is NULL — should return 1 without crash */ + const char buf[] = "LD_PRELOAD=/evil.so\0PATH=/bin\0"; + + int ret = owl_scan_environ_for_preload(buf, sizeof(buf) - 1, NULL, 0); + ASSERT_EQ(ret, 1); +} + +/* ------------------------------------------------------------------------- + * I/O wrapper: owl_check_preload_env + * ----------------------------------------------------------------------- */ + +TEST(check_preload_self) { + /* Test process should not have LD_PRELOAD set */ + char value[256] = {0}; + int ret = owl_check_preload_env(getpid(), value, sizeof(value)); + ASSERT_EQ(ret, 0); +} + +/* ------------------------------------------------------------------------- + * Runner + * ----------------------------------------------------------------------- */ + +int main(void) +{ + printf("=== Owlbear Preload Detection Tests ===\n"); + + RUN_TEST(scan_preload_found); + RUN_TEST(scan_preload_not_found); + RUN_TEST(scan_null_buffer_returns_error); + RUN_TEST(scan_preload_null_output); + RUN_TEST(check_preload_self); + + TEST_SUMMARY(); + return test_failures; +}