【Linux技巧】LD_AUDIT-LD_Preload

0x01 前言

本文是对以下文章的学习:

0x02 LD_AUDIT

虽然ld.so帮助手册上说了LD_PRELOAD能在其他共享对象加载前加载,但是它真的是这样吗?

LD_PRELOAD
A list of additional, user-specified, ELF shared objects to be loaded before all others. This feature can be used to selectively override functions in other shared objects.

LD_AUDIT这个变量,如果这个环境变量存在,链接器将会从指定路径加载共享库,然后从中调用特定的函数。

1
2
3
4
5
6
7
8
9
// preloadlib.cpp
//g++ -fPIC -shared -g preloadlib.c -o preloadlib.so
#include <stdio.h>

__attribute__((constructor))

static void init(void) {
printf("I'm loaded from LD_PRELOAD\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// auditlib.cpp
//g++ -fPIC -shared -g auditlib.c -o auditlib.so

#include <stdio.h>

__attribute__((constructor)) //构造函数

static void init(void) {
printf("I'm loaded from LD_AUDIT\n");
}

/*la_version 此函数必须被定义。它执行动态链接器和审计库之间的初始握手。当调用此函数时,动态链接器将传递该连接器支持的审计接口的最高版本(按版本计算),返回常量LAV_CURENT,表示用户构建AUDIT模块的link.h的版本,如果ld.so不支持该版本的审计接口,将不会激活AUDIT模块,如果返回值为0也不会激活AUDIT模块。
*/
unsigned int la_version(unsigned int version) {
// Version == 0 means the library will be ignored by the linker.
if (version == 0) {
return version;
}
return LAV_CURRENT;

}
1
LD_PRELOAD=~/preloadlib.so LD_AUDIT=~/auditlib.so whoami

image-20220322113030227

从结果来看其实LD_AUDIT先于LD_PRELOAD执行的。

防御LD_PRELOAD

我们知道了LD_AUDIT可以先于LD_PRELOAD加载,那么我们是否可以用LD_AUDIT对抗LD_PRELOAD函数?

la_objsearch()

The dynamic linker invokes this function to inform the auditinglibrary that it is about to search for a shared object. The name argument is the filename or pathname that is to be searched for.

As its function result, la_objsearch() returns the pathname that the dynamic linker should use for further processing. If NULL is returned, then this pathname is ignored for further processing. If this audit library simply intends to monitor search paths, then name should be returned.

la_objsearch函数,每当有一个共享库要被加载时都会调用这个函数,函数应当返回调用的文件路径,如果返回NULL的话,将不会加载。

指定LD_AUDIT开始审计后,如果加载程序要预加载一个不需要的库,我们可以通过检索LD_PRELOAD环境变量在它加载之前识别这个动作,我们可以返回NULL来阻止它加载。

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
// auditlib.so
//g++ -fPIC -shared -g -o auditlib.so auditlib.cpp
#include <stdio.h>
#include <stdlib.h>
#include <link.h>
#include <string.h>

const char* preloaded;
unsigned int la_version(unsigned int version) {
// Version == 0 means the library will be ignored by the linker.
preloaded = getenv("LD_PRELOAD");
if (version == 0) {
return version;
}
return LAV_CURRENT;

}
char * la_objsearch(const char *name, uintptr_t *cookie, unsigned int flag){

if(NULL != preloaded && strcmp(name,preloaded) == 0){
fprintf(stderr,"Disabling the loading of a 'preload' library: %s \n",name);
return NULL;
}
return (char *)name;
}

LD_PRELOAD继续使用上一个案例的Demo代码进行测试:

image-20220322164101829

对比可以发现,LD_AUDIT成功让LD_PRELOAD里的内容不执行了。

那么防守人员要如何运用这个来对LD_PRELOAD进行防御呢?原本我以为这个也有向ld.so.preload一样的文件存在,但是并没有。。所以我们只能通过登录时设置环境变量,比如/etc/profile或者是/etc/environment,又或者是对应用户的bashrc文件。

image-20220322190728627

在没有加载LD_AUDIT共享库的时候,可以看到libprocesshider确实是工作了,可以看到我们ps命令找不到evil_script的进程。

image-20220322192510883

把LD_AUDIT写进登录文件,重新登录系统,并重新启动evil_script。

image-20220322193328909

但是并没什么用,ldd一个进程,但还是有加载恶意的so文件。为什么呢?

image-20220322195110346

其实这里问题出在auditlib.cpp 代码里面我们只检查了环境变量LD_PRELOAD但是没有检查ld.so.preload文件。

image-20220322210132417

不过github上已经有大佬实现了:libpreloadvaccine

我这边对着已经实现的源码造轮子:

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
// auditlib.so
//g++ -fPIC -shared -g -o auditlib.so auditlib.cpp
#include <stdio.h>
#include <stdlib.h>
#include <link.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

#define UNUSED(x) (void)(x)

bool file_contains_object(const char *object, const char *specified_file);


const char *preloaded;
const char *LD_SO_PRELOAD_PATH="/etc/ld.so.preload";// ld.so.preload的路径
unsigned int la_version(unsigned int version) {
// Version == 0 means the library will be ignored by the linker.
if (version == 0) {
return version;
}
return LAV_CURRENT;

}
char * la_objsearch(const char *name, uintptr_t *cookie, unsigned int flag){
//不需要
UNUSED(cookie);
UNUSED(flag);

//读取LD_PRELOAD环境变量
preloaded = getenv("LD_PRELOAD");

//读取ld.so.preload文件
bool in_ld_so_preload = file_contains_object(name, LD_SO_PRELOAD_PATH);
//如果为真就返回NULL,直接dis preload
if((NULL != preloaded && strcmp(name,preloaded) == 0) || in_ld_so_preload ){
fprintf(stderr,"Disabling the loading of a 'preload' library: %s \n",name);
return NULL;
}
return (char *)name;
}


bool file_contains_object(const char *object, const char *specified_file) {
bool contains_object = false;

int fd = 0;
struct stat file_info = {0};
size_t size = 0;
//只读方式打开文件,如果文件不存在则返回false
if ((fd = open(specified_file, O_RDONLY)) >= 0) {
//获取指定文件的大小和映射文件缓冲区的维度
if (fstat(fd, &file_info) == 0) {
size = file_info.st_size;
char mapped_file[size];

if (read(fd, mapped_file, size) >= 0) {
contains_object = (strstr(mapped_file, object) != NULL);
}
}
}
//关闭文件操作符
if (fd) {
close(fd);
}

return contains_object;
}

再次使用ldd命令查看静态程序的加载动态链接中已经没有恶意的/etc/ld.so.preload文件所加载的动态链接文件.

image-20220322210324148

那么能否用在PAM上呢?防御之前的PAM结合LD_PRELOAD劫持SSHD记录密码?Linux键盘记录后门

这里我使用了ld_preload去劫持pam_get_item发送密码给http服务器,而ld_audit则是用libpreloadvaccine,当sshd启动时确实显示说是拒绝了,但是后续当ssh客户端去连接服务器的时候,却无法激活AUDIT模块,导致防御LD_RELOAD失败。

image-20220323113545585

所以这里我们还需要修改一下audit的la_version函数

1
2
3
4
5
6
// auditlib.so
//g++ -fPIC -shared -g -o auditlib.so auditlib.cpp

unsigned int la_version(unsigned int version) {
return version;
}

拦截成功.

image-20220323152504953

LD_AUDIT 隐藏进程

既然LD_AUDIT可以先于LD_PRELOAD加载,那我们是不是可以使用LD_AUDIT来像LD_PRELOAD那样来隐藏进程?

image-20220323151927126

image-20220323151942094

当一个动态链接文件即将被加载时,LD_AUDIT将会调用la_objopen函数,在objopen函数里面, struct link map将会保存要加载的动态链接库的名称,cookie会声明一个指针指向该对象标识符,并将其标记为需要审计。然后传给la_symbind64函数,la_symbind64函数除了可以提供信息也可以修改程序行为。(如果是32位则需要使用la_symbind32

修改libprocesshide.c添加以下代码后重新编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int la_version(unsigned int version) {
return version;
}

unsigned int la_objopen(struct link_map *map, Lmid_t lmid, uintptr_t *cookie){
return LA_FLG_BINDTO | LA_FLG_BINDFROM;
}

uintptr_t la_symbind64(Elf64_Sym *sym, unsigned int ndx, uintptr_t *refcook, uintptr_t *defcook, unsigned int *flags, const char *symname) {
if (strcmp(symname, "readdir") == 0) {
return readdir;
}
return sym->st_value;
}

image-20220322211756644

缺点在于:需要设置LD_AUDIT环境变量,而防守人员只需要unset LD_AUDIT变量即可防御。

0x03 总结

对于使用LD_PRELOAD的rootkit可以使用LD_AUDIT与其对抗。
对于使用LD_AUDIT的rootkit应注意LD_AUDIT环境变量有没有被设置。

本文写的还是相当拉跨的,还请各位师傅斧正。