You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ubuntu 20.04.3 LTS下LD_PRELOAD Hook仅记录少量命令的问题排查

问题排查:LD_PRELOAD劫持execve仅记录部分命令

问题背景

编写了LD_PRELOAD Hook代码劫持execve函数以记录系统执行的命令,编译并设置环境变量后,仅捕获到lesspipe相关命令,ls、dir等命令无日志输出。

代码与操作步骤

Hook代码:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <time.h>

// Log file location
#define LOG_FILE "/home/maifee/cmd-log.log"

// Function to get the process name
void get_process_name(pid_t pid, char *name, size_t len) {
    char path[256];
    snprintf(path, sizeof(path), "/proc/%d/comm", pid);
    FILE *file = fopen(path, "r");
    if (file) {
        fgets(name, len, file);
        name[strcspn(name, "\n")] = 0; // Remove newline character
        fclose(file);
    } else {
        strncpy(name, "unknown", len);
    }
}

// Override execve
int execve(const char *filename, char *const argv[], char *const envp[]) {
    // Get the original execve function
    int (*original_execve)(const char *, char *const[], char *const[]) = dlsym(RTLD_NEXT, "execve");

    // Log the command to a file
    FILE *log_file = fopen(LOG_FILE, "a");
    if (log_file) {
        pid_t pid = getpid();
        pid_t ppid = getppid();
        char pname[256], ppname[256];
        get_process_name(pid, pname, sizeof(pname));
        get_process_name(ppid, ppname, sizeof(ppname));
        // print time [2025.12.30 23:59:59]
        time_t t = time(NULL);
        struct tm tm = *localtime(&t);
        fprintf(log_file, "[%d.%d.%d %d:%d:%d] ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
        fprintf(log_file, "PID: %d (%s), PPID: %d (%s), Command: ", pid, pname, ppid, ppname);
        for (int i = 0; argv[i]; i++) {
            fprintf(log_file, "%s ", argv[i]);
        }
        fprintf(log_file, "\n");
        fclose(log_file);
    }

    // NEVER call execve directly, it will cause an infinite loop
    // NEVER remove this line
    return original_execve(filename, argv, envp);
}

编译命令:

gcc -shared -fPIC -o ld_preload_hook.so ld_preload_hook.c -ldl

环境变量设置:

export LD_PRELOAD=$PWD/ld_preload_hook.so
source ~/.zshrc
source ~/.bashrc

问题原因分析

1. Shell内置命令不触发execve

bash/zsh等主流shell会将ls、cd、echo等常用命令实现为内置命令,执行时不会创建新进程,自然不会调用execve系统调用,因此Hook无法捕获。

验证方法:执行type ls,若输出为ls is a shell builtin,则说明是内置命令;若输出为ls is /usr/bin/ls,则为外部命令。

2. 仅Hook了execve,未覆盖其他exec系列封装函数

很多程序不会直接调用execve,而是使用libc提供的execv、execvp、execl等封装函数。部分libc实现中,这些封装函数可能直接发起系统调用而非调用libc的execve函数,导致Hook失效。

3. LD_PRELOAD作用范围有限

仅在当前shell设置LD_PRELOAD时,仅对该shell启动的进程生效;若进程以setuid/setgid权限运行,环境变量会被清除(但ls一般不属于此类)。

解决方案

1. 处理Shell内置命令

LD_PRELOAD无法直接捕获内置命令,可通过以下方式变通:

  • 强制使用外部命令:执行command ls或直接调用二进制文件/usr/bin/ls
  • 禁用shell内置命令:如bash中执行enable -n ls(会影响使用体验)
  • 改用PTrace机制:通过跟踪shell系统调用或内部逻辑记录内置命令(需更复杂代码)

2. Hook所有exec系列函数

修改代码,覆盖libc中所有exec系列函数,确保无论程序调用哪个封装函数都能被捕获。示例修改:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <time.h>
#include <stdarg.h>

#define LOG_FILE "/home/maifee/cmd-log.log"

// 通用日志函数
void log_exec(const char *filename, char *const argv[]) {
    FILE *log_file = fopen(LOG_FILE, "a");
    if (!log_file) return;

    pid_t pid = getpid();
    pid_t ppid = getppid();
    char pname[256], ppname[256];
    
    // 获取当前进程名
    char path[256];
    snprintf(path, sizeof(path), "/proc/%d/comm", pid);
    FILE *file = fopen(path, "r");
    if (file) {
        fgets(pname, sizeof(pname), file);
        pname[strcspn(pname, "\n")] = 0;
        fclose(file);
    } else {
        strncpy(pname, "unknown", sizeof(pname));
    }
    // 获取父进程名
    snprintf(path, sizeof(path), "/proc/%d/comm", ppid);
    file = fopen(path, "r");
    if (file) {
        fgets(ppname, sizeof(ppname), file);
        ppname[strcspn(ppname, "\n")] = 0;
        fclose(file);
    } else {
        strncpy(ppname, "unknown", sizeof(ppname));
    }

    time_t t = time(NULL);
    struct tm tm = *localtime(&t);
    fprintf(log_file, "[%d.%d.%d %d:%d:%d] ", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec);
    fprintf(log_file, "PID: %d (%s), PPID: %d (%s), Command: ", pid, pname, ppid, ppname);
    for (int i = 0; argv[i]; i++) {
        fprintf(log_file, "%s ", argv[i]);
    }
    fprintf(log_file, "\n");
    fclose(log_file);
}

// Hook execve
int execve(const char *filename, char *const argv[], char *const envp[]) {
    int (*original_execve)(const char *, char *const[], char *const[]) = dlsym(RTLD_NEXT, "execve");
    log_exec(filename, argv);
    return original_execve(filename, argv, envp);
}

// Hook execv
int execv(const char *path, char *const argv[]) {
    int (*original_execv)(const char *, char *const[]) = dlsym(RTLD_NEXT, "execv");
    log_exec(path, argv);
    return original_execv(path, argv);
}

// Hook execvp
int execvp(const char *file, char *const argv[]) {
    int (*original_execvp)(const char *, char *const[]) = dlsym(RTLD_NEXT, "execvp");
    log_exec(file, argv);
    return original_execvp(file, argv);
}

// Hook execl
int execl(const char *path, const char *arg, ...) {
    int (*original_execl)(const char *, const char *, ...) = dlsym(RTLD_NEXT, "execl");
    va_list args;
    va_start(args, arg);
    char *const argv[1024];
    argv[0] = (char*)arg;
    int i = 1;
    while ((argv[i] = va_arg(args, char*)) != NULL) {
        i++;
    }
    va_end(args);
    log_exec(path, argv);
    return original_execl(path, arg, args);
}

// 可继续添加execlp、execle、execvpe的Hook

编译命令不变:

gcc -shared -fPIC -o ld_preload_hook.so ld_preload_hook.c -ldl

3. 确保LD_PRELOAD全局生效

  • 用户级全局生效:将export LD_PRELOAD=/path/to/ld_preload_hook.so添加到~/.bashrc或~/.zshrc,重新登录后生效
  • 系统级全局生效:编辑/etc/ld.so.preload文件,添加so文件路径(会影响所有进程,谨慎操作)

验证方法

  1. 执行/usr/bin/ls直接调用外部二进制文件,检查是否被记录
  2. 执行type ls确认命令类型,针对性验证

内容的提问来源于stack exchange,提问作者Maifee Ul Asad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 06:30:54