diff --git a/.gitignore b/.gitignore index aa09200..c20e03a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build/ output.txt +logs/ \ No newline at end of file diff --git a/Makefile b/Makefile index 23c27a7..3566a30 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,11 @@ ifeq ($(HOOK),1) CFLAGS += -DHOOK endif +# 如果跳过检查,只需执行 make NO_CONFIG_CHECK=1 +ifeq ($(NO_CONFIG_CHECK),1) + CFLAGS += -DNO_CONFIG_CHECK +endif + .PHONY: all clean debug hook rebuild pre_build all: pre_build $(TARGET) $(HOOK_TARGET) @@ -40,6 +45,9 @@ endif ifeq ($(HOOK),1) @echo "Building with hook flags..." endif +ifeq ($(NO_CONFIG_CHECK),1) + @echo "Building with NO_CONFIG_CHECK defined..." +endif $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c @mkdir -p $(BUILD_DIR) diff --git a/README.md b/README.md index 49e70bc..6e3511c 100644 --- a/README.md +++ b/README.md @@ -1,131 +1,233 @@ -# execve 拦截器 +# Execve Hook Project -## 简介 +本项目是一个基于 `LD_PRELOAD` 机制的系统调用拦截库,主要用于监控和控制 Linux 系统中的命令执行(`execve`)。它能够拦截命令执行请求,检查配置规则,记录执行日志,甚至通过 PTY(伪终端)捕获命令的输入输出。 -本项目是一个通过动态链接库预加载 (LD\_PRELOAD) 方式实现的 `execve` 系统调用拦截器。它可以根据预定义的规则,在程序执行指定的命令时进行警告、阻止或记录操作。 +## 核心逻辑概述 -**主要功能:** +整个 C 代码库的逻辑可以分为以下几个核心模块: -* **命令拦截与告警:** 当执行配置中指定的命令时,可以显示警告信息并询问用户是否继续执行。 -* **命令阻止:** 可以完全阻止执行配置中指定的命令。 -* **参数匹配:** 支持基于命令参数进行更细粒度的规则匹配。 -* **配置热加载:** 能够检测到配置文件修改并自动重新加载规则。 -* **日志记录:** 记录所有通过 `execve` 执行的命令及其参数。 -* **输出重定向与错误检测:** 将执行命令的标准输出和错误输出记录到日志文件,并能在检测到错误时发出提示。 -* **仅拦截终端 Shell:** 默认只在终端 Shell (如 bash, zsh, fish, sh) 中拦截 `execve` 调用,避免影响其他程序。 -* **功能开关:** 可以通过配置文件中的 `enabled` 字段全局启用或禁用拦截功能。 +### 1. `execve` 拦截器 (`src/execve_interceptor.c`) -## 安装 +这是项目的核心入口。 -1. **前提条件:** - * 安装 `gcc` 等编译工具。 - * 安装 `json-c` 库 (开发版本)。在 Debian/Ubuntu 上可以使用 `sudo apt-get install libjson-c-dev` 安装,在 CentOS/RHEL 上可以使用 `sudo yum install json-c-devel` 安装。 +- **拦截机制**: 通过 `LD_PRELOAD` 预加载动态库,覆盖系统默认的 `execve` 函数。 +- **主要流程**: + 1. **环境清理**: 在调用前移除 `LD_PRELOAD` 环境变量,防止对子进程造成非预期的递归影响。 + 2. **配置加载**: 调用 `config.c` 从共享内存中加载配置。 + 3. **规则匹配**: 使用 `rules.c` 检查当前执行的命令是否命中特定规则。 + 4. **行为决策**: 根据规则决定是否需要拦截、记录日志或通过 PTY 执行。 + 5. **执行原函数**: 最终调用原始的 `execve` 系统调用执行目标程序。 -2. **编译:** - ```bash - gcc -shared -fPIC execve_interceptor.c -o execve_interceptor.so -ldl -ljson-c - ``` - * 确保你的代码文件名为 `execve_interceptor.c`。 +```mermaid +sequenceDiagram + participant User as User/Shell + participant Hook as execve_interceptor + participant Config as Config (Shm) + participant Rules as Rules Engine + participant PTY as PTY/IO Handler + participant Sys as Original execve -3. **创建配置和日志目录 (如果不存在):** - ```bash - mkdir -p config logs - ``` + User->>Hook: execve(filename, argv, envp) + Hook->>Hook: Remove LD_PRELOAD + Hook->>Hook: dlsym(RTLD_NEXT, "execve") + + Hook->>Config: load_config() + Config-->>Hook: ConfigData* + + alt Not Terminal Shell or Config Disabled + Hook->>Sys: orig_execve(...) + end -4. **创建配置文件:** - * 在 `config` 目录下创建 `execve_rules.json` 文件,并按照以下格式配置规则: + loop Check Rules + Hook->>Rules: args_match(argv, rule) + Rules-->>Hook: Match Result + end - ```json - { - "enabled": true, - "rules": [ - { - "cmd": "nvidia-smi", - "type": "warn", - "msg": "在沐曦环境下请执行mx-smi" - }, - { - "cmd": "rm", - "type": "error", - "msg": "Error: rm command is forbidden" - }, - { - "cmd": "pip", - "type": "warn", - "msg": "使用pip安装torch时,请注意使用厂家支持版本", - "args": ["install", "torch"] - } - ] - } - ``` - - * `enabled`: 布尔值,用于启用或禁用整个拦截器功能。 - * `rules`: 一个 JSON 数组,包含多个规则对象。 - * `cmd`: 要拦截的命令名称。 - * `type`: 拦截类型,可以是 `"warn"` (警告) 或 `"error"` (阻止)。 - * `msg`: 当规则匹配时显示的消息。 - * `args` (可选): 一个字符串数组,指定需要匹配的命令参数。只有当所有指定的参数都存在时,规则才匹配。 - -## 配置 - -* **`config/execve_rules.json`:** 这是主要的配置文件,用于定义拦截规则。你可以根据需要添加、修改或删除规则。 -* **`logs/execve.log`:** 记录所有通过 `execve` 执行的命令及其参数。 -* **`logs/execve_out.log`:** 记录被拦截命令的标准输出和错误输出。 - -## 使用 - -要使用该拦截器,你需要通过 `LD_PRELOAD` 环境变量在执行命令前加载编译好的动态链接库。 - -**示例:** - -```bash -export LD_PRELOAD=$(pwd)/execve_interceptor.so -nvidia-smi -rm test.txt -pip install torch + alt Rule Type: SKIP + Hook->>Sys: orig_execve(...) + else Rule Type: WARN + Hook->>User: Print Warning + User->>Hook: Confirm (Y/N) + alt No + Hook->>User: Exit + end + else Rule Type: ERROR + Hook->>User: Print Error + Hook->>User: Exit + else Rule Type: LOG/DEFAULT (Match Found) + Hook->>PTY: dupIO(filename, argv, envp) + PTY->>PTY: forkpty() + par Child Process + PTY->>Sys: orig_execve(...) + and Parent Process + PTY->>PTY: handle_io() (Capture Output) + PTY->>User: Forward Output + end + else No Match + Hook->>Sys: orig_execve(...) + end ``` -## 前置依赖: +### 2. 配置管理 (`src/config.c`) -- jsonc: +- **共享内存**: 配置数据不直接从文件读取,而是从共享内存(Shared Memory)中加载。 +- **机制**: 这意味着有一个外部进程(通常是配套的 Go 服务)负责解析配置文件(如 JSON/YAML)并将结构化数据写入共享内存,C 代码只需高效读取即可。 -```bash -# Ubuntu/Debian -sudo apt-get install libjson-c-dev +### 3. 规则引擎 (`src/rules.c`) -# CentOS/RHEL -sudo yum install json-c-devel +- **功能**: 负责解析和匹配命令执行规则。 +- **匹配逻辑**: 检查命令名称(`filename`)以及参数列表(`argv`)是否符合预定义的条件。 +### 4. I/O 捕获与 PTY (`src/pty_dup.c`) + +- **目的**: 为了完整记录交互式命令的输入输出(Session Recording)。 +- **实现**: + - 使用 `forkpty` 创建一个新的伪终端会话。 + - 在子进程中执行目标命令。 + - 父进程负责在主终端和伪终端之间转发数据(`stdin`, `stdout`, `stderr`)。 + - 单独处理 `stderr` 管道以区分标准输出和错误输出。 + - 处理信号(如 `SIGINT`, `SIGCHLD`)以确保子进程正确退出。 + +```mermaid +flowchart TD + A[dupIO Called] --> B{forkpty} + B -- Error --> C[Exit] + B -- Child Process --> D[Setup Signals] + D --> E[Close Pipe Read End] + E --> F[Dup2 Stderr -> Pipe Write End] + F --> G[Return to execve_interceptor] + G --> H[Call orig_execve] + + B -- Parent Process --> I[Close Pipe Write End] + I --> J[Get Log File Paths] + J --> K[handle_io Loop] + + subgraph IO_Loop [IO Handling Loop] + L{Select/Poll} + L -- Stdin Data --> M[Write to Master PTY] + L -- Master PTY Data --> N[Read Output] + N --> O[Write to Stdout] + N --> P[Write to Log File] + L -- Stderr Pipe Data --> Q[Read Stderr] + Q --> R[Write to Stderr] + Q --> S[Write to Log File] + end + + K --> IO_Loop + IO_Loop -- Child Exit --> T[Check Exit Status] + T -- Error Code --> U[Send Params to Socket] + T -- Success --> V[Exit Parent] ``` -## 编译: +### 5. Write 调用拦截 (`src/hook_write.c`) -- cd execve_hook && make +这是一个独立的模块(编译为 `hook_write.so`),用于更细粒度的输出捕获。 -## 测试: - -- ./test_bash.sh - -## 配置文件格式: - -```json -[ - { - "cmd": "nvidia-smi", // 将匹配的命令 - "type": "warn", // 如果为warn,则会提示是否继续执行 - "msg": "在沐曦环境下请执行mx-smi" // 提示信息 - }, - { - "cmd": "rm", // 匹配的命令 - "type": "error", // 如果为error,则会拦截命令执行 - "msg": "Error: rm command is forbidden" // 提示信息 - }, - { - "cmd": "pip", // 匹配的命令 - "type": "warn", - "msg": "使用pip安装torch时,请注意使用厂家支持版本", - "args": ["install", "torch"] //当存在args,则这里的参数必须全部存在 - } -] +- **拦截对象**: `write`, `writev`, `fwrite`, `puts`, `printf` 等标准输出函数。 +- **功能**: 将进程写入 `stdout` 或 `stderr` 的内容同时写入到一个指定的日志文件中。 +- **用途**: 即使不使用 PTY,也能捕获程序的输出内容。 +```mermaid +flowchart LR + A[Application] -->|Calls write/printf| B(Hook Function) + B --> C{Log FD Open?} + C -- Yes --> D[Write to Log File] + C -- No --> E[Skip Log] + D --> F[Call Original Function] + E --> F + F --> G[Output to Terminal] ``` + +### 6. 进程间通信与错误上报 (`src/client.c`) + +- **方式**: Unix Domain Socket (`/etc/exec_hook/exec.sock`)。 +- **作用**: 将拦截到的执行信息(文件名、参数、环境变量、日志路径等)发送给后端服务(Go Service)。 +- **流程**: 在 `execve` 拦截阶段,如果需要上报,会通过此模块连接 Socket 并发送数据。 + +#### 错误上报机制 + +当被拦截的命令执行失败(退出码非 0 或被信号终止)时,系统会触发错误上报流程。 + +1. **执行与捕获**: `pty_dup.c` 中的 `handle_io` 循环负责实时捕获子进程的 `stdout` 和 `stderr`。 +2. **日志记录**: 捕获到的 `stderr` 内容会被实时写入到特定的日志文件中(由 `GET_LOG_FILE` 宏定义路径)。 +3. **触发上报**: + - 父进程等待子进程退出。 + - 检查子进程退出状态 (`child_status`)。 + - 如果 `WIFEXITED` 且退出码非 0,或者 `WIFSIGNALED`(被信号终止),则调用 `send_exec_params`。 +4. **发送数据**: `send_exec_params` 将以下信息打包发送给 Go 服务: + - **Filename**: 执行的命令名称。 + - **CWD**: 当前工作目录。 + - **Argv**: 完整的参数列表。 + - **Envp**: 完整的环境变量列表。 + - **Log Path**: 记录了错误输出的日志文件绝对路径。 +5. **等待响应**: 客户端等待服务端返回处理结果(如 AI 分析建议),并将其打印到终端。 + +```mermaid +sequenceDiagram + participant Parent as Parent Process + participant Child as Child Process (Cmd) + participant Log as Log File (Stderr) + participant Socket as Unix Socket + participant Server as Go Service + + Parent->>Child: forkpty() & exec() + loop Execution + Child->>Parent: Stderr Output (Pipe) + Parent->>Log: Write Stderr Content + end + Child-->>Parent: Exit (Status != 0) + + Parent->>Parent: Check Exit Code + + alt Exit Code != 0 + Parent->>Socket: Connect + Parent->>Socket: Send Filename + Parent->>Socket: Send CWD + Parent->>Socket: Send Argv & Envp + Parent->>Socket: Send Log Path (Error Log) + + Socket->>Server: Forward Request + Server->>Server: Analyze Error (Read Log) + Server-->>Socket: Response (Suggestion) + Socket-->>Parent: Receive Response + Parent->>Parent: Print Suggestion + end +``` + +#### 通信协议格式 + +数据通过 Socket 顺序发送,格式如下(均为二进制流): + +| 字段 | 类型 | 说明 | +| :--- | :--- | :--- | +| `filename_len` | `size_t` | 文件名长度 | +| `filename` | `char[]` | 文件名内容 | +| `pwd_len` | `size_t` | 当前工作目录长度 | +| `pwd` | `char[]` | 当前工作目录内容 | +| `argc` | `int` | 参数个数 | +| `arg_len` | `size_t` | (循环) 参数长度 | +| `arg_str` | `char[]` | (循环) 参数内容 | +| `envc` | `int` | 环境变量个数 | +| `env_len` | `size_t` | (循环) 环境变量长度 | +| `env_str` | `char[]` | (循环) 环境变量内容 | +| `log_path_len` | `size_t` | 日志路径长度 | +| `log_path` | `char[]` | 日志路径内容 | + +## 编译构建 + +项目使用 `Makefile` 进行管理。 + +- **默认构建**: `make` (生成 `intercept.so`) +- **开启 Hook 功能**: `make HOOK=1` (定义 `HOOK` 宏,启用拦截逻辑) +- **清理**: `make clean` + +## 目录结构说明 + +- `src/`: 源代码目录 + - `execve_interceptor.c`: `execve` 拦截主逻辑 + - `hook_write.c`: `write` 系列函数拦截逻辑 + - `config.c`: 共享内存配置加载 + - `pty_dup.c`: 伪终端处理 + - `client.c`: Socket 通信客户端 + - `rules.c`: 规则匹配逻辑 +- `build/`: 编译输出目录 diff --git a/src/execve_interceptor.c b/src/execve_interceptor.c index d3f6c20..180de05 100644 --- a/src/execve_interceptor.c +++ b/src/execve_interceptor.c @@ -124,6 +124,7 @@ int enhance_execve(const char *filename, char *const argv[], #endif } +#ifndef NO_CONFIG_CHECK // Current configuration information DEBUG_LOG("Current Config rule count : %d", shared_config->rule_count); @@ -216,6 +217,27 @@ int enhance_execve(const char *filename, char *const argv[], #endif } + #else + DEBUG_LOG("NO_CONFIG_CHECK defined, skipping config checks."); +#endif + + // 只有当标准输入和标准输出都是终端时,才启用 PTY + // 1. 如果 stdin 不是终端(如管道输入),不需要 PTY + // 2. 如果 stdout 不是终端(如重定向到文件),使用 PTY 会导致输出包含颜色代码等控制字符,破坏文件内容 + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) { + DEBUG_LOG("Not a tty (stdin or stdout), skipping PTY setup."); +#ifdef HOOK + if (orig_execve) { + orig_execve(filename, argv, envp); + perror("orig_execve failed"); + exit(1); + } +#endif + execvp(filename, argv); + perror("execvp failed"); + exit(1); + } + write_log(filename, argv); // Duplicate stdout and stderr to the log file diff --git a/src/init_cleanup.c b/src/init_cleanup.c index e8ddda0..7e22821 100644 --- a/src/init_cleanup.c +++ b/src/init_cleanup.c @@ -30,7 +30,7 @@ __attribute__((destructor)) void cleanup_shared_memory() { // shared_config = NULL; // } #ifdef DEBUG - // print_stacktrace(); + print_stacktrace(); #endif // Note: We don't delete the shared memory segment here, as it might be // used by other processes. A separate mechanism would be needed to manage diff --git a/src/pty_dup.c b/src/pty_dup.c index 4b35200..59ea1ea 100644 --- a/src/pty_dup.c +++ b/src/pty_dup.c @@ -11,6 +11,17 @@ FILE *log_file = NULL; pid_t child_pid; int child_status = -1; +static int pty_master_fd = -1; + +void handle_sigwinch(int sig) { + (void)sig; + struct winsize win; + if (ioctl(STDIN_FILENO, TIOCGWINSZ, &win) != -1) { + if (pty_master_fd != -1) { + ioctl(pty_master_fd, TIOCSWINSZ, &win); + } + } +} // void dupIO() { // pid_t pid; @@ -73,6 +84,7 @@ void dupIO(const char *filename, char *const argv[], char *const envp[]) { signal(SIGINT, handle_sigint); signal(SIGCHLD, handle_sigchld); + signal(SIGWINCH, handle_sigwinch); if (ioctl(STDIN_FILENO, TIOCGWINSZ, &win) < 0) { perror("ioctl TIOCGWINSZ failed"); @@ -83,6 +95,10 @@ void dupIO(const char *filename, char *const argv[], char *const envp[]) { DEBUG_LOG("forkpty result is: %d.", pid); child_pid = pid; + if (pid > 0) { + pty_master_fd = master; + } + if (pid < 0) { perror("forkpty failed"); exit(1); diff --git a/src/terminal_utils.c b/src/terminal_utils.c index 065c200..b89251b 100644 --- a/src/terminal_utils.c +++ b/src/terminal_utils.c @@ -17,27 +17,31 @@ void setup_termios(struct termios *term) { // 控制模式标志 term->c_cflag = CS8 | CREAD; // 8位字符,启用接收器 - // 本地模式标志 - 禁用特殊输入处理 - term->c_lflag = ISIG | IEXTEN; // 仅保留信号处理和扩展功能 + // 本地模式标志 - 启用规范模式、信号处理和回显 + term->c_lflag = ICANON | ECHO | ISIG | IEXTEN; // 启用规范模式、回显、信号处理和扩展功能 // 控制字符 - term->c_cc[VINTR] = 0x03; // Ctrl-C - term->c_cc[VQUIT] = 0x1c; // Ctrl-反斜杠 - term->c_cc[VERASE] = 0x7f; // Backspace - term->c_cc[VKILL] = 0x15; // Ctrl-U - term->c_cc[VEOF] = 0x04; // Ctrl-D - term->c_cc[VTIME] = 0; - term->c_cc[VMIN] = 1; - term->c_cc[VSWTC] = 0; - term->c_cc[VSTART] = 0x11; // Ctrl-Q - term->c_cc[VSTOP] = 0x13; // Ctrl-S - term->c_cc[VSUSP] = 0x1a; // Ctrl-Z - term->c_cc[VEOL] = 0; - term->c_cc[VREPRINT] = 0x12; // Ctrl-R - term->c_cc[VDISCARD] = 0x0f; // Ctrl-O - term->c_cc[VWERASE] = 0x17; // Ctrl-W - term->c_cc[VLNEXT] = 0x16; // Ctrl-V - term->c_cc[VEOL2] = 0; + term->c_cc[VINTR] = 0x03; // Ctrl-C (中断) + term->c_cc[VQUIT] = 0x1c; // Ctrl-\ (退出) + term->c_cc[VERASE] = 0x7f; // Backspace (擦除) + term->c_cc[VKILL] = 0x15; // Ctrl-U (清除行) + term->c_cc[VEOF] = 0x04; // Ctrl-D (文件结束) + term->c_cc[VEOL] = 0; // 行结束字符 + term->c_cc[VEOL2] = 0; // 行结束字符2 + term->c_cc[VSTART] = 0x11; // Ctrl-Q (开始) + term->c_cc[VSTOP] = 0x13; // Ctrl-S (停止) + term->c_cc[VSUSP] = 0x1a; // Ctrl-Z (暂停) + // term->c_cc[VDSUSP] = 0x19; // Ctrl-Y (延迟暂停) + term->c_cc[VREPRINT] = 0x12; // Ctrl-R (重打印) + term->c_cc[VDISCARD] = 0x0f; // Ctrl-O (丢弃) + term->c_cc[VWERASE] = 0x17; // Ctrl-W (擦除词) + term->c_cc[VLNEXT] = 0x16; // Ctrl-V (字面值下一个) + term->c_cc[VMIN] = 1; // 规范模式:最少读取1个字符 + term->c_cc[VTIME] = 0; // 规范模式:无超时 + +#ifdef VSWTC + term->c_cc[VSWTC] = 0; // 切换字符(通常未使用) +#endif // 设置输入输出速度 cfsetispeed(term, B38400); @@ -83,7 +87,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, } while (1) { - DEBUG_LOG("poll....."); + // DEBUG_LOG("poll....."); int ret = poll(fds, 3, 100); // 修改为监控3个fd if (ret < 0) { if (errno == EINTR) continue; @@ -92,7 +96,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, } // 优先处理 PTY 输出,确保缓冲区中的数据被完全读出 - DEBUG_LOG("Handling pty output..."); + // DEBUG_LOG("Handling pty output..."); if (fds[1].revents & (POLLIN | POLLHUP)) { ssize_t n = read(master_fd, buffer, sizeof(buffer)); if (n > 0) { @@ -104,7 +108,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, // 处理stderr // 处理stderr - DEBUG_LOG("Handling stderr output..."); + // DEBUG_LOG("Handling stderr output..."); if (fds[2].revents & (POLLIN | POLLHUP)) { ssize_t n = read(stderr_fd, buffer, sizeof(buffer)); if (n > 0) { @@ -123,9 +127,9 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, } // 输出带颜色的错误信息 - write(STDERR_FILENO, "\033[31m", 5); // 设置红色 + // write(STDERR_FILENO, "\033[31m", 5); // 设置红色 write(STDERR_FILENO, converted, conv_len); - write(STDERR_FILENO, "\033[0m", 4); // 重置颜色 + // write(STDERR_FILENO, "\033[0m", 4); // 重置颜色 // 写入日志 write(stderr_log_fd, converted, conv_len); @@ -133,12 +137,12 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, } // 检查子进程状态 - DEBUG_LOG("Checking child status: %d", child_status); + // DEBUG_LOG("Checking child status: %d", child_status); if (child_status != -1) { // 再次尝试读取可能残留的输出 while (1) { ssize_t n = read(master_fd, buffer, sizeof(buffer)); - DEBUG_LOG("Read n is: %ld", n); + // DEBUG_LOG("Read n is: %ld", n); if (n < 0) { // 读取错误处理 break; @@ -164,7 +168,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, } } - DEBUG_LOG("fflush."); + // DEBUG_LOG("fflush."); // 确保所有输出都已刷新 fflush(stdout); fflush(stderr); @@ -173,7 +177,7 @@ void handle_io(int master_fd, int stderr_fd, const char *stdout_log, } // 处理标准输入 - DEBUG_LOG("Handling stdin"); + // DEBUG_LOG("Handling stdin"); if (fds[0].revents & POLLIN) { ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer)); if (n <= 0) break;