今天上午,我写了一个检测我自己的rootkit的代码,但是那个代码只能检测hook住函数开头ftrace stub的情况:
https://blog.csdn.net/dog250/article/details/105465553
手艺人不能就此罢休。我下午准备写一个x86_64指令解析器的,这样就是把内核的整个TEXT段作为一个状态机的输入,然后整个TEXT段在这个解析器里过一遍,就能把所有的call/jmp指令的操作数给过滤出来,然后判断这些操作数的偏移是不是越过了TEXT段的范围,比方说,如果callee的地址是一个内核模块地址范围的地址,那基本就可以说明内核函数call了一个 在别处的函数 ,在我看来,除了export出来的回调函数,这种情况并不多,详细检测这个callee地址,应该能看出其所以然来:
- 是正常的export回调函数调用吗?
- 是正常的kpatch打入的hotfix吗?
- 是恶意注入的代码吗?
无奈下午一觉醒来就快六点了,风雨大作,电闪雷鸣,时间不够了,我也就只能写下面的简单POC了:
- 该POC可以过滤两类hook:ftrace stub hook和ip_local_deliver里的中间hook。
代码如下:
#include <linux/module.h>
#include <linux/kallsyms.h>
#define TEXT_SIZE 0xff0000
static int __init checker_init(void)
{
s32 offset;
int i = 0;
unsigned char *pos;
unsigned int *code;
unsigned long *lcode, target;
char *__text;// = 0xffffffff81000000;
__text = (void *)kallsyms_lookup_name("_text");
for (i = 0; i < TEXT_SIZE;) {
pos = &__text[i];
code = (unsigned int *)&pos[5];
lcode = (unsigned long *)pos;
// 下面的if语句过滤ftrace函数开头的call hook
if (*code == 0xe5894855 && *lcode != 0x8948550000441f0f && pos[0] == 0xe8) {
offset = *(s32 *)&pos[1];
target = (unsigned long)__text + i + offset;
if (target > (unsigned long)__text + TEXT_SIZE) {
printk("caller address: %llx callee address[请详查]: %llx\n",
(unsigned long)__text + i, target);
}
i += 9;
continue;
}
// 下面的语句过滤函数中间的call hook。
// 没办法,我只能这样过滤了,实际上正规的方法是扫描整个指令,在状态机中找call指令:
// call的偏移如果越过内核TEXT段,基本就要详细check一下了!
code = (unsigned int *)&pos[5];
if (*code == 0x7501f883 && pos[0] == 0xe8 /*&& pos[5] == 0x90*/ &&
((*(pos - 1) != 0xc1) &&
(*(pos - 1) != 0x83 && *(pos - 2) != 0x48) &&
(*(pos - 1) != 0x25 && *(pos - 2) != 0x04) &&
(*(pos - 1) != 0xe8) &&
(*(pos - 1) != 0x55) &&
(*(pos - 1) != 0x1d) &&
(*(pos - 1) != 0xe9) &&
(*(pos - 2) != 0x4c) &&
(*(pos - 2) != 0x0f) &&
(*(pos - 2) != 0x81) && // for set_mode
(*(pos - 3) != 0xe8) && // for xhci_dbg_cmd_ptrs
(*(pos - 3) != 0x4c) &&
//(*(pos - 5) != 0x48) && // for set_max_huge_pages
(*(pos -2) != 0x44 && *(pos - 1) != 0x89))) {
offset = *(s32 *)&pos[1];
target = (unsigned long)__text + i + offset + 5;
if (target > (unsigned long)__text + TEXT_SIZE) {
printk("[middle hook] caller address: %llx callee address[请详查]: %llx\n",
(unsigned long)__text + i, target);
}
i += 9;
continue;
}
i ++;
}
return -1;
}
module_init(checker_init);
MODULE_LICENSE("GPL");
来吧,演示一下吧。
如果空加载这个模块,不会有任何输出,但是我注入了两个恶意的rootkit:
- 隐藏进程和CPU利用率。
- 统计iptables DROP的数量(不算恶意…)。
关于以上第二个,参见:
https://blog.csdn.net/dog250/article/details/105206753
为了便于验证和归档,我再次给出代码:
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/kallsyms.h>
#include <linux/cpu.h>
char *stub;
char *addr = NULL;
// 传入ip_local_deliver的地址
static unsigned long laddr = 0xffffffffa0267000;
module_param(laddr, ulong, 0644);
// 计数INPUT链上的被DROP的数据包的数量
static unsigned int counter = 0;
module_param(counter, int, 0444);
void test_stub1(void) __attribute__ ((aligned (1024)));
void test_stub2(void) __attribute__ ((aligned (1024)));
void test_stub1(void)
{
printk("yes\n");
}
void test_stub2(void)
{
printk("yes yes\n");
}
#define FTRACE_SIZE 5
#define POKE_OFFSET 173
#define POKE_LENGTH 5
#define COND_LENGTH 5
#define COUNTE_LENGTH 8
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;
static unsigned int pos, target;
static int __init hotfix_init(void)
{
unsigned char e8_call[POKE_LENGTH];
unsigned char incl[COUNTE_LENGTH];
unsigned char cond[COND_LENGTH];
s32 offset, i;
u32 low32 = (unsigned int)(((unsigned long)&counter) & 0xffffffff);
laddr = (void *)kallsyms_lookup_name("ip_local_deliver");
_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
_text_mutex = (void *)kallsyms_lookup_name("text_mutex");
if (!laddr || !_text_poke_smp || !_text_mutex) {
printk("not found\n");
return -1;
}
addr = (void *)laddr;
stub = (void *)test_stub1;
// 两个函数的call地址偏移
offset = (s32)((long)stub - (long)addr - FTRACE_SIZE);
// 两个函数指令相对偏移
pos = (unsigned int)((long)stub - (long)addr);
_text_poke_smp(&stub[0], &addr[POKE_OFFSET], POKE_LENGTH);
// 调节校准call nf_hook_slow的相对地址偏移
target = *((unsigned int *)&addr[POKE_OFFSET + 1]);
target -= pos;
target += POKE_OFFSET;
_text_poke_smp(&stub[1], &target, sizeof(target));
// 填充条件判断:只有返回DROP才会被计数
cond[0] = 0x83; // cmp $0x1, %eax
cond[1] = 0xf8;
cond[2] = 0x01;
cond[3] = 0x74; // jz $ret
cond[4] = 0x07; // skip "incl $counter"
_text_poke_smp(&stub[POKE_LENGTH], &cond, COND_LENGTH);
// 插入的指令中需要save/restore寄存器,但这里简单,略过
incl[0] = 0xff; // incl $counter
incl[1] = 0x04;
incl[2] = 0x25;
(*(u32 *)(&incl[3])) = low32;
incl[7] = 0xc3; // retq
_text_poke_smp(&stub[POKE_LENGTH + COND_LENGTH], &incl, 8);
// call比jmp方便,可以自动帮忙return,不然还要自己jmp回来,但是代价是push/pop
e8_call[0] = 0xe8;
(*(s32 *)(&e8_call[1])) = offset - POKE_OFFSET;
for (i = 5; i < POKE_LENGTH; i++) {
e8_call[i] = 0x90; // nop 占位符
}
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&addr[POKE_OFFSET], e8_call, POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
return 0;
}
static void __exit hotfix_exit(void)
{
target -= POKE_OFFSET;
target += pos;
_text_poke_smp(&stub[1], &target, sizeof(target));
get_online_cpus();
mutex_lock(_text_mutex);
_text_poke_smp(&addr[POKE_OFFSET], &stub[0], POKE_LENGTH);
mutex_unlock(_text_mutex);
put_online_cpus();
}
module_init(hotfix_init);
module_exit(hotfix_exit);
MODULE_LICENSE("GPL");
注入这两个之后,再次加载checker:
[root@localhost test]# insmod ./check.ko
insmod: ERROR: could not insert module ./check.ko: Operation not permitted
[root@localhost test]# dmesg
[34980.244454] caller address: ffffffff810b4f10 callee address[请详查]: ffffffffa011a000
[34980.244456] caller address: ffffffff810b4fb0 callee address[请详查]: ffffffffa011a000
[34980.251519] [middle hook] caller address: ffffffff81561ebd callee address[请详查]: ffffffffa0252000
[34980.251666] caller address: ffffffff8158
我们用crash命令查一下:
...
crash> dis ffffffffa0123000 10
dis: WARNING: ffffffffa0123000: no associated kernel symbol found
0xffffffffa0123000: nopl 0x0(%rax,%rax,1)
0xffffffffa0123005: push %rbp
0xffffffffa0123006: cmp $0x1,%rsi
0xffffffffa012300a: mov %rsp,%rbp
0xffffffffa012300d: je 0xffffffffa0123017
0xffffffffa012300f: cmpw $0x4d2,0xe(%rsi)
0xffffffffa0123015: je 0xffffffffa0123020
0xffffffffa0123017: pop %rbp
0xffffffffa0123018: retq
0xffffffffa0123019: nopl 0x0(%rax)
crash> dis ffffffffa0252000 10
0xffffffffa0252000 <test_stub1>: callq 0xffffffff815586a0 <nf_hook_slow>
0xffffffffa0252005 <test_stub1+5>: cmp $0x1,%eax
0xffffffffa0252008 <test_stub1+8>: je 0xffffffffa0252011 <test_stub1+17>
0xffffffffa025200a <test_stub1+10>: incl 0xffffffffa0254280
0xffffffffa0252011 <test_stub1+17>: retq
0xffffffffa0252012 <test_stub1+18>: callq 0xffffffff8162e40d <printk>
0xffffffffa0252017 <test_stub1+23>: pop %rbp
0xffffffffa0252018 <test_stub1+24>: retq
0xffffffffa0252019 <test_stub1+25>: nop
0xffffffffa025201a <test_stub1+26>: nop
crash>
一把就揪出来了!
当然了,如果有时间,我会写一个完整的x86_64指令解析状态机,这样就可以搜集所有的jmp/call目标了,逐一检查这些目标,看看哪些是可疑的,最终揪出真凶。
除了jmp/call的目标,内核数据结构的回调函数也是检测目标之一,比如系统调用表的内容,肯定要在TEXT段中,比如很多inet回调函数,也必须处在TEXT中。
我之所以没有通过读取/proc/kallsyms,那是怕它被hook掉啊!而且通过kallsyms_lookup_name得到的_text是不是也是可信的,也是有问题的,kallsyms_lookup_name本身被hook掉怎么办?
然而,这些问题并不大,我们心里要有个数,基本上,内核代码的位置就是在0xffffffff81xxxxxx这些地址上的,并且内核函数的布局是很连续紧凑的,这些特点我们心里都有数,如果非要揪着这些细节说这个方法不严谨,那就杠精嫌疑了,没意思。
其实,本来也没什么意思。
浙江温州皮鞋湿,下雨进水不会胖。