替换一个曾经在内存中的函数,使得执行流流入咱们本人的逻辑,而后再调用原始的函数,这是一个很古老的话题了。比方有个函数叫做funcion,而你心愿统计一下调用function的次数,最间接的办法就是 如果有谁调用function的时候,调到上面这个就好了 :
void new_function(){ count++; return function();}
网上很多文章给出了实现这个思路的Trick,而且始终以来计算机病毒也都采纳了这种移花接木的手腕来实现本人的目标。然而,当你亲自去测试时,发现事件并不那么简略。
网上给出的许多办法均不再实用了,起因是在晚期,这样做的人比拟少,处理器和操作系统大可不必理睬一些不合乎惯例的做法,然而随着这类Trick开始做好事影响到失常的业务逻辑时,处理器厂商以及操作系统厂商或者社区便不得不在底层减少一些限制性机制,以避免这类Trick持续起作用。
常见的措施有两点:
- 可执行代码段不可写
这个措施便封堵住了你想通过简略memcpy的形式替换函数指令的计划。
- 内存buffer不可执行
这个措施便封堵住了你想把执行流jmp到你的一个保留指令的buffer的计划。
- stack不可执行
别看这些措施都比拟low,一看谁都懂,它们却防止了大量的缓冲区溢出带来的危害。
那么如果咱们想用替换函数的Trick做失常的事件,怎么办?
我来简略谈一下我的办法。首先我不会去HOOK用户态的过程的函数,因为这样意义不大,改一下重启服务会好很多。所以说,本文特指HOOK内核函数的做法。毕竟内核从新编译,重启设施代价十分大。
咱们晓得,咱们目前所应用的简直所有计算机都是冯诺伊曼式的对立存储式计算机,即指令和数据是存在一起的,这就意味着咱们必然能够在操作系统层面随便解释内存空间的含意。
咱们在做正当的事件,所以我假如咱们曾经拿到了零碎的root权限并且能够编译和插入内核模块。那么接下来的事件仿佛就是一个流程了。
是的,批改页表项即可,即使无奈简略地通过memcpy来替换函数指令,咱们还是能够用以下的步骤来进行指令替换:
- 从新将函数地址对应的物理内存映射成可写;
- 用本人的jmp指令替换函数指令;
- 解除可写映射。
十分侥幸,内核曾经有了现成的 text_poke/text_poke_smp 函数来实现下面的事件。
同样的,针对一个堆上或者栈上调配的buffer不可执行,咱们仍然有方法。方法如下:
- 编写一个stub函数,实现随便,其代码指令和buffer相当;
- 用下面重映射函数地址为可写的办法用buffer重写stub函数;
- 将stub函数保留为要调用的函数指针。
是不是有点意思呢?上面是一个步骤示意图:
上面是一个代码,我稍后会针对这个代码,说几个细节方面的货色:
#include <linux/kernel.h>#include <linux/kprobes.h>#include <linux/cpu.h>#include <linux/module.h>#include <net/tcp.h>#define OPTSIZE 5// saved_op保留跳转到原始函数的指令char saved_op[OPTSIZE] = {0};// jump_op保留跳转到hook函数的指令char jump_op[OPTSIZE] = {0};static unsigned int (*ptr_orig_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);static unsigned int (*ptr_ipv4_conntrack_in)(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state);// stub函数,最终将会被保留指令的buffer笼罩掉static unsigned int stub_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state){ printk("hook stub conntrackn"); return 0;}// 这是咱们的hook函数,当内核在调用ipv4_conntrack_in的时候,将会达到这个函数。static unsigned int hook_ipv4_conntrack_in(const struct nf_hook_ops *ops, struct sk_buff *skb, const struct net_device *in, const struct net_device *out, const struct nf_hook_state *state){ printk("hook conntrackn"); // 仅仅打印一行信息后,调用原始函数。 return ptr_orig_conntrack_in(ops, skb, in, out, state);}static void *(*ptr_poke_smp)(void *addr, const void *opcode, size_t len);static __init int hook_conn_init(void){ s32 hook_offset, orig_offset; // 这个poke函数实现的就是重映射,写text段的事 ptr_poke_smp = kallsyms_lookup_name("text_poke_smp"); if (!ptr_poke_smp) { printk("err"); return -1; } // 嗯,咱们就是要hook住ipv4_conntrack_in,所以要先找到它! ptr_ipv4_conntrack_in = kallsyms_lookup_name("ipv4_conntrack_in"); if (!ptr_ipv4_conntrack_in) { printk("err"); return -1; } // 第一个字节当然是jump jump_op[0] = 0xe9; // 计算指标hook函数到以后地位的绝对偏移 hook_offset = (s32)((long)hook_ipv4_conntrack_in - (long)ptr_ipv4_conntrack_in - OPTSIZE); // 前面4个字节为一个绝对偏移 (*(s32*)(&jump_op[1])) = hook_offset; // 事实上,咱们并没有保留原始ipv4_conntrack_in函数的头几条指令, // 而是间接jmp到了5条指令后的指令,对应上图,应该是指令buffer里没 // 有old inst,间接就是jmp y了,为什么呢?前面细说。 saved_op[0] = 0xe9; // 计算指标原始函数将要执行的地位到以后地位的偏移 orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE)); (*(s32*)(&saved_op[1])) = orig_offset; get_online_cpus(); // 替换操作! ptr_poke_smp(stub_ipv4_conntrack_in, saved_op, OPTSIZE); ptr_orig_conntrack_in = stub_ipv4_conntrack_in; barrier(); ptr_poke_smp(ptr_ipv4_conntrack_in, jump_op, OPTSIZE); put_online_cpus(); return 0;}module_init(hook_conn_init);static __exit void hook_conn_exit(void){ get_online_cpus(); ptr_poke_smp(ptr_ipv4_conntrack_in, saved_op, OPTSIZE); ptr_poke_smp(stub_ipv4_conntrack_in, stub_op, OPTSIZE); barrier(); put_online_cpus();}module_exit(hook_conn_exit);MODULE_DESCRIPTION("hook test");MODULE_LICENSE("GPL");MODULE_VERSION("1.1");
测试是OK的。
须要C/C++ Linux服务器架构师学习材料加群812855908(材料包含C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),收费分享
在下面的代码中,saved_op中为什么没有old inst呢?间接就是一个jmp y,这岂不是将原始函数中的头几个字节的指令给脱漏了吗?
其实说到这里,还真有个不好玩的Trick,起初我真的就是老老实实保留了前5个本人的指令,而后当须要调用原始ipv4_conntrack_in时,就先执行那5个保留的指令,也是OK的。随后我objdump这个函数发现了上面的代码:
0000000000000380 <ipv4_conntrack_in>: 380: e8 00 00 00 00 callq 385 <ipv4_conntrack_in+0x5> 385: 55 push %rbp 386: 49 8b 40 18 mov 0x18(%r8),%rax 38a: 48 89 f1 mov %rsi,%rcx 38d: 8b 57 2c mov 0x2c(%rdi),%edx 390: be 02 00 00 00 mov $0x2,%esi 395: 48 89 e5 mov %rsp,%rbp 398: 48 8b b8 e8 03 00 00 mov 0x3e8(%rax),%rdi 39f: e8 00 00 00 00 callq 3a4 <ipv4_conntrack_in+0x24> 3a4: 5d pop %rbp 3a5: c3 retq 3a6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 3ad: 00 00 00
留神前5个指令: e8 00 00 00 00 callq 385 <ipv4_conntrack_in+0x5>
能够看到,这个是能够疏忽的。因为不管怎么说都是紧接着执行上面的指令。所以说,我就省去了inst的保留。
如果依照我的图示中惯例的办法的话,代码略微改一下即可:
char saved_op[OPTSIZE+OPTSIZE] = {0};... // 减少一个指令拷贝的操作 memcpy(saved_op, (unsigned char *)ptr_ipv4_conntrack_in, OPTSIZE); saved_op[OPTSIZE] = 0xe9; orig_offset = (s32)((long)ptr_ipv4_conntrack_in + OPTSIZE - ((long)stub_ipv4_conntrack_in + OPTSIZE + OPTSIZE)); (*(s32*)(&saved_op[OPTSIZE+1])) = orig_offset;...
然而以上的只是玩具。
有个十分事实的问题。在我保留原始函数的头n条指令的时候,n到底是多少呢?在本例中,显然n是5,合乎现在Linux内核函数第一条指令简直都是callq xxx的常规。
然而,如果一个函数的第一条指令是上面的样子:
op d1 d2 d3 d4 d5
即一个操作码须要5个操作数,我要是只保留5个字节,最初在stub中的指令将会是上面的样子:
op d1 d2 d3 d4 0xe9 off1 off2 off3 off4
这显然是谬误的,op操作码会将jmp指令0xe9解释成操作数。
解药呢?当然有咯。
咱们不能鲁莽地备份固定长度的指令,而是应该这样做:
curr = 0if orig[0] 为单字节操作码 saved_op[curr] = orig[curr]; curr++;else if orig[0] 携带1个1字节操作数 memcpy(saved_op, orig, 2); curr += 2;else if orig[0] 携带2字节操作数 memcpy(saved_op, orig, 3); curr += 3;...saved_op[curr] = 0xe9; // jmpoffset = ...(*(s32*)(&saved_op[curr+1])) = offset;
这是正确的做法。