根据call/jmp操作数偏移检测内核是否被rootkit控制_call有偏移吗

今天上午,我写了一个检测我自己的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:

  1. 隐藏进程和CPU利用率。
  2. 统计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这些地址上的,并且内核函数的布局是很连续紧凑的,这些特点我们心里都有数,如果非要揪着这些细节说这个方法不严谨,那就杠精嫌疑了,没意思。

其实,本来也没什么意思。


浙江温州皮鞋湿,下雨进水不会胖。

原文链接: https://blog.csdn.net/dog250/article/details/105474909

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    根据call/jmp操作数偏移检测内核是否被rootkit控制_call有偏移吗

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/406027

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:34
下一篇 2023年4月26日 上午9:34

相关推荐