LD_PRELOAD 学习

打CTF的时候粗略学过LD_PRELOAD的用法,可以用来绕过php的disable_functions,也可以用于一些特殊场景,改变原有逻辑的预期返回。现在来具体写一篇文章来梳理一下知识点,因为下一个要做的安卓方面的项目需要这个技能点。

简单使用

很好理解,就是一个环境变量,提前载入动态链接库,先到先得。举个例子:

劫持函数调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>

// 要劫持的函数,这里的前面要与原来保持一致
int open(const char *pathname, int flags) {
// 获取真正的 open 函数地址
int (*original_open)(const char *, int);
original_open = dlsym(RTLD_NEXT, "open");

printf("调用了open函数: %s\n", pathname);

// 还是返回原始的 open 指针
return original_open(pathname, flags);
}

注释很清晰。重点看original_open = dlsym(RTLD_NEXT, "open");这一行。其中dlsym能够在动态库中搜索符号(函数名)。RTLD_NEXT标识让动态链接器跳过当前的这个库,去寻找下一个出现的"open"函数”。

1
2
3
gcc -shared -fPIC -o hook.so hook.c -ldl
# -ldl 是链接动态链接库
LD_PRELOAD=./hook.so cat /etc/passwd

终端会打印:

1
2
3
4
5
6
7
调用了open函数: /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
...

constructor属性

利用__attribute__((constructor)),可以让动态链接库在加载时立即被调用。类似PHP的__construct()。举个例子:

1
2
3
4
5
6
#define _GNU_SOURCE
#include <stdio.h>

void __attribute__((constructor)) my_init() {
printf("hook!\n");
}

代码更简单了,而且不需要等待原来的函数被调用。

1
2
3
4
5
6
7
8
gcc -shared -fPIC -o hook2.so hook2.c -ldl
LD_PRELOAD=./hook2.so cat /etc/passwd
hook!
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
...

安卓上的应用

来看这篇文章,讲到Magisk在init的第二阶段就是通过LD_PRELOAD把自己的payload.so注入到原来的init里,替换security_load_policy函数为自己的实现,以实现selinux 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
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
94
95
96
// native/src/init/selinux.cpp

// #define MOCK_COMPAT SELINUXMOCK "/compatible"
// #define MOCK_LOAD SELINUXMOCK "/load"
// #define MOCK_ENFORCE SELINUXMOCK "/enforce"

// #define SELINUX_LOAD /sys/fs/selinux/load
// #define SELINUX_ENFORCE /sys/fs/selinux/enforce

bool MagiskInit::hijack_sepolicy() {
xmkdir(SELINUXMOCK, 0);

if (access("/system/bin/init", F_OK) == 0) {
// On 2SI devices, the 2nd stage init file is always a dynamic executable.
dump_preload("/dev/preload.so", 0644);
setenv("LD_PRELOAD", "/dev/preload.so", 1);
}

// Hijack the "load" and "enforce" node in selinuxfs to manipulate
// the actual sepolicy being loaded into the kernel
auto hijack = [&] {
LOGD("Hijack [" SELINUX_LOAD "]\n");
close(xopen(MOCK_LOAD, O_CREAT | O_RDONLY, 0600));
xmount(MOCK_LOAD, SELINUX_LOAD, nullptr, MS_BIND, nullptr);
LOGD("Hijack [" SELINUX_ENFORCE "]\n");
mkfifo(MOCK_ENFORCE, 0644);
xmount(MOCK_ENFORCE, SELINUX_ENFORCE, nullptr, MS_BIND, nullptr);
};

string dt_compat;
if (access(SELINUX_ENFORCE, F_OK) != 0) {
// selinuxfs not mounted yet. Hijack the dt fstab nodes first
// and let the original init mount selinuxfs for us.
// This only happens on Android 8.0 - 9.0
// 省略......
} else {
hijack();
}

// Read all custom rules into memory
string rules;
if (auto dir = xopen_dir("/data/" PREINITMIRR)) {
for (dirent *entry; (entry = xreaddir(dir.get()));) {
auto name = "/data/" PREINITMIRR "/"s + entry->d_name;
auto rule_file = name + "/sepolicy.rule";
if (xaccess(rule_file.data(), R_OK) == 0 &&
access((name + "/disable").data(), F_OK) != 0 &&
access((name + "/remove").data(), F_OK) != 0) {
LOGD("Load custom sepolicy patch: [%s]\n", rule_file.data());
full_read(rule_file.data(), rules);
rules += '\n';
}
}
}

// Create a new process waiting for init operations
if (xfork()) {
// In parent, return and continue boot process
return true;
}

// 省略......

// This open will block until init calls security_getenforce
int fd = xopen(MOCK_ENFORCE, O_WRONLY);

// Cleanup the hijacks
umount2("/init", MNT_DETACH);
xumount2(SELINUX_LOAD, MNT_DETACH);
xumount2(SELINUX_ENFORCE, MNT_DETACH);

// Load and patch policy
auto sepol = unique_ptr<sepolicy>(sepolicy::from_file(MOCK_LOAD));
sepol->magisk_rules();
sepol->load_rules(rules);

// Load patched policy into kernel
sepol->to_file(SELINUX_LOAD);

// Write to the enforce node ONLY after sepolicy is loaded. We need to make sure
// the actual init process is blocked until sepolicy is loaded, or else
// restorecon will fail and re-exec won't change context, causing boot failure.
// We (ab)use the fact that init reads the enforce node, and because
// it has been replaced with our FIFO file, init will block until we
// write something into the pipe, effectively hijacking its control flow.

string enforce = full_read(SELINUX_ENFORCE);
xwrite(fd, enforce.data(), enforce.length());
close(fd);

// At this point, the init process will be unblocked
// and continue on with restorecon + re-exec.

// Terminate process
exit(0);
}