execve_hook/README.md

24 KiB
Raw Blame History

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: 退出程序

工作模式

  1. 客户端职责(静默模式)

    • 捕获所有终端输入(键盘、鼠标)
    • 监听窗口大小变化SIGWINCH
    • 将原始事件发送给服务端
    • 接收服务端消息并输出到终端
    • 不做任何业务逻辑处理
  2. 服务端职责(完全控制)

    • 接收客户端的所有输入事件
    • 处理业务逻辑AI 分析、日志查询等)
    • 渲染 TUI 界面或格式化输出
    • 控制连接生命周期
    • 决定何时关闭连接
  3. 终端状态管理

    • 进入时:客户端设置 Raw 模式 + 启用鼠标跟踪
    • 运行时:保存原始 termios 设置
    • 退出时:自动恢复原始设置 + 禁用鼠标跟踪 + 输出换行符

核心逻辑概述

整个项目的逻辑可以分为以下几个核心模块:

1. execve 拦截器 (src/execve_interceptor.c)

这是项目的核心入口。

  • 拦截机制: 通过 LD_PRELOAD 预加载动态库,覆盖系统默认的 execve 函数。
  • 主要流程:
    1. 环境清理: 在调用前移除 LD_PRELOAD 环境变量,防止对子进程造成非预期的递归影响。
    2. 配置加载: 调用 config.c 从共享内存中加载配置。
    3. 规则匹配: 使用 rules.c 检查当前执行的命令是否命中特定规则。
    4. 行为决策: 根据规则决定是否需要拦截、记录日志或通过 PTY 执行。
    5. 执行原函数: 最终调用原始的 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 等标准输出函数。
  • 功能: 将进程写入 stdoutstderr 的内容同时写入到一个指定的日志文件中。
  • 用途: 即使不使用 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 个线程

  1. 主线程

    • 连接 Socket
    • 发送初始化消息(命令信息、环境变量、日志路径、终端信息)
    • 等待其他线程完成
    • 清理资源和恢复终端
  2. 响应监听线程 (response_listener_thread)

    • 持续监听服务端消息
    • 收到 MSG_TYPE_SERVER_RESPONSE:直接输出到终端
    • 收到 MSG_TYPE_CLOSE:退出循环,触发程序结束
  3. 窗口监控线程 (window_monitor_thread)

    • 监听 SIGWINCH 信号(窗口大小变化)
    • 使用 条件变量 (pthread_cond_t) 实现事件驱动等待
    • 信号触发时立即唤醒,无需轮询
    • 自动发送 MSG_TYPE_WINDOW_SIZE_UPDATE 消息
  4. 输入监听线程 (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);  // 确保提示符在新行

优雅退出机制

  1. 服务端发送关闭消息MSG_TYPE_CLOSE
  2. 响应线程退出:收到关闭消息后 break
  3. 主线程检测pthread_join(response_thread) 返回
  4. 设置退出标志g_should_exit = 1
  5. 取消其他线程pthread_cancel(window_thread), pthread_cancel(input_thread)
  6. 清理终端状态cleanup_terminal()
    • 禁用鼠标跟踪
    • 恢复原始 termios 设置
    • 输出换行符
  7. 关闭连接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 或被信号终止)时,系统会触发错误上报流程。

  1. 执行与捕获: pty_dup.c 中的 handle_io 循环负责实时捕获子进程的 stdoutstderr
  2. 日志记录: 捕获到的 stderr 内容会被实时写入到特定的日志文件中(由 GET_LOG_FILE 宏定义路径)。
  3. 触发上报:
    • 父进程等待子进程退出。
    • 检查子进程退出状态 (child_status)。
    • 如果 WIFEXITED 且退出码非 0或者 WIFSIGNALED(被信号终止),则调用 seeking_solutions
  4. 建立连接: seeking_solutions 通过 Socket 连接到 Go 服务,发送以下信息:
    • Filename: 执行的命令名称
    • CWD: 当前工作目录
    • Argv: 完整的参数列表
    • Envp: 完整的环境变量列表
    • Log Path: 记录了错误输出的日志文件绝对路径
    • Terminal Info: 终端尺寸、类型、termios 设置
  5. 实时交互: 连接建立后,客户端进入实时交互模式:
    • 服务端完全控制输出加载动画、AI 响应、TUI 界面等)
    • 客户端捕获所有用户输入(键盘、鼠标、窗口调整)
    • 服务端决定何时关闭连接
  6. 优雅退出: 服务端发送 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 通信

  1. 启动 Go 测试服务器
cd go_service
sudo go run ./cmd/tests/test_socket_server
  1. 运行 C 测试客户端
cd execve_hook
./build/test_socket_client
  1. 测试功能
    • 调整终端窗口大小(观察服务器接收窗口更新)
    • 移动鼠标和点击(观察鼠标事件)
    • 输入键盘字符(观察终端输入事件)
    • 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+

相关文档