Linux Rootkit 实验 | 0001 基于修改sys_call_table的系统调用挂钩
夜阑风静縠纹平。小舟从此逝,江海寄余生。
实验说明
Rootkit 的一种经典形式是通过 Hook 系统调用实现。在本次实验中,我们将实现简单的系统调用挂钩方案,并且基于这个方案实现最基本的文件监视工具,同时加深对 LKM 的理解。
实验环境
uname -a:
Linux kali 4.6.0-kali1-amd64 #1 SMP Debian 4.6.4-1kali1 (2016-07-21) x86_64 GNU/Linux
GCC version:6.1.1
上述环境搭建于虚拟机,另外在没有特殊说明的情况下,均以 root 权限执行。
注:后面实验参考的是4.11的源码,可以在线阅览。
实验过程
一 基于修改sys_call_table的系统调用挂钩
Linux 内核在内存中维护了一份系统调用向量表,它是一个元素为函数指针的一维数组,定义见arch/x86/entry/syscall_64.c
:
所以最直接的思路就是修改这张表,把对应的系统调用地址更换为我们的函数地址。问题转化为三个子问题:
- 找到这张表在内存中的地址
- 这张表所在内存有写保护,我们要关掉写保护
- 修改这张表(之后要开启写保护)
接下来按这个步骤进行实验。
1 寻找sys_call_table内存地址
寻找系统调用表的地址的方法不止一种。这里先介绍一种,并为其他方法留坑。
要注意的一点是,只有内核中导出的函数和变量符号才能被我们直接引用,没有导出的那些对我们是透明的。参考网友的评论可知,在2.6内核后sys_call_table
是不可见的。
① 暴力搜索
原理:内核内存空间的起始地址PAGE_OFFSET
变量和sys_close
系统调用对我们是可见的(sys_open
/sys_read
等并未导出);系统调用号(即sys_call_table
中的元素下标)在同一ABI
(x86与x64属于不同ABI)中是高度后向兼容的;这个系统调用号我们也是可以直接引用的(如__NR_close
)。所以我们可以从内核空间起始地址开始,把每一个指针大小的内存假设成sys_call_table
的地址,并用__NR_close
索引去访问它的成员,如果这个值与sys_close
的地址相同的话,就可以认为找到了sys_call_table
的地址(但是师傅说这种方法可能被欺骗)。
我们先简单看一下PAGE_OFFSET
的定义(x64):
接下来看我们的搜索函数:
测试用 LKM 模块代码如下(后面将在此模块上添加代码):
Makefile:
TARGET = sys_call_table
obj-m := ${TARGET}ko.o
${TARGET}ko-objs := ${TARGET}.o
default:
${MAKE} modules \
--directory "/lib/modules/$(shell uname --release)/build" \
M="$(shell pwd)"
clean:
${MAKE} clean \
--directory "/lib/modules/$(shell uname --release)/build" \
M="$(shell pwd)"
我们没有使用第一次实验中的module_init
和module_exit
两个宏去指定入口函数和出口函数,那样也是可以的,这里只是使用了默认的入口函数名和出口函数名。
测验结果如下:
② 从/boot/System.map提取
暂略,见【拓展阅读】3。
③ 使用未导出函数机器码搜索
暂略,见【拓展阅读】4
2 关掉写保护
找到地方了,下面要关闭写保护。CR0
寄存器从0数的第16比特控制了对只读内存的写保护是否开启,详见【已参考】3。巧的是,我们可以用内核自己的read_cr0
/write_cr0
去读写CR0
,并用它提供的clear_bit
/set_bit
接口去做位运算。我们把它们封装一下:
接着在入口函数中添加一些测试代码:
测试结果如下:
3 修改sys_call_table
至此,修改就很简单了。配合后面第二部分文件监视,我们将修改三个系统调用:sys_open
/sys_unlink
/sys_unlinkat
。我们的思路是,在入口函数
中先备份原始的系统调用,然后修改成我们自己的。在出口函数
中恢复原始的系统调用。
修改
恢复
至此,系统调用挂钩就完成了。缺少的函数定义和声明在下一部分加上,同时在下一部分一并演示。
二 基于系统调用挂钩的初级文件监视
这里补上缺少的函数定义:
编译加载模块,测试结果如下:
中间多出来的/tmp/sh-thd-
那两行是我在rm hello
时按了tab
进行文件名补全才出现的,应该是补全功能产生的临时文件。
unlink
和unlinkat
几乎相同,关于差异可man unlinkat
。
注意在测试结束后卸载模块,恢复默认系统调用。
实验问题
【问题一】
KERN_ALERT 是干嘛的?
【问题二】
前面说暴力搜索系统调用表的方法可能被欺骗,具体是怎样的欺骗方法?
实验总结与思考
配合着源码在线阅览,边做边能查到内核代码的感觉非常棒。
本次实验中的dmesg -C && dmesg -w
比第一次实验中的grep
要方便许多。
实验过程中深感自己学识浅薄,静水流深呐。
一个朋友在 FreeBuf 文章下评论说:“写得不错。但获取sys_call_table的地址对hook这一大目标并没有起到多大作用,甚至是多余的。”
后来他又说:“回复有所歧义,说不需要知道sys_call_table的地址是针对2.6以前的内核版本,之前的版本可以直接引用sys_call_table变量,多谢提醒!另外除了利用system.map获取table的地址外,可以读取IDT的值,之后找到int $0×80的入口点,后三个字节的值就是table的地址,还没验证。”
另一个朋友会说:“这就是Windows的SSDT HOOK在Linux核上的翻版啊。”
作者回复说:“是的,眼力不错。都是基于修改系统调用表的系统调用挂钩。Linux 的系统调用表叫 sys_call_table / ia32_sys_call_table,Windows 的系统调用表大家通常叫 SSDT。显然,从学习、实践与理解的角度看,Linux 更适合用来起步。”
他们的讨论让我学到了知识。社区需要的正是这种讨论,正是这种学习的氛围。谢谢各位师傅的分享。
参考资料
已参考
拓展阅读