Linux内核二进制hook的手艺-一个简单的demo

有时候,我们希望修改Linux的内核某些行为,或者更一般的,我们需要进行一些统计,比如说我们希望实时统计出当前系统的TCP半连接数量。

但问题是,可能没有现成的工具可以使用,也不能重新编译内核,那么怎么办呢?

kpatch机制就是干这个的。

你可能认为我接下来要开始介绍kpatch的原理和用法了。但并不是,我不善于为工具写文档介绍用法,我更擅长手艺人的做法。下面的篇幅,正式开始展示一个不使用kpatch技术来实现等效功能的方法。

我以一个demo开始介绍其原理,下一篇文章我将介绍一个正式的可以使用的例子。如果有一些细节上的疑问,建议在我前两天写的文章中试着找找答案,如果还有问题需要解决,直接email我或者wechat我都可以。

我前面的文章链接如下:
https://blog.csdn.net/dog250/article/details/105135219
https://blog.csdn.net/dog250/article/details/105129254
https://blog.csdn.net/dog250/article/details/105093969

好了,让我们直接上demo模块代码:

#include <linux/module.h>
#include <linux/proc_fs.h>

static ssize_t sample_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
	int n = 0;
	char kb[16];

	if (*ppos != 0) {
		return 0;
	}

	n = sprintf(kb, "%d\n", 1234);
	memcpy(ubuf, kb, n);
	*ppos += n;
	return n;
}

static struct file_operations sample_ops = {
	.owner = THIS_MODULE,
	.read = sample_read,
};

static struct proc_dir_entry *ent;
static int __init sample_init(void)
{
	ent = proc_create("test", 0660, NULL, &sample_ops);
	if (!ent)
		return -1;

	return 0;
}

static void __exit sample_exit(void)
{
	proc_remove(ent);
}

module_init(sample_init);
module_exit(sample_exit);
MODULE_LICENSE("GPL");

该模块会在procfs文件系统中创建/proc/test这个文件,我的目标是,每次读取这个文件的时候,另一个内核模块中的一个计数器counter这个值递增1.

由于模块已经写好了,我们不可能去修改它,这就是展示手艺的点了,我们从外部在另一个模块里来增加计数器递增的操作。

为了hook住sample_read这个函数,我们先看它的反汇编:

crash> dis sample_read
0xffffffffa00e1000 <sample_read>:       nopl   0x0(%rax,%rax,1) [FTRACE NOP]
0xffffffffa00e1005 <sample_read+5>:     push   %rbp
0xffffffffa00e1006 <sample_read+6>:     mov    %rsp,%rbp
0xffffffffa00e1009 <sample_read+9>:     push   %r13
0xffffffffa00e100b <sample_read+11>:    push   %r12
0xffffffffa00e100d <sample_read+13>:    push   %rbx
0xffffffffa00e100e <sample_read+14>:    mov    %rcx,%rbx
0xffffffffa00e1011 <sample_read+17>:    sub    $0x18,%rsp
0xffffffffa00e1015 <sample_read+21>:    mov    %gs:0x28,%rax
0xffffffffa00e101e <sample_read+30>:    mov    %rax,-0x20(%rbp)
0xffffffffa00e1022 <sample_read+34>:    xor    %eax,%eax
// 下面是if (*ppos != 0) 判断语句
0xffffffffa00e1024 <sample_read+36>:    cmpq   $0x0,(%rcx)
0xffffffffa00e1028 <sample_read+40>:    jne    0xffffffffa00e105a <sample_read+90>
// 下面的两行就是HOOK点!!!
0xffffffffa00e102a <sample_read+42>:    lea    -0x30(%rbp),%rdi
0xffffffffa00e102e <sample_read+46>:    mov    %rsi,%r13
0xffffffffa00e1031 <sample_read+49>:    mov    $0x4d2,%edx
0xffffffffa00e1036 <sample_read+54>:    mov    $0xffffffffa00e2024,%rsi
0xffffffffa00e103d <sample_read+61>:    callq  0xffffffff812fd960 <sprintf>

我们只需要将lea和后面mov两行替换成一个jmp/call到自己定义的地址就可以了,同时把lea/mov移到自定义的逻辑里面去执行,执行完成后,在jmp/ret回来。

本例中,为了避免jmp过去后再jmp回来,所以使用call,因为ret可以自动jmp回来。

非常简单的逻辑,直接上代码吧:

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/cpu.h>

char *stub;
char *addr = NULL;

// laddr 参数保存sample_read的地址,通过/proc/kallsyms查询后传入
static unsigned long laddr = 0xffffffffa0267000;
module_param(laddr, ulong, 0644);

// 独立的计数器,每次读取/proc/test时,该计数器加1
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 POKE_OFFSET		42 // sample_read的42偏移处需要被poke成jmp
#define POKE_LENGTH		7  // 被poke的必须是完整的指令行,如果有空余的,用nop填充

// text_poke和text_mutex通过/proc/kallsyms查询地址
static void *(*_text_poke_smp)(void *addr, const void *opcode, size_t len);
static struct mutex *_text_mutex;

static int __init hotfix_init(void)
{
	unsigned char e8_call[POKE_LENGTH];
	unsigned char incl[8];
	s32 offset;
	u32 low32 = (unsigned int)(((unsigned long)&counter) & 0xffffffff);

	addr = (void *)laddr;

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

	stub = (void *)test_stub1;


	offset = (s32)((long)stub - (long)addr - 5);

	// 插入的指令中需要save/restore寄存器,但这里简单,略过
	incl[0] = 0xff;
	incl[1] = 0x04;
	incl[2] = 0x25;
	(*(u32 *)(&incl[3])) = low32; // 写入需要递增的counter变量地址
	incl[7] = 0xc3; // retq 指令

	// 执行poke:1. 首先拷贝原始函数中的指令; 2. 其次写入新增的计数器递增指令 
	_text_poke_smp(&stub[0], &addr[POKE_OFFSET], POKE_LENGTH);
	_text_poke_smp(&stub[POKE_LENGTH], &incl, 8);

	// call比jmp方便,可以自动帮忙return,不然还要自己jmp回来,但是代价是push/pop
	e8_call[0] = 0xe8;
	(*(s32 *)(&e8_call[1])) = offset - POKE_OFFSET;
	e8_call[5] = 0x90; e8_call[6] = 0x90; // nop 占位符
	get_online_cpus();
	mutex_lock(_text_mutex);
	// 执行call指令替换
	_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)
{
	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");

我们看下效果:

[root@localhost ~]# cat /proc/test
1234
[root@localhost ~]# cat /sys/module/nowa/parameters/counter
1
[root@localhost ~]# cat /proc/test
1234
[root@localhost ~]# cat /sys/module/nowa/parameters/counter
2
[root@localhost ~]# cat /proc/test
1234
[root@localhost ~]# cat /sys/module/nowa/parameters/counter
3

已经实现效果了。我们看一下原始的sample_read和test_stub1变成了什么样子:

crash> dis sample_read
...
0xffffffffa00e1024 <sample_read+36>:    cmpq   $0x0,(%rcx)
0xffffffffa00e1028 <sample_read+40>:    jne    0xffffffffa00e105a <sample_read+90>
// 注意下面的7个字节的指令,已经被替换了
0xffffffffa00e102a <sample_read+42>:    callq  0xffffffffa0264000 <test_stub1>
0xffffffffa00e102f <sample_read+47>:    nop
0xffffffffa00e1030 <sample_read+48>:    nop
0xffffffffa00e1031 <sample_read+49>:    mov    $0x4d2,%edx
...
crash> dis test_stub1
// 下面的7字节指令拷贝自原始函数
0xffffffffa0264000 <test_stub1>:        lea    -0x30(%rbp),%rdi
0xffffffffa0264004 <test_stub1+4>:      mov    %rsi,%r13
// 计数器递增指令
0xffffffffa0264007 <test_stub1+7>:      incl   0xffffffffa0266278
0xffffffffa026400e <test_stub1+14>:     retq
...

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

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值