From f73c43d33d130f667c9f7f8d85527422433db798 Mon Sep 17 00:00:00 2001 From: caimengci Date: Fri, 28 Aug 2026 15:20:24 +0800 Subject: [PATCH 1/2] feat: add device-observe tool --- docs/device-observe.md | 180 ++++++++++++++++++++++ observe/device-observe.cpp | 302 +++++++++++++++++++++++++++++++++++++ observe/device-observe.h | 50 ++++++ 3 files changed, 532 insertions(+) create mode 100644 docs/device-observe.md create mode 100644 observe/device-observe.cpp create mode 100644 observe/device-observe.h diff --git a/docs/device-observe.md b/docs/device-observe.md new file mode 100644 index 0000000..f30450e --- /dev/null +++ b/docs/device-observe.md @@ -0,0 +1,180 @@ +# device-observe + +## 功能描述 + +device-observe 是基于 eBPF 的外设使用实时监控工具,能够记录系统中摄像头(V4L2)和麦克风(ALSA 录音设备)的打开、关闭、流启停等操作,输出调用进程 PID、时间戳、设备路径等完整信息。 + +## 使用方式 + +```bash +$ sudo ./build/observe/device-observe -h +Usage: device-observe [OPTION...] +device-observe - Monitor camera and microphone device access + +USAGE: device-observe [OPTIONS] + +EXAMPLES: + device-observe # Monitor all camera and mic events + device-observe -c # Monitor camera only + device-observe -m # Monitor microphone only + device-observe -p 1234 # Monitor process 1234 only + device-observe -t -v # Timestamp + verbose output + + -p, --pid=PID Trace process with this PID only + -c, --camera Monitor camera devices (default: on) + -C, --no-camera Disable camera monitoring + -m, --microphone Monitor microphone devices (default: on) + -M, --no-microphone Disable microphone monitoring + -t, --timestamp Include timestamp in output + -v, --verbose Verbose output (show all ioctl events) + -?, --help Give this help list +``` + +## 参数说明 + +- `-p, --pid=PID`:只跟踪指定 PID 的进程。 +- `-c, --camera`:启用摄像头监控(默认开启)。 +- `-C, --no-camera`:关闭摄像头监控。 +- `-m, --microphone`:启用麦克风监控(默认开启)。 +- `-M, --no-microphone`:关闭麦克风监控。 +- `-t, --timestamp`:在输出中包含时间戳。 +- `-v, --verbose`:详细模式,显示所有 ioctl 事件(默认仅显示 STREAM_ON/OFF)。 + +## 使用示例 + +### 示例输出 + +1) 监控所有摄像头和麦克风事件(默认): + +```bash +$ sudo ./build/observe/device-observe +DEVICE EVENT IOCTL PID COMM UID PATH +Tracing device access... Hit Ctrl-C to end. +CAMERA OPEN 1234 wechat 1000 /dev/video0 fd=3 +CAMERA STREAM_ON VIDIOC_STREAMON 1234 wechat 1000 /dev/video0 fd=3 +CAMERA STREAM_OFF VIDIOC_STREAMOFF 1234 wechat 1000 /dev/video0 fd=3 +CAMERA CLOSE 1234 wechat 1000 /dev/video0 fd=3 +MIC OPEN 5678 pipewire 0 /dev/snd/pcmC0D0c fd=12 +MIC STREAM_ON SNDRV_PCM_IOCTL_START 5678 pipewire 0 /dev/snd/pcmC0D0c fd=12 +``` + +2) 仅监控摄像头,带时间戳: + +```bash +$ sudo ./build/observe/device-observe -C -t +TIME DEVICE EVENT IOCTL PID COMM UID PATH +10:23:45 CAMERA OPEN 1234 wechat 1000 /dev/video0 fd=3 +``` + +3) 跟踪指定进程的麦克风使用: + +```bash +$ sudo ./build/observe/device-observe -C -p 19876 +``` + +## 技术原理 + +### 整体架构 + +``` + 用户进程 + | + | openat(/dev/video0) openat(/dev/snd/pcmC0D0c) ioctl(VIDIOC_STREAMON) + v + ---- Linux Kernel ---- + +--------------------+ +------------------+ +------------------+ + | sys_enter_openat |---->| sys_enter_ioctl | | sys_enter_close | + | (tracepoint) | | (tracepoint) | | (tracepoint) | + +--------------------+ +------------------+ +------------------+ + | | | + v v v + +--------------------------------------------------------------+ + | eBPF Programs | + | - openat enter: 读 filename,匹配设备路径前缀 | + | - openat exit: 取 fd,发 DEV_EVT_OPEN,注册 fd_table | + | - ioctl enter: 查 fd_table,匹配 STREAM_ON/OFF cmd | + | - close enter: 查 fd_table,发 DEV_EVT_CLOSE,清理 | + +--------------------------------------------------------------+ + | + v bpf_ringbuf + +------------------+ + | 用户空间程序 | 格式化输出到终端 + +------------------+ +``` + +### 设备识别方式 + +| 设备 | 设备文件路径模式 | 内核子系统 | Major 号 | +|--------|-------------------------------|-----------|----------| +| 摄像头 | `/dev/video*` | V4L2 | 81 | +| 麦克风 | `/dev/snd/pcmCDc` | ALSA PCM | 116 | + +- 摄像头:匹配路径前缀 `/dev/video`(V4L2 设备,major=81) +- 麦克风:匹配路径前缀 `/dev/snd/pcmC` 且路径末尾字符为 `c`(capture 设备,playback 为 `p`) + +### 关键 ioctl 命令 + +| 设备 | 命令 | 值 | 含义 | +|--------|--------------------------|-----------|------------------| +| 摄像头 | VIDIOC_STREAMON | 0x40045612 | 开始视频采集 | +| 摄像头 | VIDIOC_STREAMOFF | 0x40045613 | 停止视频采集 | +| 麦克风 | SNDRV_PCM_IOCTL_START | 0x00004142 | 开始录音 | +| 麦克风 | SNDRV_PCM_IOCTL_DROP | 0x00004143 | 停止录音 | + +### BPF 数据流 + +1. **openat enter**:读用户空间 filename,匹配设备路径前缀,将设备类型和路径存入 `open_pending` map(key: pid_tgid) +2. **openat exit**:取返回值作为 fd,查 `open_pending` 获取设备信息,发送 DEV_EVT_OPEN 事件,将 (tgid, fd) → device_type 存入 `fd_table` +3. **ioctl enter**:查 `fd_table` 判断 fd 是否为已知设备,匹配关键 ioctl 命令,发送 DEV_EVT_STREAM_ON/DEV_EVT_STREAM_OFF 事件 +4. **close enter**:查 `fd_table` 判断 fd 是否为已知设备,发送 DEV_EVT_CLOSE 事件,清理 `fd_table` + +### BPF Maps + +| Map 名称 | 类型 | Key | Value | 用途 | +|---------------|------------------------|-------------------|-------------------|----------------------| +| open_pending | BPF_MAP_TYPE_HASH | pid_tgid | open_info struct | openat enter→exit 传参 | +| fd_table | BPF_MAP_TYPE_HASH | (tgid<<32 \| fd) | u32 device_type | ioctl/close 时查询 | +| events | BPF_MAP_TYPE_RINGBUF | - | devobs_event | 事件输出到用户空间 | + +### 事件结构体 + +```c +struct devobs_event +{ + __u64 timestamp_ns; // 纳秒级时间戳 + __u32 pid; // 进程 ID + __u32 tid; // 线程 ID + __u32 uid; // 用户 ID + __u32 device_type; // DEV_TYPE_CAMERA / DEV_TYPE_MICROPHONE + __u32 event_type; // DEV_EVT_OPEN / CLOSE / STREAM_ON / STREAM_OFF + int fd; // 文件描述符 + __u32 ioctl_cmd; // ioctl 命令码 + char comm[16]; // 进程名 + char device_path[64]; // 设备路径 +}; +``` + +### 用户空间过滤 + +通过 BPF skeleton 的 rodata 机制,用户空间在 BPF 加载前写入过滤条件: + +| rodata 变量 | 类型 | 默认值 | 说明 | +|---------------------|------|--------|---------------------------| +| filter_camera | bool | true | 是否启用摄像头监控 | +| filter_microphone | bool | true | 是否启用麦克风监控 | +| target_pid | int | 0 | 过滤指定 PID(0 表示不过滤)| + +### PipeWire 场景说明 + +在 Deepin/UOS 等现代 Linux 桌面上,应用通常不直接打开 ALSA/V4L2 设备节点,而是通过 PipeWire(或旧版 PulseAudio)代理访问。此时: + +- 本工具会记录 **pipewire 守护进程** 打开设备节点的行为 +- PipeWire 作为中间层,打开 `/dev/video0` 或 `/dev/snd/pcmC0D0c` 进行实际的硬件交互 +- 若需追溯最终请求应用,需结合 PipeWire 的 D-Bus 接口或 `pw-cli` 命令进一步查询 + +## 已知限制 + +1. 仅识别通过标准设备路径打开的设备(`/dev/video*`、`/dev/snd/pcmC*D*c`),不识别通过符号链接或 `/dev/v4l/by-path/` 等路径的打开 +2. PipeWire 代理场景下记录的是 pipewire 进程而非最终应用 +3. 不追踪通过 dup/dup2 继承的 fd +4. 不追踪通过 sendmsg/SCM_RIGHTS 传递的 fd diff --git a/observe/device-observe.cpp b/observe/device-observe.cpp new file mode 100644 index 0000000..57b8bf9 --- /dev/null +++ b/observe/device-observe.cpp @@ -0,0 +1,302 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd +// +// SPDX-License-Identifier: LGPL-2.1 + +/** + * device-observe - Monitor camera and microphone device access + * + * Tracks which processes open/use camera (V4L2) and microphone (ALSA capture) + * devices, recording PID, timestamp, and operation type. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include "device-observe.skel.h" +#include "device-observe.h" +#include "com.h" + +static struct ring_buffer *rb = NULL; +static struct device_observe_bpf *obj = NULL; +static volatile bool exiting = false; + +struct env +{ + __u32 pid; + bool camera; + bool microphone; + bool timestamp; + bool verbose; +} env = { + .pid = 0, + .camera = true, + .microphone = true, + .timestamp = false, + .verbose = false, +}; + +static const struct argp_option opts[] = { + {"pid", 'p', "PID", 0, "Trace process with this PID only"}, + {"camera", 'c', NULL, 0, "Monitor camera devices (default: on)"}, + {"no-camera", 'C', NULL, 0, "Disable camera monitoring"}, + {"microphone",'m', NULL, 0, "Monitor microphone devices (default: on)"}, + {"no-microphone", 'M', NULL, 0, "Disable microphone monitoring"}, + {"timestamp", 't', NULL, 0, "Include timestamp in output"}, + {"verbose", 'v', NULL, 0, "Verbose output (show all ioctl events)"}, + {NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help"}, + {}, +}; + +static const char program_doc[] = + "device-observe - Monitor camera and microphone device access\n" + "\n" + "USAGE: device-observe [OPTIONS]\n" + "\n" + "EXAMPLES:\n" + " device-observe # Monitor all camera and mic events\n" + " device-observe -c # Monitor camera only\n" + " device-observe -m # Monitor microphone only\n" + " device-observe -p 1234 # Monitor process 1234 only\n" + " device-observe -t -v # Timestamp + verbose output\n"; + +static error_t parse_arg(int key, char *arg, struct argp_state *state) +{ + switch (key) + { + case 'p': + errno = 0; + env.pid = strtoul(arg, NULL, 10); + if (errno) + { + fprintf(stderr, "invalid PID: %s\n", arg); + argp_usage(state); + } + break; + case 'c': + env.camera = true; + break; + case 'C': + env.camera = false; + break; + case 'm': + env.microphone = true; + break; + case 'M': + env.microphone = false; + break; + case 't': + env.timestamp = true; + break; + case 'v': + env.verbose = true; + break; + case 'h': + argp_state_help(state, stderr, ARGP_HELP_STD_HELP); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +static const char *device_type_str(__u32 type) +{ + switch (type) + { + case DEV_TYPE_CAMERA: + return "CAMERA"; + case DEV_TYPE_MICROPHONE: + return "MIC"; + default: + return "UNKNOWN"; + } +} + +static const char *event_type_str(__u32 type) +{ + switch (type) + { + case DEV_EVT_OPEN: + return "OPEN"; + case DEV_EVT_CLOSE: + return "CLOSE"; + case DEV_EVT_STREAM_ON: + return "STREAM_ON"; + case DEV_EVT_STREAM_OFF: + return "STREAM_OFF"; + case DEV_EVT_IOCTL: + return "IOCTL"; + default: + return "UNKNOWN"; + } +} + +static const char *ioctl_cmd_name(__u32 device_type, __u32 cmd) +{ + if (device_type == DEV_TYPE_CAMERA) + { + if (cmd == V4L2_STREAM_ON) + return "VIDIOC_STREAMON"; + if (cmd == V4L2_STREAM_OFF) + return "VIDIOC_STREAMOFF"; + return NULL; + } + if (device_type == DEV_TYPE_MICROPHONE) + { + if (cmd == ALSA_PCM_START) + return "SNDRV_PCM_IOCTL_START"; + if (cmd == ALSA_PCM_DROP) + return "SNDRV_PCM_IOCTL_DROP"; + return NULL; + } + return NULL; +} + +static void print_event(const struct devobs_event *e) +{ + char ts[32] = ""; + if (env.timestamp) + { + time_t t = e->timestamp_ns / 1000000000ULL; + struct tm *tm_info = localtime(&t); + if (tm_info) + strftime(ts, sizeof(ts), "%H:%M:%S", tm_info); + } + + const char *iname = ioctl_cmd_name(e->device_type, e->ioctl_cmd); + + if (env.timestamp) + printf("%-8s ", ts); + + printf("%-12s %-10s %-24s %-6u %-16s %-6u ", + device_type_str(e->device_type), + event_type_str(e->event_type), + iname ? iname : "", + e->pid, + e->comm, + e->uid); + + /* non-verbose mode: skip generic ioctl (only show STREAM_ON/OFF) */ + if (e->event_type == DEV_EVT_IOCTL && !env.verbose) + { + printf("%s fd=%d\n", e->device_path, e->fd); + return; + } + + printf("%s fd=%d", e->device_path, e->fd); + + if (e->event_type == DEV_EVT_IOCTL && iname) + printf(" cmd=0x%x", e->ioctl_cmd); + + printf("\n"); +} + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + const struct devobs_event *e = (const struct devobs_event *)data; + print_event(e); + return 0; +} + +static void sig_handler(int sig) +{ + exiting = true; +} + +int main(int argc, char **argv) +{ + int err; + static const struct argp argp = { + .options = opts, + .parser = parse_arg, + .doc = program_doc, + }; + + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) + return err; + + if (!env.camera && !env.microphone) + { + fprintf(stderr, "error: at least one of --camera or --microphone must be enabled\n"); + return 1; + } + + /* print column headers */ + if (env.timestamp) + printf("%-8s ", "TIME"); + printf("%-12s %-10s %-24s %-6s %-16s %-6s %s\n", + "DEVICE", "EVENT", "IOCTL", "PID", "COMM", "UID", "PATH"); + + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + + obj = device_observe_bpf__open(); + if (!obj) + { + fprintf(stderr, "failed to open BPF object\n"); + return 1; + } + + /* set filter via rodata */ + obj->rodata->filter_camera = env.camera; + obj->rodata->filter_microphone = env.microphone; + obj->rodata->target_pid = env.pid; + + err = device_observe_bpf__load(obj); + if (err) + { + fprintf(stderr, "failed to load BPF object: %d\n", err); + goto cleanup; + } + + err = device_observe_bpf__attach(obj); + if (err) + { + fprintf(stderr, "failed to attach BPF programs: %d\n", err); + goto cleanup; + } + + rb = ring_buffer__new( + bpf_map__fd(obj->maps.events), + handle_event, + NULL, + NULL); + if (!rb) + { + fprintf(stderr, "failed to create ring buffer\n"); + err = -1; + goto cleanup; + } + + printf("Tracing device access... Hit Ctrl-C to end.\n"); + + while (!exiting) + { + err = ring_buffer__poll(rb, 100); + if (err == -EINTR) + { + err = 0; + break; + } + if (err < 0) + { + printf("Error polling ring buffer: %d\n", err); + break; + } + } + +cleanup: + if (rb) + ring_buffer__free(rb); + device_observe_bpf__destroy(obj); + return err != 0; +} diff --git a/observe/device-observe.h b/observe/device-observe.h new file mode 100644 index 0000000..ae92867 --- /dev/null +++ b/observe/device-observe.h @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd +// +// SPDX-License-Identifier: LGPL-2.1 + +#ifndef __DEVICE_OBSERVE_H +#define __DEVICE_OBSERVE_H + +/* 设备类型 */ +enum devobs_type +{ + DEV_TYPE_CAMERA = 1, + DEV_TYPE_MICROPHONE = 2, +}; + +/* 事件类型 */ +enum devobs_evt +{ + DEV_EVT_OPEN = 1, + DEV_EVT_CLOSE = 2, + DEV_EVT_STREAM_ON = 3, + DEV_EVT_STREAM_OFF = 4, + DEV_EVT_IOCTL = 5, +}; + +#define DEV_OBS_PATH_MAX 64 + +/* 内核/用户空间共享的事件结构体 */ +struct devobs_event +{ + __u64 timestamp_ns; + __u32 pid; + __u32 tid; + __u32 uid; + __u32 device_type; + __u32 event_type; + int fd; + __u32 ioctl_cmd; + char comm[16]; + char device_path[DEV_OBS_PATH_MAX]; +}; + +/* V4L2 关键 ioctl 命令 */ +#define V4L2_STREAM_ON 0x40045612UL /* VIDIOC_STREAMON */ +#define V4L2_STREAM_OFF 0x40045613UL /* VIDIOC_STREAMOFF */ + +/* ALSA 关键 ioctl 命令 */ +#define ALSA_PCM_START 0x00004142UL /* SNDRV_PCM_IOCTL_START _IO(\'A\', 0x42) */ +#define ALSA_PCM_DROP 0x00004143UL /* SNDRV_PCM_IOCTL_DROP _IO(\'A\', 0x43) */ + +#endif /* __DEVICE_OBSERVE_H */ From d71098826b85d48623f634026a7e4f091a822878 Mon Sep 17 00:00:00 2001 From: caimengci Date: Tue, 1 Sep 2026 15:33:45 +0800 Subject: [PATCH 2/2] feat: add preload-guard LD_PRELOAD monitor and enforce tool Monitor and control LD_PRELOAD usage via eBPF: - execve/execveat tracepoints capture LD_PRELOAD from envp and stage the value in a pending LRU map keyed by tgid - kprobe/begin_new_exec confirms successful exec and emits [DETECT] - lsm/mmap_file guard returns -EPERM for unlisted preload libraries in enforce mode, aborting process startup - whitelists keyed by inode (dev+ino) and uid, loaded from policy file (so=/uid=) or auto-collected system libraries - ringbuf audit events ([DETECT]/[BLOCK]) consumed by the userspace daemon - add demo/preload-guard suite covering baseline/monitor/enforce/allow cases --- policy/preload-guard.cpp | 500 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 policy/preload-guard.cpp diff --git a/policy/preload-guard.cpp b/policy/preload-guard.cpp new file mode 100644 index 0000000..c5def13 --- /dev/null +++ b/policy/preload-guard.cpp @@ -0,0 +1,500 @@ +// SPDX-FileCopyrightText: 2025 UnionTech Software Technology Co., Ltd +// +// SPDX-License-Identifier: LGPL-2.1 + +/** + * @file preload-guard.cpp + * @brief LD_PRELOAD 环境变量监测与管控用户空间程序 + */ + +#include "log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "com.h" +#include "preload-guard.skel.h" + +struct Target +{ + uint32_t dev; + uint64_t ino; +}; + +struct AuditEvent +{ + uint32_t type; + pid_t pid; + uint32_t uid; + char comm[16]; + char preload_val[64]; + struct Target so_target; +}; + +struct Config +{ + uint32_t enforce; + uint32_t reserved[7]; +}; + +struct RuleUid +{ + uint32_t uid; +}; + +static preload_guard_bpf *obj = nullptr; +static int allowed_so_fd = -1; +static int allowed_uid_fd = -1; +static int config_fd = -1; +static int log_map_fd = -1; +static struct ring_buffer *rb = nullptr; +static std::atomic exit_flag(false); +static const char *policy_file = nullptr; +static int enforce_mode = 0; +static std::map> system_lib_cache; + +static struct option lopts[] = { + {"policy-file", required_argument, 0, 'p'}, + {"enforce", no_argument, 0, 'e'}, + {"help", no_argument, 0, 'h'}, + {0, 0, 0, 0}, +}; + +struct HelpMsg +{ + const char *argparam; + const char *msg; +}; + +static HelpMsg help_msg[] = { + {"", "specify the whitelist policy file\n"}, + {"", "enable enforce mode (block unlisted .so loading)\n"}, + {"", "print this help message\n"}, +}; + +static inline uint32_t dev_old2new(dev_t old) +{ + uint32_t major = gnu_dev_major(old); + uint32_t minor = gnu_dev_minor(old); + return ((major & 0xfff) << 20) | (minor & 0xfffff); +} + +static void usage(const char *arg0) +{ + printf("Usage: %s [option]\n", arg0); + printf(" Monitor and control LD_PRELOAD usage via eBPF.\n\n"); + printf("Options:\n"); + for (int i = 0; lopts[i].name; i++) + { + printf(" -%c, --%s %s\t%s", + lopts[i].val, + lopts[i].name, + help_msg[i].argparam, + help_msg[i].msg); + } + printf("\nPolicy file format:\n"); + printf(" # Comment lines start with #\n"); + printf(" so= # Whitelist a shared library or directory\n"); + printf(" uid= # Whitelist a user (all preloads allowed)\n"); +} + +static std::string long_opt2short_opt(const option opts[]) +{ + std::string sopts; + for (int i = 0; opts[i].name; i++) + { + sopts += opts[i].val; + switch (opts[i].has_arg) + { + case no_argument: + break; + case required_argument: + sopts += ":"; + break; + case optional_argument: + sopts += "::"; + break; + default: + abort(); + } + } + return sopts; +} + +static void register_signal(void) +{ + struct sigaction sa = {}; + sa.sa_handler = [](int) { exit_flag = true; }; + sigaction(SIGINT, &sa, nullptr); + sigaction(SIGTERM, &sa, nullptr); +} + +static void parse_args(int argc, char **argv) +{ + std::string sopts = long_opt2short_opt(lopts); + int opt = 0; + + while ((opt = getopt_long(argc, argv, sopts.c_str(), lopts, nullptr)) != -1) + { + switch (opt) + { + case 'p': + policy_file = optarg; + break; + case 'e': + enforce_mode = 1; + break; + case 'h': + usage(argv[0]); + exit(EXIT_SUCCESS); + default: + usage(argv[0]); + exit(EXIT_FAILURE); + } + } +} + +static void path2target(const char *path, struct Target *target) +{ + struct stat st = {}; + if (stat(path, &st) == 0) + { + target->dev = dev_old2new(st.st_dev); + target->ino = st.st_ino; + } +} + +static void user2uid(const char *user, uid_t *uid) +{ + struct passwd *pw = getpwnam(user); + if (!pw) + { + pr_error("user not found: %s", user); + exit(EXIT_FAILURE); + } + *uid = pw->pw_uid; +} + +static void collect_dir(const char *dir_path) +{ + DIR *dir = opendir(dir_path); + if (!dir) + return; + + struct dirent *entry = nullptr; + while ((entry = readdir(dir)) != nullptr) + { + if (entry->d_type == DT_DIR) + { + if (strcmp(entry->d_name, ".") == 0 || + strcmp(entry->d_name, "..") == 0) + continue; + + char sub[PATH_MAX]; + snprintf(sub, sizeof(sub), "%s/%s", dir_path, entry->d_name); + collect_dir(sub); + continue; + } + + if (entry->d_type != DT_REG && entry->d_type != DT_LNK && + entry->d_type != DT_UNKNOWN) + continue; + + std::string name = entry->d_name; + if (name.find(".so") == std::string::npos) + continue; + + std::string full = std::string(dir_path) + "/" + name; + struct stat st = {}; + if (stat(full.c_str(), &st) == 0) + system_lib_cache[full] = std::make_tuple(st.st_dev, st.st_ino); + } + + closedir(dir); +} + +static void collect_system_libs(void) +{ + const char *lib_dirs[] = { + "/lib", + "/lib64", + "/usr/lib", + "/usr/lib64", + "/lib/x86_64-linux-gnu", + "/lib/aarch64-linux-gnu", + "/lib/loongarch64-linux-gnu", + nullptr, + }; + + for (int d = 0; lib_dirs[d]; d++) + collect_dir(lib_dirs[d]); + + pr_info("Collected %zu system libraries into cache", system_lib_cache.size()); +} + +static void add_library_rule(const char *path) +{ + struct Target target = {}; + uint8_t allow = 1; + + path2target(path, &target); + if (!target.dev || !target.ino) + { + pr_warn("skip invalid library target: %s", path); + return; + } + + if (bpf_map_update_elem(allowed_so_fd, &target, &allow, BPF_ANY) != 0) + { + pr_error("bpf_map_update_elem allowed_so failed for %s: %s", + path, strerror(errno)); + exit(EXIT_FAILURE); + } +} + +static void add_libraries_recursively(const char *dir_path) +{ + DIR *dir = opendir(dir_path); + if (!dir) + return; + + struct dirent *entry = nullptr; + while ((entry = readdir(dir)) != nullptr) + { + if (entry->d_type == DT_DIR) + { + if (strcmp(entry->d_name, ".") == 0 || + strcmp(entry->d_name, "..") == 0) + continue; + + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); + add_libraries_recursively(full_path); + continue; + } + + if (entry->d_type != DT_REG && entry->d_type != DT_LNK && + entry->d_type != DT_UNKNOWN) + continue; + + std::string name = entry->d_name; + if (name.find(".so") == std::string::npos) + continue; + + char full_path[PATH_MAX]; + snprintf(full_path, sizeof(full_path), "%s/%s", dir_path, entry->d_name); + add_library_rule(full_path); + } + + closedir(dir); +} + +static void load_default_system_rules(void) +{ + size_t count = 0; + for (const auto &kv : system_lib_cache) + { + add_library_rule(kv.first.c_str()); + count++; + } + pr_info("Loaded %zu system library rules (auto mode)", count); +} + +static void load_policy_file(const char *filename) +{ + FILE *file = fopen(filename, "r"); + if (!file) + { + pr_error("fopen %s failed: %s", filename, strerror(errno)); + exit(EXIT_FAILURE); + } + + size_t so_rules = 0; + size_t uid_rules = 0; + char line[8192]; + + while (fgets(line, sizeof(line), file)) + { + char type[16] = {}; + char content[4096] = {}; + + if (line[0] == '#' || line[0] == '\n') + continue; + + if (sscanf(line, "%15[^=]=%4095s", type, content) != 2) + continue; + + if (strcmp(type, "so") == 0) + { + struct stat st = {}; + if (stat(content, &st) != 0) + { + pr_error("Cannot access %s: %s", content, strerror(errno)); + continue; + } + + if (S_ISDIR(st.st_mode)) + { + add_libraries_recursively(content); + } + else + { + add_library_rule(content); + } + so_rules++; + } + else if (strcmp(type, "uid") == 0) + { + struct RuleUid rule = {}; + uint8_t allow = 1; + user2uid(content, &rule.uid); + if (bpf_map_update_elem(allowed_uid_fd, &rule.uid, &allow, BPF_ANY) != 0) + { + pr_error("bpf_map_update_elem allowed_uid failed for %s: %s", + content, strerror(errno)); + fclose(file); + exit(EXIT_FAILURE); + } + uid_rules++; + } + + pr_info("Rule: %s=%s", type, content); + } + + fclose(file); + pr_info("Loaded %zu so-rules and %zu uid-rules", so_rules, uid_rules); +} + +static void set_config(void) +{ + uint32_t key = 0; + struct Config cfg = {}; + cfg.enforce = enforce_mode; + + if (bpf_map_update_elem(config_fd, &key, &cfg, BPF_ANY) != 0) + { + pr_error("bpf_map_update_elem config failed: %s", strerror(errno)); + exit(EXIT_FAILURE); + } + pr_info("Mode: %s", enforce_mode ? "ENFORCE" : "MONITOR"); +} + +static int handle_event(void *ctx, void *data, size_t data_sz) +{ + (void)ctx; + const struct AuditEvent *ev = static_cast(data); + + if (data_sz < sizeof(*ev)) + return 0; + + if (ev->type == 0) + { + pr_info("[DETECT] pid=%u uid=%u comm=%s LD_PRELOAD=%s", + ev->pid, ev->uid, ev->comm, ev->preload_val); + } + else if (ev->type == 1) + { + pr_warn("[BLOCK] pid=%u uid=%u comm=%s blocked .so load (dev=%x ino=%lu)", + ev->pid, ev->uid, ev->comm, + ev->so_target.dev, ev->so_target.ino); + } + else if (ev->type == 2) + { + pr_warn("[BLOCK] pid=%u uid=%u comm=%s env entries exceed limit", + ev->pid, ev->uid, ev->comm); + } + + return 0; +} + +static void ringbuf_worker(void) +{ + while (!exit_flag) + { + int err = ring_buffer__poll(rb, 1000); + if (err < 0 && err != -EINTR) + { + pr_error("Error polling ring buffer: %d", err); + sleep(1); + } + } +} + +int main(int argc, char **argv) +{ + parse_args(argc, argv); + register_signal(); + collect_system_libs(); + + obj = preload_guard_bpf::open_and_load(); + if (!obj) + { + pr_error("failed to open and load preload-guard BPF object"); + return EXIT_FAILURE; + } + + allowed_so_fd = bpf_get_map_fd(obj->obj, "allowed_so", goto err_out); + allowed_uid_fd = bpf_get_map_fd(obj->obj, "allowed_uid", goto err_out); + config_fd = bpf_get_map_fd(obj->obj, "pg_config", goto err_out); + log_map_fd = bpf_get_map_fd(obj->obj, "logs", goto err_out); + + rb = ring_buffer__new(log_map_fd, handle_event, nullptr, nullptr); + if (!rb) + { + pr_error("failed to create ring buffer"); + goto err_out; + } + + if (policy_file) + load_policy_file(policy_file); + else + load_default_system_rules(); + + set_config(); + + if (preload_guard_bpf::attach(obj) != 0) + { + pr_error("failed to attach preload-guard BPF programs"); + goto err_out; + } + + pr_info("preload-guard started (mode=%s)", + enforce_mode ? "enforce" : "monitor"); + + { + std::thread rb_thread(ringbuf_worker); + while (!exit_flag) + sleep(1); + rb_thread.join(); + } + + ring_buffer__free(rb); + preload_guard_bpf::destroy(obj); + return EXIT_SUCCESS; + +err_out: + if (rb) + ring_buffer__free(rb); + if (obj) + preload_guard_bpf::destroy(obj); + return EXIT_FAILURE; +}