在上文中,左右手互搏,最终成功将二进制stub函数注入到了Linux内核的text段本身,逃过了jmp/call的越界检测:
https://blog.csdn.net/dog250/article/details/105496996
真把二进制注入到Linux内核的text自身就很难检测了,任何检测机制均类似免疫系统一样,如此一来很难分清敌我。
如果把采用越界call的rootkit看作是病毒入侵,那么直接注入text段的rootkit就像癌细胞!
最直接的方法就是为内核的text计算摘要了,定期执行计算,只要发现有变化,就dump出代码,xed解析并和基准进行diff:
// scanner.stp
%{
#include <linux/module.h>
#include <linux/crypto.h>
#include <linux/scatterlist.h>
#define SHA1_LENGTH 20
%}
function scan_text (type:long)
%{
int i ,ret;
unsigned int size;
unsigned char *__text;
unsigned char *__etext;
unsigned char *plaintext = NULL;
unsigned char hashtext[SHA1_LENGTH];
struct scatterlist sg;
struct hash_desc desc;
__text = (void *)kallsyms_lookup_name("_text");
__etext = (void *)kallsyms_lookup_name("_etext");
__text ++;
size = __etext - __text;
if (STAP_ARG_type == 0) {
STAP_PRINTF("90 ");
} else {
plaintext = (unsigned char *)vzalloc(size);
if (!plaintext) {
return;
}
}
for (i = 0; i < size; i++) {
if (STAP_ARG_type == 0) {
STAP_PRINTF("%x ", __text[i]);
} else {
plaintext[i] = __text[i];
}
}
if (STAP_ARG_type == 0) {
return;
}
memset(hashtext, 0, SHA1_LENGTH);
sg_init_one(&sg, plaintext, size);
desc.tfm = crypto_alloc_hash("sha1", 0, CRYPTO_ALG_ASYNC);
desc.flags = 0;
ret = crypto_hash_init(&desc);
if (ret) {
vfree(plaintext);
return;
}
ret = crypto_hash_update(&desc, &sg, size);
if (ret) {
vfree(plaintext);
return;
}
ret = crypto_hash_final(&desc, hashtext);
if (ret) {
vfree(plaintext);
return;
}
crypto_free_hash(desc.tfm);
for (i = 0; i < 20; i++) {
STAP_PRINTF("%02x ", hashtext[i]&0xff);
}
STAP_PRINTF("\n");
vfree(plaintext);
%}
probe begin
{
// type 为0为dump,type为1为摘要
scan_text($1);
exit(); // oneshot模式
}
我们先算一下没有加载drop模块时的text段摘要,并且dump出基准的code:
[root@localhost obj]# stap -g ./scanner.stp 1
08 b2 05 20 7f a6 fe 4f c0 70 b7 71 25 6c a0 e8 80 d9 d3 61
[root@localhost obj]# stap -g ./scanner.stp 0 >./hex1
现在加载drop模块,执行注入,再次计算摘要:
[root@localhost obj]# insmod ./drop.ko
[root@localhost obj]# stap -g ./scanner.stp 1
da 9b e5 a7 1b f9 24 39 3f 8d a5 ca 16 f1 9f a7 72 33 a1 34
变了,变了!到底什么变了?再次dump出code,并用xed解析:
[root@localhost obj]# stap -g ./scanner.stp 0 >./hex2
[root@localhost obj]# ./xed -ih ./hex1 -64 >./code1
[root@localhost obj]# ./xed -ih ./hex2 -64 >./code2
来吧,揪出真凶来:
[root@localhost obj]# diff code1 code2
113c113
< XDIS 1d0: CALL BASE E8DE67FFFF call 0xffffffffffff69b3
---
> XDIS 1d0: CALL BASE E8CB845500 call 0x5586a0
116c116
< XDIS 1da: BINARY BASE FF042580E20AA0 inc dword ptr [0xffffffffa00ae280]
---
> XDIS 1da: BINARY BASE FF042580620AA0 inc dword ptr [0xffffffffa00a6280]
1452815c1452815
< XDIS 561ebd: CALL BASE E8DE67FFFF call 0x5586a0
---
> XDIS 561ebd: CALL BASE E80EE3A9FF call 0x1d0
1691134c1691134
< #Total DECODE cycles: 2294206070
---
> #Total DECODE cycles: 2293072802
1691136c1691136
< #Total tail DECODE cycles: 2298560197
---
> #Total tail DECODE cycles: 2293479218
1691138,1691139c1691138,1691139
< #Total cycles/instruction DECODE: 1356.65
< #Total tail cycles/instruction DECODE: 1359.19
---
> #Total cycles/instruction DECODE: 1355.98
> #Total tail cycles/instruction DECODE: 1356.18
不光揪出来了ip_local_deliver被篡改,就连注入到stext的nop序列中的内容都被揪出来了:
inc dword ptr [0xffffffffa00ae280]
哦,增加一个计数器的值,还好,只是做了个统计,没有偷经理的皮鞋👞。
要做到摘要校验的有效性,必须在系统初启尚未有机会被注入的时候,第一时间拿到干净的摘要,并且保证该摘要本身不会被篡改,以此作为后续比较的基准。
并不建议0号线程的cpu_idle_loop中做摘要计算,因为内核函数本身就可能后续被篡改,我建议摘要的计算作为1号进程systemd/init的启动钩子比较妥当。
内核被rootkit篡改,事实上只要有root权限,就都不是事儿,而无论是注入代码还是读写/dev/mem,均需要内核模块机制,一般而言/dev/mem是不可写甚至不可访问的,你需要systemtap先hook掉devmem_is_allowed:
stap -g -e 'probe kernel.function("devmem_is_allowed").return { $return = 1 }'
而stap的底层也是靠内核模块机制起作用的。
为了让这一切的门槛更高一些,内核模块签名就显得重要!即便你有root权限,你没有签名私钥也是白搭,至少签名私钥的保管要严格得多,攻破签名服务器本身就很不容易!
换句话说,Unix/Linux的root权限太大了!
唉…
浙江温州皮鞋湿,下雨进水不会胖。