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

### Added
- **WP2: HMAC-SHA256 code integrity** (`daemon/hmac_sha256.c`): replaces CRC32 with HMAC-SHA256 using per-session 256-bit random key from `/dev/urandom`. Uses OpenSSL one-shot `HMAC()` (not deprecated `HMAC_CTX_*`). Graviton3 ARMv8.4 SHA extensions accelerate computation.
- `daemon/hmac_sha256.h`: public API (`owl_hmac_sha256`, `owl_hmac_generate_key`)
- `tests/test_hmac_sha256.c`: 10 unit tests (TDD) — RFC 4231 known-answer vectors, empty/large input, key change, null inputs, key generation, integrity baseline+check, violation detection
- Buffer-based integrity functions (`owl_integrity_baseline_buffer`, `owl_integrity_check_buffer`) for testable verification without /proc I/O
- `scripts/verify.sh`: HMAC-SHA256 baseline assertion in protected phase, ld_preload_hook baseline assertion

### Changed
- `daemon/integrity.h`: `baseline_crc` replaced with `baseline_hmac[32]` + `hmac_key[32]` in `struct owl_integrity`
- `daemon/integrity.c`: baseline and check functions use HMAC-SHA256 via buffer variants; CRC32 retained for heartbeat `state_hash`
- `daemon/Makefile`: added `hmac_sha256.c`, linked `-lssl -lcrypto`
- `tests/Makefile`: added `test_hmac_sha256` suite, updated `test_integrity` linking with OpenSSL
- `tests/test_integrity.c`: init test checks HMAC fields instead of CRC32
- `scripts/verify.sh`: version bumped to v2.3.0
- `README.md`: updated status (v2.3.0, 125 tests, 14 suites), HMAC-SHA256 in daemon description, removed CRC32 limitation

## [2.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 @@ -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 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. LD_PRELOAD environ scanning. Process ancestry tree for correlation.
- **daemon/** - epoll on chardev + BPF ring buffer. Policy engine. Signature scanner. HMAC-SHA256 code integrity. Self-protection watchdog. TracerPid debugger detection. LD_PRELOAD environ scanning. Process ancestry tree for correlation.
- **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.
- **scripts/verify.sh** - E2E test. Baseline (cheats succeed) vs protected (cheats blocked). Machine-generated results.

## Status

v2.2.0. 115 unit tests, 13 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. Process tree tracks ancestry for correlation engine.
v2.3.0. 125 unit tests, 14 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. Process tree tracks ancestry for correlation engine.

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

## Getting started

Expand Down
3 changes: 2 additions & 1 deletion daemon/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ CFLAGS := -std=c11 -Wall -Wextra -Werror \
-D_GNU_SOURCE -D_FORTIFY_SOURCE=2 \
$(INCLUDES) $(CFLAGS_EXTRA) \
-I../ebpf
LDFLAGS := -lpthread -lcurl -lbpf -lelf -lz
LDFLAGS := -lpthread -lcurl -lbpf -lelf -lz -lssl -lcrypto
DEPFLAGS = -MMD -MP -MF $(@:.o=.d)

# Source files
Expand All @@ -22,6 +22,7 @@ SRCS := main.c \
process_tree.c \
sig_loader.c \
integrity.c \
hmac_sha256.c \
self_protect.c \
debugger_detect.c \
preload_detect.c \
Expand Down
59 changes: 59 additions & 0 deletions daemon/hmac_sha256.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* hmac_sha256.c - HMAC-SHA256 via OpenSSL one-shot HMAC()
*
* Uses the non-deprecated HMAC() function (OpenSSL 3.0+).
* Only HMAC_CTX_* is deprecated; the one-shot variant is stable.
*/

#include <fcntl.h>
#include <unistd.h>

#include <openssl/evp.h>
#include <openssl/hmac.h>

#include "hmac_sha256.h"

int owl_hmac_sha256(const uint8_t *key, size_t key_len,
const uint8_t *data, size_t data_len,
uint8_t *out)
{
if (!key || !out)
return -1;
if (!data && data_len > 0)
return -1;

unsigned int md_len = 0;
const uint8_t empty = 0;
const uint8_t *d = data ? data : &empty;

unsigned char *result = HMAC(EVP_sha256(), key, (int)key_len,
d, data_len, out, &md_len);
if (!result || md_len != OWL_HMAC_SHA256_LEN)
return -1;

return 0;
}

int owl_hmac_generate_key(uint8_t *key, size_t key_len)
{
if (!key || key_len == 0)
return -1;

int fd = open("/dev/urandom", O_RDONLY);
if (fd < 0)
return -1;

size_t total = 0;
while (total < key_len) {
ssize_t n = read(fd, key + total, key_len - total);
if (n <= 0) {
close(fd);
return -1;
}
total += (size_t)n;
}

close(fd);
return 0;
}
40 changes: 40 additions & 0 deletions daemon/hmac_sha256.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* SPDX-License-Identifier: GPL-2.0-only */
/*
* hmac_sha256.h - HMAC-SHA256 wrapper for code integrity verification
*
* Uses OpenSSL one-shot HMAC() with EVP_sha256().
* Per-session 256-bit random key from /dev/urandom.
*/

#ifndef OWLBEAR_HMAC_SHA256_H
#define OWLBEAR_HMAC_SHA256_H

#include <stddef.h>
#include <stdint.h>

#define OWL_HMAC_SHA256_LEN 32

/**
* owl_hmac_sha256 - Compute HMAC-SHA256 of a buffer
* @key: HMAC key
* @key_len: Key length in bytes
* @data: Input data (may be NULL if data_len is 0)
* @data_len: Input data length
* @out: Output buffer (must be at least OWL_HMAC_SHA256_LEN bytes)
*
* Returns 0 on success, -1 on error.
*/
int owl_hmac_sha256(const uint8_t *key, size_t key_len,
const uint8_t *data, size_t data_len,
uint8_t *out);

/**
* owl_hmac_generate_key - Generate a random key from /dev/urandom
* @key: Output buffer
* @key_len: Number of random bytes to generate
*
* Returns 0 on success, -1 on error.
*/
int owl_hmac_generate_key(uint8_t *key, size_t key_len);

#endif /* OWLBEAR_HMAC_SHA256_H */
58 changes: 51 additions & 7 deletions daemon/integrity.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
/*
* integrity.c - Userspace code integrity verification
*
* CRC32 hash of game .text segment for runtime code modification detection.
* HMAC-SHA256 of game .text segment for runtime code modification detection.
* CRC32 retained for heartbeat state_hash.
*/

#include <errno.h>
Expand All @@ -13,6 +14,7 @@
#include <unistd.h>

#include "integrity.h"
#include "hmac_sha256.h"

/* -------------------------------------------------------------------------
* CRC32 — standard polynomial 0xEDB88320 (reflected)
Expand Down Expand Up @@ -190,14 +192,20 @@ int owl_integrity_baseline(struct owl_integrity *ctx, pid_t pid)
return -1;
}

ctx->baseline_crc = owl_crc32(buf, ctx->text_size);
ctx->baseline_set = true;
if (owl_integrity_baseline_buffer(ctx, buf, ctx->text_size) < 0) {
free(buf);
return -1;
}
free(buf);

printf("owlbeard: integrity baseline: text=0x%lx size=%lu crc32=0x%08x\n",
char hex[OWL_HMAC_SHA256_LEN * 2 + 1];
for (size_t i = 0; i < OWL_HMAC_SHA256_LEN; i++)
sprintf(hex + 2 * i, "%02x", ctx->baseline_hmac[i]);

printf("owlbeard: integrity baseline: text=0x%lx size=%lu hmac=%s\n",
(unsigned long)ctx->text_start,
(unsigned long)ctx->text_size,
ctx->baseline_crc);
hex);

return 0;
}
Expand All @@ -217,10 +225,46 @@ int owl_integrity_check(const struct owl_integrity *ctx)
return -1;
}

uint32_t current_crc = owl_crc32(buf, ctx->text_size);
int result = owl_integrity_check_buffer(ctx, buf, ctx->text_size);
free(buf);

if (current_crc != ctx->baseline_crc)
return result;
}

/* -------------------------------------------------------------------------
* Buffer-based integrity functions (testable without /proc I/O)
* ----------------------------------------------------------------------- */

int owl_integrity_baseline_buffer(struct owl_integrity *ctx,
const uint8_t *buf, size_t len)
{
if (!ctx || !buf)
return -1;

if (owl_hmac_generate_key(ctx->hmac_key, OWL_HMAC_SHA256_LEN) < 0)
return -1;

if (owl_hmac_sha256(ctx->hmac_key, OWL_HMAC_SHA256_LEN,
buf, len, ctx->baseline_hmac) < 0)
return -1;

ctx->baseline_set = true;
return 0;
}

int owl_integrity_check_buffer(const struct owl_integrity *ctx,
const uint8_t *buf, size_t len)
{
if (!ctx || !buf || !ctx->baseline_set)
return -1;

uint8_t current[OWL_HMAC_SHA256_LEN];

if (owl_hmac_sha256(ctx->hmac_key, OWL_HMAC_SHA256_LEN,
buf, len, current) < 0)
return -1;

if (memcmp(current, ctx->baseline_hmac, OWL_HMAC_SHA256_LEN) != 0)
return 1; /* Integrity violation */

return 0;
Expand Down
46 changes: 38 additions & 8 deletions daemon/integrity.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
/*
* integrity.h - Userspace code integrity verification
*
* Computes a CRC32 hash of the game's .text segment from /proc/<pid>/mem
* at baseline, then periodically re-checks. Any mismatch indicates
* runtime code modification (code injection, inline hooks, etc.).
* Computes an HMAC-SHA256 of the game's .text segment from /proc/<pid>/mem
* at baseline using a per-session random key, then periodically re-checks.
* Any mismatch indicates runtime code modification (code injection,
* inline hooks, etc.).
*/

#ifndef OWLBEAR_INTEGRITY_H
Expand All @@ -15,12 +16,15 @@
#include <stdint.h>
#include <sys/types.h>

#include "hmac_sha256.h"

/* Integrity checker state */
struct owl_integrity {
pid_t target_pid;
uint64_t text_start; /* Virtual address of .text */
uint64_t text_size; /* Size of .text in bytes */
uint32_t baseline_crc; /* CRC32 captured at baseline */
uint64_t text_start; /* Virtual address of .text */
uint64_t text_size; /* Size of .text in bytes */
uint8_t baseline_hmac[OWL_HMAC_SHA256_LEN]; /* HMAC-SHA256 at baseline */
uint8_t hmac_key[OWL_HMAC_SHA256_LEN]; /* Per-session random key */
bool baseline_set;
};

Expand All @@ -31,12 +35,13 @@ struct owl_integrity {
void owl_integrity_init_ctx(struct owl_integrity *ctx);

/**
* owl_integrity_baseline - Capture baseline .text CRC32
* owl_integrity_baseline - Capture baseline .text HMAC-SHA256
* @ctx: Integrity context
* @pid: Target PID
*
* Parses /proc/<pid>/maps to find the first r-xp segment,
* reads it via /proc/<pid>/mem, and computes CRC32.
* reads it via /proc/<pid>/mem, and computes HMAC-SHA256
* with a fresh random key.
*
* Returns 0 on success, -1 on error.
*/
Expand All @@ -52,6 +57,31 @@ int owl_integrity_baseline(struct owl_integrity *ctx, pid_t pid);
*/
int owl_integrity_check(const struct owl_integrity *ctx);

/**
* owl_integrity_baseline_buffer - Capture baseline HMAC-SHA256 of a buffer
* @ctx: Integrity context
* @buf: Data buffer
* @len: Buffer length
*
* Generates a random key, computes HMAC-SHA256, stores both.
* Returns 0 on success, -1 on error.
*/
int owl_integrity_baseline_buffer(struct owl_integrity *ctx,
const uint8_t *buf, size_t len);

/**
* owl_integrity_check_buffer - Verify buffer against stored baseline
* @ctx: Integrity context (must have baseline set)
* @buf: Data buffer
* @len: Buffer length
*
* Returns 0 if buffer matches baseline.
* Returns 1 if buffer has changed (integrity violation).
* Returns -1 on error.
*/
int owl_integrity_check_buffer(const struct owl_integrity *ctx,
const uint8_t *buf, size_t len);

/**
* owl_crc32 - Compute CRC32 of a buffer
* @buf: Data buffer
Expand Down
25 changes: 23 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 (v2.2.0)
# Owlbear E2E Verification Report (v2.3.0)
# Generated: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Host: $(uname -n)
# Kernel: $(uname -r)
Expand Down Expand Up @@ -539,6 +539,18 @@ phase_baseline() {
assert_fail "baseline/dev_mem_reader should succeed or skip without module" "exit_code=${rc}"
fi

# --- ld_preload_hook (baseline) ---
run_cheat_captured "${phase_dir}" "ld_preload_hook" \
"${cheats_dir}/ld_preload_hook.bin" \
"${PROJECT_DIR}/game/owlbear-game" "--no-curses"

rc=$(cat "${phase_dir}/ld_preload_hook/exit_code")
if [ "$rc" -eq 0 ] || [ "$rc" -eq 124 ]; then
assert_pass "baseline/ld_preload_hook runs without module (code=${rc})"
else
assert_fail "baseline/ld_preload_hook should succeed 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 @@ -855,6 +867,15 @@ phase_protected() {
assert_pass "protected/code integrity baseline captured"
fi

# Check integrity baseline uses HMAC-SHA256 (not CRC32)
if [ -f "${phase_dir}/daemon_stdout.log" ] && \
grep -q "hmac=" "${phase_dir}/daemon_stdout.log" 2>/dev/null; then
assert_pass "protected/code integrity uses HMAC-SHA256"
elif [ -f "${phase_dir}/daemon_stdout.log" ] && \
grep -q "crc32=" "${phase_dir}/daemon_stdout.log" 2>/dev/null; then
assert_fail "protected/code integrity still uses CRC32 (should be HMAC-SHA256)"
fi

stop_daemon

# Unload module
Expand Down Expand Up @@ -980,7 +1001,7 @@ FOOTER
main() {
echo ""
echo -e "${BOLD}================================================${NC}"
echo -e "${BOLD} Owlbear E2E Verification (v2.2.0)${NC}"
echo -e "${BOLD} Owlbear E2E Verification (v2.3.0)${NC}"
echo -e "${BOLD} Evidence Package Builder${NC}"
echo -e "${BOLD}================================================${NC}"
echo ""
Expand Down
Loading
Loading