From a79a1569b61b83935ee7ac983e248100205f5ebb Mon Sep 17 00:00:00 2001 From: Brad Edwards Date: Sun, 15 Mar 2026 06:37:58 +0000 Subject: [PATCH] LD_PRELOAD detection on process exec --- CHANGELOG.md | 15 +++++++ README.md | 4 +- daemon/Makefile | 1 + daemon/event_pipeline.c | 42 +++++++++++++++++++ daemon/preload_detect.c | 71 ++++++++++++++++++++++++++++++++ daemon/preload_detect.h | 42 +++++++++++++++++++ scripts/verify.sh | 24 +++++++++-- tests/Makefile | 10 +++-- tests/test_event_pipeline | Bin 26584 -> 26864 bytes tests/test_preload_detect | Bin 0 -> 21208 bytes tests/test_preload_detect.c | 80 ++++++++++++++++++++++++++++++++++++ 11 files changed, 281 insertions(+), 8 deletions(-) create mode 100644 daemon/preload_detect.c create mode 100644 daemon/preload_detect.h create mode 100755 tests/test_preload_detect create mode 100644 tests/test_preload_detect.c 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 e43a45ad98fdda248f760fb127707c712fc6ad68..1098ee0af1904287f67be10f2bc25acb91de67fb 100755 GIT binary patch delta 7379 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 delta 5951 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 diff --git a/tests/test_preload_detect b/tests/test_preload_detect new file mode 100755 index 0000000000000000000000000000000000000000..3bce9e1fb3b739658cb18b78d0dea1cc5dea38a2 GIT binary patch literal 21208 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 k!*$mdlC*44xyL+0_RG3-nt4>D=mWYiA/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; +}