Linux内核二进制hook的手艺-总结

近几天可算是过了把贼瘾,纯手工玩了一把内核的二进制hook。

在本系列的最后一篇文章中,我演示了一个实际的例子,统计了在INPUT链上iptables规则DROP掉的数据包的数量计数:
Linux内核二进制hook的手艺-实际的例子计数iptables DROP

文章遗漏一个观感上的细节,本文先补上,并以此为开始进行一下总结。

我们先看看被hook之后的ip_local_deliver变成了什么样子。作为对比,我们先看它开始的样子:

crash> dis ip_local_deliver
...
0xffffffff81561eb5 <ip_local_deliver+165>:      movq   $0xffffffff81561ad0,-0x18(%rbp)
0xffffffff81561ebd <ip_local_deliver+173>:      callq  0xffffffff815586a0 <nf_hook_slow>
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)
...

我们看下它被hook后的样子:

crash> dis ip_local_deliver
...
0xffffffff81561eb5 <ip_local_deliver+165>:      movq   $0xffffffff81561ad0,-0x18(%rbp)
0xffffffff81561ebd <ip_local_deliver+173>:      callq  0xffffffffa0392000 <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)

可见,ip_local_deliver在本来应该call nf_hook_slow的地方,被hook成了call test_stub1。

OK,我们看下test_stub1:

crash> dis test_stub1
0xffffffffa0392000 <test_stub1>:        callq  0xffffffff815586a0 <nf_hook_slow>
0xffffffffa0392005 <test_stub1+5>:      cmp    $0x1,%eax
0xffffffffa0392008 <test_stub1+8>:      je     0xffffffffa0392011 <test_stub1+17>
0xffffffffa039200a <test_stub1+10>:     incl   0xffffffffa0394280
0xffffffffa0392011 <test_stub1+17>:     retq

嗯,很简单干净的逻辑,先是复制了原始函数中被poke的指令,然后添加了额外的逻辑。

强调两个要点:

  • 这里的test_stub1是用汇编语言拼接而成的,之所以不能用C,是因为害怕编译器并不知道原始函数的寄存器使用情况而冲掉寄存器的值,所以必须用汇编手工进行必要的save/restore操作。不能用C,也是一种遗憾!
  • 模块中的test_stub1是实现定义的函数stub,而不是使用kmalloc/vmalloc分配的内存,为什么呢?因为kmalloc/vmalloc分配的内存无法指定range,而我们为了保证stub和原始函数的32位偏移有效,比如在0xffffffff00000000以上分配内存,而module的内存均在此之上:
// arch/x86/include/asm/pgtable_64_types.h
#define MODULES_VADDR    _AC(0xffffffffa0000000, UL)
#define MODULES_END      _AC(0xffffffffff000000, UL)
// arch/x86/kernel/module.c
void *module_alloc(unsigned long size)
{
   if (PAGE_ALIGN(size) > MODULES_LEN)
       return NULL;
   return __vmalloc_node_range(size, 1, MODULES_VADDR, MODULES_END,
               GFP_KERNEL | __GFP_HIGHMEM, PAGE_KERNEL_EXEC,
               -1, __builtin_return_address(0));
}

我们自己调用的alloc API将无法保证这一点。

注意⚠️, test_stub1并没有拷贝整个ip_local_deliver函数指令,然后加入了额外的计数逻辑,而是仅仅包含了新的额外计数逻辑! 这是一个真正的 Online LivePatch

这是和标准的kpatch机制根本的不同。

然而,我的这种手工玩法不适合指令非常复杂的场景,在被hook函数指令太复杂的情况下,找不到明确的hook点,或者说hook点需要poke的指令太多,那就只能拷贝整个函数了,这就是kpatch的做法,当然,我下面的文章描述了该方案的手工做法:
https://blog.csdn.net/dog250/article/details/105093969
https://blog.csdn.net/dog250/article/details/105129254
https://blog.csdn.net/dog250/article/details/105135219

poke函数指令数量不能太多,因为poke操作并不是原子的,且不能保证所有的线程都退出了被poke的指令序列。

典型的做法是将poke首指令先原子写成int3,阻滞指令进入并前向推进,当所有指令poke完毕后,再释放int3.关于这个,详见:
https://blog.csdn.net/dog250/article/details/84201114
https://blog.csdn.net/dog250/article/details/84258601
https://blog.csdn.net/dog250/article/details/84572893

在我看来,完全拷贝整个函数指令的话,会浪费那么一点点廉价的内存,这并不是什么大问题,当然对于纯手艺而言,当然就是精益求精。

作为手艺人而言,我那些不能上生产环境的杂耍就说到这里,现在再说说可以上生产环境的标准kpatch。

kpatch采用了更加标准化的做法,比如采用了标准的ftrace机制。

我们知道,在开启了ftrace的内核上,每一个函数的最开始5个字节是nop,然后才是函数栈帧的操作:

crash> dis ip_rcv
0xffffffff81561ee0 <ip_rcv>:    nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffff81561ee5 <ip_rcv+5>:  push   %rbp
0xffffffff81561ee6 <ip_rcv+6>:  mov    %rsp,%rbp

现在,如果我们的一个kpatch将某个函数给patch了,会发生什么呢?比方说,我们对函数set_next_buddy打了kpatch。

我们试着dis一下:

crash> dis set_next_buddy
dis: set_next_buddy: duplicate text symbols found:
ffffffff810b9450 (t) set_next_buddy /usr/src/debug/kernel-3.10.0/linux-3.10.0.x86_64/kernel/sched/fair.c: 4536
ffffffffa0240410 (t) set_next_buddy [kpatch_y9h83dum]

现在,我们发现在系统中出现了两个set_next_buddy符号,一个是内核固有的,另一个当然就是kpatch修改过的,我们分别看一下:

crash> dis ffffffff810b9450
0xffffffff810b9450 <set_next_buddy>:    callq  0xffffffff81646df0 <ftrace_regs_caller>
0xffffffff810b9455 <set_next_buddy+5>:  push   %rbp
0xffffffff810b9456 <set_next_buddy+6>:  cmpq   $0x0,0x150(%rdi)
0xffffffff810b945e <set_next_buddy+14>: mov    %rsp,%rbp
0xffffffff810b9461 <set_next_buddy+17>: jne    0xffffffff810b947a <set_next_buddy+42>
0xffffffff810b9463 <set_next_buddy+19>: jmp    0xffffffff810b9481 <set_next_buddy+49>
0xffffffff810b9465 <set_next_buddy+21>: nopl   (%rax)
0xffffffff810b9468 <set_next_buddy+24>: mov    0x148(%rdi),%rax
0xffffffff810b946f <set_next_buddy+31>: mov    %rdi,0x40(%rax)
0xffffffff810b9473 <set_next_buddy+35>: mov    0x140(%rdi),%rdi
0xffffffff810b947a <set_next_buddy+42>: test   %rdi,%rdi
0xffffffff810b947d <set_next_buddy+45>: jne    0xffffffff810b9468 <set_next_buddy+24>
0xffffffff810b947f <set_next_buddy+47>: pop    %rbp
0xffffffff810b9480 <set_next_buddy+48>: retq
0xffffffff810b9481 <set_next_buddy+49>: cmpl   $0x5,0x1f8(%rdi)
0xffffffff810b9488 <set_next_buddy+56>: jne    0xffffffff810b947a <set_next_buddy+42>
0xffffffff810b948a <set_next_buddy+58>: pop    %rbp
0xffffffff810b948b <set_next_buddy+59>: retq
// 再看另一个kpatch中的同名符号
crash> dis ffffffffa0240410
0xffffffffa0240410 <set_next_buddy>:    nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffffa0240415 <set_next_buddy+5>:  push   %rbp
0xffffffffa0240416 <set_next_buddy+6>:  cmpq   $0x0,0x150(%rdi)
0xffffffffa024041e <set_next_buddy+14>: mov    %rsp,%rbp
0xffffffffa0240421 <set_next_buddy+17>: jne    0xffffffffa0240441 <set_next_buddy+49>
0xffffffffa0240423 <set_next_buddy+19>: jmp    0xffffffffa0240448 <set_next_buddy+56>
0xffffffffa0240425 <set_next_buddy+21>: nopl   (%rax)
0xffffffffa0240428 <set_next_buddy+24>: mov    0x38(%rdi),%eax
0xffffffffa024042b <set_next_buddy+27>: test   %eax,%eax
0xffffffffa024042d <set_next_buddy+29>: je     0xffffffffa0240446 <set_next_buddy+54>
0xffffffffa024042f <set_next_buddy+31>: mov    0x148(%rdi),%rax
0xffffffffa0240436 <set_next_buddy+38>: mov    %rdi,0x40(%rax)
0xffffffffa024043a <set_next_buddy+42>: mov    0x140(%rdi),%rdi
0xffffffffa0240441 <set_next_buddy+49>: test   %rdi,%rdi
0xffffffffa0240444 <set_next_buddy+52>: jne    0xffffffffa0240428 <set_next_buddy+24>
0xffffffffa0240446 <set_next_buddy+54>: pop    %rbp
0xffffffffa0240447 <set_next_buddy+55>: retq
0xffffffffa0240448 <set_next_buddy+56>: cmpl   $0x5,0x1f8(%rdi)
0xffffffffa024044f <set_next_buddy+63>: jne    0xffffffffa0240441 <set_next_buddy+49>
0xffffffffa0240451 <set_next_buddy+65>: pop    %rbp
0xffffffffa0240452 <set_next_buddy+66>: retq

我们看到,ftrace_regs_caller的调用事实上将原始的set_next_buddy函数变成了一个僵尸空壳子了,它留在那里的作用貌似就是为了kpatch的卸载…有点尴尬。

ftrace_regs_caller函数我这里就不解析了,它其实很简单,就是一个中间封装,内部调用了register到ftrace的operation的function。

我这里的要点在于, 基于ftrace的kpatch对内核二进制的修改,是可以上生产环境的,虽然它这机制和我那纯手工的没啥区别,可能还浪费内存,但经理说好就是好。

最后,说下这一切和kprobe的关联。

值得一提的是,kprobe虽然是标准的方案,但同样是不能上线的。因为经理认为,从名字上看,kprobe只是一个probe辅助,调试用的,而不是一个function,这非常有意思。

kpatch这个名字取的就特别好,但是kpatch的核心却是ftarce,幸亏经理不知道这其中的究竟,如果经理知道了kpatch的底层是ftrace,从ftrace这个名字上看,你觉得经理会做出什么决定呢?

还记得我上周那个stap实时统计系统TCP半连接队列长度的demo吗:
https://blog.csdn.net/dog250/article/details/105022347
文中说了,之所以这是一种生产环境无法上线的 逆经理操作 ,原因在于stap/kprobe使用了一种debug技术,即int3陷入.

stap/kprobe会将被probe的function/statement的第一个指令替换成int3断点,当该指令执行的时候,会发生一次陷入,然后执行对应的handler,这是一个非常伤身的操作。我们可以试一下:

stap -e 'probe kernel.statement("reqsk_queue_removed@include/net/request_sock.h:232") {printf("hit\n"}'

由于reqsk_queue_removed是个inline函数,我们dis一个明确调用它的函数,inet_csk_reqsk_queue_prune:

crash> dis inet_csk_reqsk_queue_prune
...
0xffffffff8156e25b <inet_csk_reqsk_queue_prune+347>:    lock incl 0x4(%r15)
// 下面的int3就是触发stap的指令
0xffffffff8156e260 <inet_csk_reqsk_queue_prune+352>:    int3  // 这就是
0xffffffff8156e261 <inet_csk_reqsk_queue_prune+353>:    testb  $0xfe,0x7b(%rsi)
0xffffffff8156e265 <inet_csk_reqsk_queue_prune+357>:    mov    0x418(%r12),%rax
...

如果我们停掉stap,再次dis同一个函数,就会发现指令被还原了:

crash> dis inet_csk_reqsk_queue_prune
...
0xffffffff8156e25b <inet_csk_reqsk_queue_prune+347>:    lock incl 0x4(%r15)
0xffffffff8156e260 <inet_csk_reqsk_queue_prune+352>:    testb  $0xfe,0x7b(%r14)
0xffffffff8156e265 <inet_csk_reqsk_queue_prune+357>:    mov    0x418(%r12),%rax

可是这个int3和二进制hook有什么关系呢?

如果你仔细观察就会发现,我手工实现的那个统计iptables DROP计数的Online LiveKpatch是不是和这个int3很像呢:

  • 在函数执行的过程中,插入一段自己的逻辑:
    • stap采用int3输出事件
    • 我的kpatch用插入call stub执行额外逻辑

是的,其实这些在效果上是雷同的, 对于kprobe,也是有一种优化机制,即不再采用int3陷入的方式,而是采用ftrace的方式去Online Patch!

关于这一点,我们可以从arm_kprobe中看到一斑:

/* Arm a kprobe with text_mutex */
static void __kprobes arm_kprobe(struct kprobe *kp)
{
    if (unlikely(kprobe_ftrace(kp))) {
        arm_kprobe_ftrace(kp);  // ftrace二进制hook的方式!
        return;
    }
    /*
     * Here, since __arm_kprobe() doesn't use stop_machine(),
     * this doesn't cause deadlock on text_mutex. So, we don't
     * need get_online_cpus().
     */
    mutex_lock(&text_mutex);
    __arm_kprobe(kp); //int3陷入的方式
    mutex_unlock(&text_mutex);
}

好了,我要说的就是,kprobe也可以不用int3,它也可以用ftrace,既然kpatch用ftrace可以上生产环境,kprobe用ftrace为什么不能呢?经理呢?


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

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

欢迎关注

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

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

    Linux内核二进制hook的手艺-总结

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

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

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

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

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

相关推荐