fix: 修复LD_PRELOAD bash -c执行时无法回显的bug, 修复窗口大小改变时无法同步子进程的bug

This commit is contained in:
QCQCQC@Debian 2025-12-05 17:13:20 +08:00
parent 3be4ba6e4b
commit cf33cff0a5
7 changed files with 291 additions and 138 deletions

1
.gitignore vendored
View File

@ -1,2 +1,3 @@
build/
output.txt
logs/

View File

@ -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)

320
README.md
View File

@ -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/`: 编译输出目录

View File

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

View File

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

View File

@ -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);

View File

@ -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;