pwnable.kr rootkit
写这题之前我以为会需要很多rootkit的前置知识,但是做完之后发现并不需要,但还是要知道内核模块相关的知识即LKM,以及内核处理syscall的过程。
逆向
逆向是解决问题的第一步,我们首先需要明白这一题的rootkit究竟做了什么事情。
1 | undefined4 init_module(void) |
sct
即system call table
,顾名思义,system call table
把syscall ID映射到对应实现syscall的内核函数地址。内核在处理syscall时并不会直接去在内核中寻找对应实现syscall的内核函数,而是以系统调用号作为偏移,在系统调用表中索引实现syscall的内核函数地址。于是,使用最多也是最经典的rootkit方法就是劫持系统调用表,通过篡改系统调用表中存放的数据以劫持系统调用。Linux内核提供了简单的获取内核函数和符号地址的方法,简单的来说,当内核编译选项CONFIG_KALLSYMS
开启时,内核会将符号地址存放在文件/proc/kallsyms
中。需要注意的是,rootkit.ko
直接使用了系统调用表的绝对地址0xc15fa020
,但在如今大部分的Linux kernel中是行不通的,当KASLR选项开启时,内核函数的地址会在每次重启内核时发生变化。通过uname -a
可以知道pwnable.kr上使用的内核大版本号为3.7,而KASLR这一特性在3.14后才被引入,所以直接使用系统调用表的绝对地址是可行的。
1 | ~ cat /proc/kallsyms | grep sys_call_table |
在kallsyms
可以找到一些重要的符号地址,比如sys_call_table
和sys_open
,其中sys_open
就是内核中真正用于处理系统调用open
的函数。取得系统调用表后,rootkit不能直接去修改表中对应系统调用的数据,还需要关闭写保护,关于写保护要细说起来就更麻烦了,这里简单的理解成开启内核内存的写权限就行。最终,通过在系统调用表对应位置写入hook函数sys_xxx_hooked
以完成系统调用的hook。
以sys_open_hooked
举例:
1 | undefined4 sys_open_hooked(undefined4 param_1,undefined4 param_2,undefined4 param_3) |
Ghidra和IDA反编译都看不到函数strstr
的参数字符串flag
,这是因为内核中传参的调用约定与用户态不同,汇编能看到strstr
的两个参数分别放在寄存器eax
和edx
中。当open的参数含有flag
子串时,sys_open_hooked
会过滤掉这一系统调用不予处理,否则使用sys_open
执向的函数,即原本用于处理系统调用open的内核函数sys_open
。
总结一下rootkit.ko
做了以下几件事:
- 保留原本处理系统调用的内核函数地址至符号
sys_xxx
中。 - 将系统调用表中存放的相关函数地址更改为
sys_xxx_hooked
。 sys_xxx_hooked
函数对原本系统调用的参数进行检查,若不包含flag
子串则使用sys_xxx
处理系统调用,否则过滤不予执行。
解决
类比用户态pwn的一些技巧,很容易联想到劫持系统调用表的方式与修改GOT表类似。那么最直接的方法,直接还原系统调用表就可以了,即把我们需要的系统调用表中的open
所存放的数据还原成sys_open
的地址。其对应的kernel module代码也比较好写,我这里提供一份不完整的伪代码:
1 |
|
麻烦之处在于需要找服务器对应版本的Linux Header去编译,所以我这里详细解释第二种方法,也是我主要参考的方式。
既然编译kernel module很麻烦,那么直接修改原本的rootkit是否可行呢?答案是肯定的。分析一下系统调用被过滤掉的主要原因,即sys_xxx_hooked
函数的被写入了系统调用表中,那么重写系统调用表就可以再次hook系统调用到正常的sys_xxx
函数中去。
那能联想到最朴素的一个思路就是,修改原本rootkit中的sys_xxx_hooked
函数的汇编代码,或者把flag
子串替换成无意义的字符串。除此之外,原本的rootkit已经存在于内核模块中,还需要把module name即rootkit
替换成其他字符串:
1 | with open("./rootkit", "rb") as f: |
我这里把jnz
指令替换为两个nop
,从而令控制流改变。这个过程还算简单,但直接放在服务器上跑是行不通的,我们需要再次分析sys_xxx_hooked
的逻辑。再次insmod
的过程的确改变了系统调用表中存放的地址,但sys_xxx_hooked
使用的并不是内核内存中的真正用于处理系统调用的sys_xxx
函数,而是从系统调用表中获得的函数地址!在系统启动时rootkit就被装载入内核中,此时内核系统调用表中存放的函数地址已经被替换为sys_xxx_hooked
,仅仅替换子串再次加载module只会再次调用第一次rootkit装载时使用的sys_xxx_hooked
,这条路似乎走向了瓶颈。
再次仔细查看init_module
的实现方式,我们需要注意到sys_xxx_hooked
通过保存在.bss
段的全局变量sys_xxx
从系统调用表中获取对应的sys_xxx
函数地址,注意这两者的区别,一个是全局变量,另一个是真正存放在内存中用于处理系统调用的内核函数地址。
而全局变量sys_xxx
,是通过如下方式赋值的:
1 | undefined init_module() |
那么答案很简单了,只需要把MOV EAX,[DAT_c15fa034]
这条命令修改为MOV EAX, [ADDR OF sys_open]
,sys_xxx_hooked
就会直接调用sys_open
而不是第一个rootkit的sys_open_hooked
。所以最终修改后的rootkit为:
1 | from base64 import b64encode |
服务器上不能直接传rawdata,所以大部分解决方式都使用了base64传输本地patch后的rootkit,我是用vi保存生成的base64编码,然后:
1 | cat antikit.base64 | base64 -d > antikit.ko |
这样就可以打开flag了,但flag格式不是纯文本,而是压缩文件,tar xvf flag
就可以读到flag了。