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文件路径(会影响所有进程,谨慎操作)
验证方法
- 执行
/usr/bin/ls直接调用外部二进制文件,检查是否被记录 - 执行
type ls确认命令类型,针对性验证
内容的提问来源于stack exchange,提问作者Maifee Ul Asad
相关产品推荐
相关产品推荐

