0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何用替换函数的Trick做正常的事情

Linux阅码场 来源:未知 作者:胡薇 2018-11-23 11:00 次阅读

浙江温州皮鞋湿,下雨进水不会胖。周六的雨夜,期待明天的雨会更大更冷。

已经多久没有编程了?很久了吧…其实我本来就不怎么会写代码,时不时的也就是为了验证一个系统特性,写点玩具而已,工程化的代码,对于我而言,实在是吃力。

最近遇到一些问题,需要特定的解法,也就有机会手写点代码了。其实这个话题记得上一次遇到是在8年前,时间过得好快。

替换一个已经在内存中的函数,使得执行流流入我们自己的逻辑,然后再调用原始的函数,这是一个很古老的话题了。比如有个函数叫做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

#include

#include

#include

#include

#define OPTSIZE5

// 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 conntrack\n");

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 conntrack\n");

// 仅仅打印一行信息后,调用原始函数。

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的。

在上面的代码中,saved_op中为什么没有old inst呢?直接就是一个jmp y,这岂不是将原始函数中的头几个字节的指令给遗漏了吗?

其实说到这里,还真有个不好玩的Trick,起初我真的就是老老实实保存了前5个自己的指令,然后当需要调用原始ipv4_conntrack_in时,就先执行那5个保存的指令,也是OK的。随后我objdump这个函数发现了下面的代码:

0000000000000380 :

380: e8 00 00 00 00 callq 385

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

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

可以看到,这个是可以忽略的。因为不管怎么说都是紧接着执行下面的指令。所以说,我就省去了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 = 0

if 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; // jmp

offset = ...

(*(s32*)(&saved_op[curr+1])) = offset;

这是正确的做法。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Linux
    +关注

    关注

    87

    文章

    11296

    浏览量

    209358
  • 函数
    +关注

    关注

    3

    文章

    4329

    浏览量

    62576

原文标题:Linux内核如何替换内核函数并调用原始函数

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    何用labview启动

    何用labview启动
    发表于 03-13 16:07

    何用Primff()函数打印串行端口信息?

    ”);// Uart可以打印串口信息。现在,我想这样:PrtTF(“Helon Test\r\n”);//“Helon Test\R\n”由串行工具恢复。所以,我的问题是如何用Primff()函数打印串行端口
    发表于 09-26 08:35

    GUI函数里面做了哪些事情,对显示界面有何作用

    请问 GUI_TOUCH_Exec()函数里面做了哪些事情,对显示界面有何作用?该函数的资料很难找到,请大家帮忙。谢谢!
    发表于 06-03 04:35

    何用Matlab去实现FFT函数和IFFT函数

    Matlab的FFT函数和IFFT函数有什么用法吗?如何用Matlab去实现FFT函数和IFFT函数呢?
    发表于 11-18 07:05

    何用printf()函数代替串口发送数据?

    何用printf()函数代替串口发送数据?
    发表于 12-01 08:01

    何用STM32通用定时器微秒延时函数

    何用STM32通用定时器微秒延时函数
    发表于 12-01 06:37

    何用__write函数替换掉原先的fputc函数

    何用__write函数替换掉原先的fputc函数呢?
    发表于 12-01 06:55

    何用2SC2539替换2SC1971

    何用2SC2539替换2SC1971
    发表于 12-22 11:40 3172次阅读

    替换数组子集函数

    Labview之替换数组子集函数,很好的Labview资料,快来下载学习吧。
    发表于 04-19 10:43 0次下载

    何用AD电路板边框

    何用AD电路板边框,感兴趣的小伙伴们可以看看。
    发表于 07-26 10:43 0次下载

    matlab升级2021a版本后有哪些函数需要替换

    使用新的函数进行替换!先到互联网上搜索一下该函数的使用方法吧!因为无法在matlab里面运行此函数了,所以老函数的用法只能靠搜索来给大家
    的头像 发表于 06-10 16:44 9827次阅读

    在C++中如何用函数实现多态

    01 — C++虚函数探索 C++是一门面向对象语言,在C++里运行时多态是由虚函数和纯虚函数实现的,现在我们看下在C++中如何用函数实现
    的头像 发表于 09-29 14:18 1694次阅读

    C语言内联函数

    函数B很小,又被频繁的调用,可能函数调用的切换时间比函数内代码的执行时间还长,这样明显划不来,那么我们就可以将这个函数声明为内联(加上 inline ),编译器在编译时,会把内联
    的头像 发表于 02-21 16:55 951次阅读
    C语言内联<b class='flag-5'>函数</b>

    何用两种不同的方法列写双容水槽传递函数

    何用两种不同的方法列写双容水槽传递函数
    的头像 发表于 03-10 16:20 3825次阅读
    如<b class='flag-5'>何用</b>两种不同的方法列写双容水槽传递<b class='flag-5'>函数</b>

    MySQL替换字符串函数REPLACE

    MySQL是目前非常流行的开源数据库管理系统之一,它具有强大的功能和性能。其中之一的字符串函数REPLACE,可以用于替换字符串中的指定字符或字符串。在本文中,我们将详细讨论MySQL替换字符串
    的头像 发表于 11-30 10:44 1545次阅读