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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ 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.0.0] - 2026-03-15

### Added
- **WP1c: Block /dev/mem and /dev/kmem via eBPF LSM** (`ebpf/owlbear_lsm.bpf.c`): extends `file_open` hook to unconditionally block `/dev/mem` and `/dev/kmem` opens with -EPERM. Physical memory access bypasses all process-level protections, so no PID allowlist check is performed. Emits `OWL_EVENT_DEV_MEM_ACCESS` (severity CRITICAL, target_pid=0). Detail string distinguishes `/dev/mem` vs `/dev/kmem`.
- `OWL_EVENT_DEV_MEM_ACCESS` (0x0106) in shared event header and BPF common header (breaking: new event type)
- `cheats/dev_mem_reader.c`: attack program that attempts to open `/dev/mem` and `/dev/kmem` and read 256 bytes of raw physical memory
- `tests/test_bpf_loader.c`: conversion test for `OWL_EVENT_DEV_MEM_ACCESS` (102 total tests)
- `scripts/verify.sh`: baseline and protected phase assertions for dev_mem_reader, handles CONFIG_STRICT_DEVMEM (EACCES) gracefully

### Changed
- `daemon/bpf_event_convert.c`: `OWL_EVENT_DEV_MEM_ACCESS` falls through to memory payload conversion
- `daemon/main.c`: `event_type_str()` and `print_event()` handle `DEV_MEM_ACCESS`
- `cheats/Makefile`: builds `dev_mem_reader.bin`
- `scripts/verify.sh`: version bumped to v2.0.0
- `README.md`: updated status (v2.0.0, 102 tests, 9 attack programs), added /dev/mem blocking

## [1.2.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 @@ -9,16 +9,16 @@ Not production software.
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.
- **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.
- **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.
- **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.
- **scripts/verify.sh** - E2E test. Baseline (cheats succeed) vs protected (cheats blocked). Machine-generated results.

## Status

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

Prototype limitations: linear signature scan, CRC32 not cryptographic, no fleet management.

Expand Down
3 changes: 2 additions & 1 deletion cheats/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ CHEAT_SRCS := mem_reader.c \
mprotect_injector.c \
mprotect_inject_via_ptrace.c \
ld_preload_hook.c \
debug_reg_setter.c
debug_reg_setter.c \
dev_mem_reader.c

CHEAT_BINS := $(CHEAT_SRCS:.c=.bin)
CHEAT_DEPS := $(CHEAT_SRCS:.c=.d)
Expand Down
89 changes: 89 additions & 0 deletions cheats/dev_mem_reader.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* dev_mem_reader - Test cheat using /dev/mem
*
* Attempts to open /dev/mem and read the first 256 bytes of physical
* memory. This bypasses all process-level protections (ptrace hooks,
* /proc/pid/mem blocks, process_vm_* interception) because it accesses
* raw physical memory directly.
*
* Should be blocked by owlbear's eBPF LSM file_open hook.
*
* Also attempts /dev/kmem if /dev/mem is blocked, to verify both
* paths are covered.
*
* Usage: dev_mem_reader
*/

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int try_dev_path(const char *path)
{
int fd;
unsigned char buf[256];
ssize_t n;

printf("[dev_mem_reader] Opening %s...\n", path);

fd = open(path, O_RDONLY);
if (fd < 0) {
int err = errno;
fprintf(stderr, "[dev_mem_reader] open(%s) failed: %s (errno=%d)\n",
path, strerror(err), err);
return -err;
}

n = read(fd, buf, sizeof(buf));
if (n < 0) {
int err = errno;
fprintf(stderr, "[dev_mem_reader] read(%s) failed: %s (errno=%d)\n",
path, strerror(err), err);
close(fd);
return -err;
}

printf("[CHEAT] Read %zd bytes from %s\n", n, path);
close(fd);
return 0;
}

int main(void)
{
int rc;

printf("[dev_mem_reader] Attempting raw physical memory access\n");
printf("[dev_mem_reader] This should be blocked by eBPF LSM\n\n");

rc = try_dev_path("/dev/mem");
if (rc == 0)
return EXIT_SUCCESS;

if (rc == -EPERM) {
fprintf(stderr, "[dev_mem_reader] Blocked by anti-cheat (EPERM)\n");
} else if (rc == -EACCES) {
fprintf(stderr, "[dev_mem_reader] Blocked by CONFIG_STRICT_DEVMEM (EACCES)\n");
} else if (rc == -ENOENT) {
fprintf(stderr, "[dev_mem_reader] /dev/mem does not exist\n");
}

/* Also try /dev/kmem */
printf("\n");
rc = try_dev_path("/dev/kmem");
if (rc == 0)
return EXIT_SUCCESS;

if (rc == -EPERM)
fprintf(stderr, "[dev_mem_reader] /dev/kmem also blocked (EPERM)\n");
else if (rc == -EACCES)
fprintf(stderr, "[dev_mem_reader] /dev/kmem blocked by kernel config (EACCES)\n");
else if (rc == -ENOENT)
fprintf(stderr, "[dev_mem_reader] /dev/kmem does not exist\n");

/* Exit 0 if at least one path was accessible, 1 otherwise.
* The E2E script checks stderr for EPERM vs EACCES. */
return EXIT_FAILURE;
}
1 change: 1 addition & 0 deletions daemon/bpf_event_convert.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ int owl_bpf_event_convert(const void *bpf_data, size_t bpf_size,
case OWL_EVENT_VM_READV_ATTEMPT:
case OWL_EVENT_VM_WRITEV_ATTEMPT:
case OWL_EVENT_MPROTECT_EXEC:
case OWL_EVENT_DEV_MEM_ACCESS:
out->payload.memory.caller_pid = bev->pid;
memcpy(out->payload.memory.caller_comm, bev->comm,
sizeof(out->payload.memory.caller_comm) < sizeof(bev->comm)
Expand Down
2 changes: 2 additions & 0 deletions daemon/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ static const char *event_type_str(uint32_t type)
case OWL_EVENT_VM_WRITEV_ATTEMPT: return "VM_WRITEV_ATTEMPT";
case OWL_EVENT_EXEC_MMAP: return "EXEC_MMAP";
case OWL_EVENT_MPROTECT_EXEC: return "MPROTECT_EXEC";
case OWL_EVENT_DEV_MEM_ACCESS: return "DEV_MEM_ACCESS";
case OWL_EVENT_MODULE_LOAD: return "MODULE_LOAD";
case OWL_EVENT_MODULE_UNKNOWN: return "MODULE_UNKNOWN";
case OWL_EVENT_CODE_INTEGRITY_FAIL: return "CODE_INTEGRITY_FAIL";
Expand Down Expand Up @@ -181,6 +182,7 @@ static void print_event(const struct owlbear_event *ev, FILE *out)
case OWL_EVENT_VM_READV_ATTEMPT:
case OWL_EVENT_VM_WRITEV_ATTEMPT:
case OWL_EVENT_MPROTECT_EXEC:
case OWL_EVENT_DEV_MEM_ACCESS:
fprintf(out, " caller_pid=%u caller=%s",
ev->payload.memory.caller_pid,
ev->payload.memory.caller_comm);
Expand Down
1 change: 1 addition & 0 deletions ebpf/owlbear_common.bpf.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#define OWL_EVENT_VM_WRITEV_ATTEMPT 0x0103
#define OWL_EVENT_EXEC_MMAP 0x0104
#define OWL_EVENT_MPROTECT_EXEC 0x0105
#define OWL_EVENT_DEV_MEM_ACCESS 0x0106
#define OWL_EVENT_MODULE_LOAD 0x0200

#define OWL_SEV_INFO 0
Expand Down
42 changes: 34 additions & 8 deletions ebpf/owlbear_lsm.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*
* Hooks:
* lsm/ptrace_access_check — block ptrace on protected PID
* lsm/file_open — block /proc/<pid>/mem access
* lsm/file_open — block /proc/<pid>/mem, /dev/mem, /dev/kmem
* lsm/mmap_file — monitor executable mmap in game
*
* Requires: CONFIG_BPF_LSM=y in kernel config.
Expand Down Expand Up @@ -44,11 +44,12 @@ int BPF_PROG(owl_ptrace_check, struct task_struct *child, unsigned int mode)
/* -------------------------------------------------------------------------
* LSM: file_open
*
* Called when a file is opened. We check if the file is /proc/<pid>/mem
* for a protected PID. If so, deny access from non-whitelisted callers.
* Called when a file is opened. Blocks:
* /dev/mem, /dev/kmem — unconditional (physical memory = system-wide threat)
* /proc/<pid>/mem — if PID is protected and caller not whitelisted
*
* Path checking in BPF is limited — we read the dentry name and check
* if it equals "mem", then verify the parent directory is a protected PID.
* parent/grandparent to distinguish /dev/mem from /proc/<pid>/mem.
* ----------------------------------------------------------------------- */

SEC("lsm/file_open")
Expand All @@ -67,15 +68,20 @@ int BPF_PROG(owl_file_open, struct file *file)
if (!dentry)
return 0;

/* Check if filename is "mem" */
/* Check if filename is "mem" or "kmem" */
dname = BPF_CORE_READ(dentry, d_name);
bpf_probe_read_kernel_str(name_buf, sizeof(name_buf), dname.name);

if (name_buf[0] != 'm' || name_buf[1] != 'e' ||
name_buf[2] != 'm' || name_buf[3] != '\0')
bool is_mem = (name_buf[0] == 'm' && name_buf[1] == 'e' &&
name_buf[2] == 'm' && name_buf[3] == '\0');
bool is_kmem = (name_buf[0] == 'k' && name_buf[1] == 'm' &&
name_buf[2] == 'e' && name_buf[3] == 'm' &&
name_buf[4] == '\0');

if (!is_mem && !is_kmem)
return 0;

/* Get parent directory name (should be the PID) */
/* Get parent directory name */
parent = BPF_CORE_READ(dentry, d_parent);
if (!parent)
return 0;
Expand All @@ -84,6 +90,26 @@ int BPF_PROG(owl_file_open, struct file *file)
bpf_probe_read_kernel_str(parent_buf, sizeof(parent_buf),
parent_name.name);

/*
* /dev/mem or /dev/kmem — unconditional block.
* Physical memory access threatens ALL processes.
*/
bool is_dev_parent = (parent_buf[0] == 'd' && parent_buf[1] == 'e' &&
parent_buf[2] == 'v' && parent_buf[3] == '\0');

if (is_dev_parent && (is_mem || is_kmem)) {
caller_pid = bpf_get_current_pid_tgid() >> 32;
emit_event(OWL_EVENT_DEV_MEM_ACCESS, OWL_SEV_CRITICAL,
caller_pid, 0,
is_kmem ? "BPF LSM: /dev/kmem blocked"
: "BPF LSM: /dev/mem blocked");
return -EPERM;
}

/* Only "mem" is relevant under /proc; kmem handled above */
if (!is_mem)
return 0;

/*
* Verify parent's parent is "proc" — this distinguishes
* /proc/<pid>/mem from other files named "mem".
Expand Down
1 change: 1 addition & 0 deletions include/owlbear_events.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum owlbear_event_type {
OWL_EVENT_VM_WRITEV_ATTEMPT = 0x0103,
OWL_EVENT_EXEC_MMAP = 0x0104,
OWL_EVENT_MPROTECT_EXEC = 0x0105,
OWL_EVENT_DEV_MEM_ACCESS = 0x0106,

/* Integrity checks (0x02xx) */
OWL_EVENT_MODULE_LOAD = 0x0200,
Expand Down
51 changes: 48 additions & 3 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.2.0)
# Owlbear E2E Verification Report (v2.0.0)
# Generated: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Host: $(uname -n)
# Kernel: $(uname -r)
Expand All @@ -324,7 +324,8 @@ HEADER
for bin in game/owlbear-game cheats/mem_reader.bin cheats/proc_mem_reader.bin \
cheats/ptrace_injector.bin cheats/ptrace_writer.bin \
cheats/vm_writer.bin cheats/mprotect_injector.bin \
cheats/mprotect_inject_via_ptrace.bin; do
cheats/mprotect_inject_via_ptrace.bin \
cheats/dev_mem_reader.bin; do
if [ ! -f "${PROJECT_DIR}/${bin}" ]; then
warn "Missing: ${bin} — building..."
missing=1
Expand Down Expand Up @@ -521,6 +522,22 @@ phase_baseline() {
assert_fail "baseline/mprotect_inject_via_ptrace injection did not complete"
fi

# --- dev_mem_reader ---
run_cheat_captured "${phase_dir}" "dev_mem_reader" \
"${cheats_dir}/dev_mem_reader.bin"

rc=$(cat "${phase_dir}/dev_mem_reader/exit_code")
if [ "$rc" -eq 0 ]; then
assert_pass "baseline/dev_mem_reader reads physical memory (code=${rc})"
elif grep -q "EACCES\|Permission denied" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null && \
! grep -q "EPERM" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
assert_skip "baseline/dev_mem_reader" "CONFIG_STRICT_DEVMEM blocks /dev/mem (EACCES)"
elif grep -q "No such file" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
assert_skip "baseline/dev_mem_reader" "/dev/mem does not exist"
else
assert_fail "baseline/dev_mem_reader should succeed or skip without module" "exit_code=${rc}"
fi

capture_dmesg > "${phase_dir}/dmesg_after.txt"
dmesg_since "${phase_mark}" "${phase_dir}/dmesg_phase_diff.txt"

Expand Down Expand Up @@ -767,6 +784,34 @@ phase_protected() {
assert_pass "protected/mprotect_inject_via_ptrace ptrace denied (EPERM)"
fi

# --- dev_mem_reader ---
run_cheat_captured "${phase_dir}" "dev_mem_reader" \
"${cheats_dir}/dev_mem_reader.bin"

# Assertion 1: open blocked with EPERM (our hook) or EACCES (kernel config)
if grep -q "EPERM" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
assert_pass "protected/dev_mem_reader blocked by eBPF LSM (EPERM)"
elif grep -q "EACCES\|Permission denied" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
assert_skip "protected/dev_mem_reader" "CONFIG_STRICT_DEVMEM blocks before LSM (EACCES)"
elif grep -q "No such file" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
assert_skip "protected/dev_mem_reader" "/dev/mem does not exist"
else
assert_fail "protected/dev_mem_reader should be blocked" \
"exit_code=$(cat "${phase_dir}/dev_mem_reader/exit_code")"
fi

# Assertion 2: DEV_MEM_ACCESS event in daemon log
if grep -q "EPERM" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
if [ -f "${phase_dir}/daemon.log" ] && \
grep -q "DEV_MEM_ACCESS" "${phase_dir}/daemon.log" 2>/dev/null; then
assert_pass "protected/dev_mem_reader DEV_MEM_ACCESS event in daemon log"
else
assert_fail "protected/dev_mem_reader DEV_MEM_ACCESS missing from daemon log"
fi
elif grep -q "EACCES\|Permission denied\|No such file" "${phase_dir}/dev_mem_reader/stderr.log" 2>/dev/null; then
assert_skip "protected/dev_mem_reader event check" "kernel blocked before LSM hook"
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
Expand Down Expand Up @@ -917,7 +962,7 @@ FOOTER
main() {
echo ""
echo -e "${BOLD}================================================${NC}"
echo -e "${BOLD} Owlbear E2E Verification (v1.2.0)${NC}"
echo -e "${BOLD} Owlbear E2E Verification (v2.0.0)${NC}"
echo -e "${BOLD} Evidence Package Builder${NC}"
echo -e "${BOLD}================================================${NC}"
echo ""
Expand Down
Binary file modified tests/test_bpf_loader
Binary file not shown.
23 changes: 23 additions & 0 deletions tests/test_bpf_loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,28 @@ TEST(bpf_convert_mprotect_event) {
ASSERT_EQ(out.payload.memory.caller_pid, 500);
}

TEST(bpf_convert_dev_mem_access_event) {
struct test_bpf_event bev;
struct owlbear_event out;

memset(&bev, 0, sizeof(bev));
bev.event_type = OWL_EVENT_DEV_MEM_ACCESS;
bev.severity = OWL_SEV_CRITICAL;
bev.pid = 600;
bev.target_pid = 0;
strncpy(bev.comm, "dev_mem_read", sizeof(bev.comm));
strncpy(bev.detail, "BPF LSM: /dev/mem blocked", sizeof(bev.detail));

int ret = owl_bpf_event_convert(&bev, sizeof(bev), &out);
ASSERT_EQ(ret, 0);
ASSERT_EQ(out.event_type, OWL_EVENT_DEV_MEM_ACCESS);
ASSERT_EQ(out.severity, OWL_SEV_CRITICAL);
ASSERT_EQ(out.source, OWL_SRC_EBPF);
ASSERT_EQ(out.pid, 600);
ASSERT_EQ(out.target_pid, 0);
ASSERT_EQ(out.payload.memory.caller_pid, 600);
}

TEST(bpf_convert_null_input_fails) {
struct owlbear_event out;

Expand Down Expand Up @@ -151,6 +173,7 @@ int main(void)
RUN_TEST(bpf_convert_module_load_event);
RUN_TEST(bpf_convert_vm_readv_event);
RUN_TEST(bpf_convert_mprotect_event);
RUN_TEST(bpf_convert_dev_mem_access_event);
RUN_TEST(bpf_convert_null_input_fails);
RUN_TEST(bpf_convert_null_output_fails);
RUN_TEST(bpf_convert_too_small_fails);
Expand Down
Loading