Linux内核二进制hook的手艺-实际的例子计数iptables DROP

上一篇文章,我展示了一个demo,通过二进制hook实现了针对既有逻辑的计数:
https://blog.csdn.net/dog250/article/details/105205966

然而demo毕竟只是一个demo,我需要用一个实际可用的例子来展示更有意思的东西。

本文来一个实际的例子,通过二进制hook对被iptables规则DROP掉的数据包进行计数。

我们以计数INPUT链中DROP掉的数据包为例来实现这个功能。首先,看下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)
...

我们看到,nf_hook_slow的返回值决定了数据包是不是被DROP,这就是我们的HOOK点。

至于说怎么知道是要修改ip_local_deliver,那就全靠对内核代码的熟悉了。

hook的偏移是173字节处,hook长度5个字节即可:

  • 把call nf_hook_slow改成call 我们自己的stub函数。
  • 在我们的stub函数中进行call nf_hook_slow。
  • 调整call nf_hook_slow的相对偏移。

我们试着去实现它,直接上模块的代码吧:

// 注意,本文的此例未考虑并发问题,正规的做法应该采用percpu变量或者atomic变量
#include <linux/module.h>
#include <linux/slab.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);

	addr = (void *)laddr;

	_text_poke_smp = (void *)0xffffffff8163e1f0;
	_text_mutex = (void *)0xffffffff81984920;

	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");

好了,加载模块,看看效果吧.

首先,我们添加一条iptables规则:

[root@localhost ~]# iptables -A INPUT -i lo -j DROP

然后确认计数器归零:

[root@localhost timer]# cat /sys/module/nowa/parameters/counter
0
[root@localhost ~]# iptables -t filter -L -v
Chain INPUT (policy ACCEPT 37 packets, 2628 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DROP       all  --  lo     any     anywhere             anywhere

来吧,发起流量,然后片刻后停掉:

[root@localhost ~]# telnet 127.0.0.1 23
Trying 127.0.0.1...
^@^C
[root@localhost ~]#

确认计数器是正确的:

[root@localhost ~]# cat /sys/module/nowa/parameters/counter
7
[root@localhost ~]# iptables -t filter -L -v
Chain INPUT (policy ACCEPT 103 packets, 7336 bytes)
 pkts bytes target     prot opt in     out     source               destination
    7   420 DROP       all  --  lo     any     anywhere             anywhere

OK,二进制hook的计数器和iptables本身的计数器一致。


好了,在完成这一切后,我们仔细思考一下,还有很多遗留问题:

  • 如果偏移不在32bit范围内怎么办?那就需要64bit绝对跳转了。
  • 复杂额外逻辑需要寄存器的时候,就需要save/restore所有寄存器了,避免和原始函数冲突。

总之了,这就是直接用二进制机器码编程了。必须说明的是,这只是手艺人或者工人们的杂耍,生产环境上这么玩,经理一定会dis的。还是乖乖用kpatch吧。


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

发布了1580 篇原创文章 · 获赞 5111 · 访问量 1113万+

猜你喜欢

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