接着上文继续说:
https://blog.csdn.net/dog250/article/details/105474909
我一心一意想写一个指令解析器,我的目的是扫描出Linux内核text段中的所有的jmp和call指令,从而检测内核是否已经被篡改。基于以下事实:
- 一般内核函数互相调用都跑不出内核的text段,从0xffffffff81000000开始的几兆空间,凡是跳转越界跑出这个空间的,都要详查,过滤掉正常的hotfix,export回调这些,剩下的就是篡改了。
- 像bridge/bonding的rx_handler回调函数,是在模块里注册的回调指针,并非立即数出现在call指令中,即便rx_handler有问题,也只是模块自己的无心错(没人傻到用这种方式进行二进制注入)。
关于指令解析器,有现成的就不用自己写了,我用的是Intel XED,代码在:
https://github.com/intelxed/xed
照着readme操作就可以了,最后会得到一个叫做xed的可执行程序:
[root@localhost obj]# pwd
/root/test/xed/kits/xed-install-base-2020-03-25-lin-x86-64/examples/obj
[root@localhost obj]#
配合下面的stap脚本,我们先看下这个指令解析的效果:
// scan_text.stp
%{
#include <linux/module.h>
%}
function scan_text:long()
%{
#define MAX_TEXT_SIZE 0xff0000
int i;
unsigned int size;
unsigned char *__text;
unsigned char *__etext;
// 内核代码的开始和结束
__text = (void *)kallsyms_lookup_name("_text");
__etext = (void *)kallsyms_lookup_name("_etext");
__text ++;
STAP_PRINTF("90 ");
size = __etext - __text;
// 将内核text段全部dump下来
for (i = 0; i < size; i++) {
STAP_PRINTF("%x ", __text[i]);
}
STAP_RETVALUE = 0;
%}
probe begin
{
scan_text();
exit(); // oneshot模式
}
我们跑一下试试看:
[root@localhost obj]# stap -g ./scanner.stp >./code.hex
[root@localhost obj]# cat code.hex |more
90 8d 2d f9 ff ff ff 48 81 ed 0 0 0 1 48 89 e8 25 ff ff 1f 0 85 c0 f 85 a7 1 0 0 48 8d 5 db ff ff ff
48 c1 e8 2e f 85 96 1 0 0 48 1 2d c2 8f ae 0 48 1 2d b3 df 94 0 48 1 2d b4 df 94 0 48 1 2d 85 ff 94 0
48 8d 3d ae ff ff ff 48 8d 1d a7 7f ae 0 48 89 f8 48 c1 ...
好的,现在让我们上xed程序:
[root@localhost obj]# ./xed -ih ./code.hex -64 >./code.txt
[root@localhost obj]#
[root@localhost obj]# cat ./code.txt |head -n 10
XDIS 0: NOP BASE 90 nop
XDIS 1: MISC BASE 8D2DF9FFFFFF lea ebp, ptr [rip-0x7]
XDIS 7: BINARY BASE 4881ED00000001 sub rbp, 0x1000000
XDIS e: DATAXFER BASE 4889E8 mov rax, rbp
XDIS 11: LOGICAL BASE 25FFFF1F00 and eax, 0x1fffff
XDIS 16: LOGICAL BASE 85C0 test eax, eax
XDIS 18: COND_BR BASE 0F85A7010000 jnz 0x1c5
XDIS 1e: MISC BASE 488D05DBFFFFFF lea rax, ptr [rip-0x25]
XDIS 25: SHIFT BASE 48C1E82E shr rax, 0x2e
XDIS 29: COND_BR BASE 0F8596010000 jnz 0x1c5
[root@localhost obj]#
这不就是内核代码吗?是的,这就是内核代码!
现在,让我们揪出里面所有的0xe8相对地址call指令:
[root@localhost obj]# cat ./code.txt |egrep '\sE8[0-9A-F]{8}\s' >./call.txt
[root@localhost obj]#
[root@localhost obj]# cat call.txt |head -n 20|tail -n 10
XDIS 21db: CALL BASE E8007D2F00 call 0x2f9ee0
XDIS 21fd: CALL BASE E85E153100 call 0x313760
XDIS 2226: CALL BASE E835250700 call 0x74760
XDIS 2254: CALL BASE E807153100 call 0x313760
XDIS 227f: CALL BASE E82C722F00 call 0x2f94b0
XDIS 2353: CALL BASE E818742F00 call 0x2f9770
XDIS 2387: CALL BASE E8D4722F00 call 0x2f9660
XDIS 23b1: CALL BASE E81A982F00 call 0x2fbbd0
XDIS 23c8: CALL BASE E840C06200 call 0x62e40d
XDIS 2408: CALL BASE E8C3972F00 call 0x2fbbd0
以最后一行为例,它在Linux内核的text段的偏移为0x2408,相对call的偏移是0x2fbbd0,加上_text基地址就是绝对地址了,一般而言,这个基地址就是0xffffffff81000000,所以该call指令的目标是0xffffffff812fbbd0,我们确认一下:
[root@localhost obj]# cat /proc/kallsyms |grep ffffffff812fbbd0
ffffffff812fbbd0 T sscanf
嗯,它超大概率是正常的内核函数之间的互相调用。
所以,我们只需要寻找相对偏移大于0xffffff的即可:
[root@localhost obj]# cat ./code.txt |egrep '\sE8[0-9A-F]{8}\s' |gawk --non-decimal-data '{if ($NF > 0xffffff)print $0}'
[root@localhost obj]#
没有,什么都没有…
当然什么都没有咯,因为我们的内核目前还是干净的!
现在,让我们注入昨天的那个hack ip_local_deliver的代码,再次尝试:
[root@localhost obj]# insmod ./drop.ko
[root@localhost obj]# stap -g ./scanner.stp >./code2.hex
[root@localhost obj]# ./xed -ih ./code2.hex -64 >./code2.txt
[root@localhost obj]# cat ./code2.txt |egrep '\sE8[0-9A-F]{8}\s' |gawk --non-decimal-data '{if ($NF > 0xffffff)print $0}'
XDIS 561ebd: CALL BASE E83EA1B41E call 0x1f0ac000
[root@localhost obj]#
输出一条,我们需要严查它了,看看它到底是什么,call到了哪里:
crash> dis 0xffffffff81561ebd 5
0xffffffff81561ebd <ip_local_deliver+173>: callq 0xffffffffa00ac000 <test_stub1>
0xffffffff81561ec2 <ip_local_deliver+178>: cmp $0x1,%eax
0xffffffff81561ec5 <ip_local_deliver+181>: jne 0xffffffff81561e69 <ip_local_deliver+89>
0xffffffff81561ec7 <ip_local_deliver+183>: jmp 0xffffffff81561e5f <ip_local_deliver+79>
0xffffffff81561ec9 <ip_local_deliver+185>: nopl 0x0(%rax)
crash>
注意第一行那个 callq 0xffffffffa00ac000 <test_stub1> 成功揪出了真凶!!
注意!上面的实验结果不是一次的结果,所以数值对不上,以下图的计算为准:
自己和自己下棋还是很有趣的,左右互搏,道高一尺,魔高一丈。
谁说我必须把test_stub1放在0xffffffffa00ac000这么远的地方了,我把它塞进Linux内核的text段行不行?
谁说不行,当然行!问题是你要找到一片空隙,足够你塞入你的stub代码。
那就扫nop序列呗!
我还真就找到了:
[root@localhost obj]# cat code.hex |egrep -o '(90\s{1}){10}' |head -n 10
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
[root@localhost obj]# cat ./code.txt |egrep '\s90\s' |more
...
XDIS 1ce: NOP BASE 90 nop
XDIS 1cf: NOP BASE 90 nop
XDIS 1d0: NOP BASE 90 nop
...
XDIS 1e2: NOP BASE 90 nop
XDIS 1e3: NOP BASE 90 nop
...
就从1d0开始吧!换算成地址就是0xffffffff810001d0。来吧,动手,修改那个drop hook:
#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;
module_param(laddr, ulong, 0644);
// 计数INPUT链上的被DROP的数据包的数量
static unsigned int counter = 0;
module_param(counter, int, 0444);
#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);
char *gap = (void *)0xffffffff810001d0;
laddr = (unsigned long)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 *)gap;
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);
target = *((unsigned int *)&addr[POKE_OFFSET + 1]);
target -= pos;
target += POKE_OFFSET;
_text_poke_smp(&stub[1], &target, sizeof(target));
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);
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);
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");
加载该模块,再用xed检测,就什么都检测不出来了!我们用crash看看就是ip_local_deliver调用了哪里:
crash> dis ip_local_deliver+173
0xffffffff81561ebd <ip_local_deliver+173>: callq 0xffffffff810001d0 <_stext+8>
我们可以看到 0xffffffff810001d0 这个地址看上去非常正常,它妥妥就在text范围内!我们看看它是什么:
crash> dis 0xffffffff810001d0 10
0xffffffff810001d0 <_stext+8>: callq 0xffffffff815586a0 <nf_hook_slow>
0xffffffff810001d5 <_stext+13>: cmp $0x1,%eax
0xffffffff810001d8 <_stext+16>: je 0xffffffff810001e1 <_stext+25>
0xffffffff810001da <_stext+18>: incl 0xffffffffa0239280
0xffffffff810001e1 <_stext+25>: retq
0xffffffff810001e2 <_stext+26>: nop
0xffffffff810001e3 <_stext+27>: nop
0xffffffff810001e4 <_stext+28>: nop
0xffffffff810001e5 <_stext+29>: nop
0xffffffff810001e6 <_stext+30>: nop
crash>
噢,我的天,它就是我们的stub函数,这下它藏匿到了一个更加不容易被检测到的地方了!
所以说咯,检测程序还需要更加智能一些。
本以为右手打赢了左手,没想到左手最后一击反扑成功!
如果你担心text即便用text poke也不能写,那就自己改页表项呗…
浙江温州皮鞋湿,下雨进水不会胖。