上一篇文章,我展示了一个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吧。
浙江温州皮鞋湿,下雨进水不会胖。