24 KiB
24 KiB
Execve Hook Project
本项目是一个基于 LD_PRELOAD 机制的系统调用拦截库,主要用于监控和控制 Linux 系统中的命令执行(execve)。它能够拦截命令执行请求,检查配置规则,记录执行日志,甚至通过 PTY(伪终端)捕获命令的输入输出。
架构概览
项目采用 C 客户端 + Go 服务端 的架构,通过 Unix Domain Socket 实现实时双向通信:
- C 客户端:作为"哑终端",负责捕获所有终端事件(键盘、鼠标、窗口大小变化)并通过 Socket 发送给服务端
- Go 服务端:完全控制终端输出,可以渲染 TUI、处理 AI 响应、格式化输出等
- 通信协议:基于结构化二进制消息的现代化协议,支持多种消息类型
关键特性
✓ 完全服务端控制:终端输出由 Go 服务完全接管
✓ 实时交互捕获:鼠标点击、键盘输入、窗口调整实时同步
✓ Raw 终端模式:客户端使用原始模式捕获所有输入
✓ 优雅退出机制:服务端控制连接生命周期,客户端自动清理终端状态
✓ TUI 支持:服务端可以使用任何 TUI 库(如 bubbletea)进行界面渲染
通信流程
sequenceDiagram
participant User as 用户终端
participant Client as C客户端
participant Socket as Unix Socket
participant Server as Go服务端
participant AI as AI服务
User->>Client: 执行命令出错
Client->>Client: 进入Raw模式
Client->>Client: 启用鼠标跟踪
Client->>Socket: 发送初始化消息
Socket->>Server: 转发初始化数据
par 持续监听
loop 输入监听
User->>Client: 键盘/鼠标输入
Client->>Socket: 发送终端输入事件
Socket->>Server: 转发事件
end
loop 窗口监听
User->>Client: 调整窗口大小
Client->>Socket: 发送窗口大小更新
Socket->>Server: 转发更新
end
loop 服务端处理
Server->>AI: 请求错误分析
AI-->>Server: 返回建议
Server->>Socket: 发送响应消息
Socket->>Client: 转发响应
Client->>User: 直接输出到终端
end
end
Server->>Socket: 发送关闭消息
Socket->>Client: 转发关闭
Client->>Client: 恢复终端设置
Client->>Client: 禁用鼠标跟踪
Client->>User: 退出程序
工作模式
-
客户端职责(静默模式)
- 捕获所有终端输入(键盘、鼠标)
- 监听窗口大小变化(SIGWINCH)
- 将原始事件发送给服务端
- 接收服务端消息并输出到终端
- 不做任何业务逻辑处理
-
服务端职责(完全控制)
- 接收客户端的所有输入事件
- 处理业务逻辑(AI 分析、日志查询等)
- 渲染 TUI 界面或格式化输出
- 控制连接生命周期
- 决定何时关闭连接
-
终端状态管理
- 进入时:客户端设置 Raw 模式 + 启用鼠标跟踪
- 运行时:保存原始 termios 设置
- 退出时:自动恢复原始设置 + 禁用鼠标跟踪 + 输出换行符
核心逻辑概述
整个项目的逻辑可以分为以下几个核心模块:
1. execve 拦截器 (src/execve_interceptor.c)
这是项目的核心入口。
- 拦截机制: 通过
LD_PRELOAD预加载动态库,覆盖系统默认的execve函数。 - 主要流程:
- 环境清理: 在调用前移除
LD_PRELOAD环境变量,防止对子进程造成非预期的递归影响。 - 配置加载: 调用
config.c从共享内存中加载配置。 - 规则匹配: 使用
rules.c检查当前执行的命令是否命中特定规则。 - 行为决策: 根据规则决定是否需要拦截、记录日志或通过 PTY 执行。
- 执行原函数: 最终调用原始的
execve系统调用执行目标程序。
- 环境清理: 在调用前移除
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
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
loop Check Rules
Hook->>Rules: args_match(argv, rule)
Rules-->>Hook: Match Result
end
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)
- 共享内存: 配置数据不直接从文件读取,而是从共享内存(Shared Memory)中加载。
- 机制: 这意味着有一个外部进程(通常是配套的 Go 服务)负责解析配置文件(如 JSON/YAML)并将结构化数据写入共享内存,C 代码只需高效读取即可。
3. 规则引擎 (src/rules.c)
- 功能: 负责解析和匹配命令执行规则。
- 匹配逻辑: 检查命令名称(
filename)以及参数列表(argv)是否符合预定义的条件。
4. I/O 捕获与 PTY (src/pty_dup.c)
- 目的: 为了完整记录交互式命令的输入输出(Session Recording)。
- 实现:
- 使用
forkpty创建一个新的伪终端会话。 - 在子进程中执行目标命令。
- 父进程负责在主终端和伪终端之间转发数据(
stdin,stdout,stderr)。 - 单独处理
stderr管道以区分标准输出和错误输出。 - 处理信号(如
SIGINT,SIGCHLD)以确保子进程正确退出。
- 使用
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)
这是一个独立的模块(编译为 hook_write.so),用于更细粒度的输出捕获。
- 拦截对象:
write,writev,fwrite,puts,printf等标准输出函数。 - 功能: 将进程写入
stdout或stderr的内容同时写入到一个指定的日志文件中。 - 用途: 即使不使用 PTY,也能捕获程序的输出内容。
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. Socket 通信客户端 (src/client.c)
现代化的 Socket 通信实现,采用多线程架构实现实时双向通信。
线程架构
客户端运行 4 个线程:
-
主线程
- 连接 Socket
- 发送初始化消息(命令信息、环境变量、日志路径、终端信息)
- 等待其他线程完成
- 清理资源和恢复终端
-
响应监听线程 (
response_listener_thread)- 持续监听服务端消息
- 收到
MSG_TYPE_SERVER_RESPONSE:直接输出到终端 - 收到
MSG_TYPE_CLOSE:退出循环,触发程序结束
-
窗口监控线程 (
window_monitor_thread)- 监听
SIGWINCH信号(窗口大小变化) - 使用 条件变量 (
pthread_cond_t) 实现事件驱动等待 - 信号触发时立即唤醒,无需轮询
- 自动发送
MSG_TYPE_WINDOW_SIZE_UPDATE消息
- 监听
-
输入监听线程 (
terminal_input_thread)- 设置终端为 Raw 模式(捕获所有输入)
- 启用 鼠标跟踪(ANSI 转义序列)
- 使用
poll()监听 stdin - 解析鼠标事件或发送原始输入
- 发送
MSG_TYPE_TERMINAL_INPUT/MSG_TYPE_MOUSE_EVENT
终端模式管理
// 进入 Raw 模式前保存原始设置
static struct termios g_original_termios;
tcgetattr(STDIN_FILENO, &g_original_termios);
// 设置 Raw 模式
setup_terminal_raw_mode(STDIN_FILENO);
// 启用鼠标跟踪
enable_mouse_tracking(STDOUT_FILENO);
// 退出时恢复
tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_original_termios);
disable_mouse_tracking(STDOUT_FILENO);
write(STDOUT_FILENO, "\r\n", 2); // 确保提示符在新行
优雅退出机制
- 服务端发送关闭消息:
MSG_TYPE_CLOSE - 响应线程退出:收到关闭消息后 break
- 主线程检测:
pthread_join(response_thread)返回 - 设置退出标志:
g_should_exit = 1 - 取消其他线程:
pthread_cancel(window_thread),pthread_cancel(input_thread) - 清理终端状态:
cleanup_terminal()- 禁用鼠标跟踪
- 恢复原始 termios 设置
- 输出换行符
- 关闭连接:
close(sock)
7. Socket 协议 (src/socket_protocol.c)
提供终端管理和消息传输的底层函数。
终端管理函数
// 设置 Raw 模式并保存原始设置
int setup_terminal_raw_mode(int fd);
// 恢复保存的原始终端设置
int restore_terminal_mode(int fd);
// 启用鼠标跟踪(ANSI 转义序列)
int enable_mouse_tracking(int fd);
// 禁用鼠标跟踪
int disable_mouse_tracking(int fd);
消息传输函数
// 发送结构化消息
int write_message(int sock, MessageType type, const void* payload, uint32_t payload_len);
// 读取结构化消息
int read_message(int sock, MessageType* type, void** payload, uint32_t* payload_len);
// 释放消息载荷
void free_message_payload(void* payload);
8. Go 服务端 (go_service/internal/services/tasks/socket.go)
新的回调式 API
服务端使用回调式 Connection 对象处理消息:
// 创建连接对象
conn := socket.NewConnection(netConn)
defer conn.Close()
// 注册消息处理器
conn.SetHandlers(socket.MessageHandlers{
OnInit: func(payload []byte) error {
// 处理初始化消息
params := parseInitPayload(payload)
// 启动 AI 分析 goroutine
go processAIRequest(conn, params)
return nil
},
OnWindowSizeUpdate: func(termInfo *socket.TerminalInfo) error {
// 更新终端尺寸
params.TerminalInfo = *termInfo
return nil
},
OnTerminalInput: func(data []byte) error {
// 处理用户输入(如需要)
return nil
},
OnMouseEvent: func(event *socket.MouseEvent) error {
// 处理鼠标事件(如需要)
return nil
},
OnKeyEvent: func(data []byte) error {
// 处理按键事件(如需要)
return nil
},
OnClose: func() error {
// 连接关闭清理
return nil
},
OnError: func(err error) {
// 错误处理
},
})
// 启动消息循环(非阻塞)
conn.Start()
// 等待连接关闭
conn.Wait()
服务端输出控制
服务端完全控制终端输出:
// 发送普通响应(会立即显示在客户端终端)
conn.SendResponse([]byte("正在分析错误...\r\n"))
// 发送格式化输出(支持 ANSI 颜色)
msg := constants.ColorGreen + "✓ 分析完成\r\n" + constants.ColorReset
conn.SendResponse([]byte(msg))
// 发送 TUI 界面更新
conn.SendResponse([]byte("\033[2J\033[H")) // 清屏
conn.SendResponse([]byte("╔════════════════╗\r\n"))
conn.SendResponse([]byte("║ AI 分析结果 ║\r\n"))
conn.SendResponse([]byte("╚════════════════╝\r\n"))
// 完成后关闭连接(客户端会自动退出)
conn.Close()
错误上报机制
当被拦截的命令执行失败(退出码非 0 或被信号终止)时,系统会触发错误上报流程。
- 执行与捕获:
pty_dup.c中的handle_io循环负责实时捕获子进程的stdout和stderr。 - 日志记录: 捕获到的
stderr内容会被实时写入到特定的日志文件中(由GET_LOG_FILE宏定义路径)。 - 触发上报:
- 父进程等待子进程退出。
- 检查子进程退出状态 (
child_status)。 - 如果
WIFEXITED且退出码非 0,或者WIFSIGNALED(被信号终止),则调用seeking_solutions。
- 建立连接:
seeking_solutions通过 Socket 连接到 Go 服务,发送以下信息:- Filename: 执行的命令名称
- CWD: 当前工作目录
- Argv: 完整的参数列表
- Envp: 完整的环境变量列表
- Log Path: 记录了错误输出的日志文件绝对路径
- Terminal Info: 终端尺寸、类型、termios 设置
- 实时交互: 连接建立后,客户端进入实时交互模式:
- 服务端完全控制输出(加载动画、AI 响应、TUI 界面等)
- 客户端捕获所有用户输入(键盘、鼠标、窗口调整)
- 服务端决定何时关闭连接
- 优雅退出: 服务端发送
MSG_TYPE_CLOSE后,客户端自动清理终端状态并退出
sequenceDiagram
participant Parent as Parent Process
participant Child as Child Process (Cmd)
participant Log as Log File (Stderr)
participant Client as Socket Client
participant Server as Go Service
participant AI as AI 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->>Client: seeking_solutions()
Client->>Client: 连接 Socket
Client->>Client: 保存原始 termios
Client->>Client: 设置 Raw 模式
Client->>Client: 启用鼠标跟踪
Client->>Server: MSG_TYPE_INIT (完整上下文)
Note over Client,Server: Filename, CWD, Argv, Envp,<br/>Log Path, Terminal Info
par 启动线程
Client->>Client: 启动响应监听线程
Client->>Client: 启动窗口监控线程
Client->>Client: 启动输入监听线程
end
Server->>Server: 解析初始化消息
Server->>Log: 读取错误日志
Server->>Client: 发送初始提示
Note over Client: "检测到命令执行出错!"
Server->>AI: 请求错误分析
loop 实时交互
par 客户端监听
Client->>Server: 终端输入事件
Client->>Server: 窗口大小更新
Client->>Server: 鼠标事件
and 服务端输出
Server->>Client: 加载动画
AI-->>Server: 流式返回建议
Server->>Client: 实时输出 AI 响应
Server->>Client: 渲染 TUI 界面
end
end
Server->>Server: 分析完成
Server->>Client: MSG_TYPE_CLOSE
Client->>Client: 响应线程退出
Client->>Client: 取消其他线程
Client->>Client: cleanup_terminal()
Note over Client: - 恢复 termios<br/>- 禁用鼠标跟踪<br/>- 输出换行符
Client->>Client: 关闭连接
Client-->>Parent: 返回
end
通信协议格式
消息头(16字节)
| 字段 | 类型 | 大小 | 说明 |
|---|---|---|---|
magic |
uint32_t |
4 | 魔数 0x42534D54 ("BSMT") |
type |
uint32_t |
4 | 消息类型(见下表) |
payload_len |
uint32_t |
4 | 载荷长度 |
reserved |
uint32_t |
4 | 保留字段 |
消息类型
| 类型值 | 名称 | 方向 | 说明 |
|---|---|---|---|
| 1 | MSG_TYPE_INIT |
C → Go | 初始化消息(包含完整上下文) |
| 2 | MSG_TYPE_WINDOW_SIZE_UPDATE |
C → Go | 终端窗口大小变化 |
| 3 | MSG_TYPE_SERVER_RESPONSE |
Go → C | 服务端响应(直接输出) |
| 4 | MSG_TYPE_CLOSE |
Go → C | 关闭连接通知 |
| 5 | MSG_TYPE_TERMINAL_INPUT |
C → Go | 终端原始输入 |
| 6 | MSG_TYPE_TERMINAL_OUTPUT |
Go → C | 终端输出(保留) |
| 7 | MSG_TYPE_MOUSE_EVENT |
C → Go | 鼠标事件(结构化) |
| 8 | MSG_TYPE_KEY_EVENT |
C → Go | 键盘事件(结构化) |
初始化消息载荷(MSG_TYPE_INIT)
按顺序包含以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
filename_len |
uint64_t |
文件名长度 |
filename |
char[] |
文件名内容 |
pwd_len |
uint64_t |
当前工作目录长度 |
pwd |
char[] |
当前工作目录内容 |
argc |
int32_t |
参数个数 |
arg_len |
uint64_t |
(循环) 参数长度 |
arg_str |
char[] |
(循环) 参数内容 |
envc |
int32_t |
环境变量个数 |
env_len |
uint64_t |
(循环) 环境变量长度 |
env_str |
char[] |
(循环) 环境变量内容 |
log_path_len |
uint64_t |
日志路径长度 |
log_path |
char[] |
日志路径内容 |
term_info_fixed |
TerminalInfoFixed |
固定终端信息结构 |
term_type_len |
uint32_t |
TERM 类型长度 |
term_type |
char[] |
TERM 类型内容 |
shell_type_len |
uint32_t |
SHELL 类型长度 |
shell_type |
char[] |
SHELL 类型内容 |
终端信息结构(TerminalInfoFixed)
typedef struct {
uint32_t is_tty; // 是否为TTY
uint16_t rows; // 行数
uint16_t cols; // 列数
uint16_t x_pixel; // X像素
uint16_t y_pixel; // Y像素
uint32_t has_termios; // 是否有termios属性
uint32_t input_flags; // termios输入标志
uint32_t output_flags; // termios输出标志
uint32_t control_flags; // termios控制标志
uint32_t local_flags; // termios本地标志
} TerminalInfoFixed;
鼠标事件载荷(MSG_TYPE_MOUSE_EVENT)
typedef struct {
uint32_t event_type; // 1=按下, 2=释放, 3=移动, 4=滚轮上, 5=滚轮下
uint32_t button; // 鼠标按钮(0=左键,1=中键,2=右键)
uint32_t x; // X坐标(列)
uint32_t y; // Y坐标(行)
uint32_t modifiers; // 修饰键(Shift, Ctrl, Alt等)
} MouseEvent;
编译构建
项目使用 Makefile 进行管理。
C 客户端
cd execve_hook
# 默认构建(生成 intercept.so)
make
# 开启 Hook 功能
make HOOK=1
# 开启调试模式
make DEBUG=1
# 编译测试程序
make test_socket_client
# 清理
make clean
Go 服务端
cd go_service
# 编译服务端
make
# 编译测试 Socket 服务器
make test_socket_server
# 运行服务
sudo ./build/bash_go_service-amd64 daemon
测试
测试 Socket 通信
- 启动 Go 测试服务器:
cd go_service
sudo go run ./cmd/tests/test_socket_server
- 运行 C 测试客户端:
cd execve_hook
./build/test_socket_client
- 测试功能:
- 调整终端窗口大小(观察服务器接收窗口更新)
- 移动鼠标和点击(观察鼠标事件)
- 输入键盘字符(观察终端输入事件)
- 30秒后服务器自动关闭连接
- 检查客户端是否正确恢复终端状态
验证终端恢复
测试完成后,终端应该:
- ✓ 提示符在新的一行开始
- ✓ 正常处理换行符(\n 不再变成 \r\n)
- ✓ 鼠标跟踪已禁用
- ✓ 可以正常输入命令
目录结构说明
execve_hook/
├── src/
│ ├── execve_interceptor.c # execve 拦截主逻辑
│ ├── hook_write.c # write 系列函数拦截
│ ├── config.c # 共享内存配置加载
│ ├── pty_dup.c # 伪终端处理
│ ├── client.c # Socket 客户端(多线程)
│ ├── client.h # 客户端头文件
│ ├── socket_protocol.c # Socket 协议实现
│ ├── socket_protocol.h # Socket 协议定义
│ └── rules.c # 规则匹配逻辑
├── tests/
│ └── test_socket_client.c # Socket 通信测试程序
├── build/ # 编译输出目录
├── Makefile # 构建脚本
├── README.md # 项目文档
├── SOCKET_PROTOCOL.md # Socket 协议文档
├── SOCKET_IMPROVEMENTS.md # 改进说明
└── TERMINAL_EVENTS.md # 终端事件捕获文档
go_service/
├── cmd/
│ └── tests/
│ └── test_socket_server/ # Go 测试服务器
│ └── main.go
├── internal/
│ ├── socket/
│ │ ├── protocol.go # Socket 协议实现(Go)
│ │ └── example.go # 使用示例
│ └── services/
│ └── tasks/
│ └── socket.go # 生产环境 Socket 服务
└── ...
技术亮点
1. 现代化通信协议
- 结构化二进制消息(高效、类型安全)
- 魔数验证(防止协议错误)
- 可扩展的消息类型系统
2. 完全的服务端控制
- 客户端只负责事件捕获和转发
- 服务端完全控制输出和交互逻辑
- 支持任意 TUI 框架和渲染方式
3. 优雅的终端管理
- 进入时保存原始 termios 设置
- 运行时使用 Raw 模式捕获所有输入
- 退出时完全恢复终端状态
4. 线程安全的并发设计
- C 端:4 个独立线程处理不同职责
- Go 端:goroutine + channel 实现并发
- 互斥锁保护共享资源
5. 实时事件捕获
- 键盘输入(Raw 模式)
- 鼠标事件(ANSI 转义序列解析)
- 窗口大小变化(SIGWINCH 信号)
应用场景
1. AI 辅助调试
- 捕获命令执行错误
- 发送给 AI 服务分析
- 实时流式返回建议
- 服务端控制输出格式
2. 交互式 TUI 应用
- 服务端使用 bubbletea 等框架
- 渲染复杂的终端界面
- 响应用户鼠标和键盘操作
- 客户端只负责事件转发
3. 会话录制与回放
- 记录所有终端交互
- 包括输入、输出、鼠标、窗口变化
- 用于审计、培训、调试
4. 远程终端控制
- 本地客户端捕获事件
- 发送到远程服务器
- 服务器处理并返回输出
- 实现远程控制终端
性能考虑
- 窗口监听:使用条件变量实现事件驱动,零 CPU 占用等待
- 输入监听轮询间隔:100ms(平衡响应性和 CPU)
- 响应监听轮询间隔:500ms(用于检测退出标志)
- 消息大小:终端事件 20-80 字节
- 线程开销:固定 4 个线程,不会增长
- 内存使用:动态分配,用完即释放
- 延迟:< 10ms(输入到转发)
兼容性
终端支持
- ✓ xterm, gnome-terminal, konsole
- ✓ iTerm2, Windows Terminal
- ✓ alacritty, kitty
- ⚠ tmux, screen(需要配置)
系统要求
- Linux 内核: 2.6+
- Glibc: 2.17+(pthread 支持)
- Go: 1.16+
相关文档
- SOCKET_PROTOCOL.md - 详细的协议规范
- SOCKET_IMPROVEMENTS.md - 改进历史和设计决策
- TERMINAL_EVENTS.md - 终端事件捕获详解