x86_64运行时动态替换函数的hotpatch机制

栏目: IT资讯 · 发布时间: 6年前

内容简介:版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/84258601

版权声明:本文为博主原创,无版权,未经博主允许可以随意转载,无需注明出处,随意修改或保持可作为原创! https://blog.csdn.net/dog250/article/details/84258601

zhejiang wenzhou skinshoe wet, rain flooding water will not fat.

昨天我发了一篇关于替换运行中的内核函数的文章:

Linux内核如何替换内核函数并调用原始函数 https://blog.csdn.net/dog250/article/details/84201114

晚上回去有朋友在朋友圈回复了我关于 “函数开头5字节跳转” 的事。今天正好要确认一个与此相关的细节,就顺便又把这问题重新撸了一遍。

其实起初我也很纳闷,以前不都是0x55开头的指令吗?怎么现在这种call self或者lea 0x0(%rsp),%rsp套路却都成了惯例。…

关于5字节跳转,说的是下面的情况:

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

请注意函数最开头的5个字节:

e8 00 00 00 00          callq  385 <ipv4_conntrack_in+0x5>

可见,它实际上call的是紧接着它下面的地址,所以说这个5字节的call指令其实是 没有用 的!

仔细看一下这5个字节,思考一下它到底有什么用。

  • 我们可以任意将它替换成 jmp $4字节相对偏移

这样,代码指令流就会进入我们自己的HOOK函数里了。

当然了,关于 “e8 00 00 00 00 callq …” 这个有很多话可以讲,比如和Link相联系的时候就比较有意思了,它可是作为一个桩指令存在。这个和HOOK无关,也不再说太多。

让我觉得最有意思的是,昨天那位朋友提到了微软的/hotpatch编译选项,我大致看了一下:

/hotpatch (Create Hotpatchable Image): https://docs.microsoft.com/en-us/cpp/build/reference/hotpatch-create-hotpatchable-image?view=vs-2017

When /hotpatch is used in a compilation, the compiler ensures that first instruction of each function is at least two bytes, which is required for hot patching.

这是一个很有意思的选项,其实编译器提供这个机制也是举手之劳吧,虽然简单,但它确实为程序员HOOK运行中的函数提供了很大的方便。

/hotpatch的实质其实就是在函数的开头和结尾填充了一些无关紧要的指令,方便HOOK来用自己的jmp指令覆盖这个无关紧要的指令。比如下面是一个函数的开头:

380:	 90
	381:	 90
	382:	 90
	383:	 90
	384:	 90
	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

0x90代表一个nop指令,即 “什么也不做”的意思,如此一来,程序员便非常容易将类似下面的指令插入到函数开头了:

380:   e9 11 22 33 44			 jmp 0x44332211
      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

无需任何额外的指令保存动作。

既然微软的编译器有这个功能可用,GCC有没有呢?看了GCC的manual,发现了一个-mhotpatch=x,y的选项,但是在x86平台不能用,还是比较不爽的。

后来发现了在编写函数的时候,可以加上下面的属性,然后编译器就可以将其编译成带有填充的指令了:

__attribute__ ((ms_hook_prologue))

那么,简单来用一下,看看效果咯。

由于时间并不是很多,我也没有那么多精力去应对不断的panic,所以这次准备在用户态搞。

由于用户态可以直接使用mprotect函数更改内存的使用权限,所以就不需要那个stub函数了。今天的这个例子,原理图如下:

x86_64运行时动态替换函数的hotpatch机制

加上ms_hook_prologue属性修饰的函数,编译好了之后,你会在函数最开头两行找到下面的 废指令

00000000004004ed <func>:
       4004ed:   48 8d a4 24 00 00 00    lea    0x0(%rsp),%rsp
       4004f4:   00
       4004f5:   55                      push   %rbp

随意替换之就好。所以对于这个例子,上面图示里的n的值就是5.

此外,上图中,我们的一个指令buffer不再是一个stub函数,而真的就是一块分配的内存,所以我们需要给它加上EXEC权限,不然会segment fault。这个在内核模块中是不能直接做的,因为分配带有EXEC权限的module_alloc并没有导出,所以如若想用它,则必须通过kallsyms_lookup_name的内省方式来做。

再一个需要注意的是,由于指令buffer是在堆上分配的,在64位系统上,它的位置和函数代码的位置之差会超过4个字节界定的相对偏移,所以就不能用0xe9+4字节相对偏移来jmp了,而要通过64位绝对地址来跳转了,指令如下:

48 b8 88 77 66 55 44 33 22 11   mov rax, 0x1122334455667788
ff e0                           jmp rax

好了,说了这么多,该上代码了:

#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>

// 保存原始函数的指针
void (*ptr_func)(int a);

// ms_hook_prologue修饰的函数,便于hook
__attribute__ ((ms_hook_prologue))
void func(int a)
{
	printf("orig %d\n", a);
}

// 自己的hook函数
void my_func(int a)
{
	a += 2;
	printf("my %d\n", a);
	// 调用原始函数
	ptr_func(a);
}

// 需要在func前面插入一个jmp $4字节偏移 的指令,一共5个字节
char jump[5] = {0};

int main()
{
	long loffset;
	int offset;
	char *inst_buf;
	void *page = (void *)((unsigned long)func & ~0xfff);
	
	// 为func所在的页面增加可写权限
	mprotect(page, 8, PROT_WRITE|PROT_EXEC);
	// 这些操作和内核版本的hook一致
	jump[0] = 0xe9;
	offset = (int)((long)my_func - (long)func - 5);
	(*(int*)(&jump[1])) = offset;
	memcpy(func, jump, 5);

	// 分配一个带有EXEC权限的buffer,里面将保存指令
	inst_buf = mmap(NULL, 1024, PROT_READ|PROT_EXEC|PROT_WRITE, MAP_ANON|MAP_PRIVATE, -1, 0);
	// 拼接mov rax, 0x1122334455667788;jmp rax指令
	inst_buf[0] = 0x48;
	inst_buf[1] = 0xb8;
	loffset = (long)func + 4;
	(*(long*)(&inst_buf[2])) = loffset;
	inst_buf[10] = 0xff;
	inst_buf[11] = 0xe0;
	
	// 指定inst_buf为原始函数指针
	ptr_func = inst_buf;
	// 调用!!
	func(2);
}

结果当然是先调用自己的hook函数,然后再调用原始函数咯:

[root@localhost hotpatch]# ./a.out
my 4
orig 4

为什么不用kprobe机制呢?kprobe的原理是 为了灵活性,使用int 3指令替换被hook的指令。 这样就可以任意编写pre/post回调函数了,但是我们也能看出来,通过int方式来hook,对效率的影响是不能忽略,特别是对于那些频繁被调用的函数,kprobe更加不可行。

kprobe非常适合做问题排查,热点分析,但不好在生产环境跑的。

其实,本文所描述的hotpatch原理还可以做的更好些,达到 在任意点插入自己的逻辑的目的,包括在函数的内部。 这样可以达到和kprobe相同的效果。当然,这需要对运行中的二进制指令序列做相对周密详细的分析。

这里还有一篇关于hotpatch的文章,比我这篇好,可以看一下:

Hotpatching a C Function on x86 https://nullprogram.com/blog/2016/03/31/

补充说明一下,朋友圈有高手最新评论,让我又知道了些新东西:

  • nop指令的实现方式有很多种,比如mov edi edi。可能很多平台并没有类似独立的0x90指令吧。不过既然有,那还是0x90纯粹些。
  • kprobe也不全部一来int 3,只有return hook的场景才依赖int 3,其它的也可以做jmp hook。
  • 线程安全,原子化操作也是生产环境必须考虑的,不然就是玩具。

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


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

小群效应

小群效应

徐志斌 / 中信出版集团 / 2017-11 / 58.00元

互联网经济时代,新零售、网红经济、知识经济多受益于社群。用户的获取、留存及订单转化直接决定了一个社群的存亡。无论是“做”群还是“用”群,每个人都需要迭代常识:了解用户行为习惯,了解社群运行规律。 《社交红利》《即时引爆》作者徐志斌历时两年,挖掘腾讯、百度、豆瓣的一手后台数据,从上百个产品中深度解读社群行为,通过大量生动案例总结出利用社交网络和海量用户进行沟通的方法论。 本书将告诉你: ......一起来看看 《小群效应》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具