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
...

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

原文链接: https://blog.csdn.net/dog250/article/details/105205966

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/406055

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午9:35
下一篇 2023年4月26日 上午9:35

相关推荐