c++ hook例子_ARM平台linux系统内核HOOK常见问题探讨二

4b484d23aca49d4a709646cdcf53be8a.png

上一次我们针对内核HOOK常见问题讲了只读内存问题以及为了避免操作被打断如何阻塞其它CPU核心,还有ARM64的参数及内存布局等基础问题。在这些问题上,已经能基本满足我们日常做HOOK的需求。

本节主要讲述另外几个HOOK相关问题。

首先是HOOK的方式。除了上节讲过的系统调用表的HOOK和各种inline HOOK外,这里再介绍几种相对兼容性、稳定性更有保障的做法。

最核心、最灵活的当然就是利用kprobe来实现相关功能。kprobe的原理是在需要HOOK的可执行代码的位置强制填上一个调试指令(x86平台)或者无效指令(ARM64平台),从而使得这段代码被执行时引发一个异常,CPU自动转到对应的异常中断代码执行。kprobe框架在对应的中断处理函数里有自己的检测代码。当发现是因为自己的原因导致的异常时,会接管这个异常的处理,从而回调我们事先设置好的函数。等我们的函数返回后,kprobe会用自己的方式执行之前被自己覆盖的代码,然后跳转到正常流程里去执行。

 基于kprobe有几种变体:

变体

一是最普通的kprobe,回调函数里只提供发生异常时的寄存器内容。这种回调函数的执行时机是被HOOK点的代码真实执行之前,通常适用于函数执行前的HOOK需求。但是这个时机点的执行环境比较特殊,中断是关闭的,且位于中断执行环境里,类似于linux上irq_work_queue或者windows上DispathDpc时回调函数的执行环境,基本只能做寄存器操作等非常有限的事情。而且如果此处耗时太多会严重影响整个系统的性能;同时,对于一些会产生中断的操作也不能在这里做,比如访问分页内存、执行一切会导致阻塞(一般直译为睡眠)的操作(比如除了自旋锁之外的各种锁操作)等等。但是,在这里对参数里提供的寄存器结构体的任何操作,最终会还原到真实的物理寄存器里去。利用这个特性,我们可以通过修改sp、pc、lr等参数,构造我们自己的后续执行环境,使得当前kprobe结束后,最终执行流程跳转到我们自己的代理函数里去。还可以通过修改lr寄存器(这里以ARM64为例,x86等其它硬件平台需要先弄清楚对应硬件架构寄存器及内存布局)里的值实现修改当前函数返回地址的效果,实现kretprobe一样的效果。

二是jprobe类型。这种类型的kprobe实际上是在上面一种执行环境的基础上,调用我们的函数前,先根据ARM64的调用约定,通过寄存器、SP指针解析所有参数,同时构造所需的函数调用环境,比如调整寄存器值,压栈等,最终调用我们的jprobe回调函数。然后在jprobe的结束地方,不能简单return。原因从刚才的描述也看得出,此时需要还原之前对寄存器、栈帧的修改,所以kprobe框架提供了一个做这件事的函数jprobe_return。如果不这么做,直接return,因为寄存器值、栈帧都是异常的,所以最终肯定会导致系统panic。同时,这个函数的执行环境依然处于中断环境里,对回调函数的代码逻辑要求依然和上述第一种是一样的。

三是kretprobe。这个类型上面已经提到过。其实就是在上述第一种的执行环境里,通过保存函数原始的返回地址(lr寄存器)然后修改这个地址为我们自己的地址从而实现对函数返回动作的监控,效果大概类似于源代码层面在每个return前加上我们自己的处理代码。比如要做execve的返回值监控,这里就是个很不错的位置。最重要的是,这个函数因为是通过修改lr寄存器实现的调用,其运行于正常执行环境,可以做所有正常环境能做的事情,我们能在这里做一些相对复杂的工作,比如读写文件、收发网络数据包等等。如果某个业务逻辑在这个时机点能满足需求,建议尽量在这里做。

 四是livepatch。其实livepatch也是基于kprobe的。它的实现原理就是注册一个kprobe,然后在回调里修改寄存器值、修改栈帧指针、保存必要数据从而构造回调函数的执行环境。这个kprobe返回后,因为寄存器值会还原成修改过的值,而包括pc寄存器在内的寄存器都是修改过的,所以当前函数的最终执行流程会来到我们事先定义好的patch函数里。patch函数被执行时,其执行环境和原函数完全一致,所以可以在这个函数里做所有业务逻辑操作,包括调用原本的函数处理逻辑或者直接返回(kprobe及livepatch框架确保不会出现因递归调用导致的死循环)。这种方式是目前最推荐的做法。不过这个方式目前只支持函数的HOOK,非函数代码,比如我们为了单次HOOK就实现监控所有sys call的效果而对svc handler做的hook,是不能使用这种方式的。后面的这种情况,只能要么inline HOOK要么最原始的kprobe然后构造所需的执行环境。

上面说的这些方式其实都是基于kprobe这个基础框架来实现的,它因为实现原理的原因(每次调用都需要陷入中断处理代码里),性能损耗稍微有些大。不过,内核有编译选项可以对这个问题进行改善。其原理就是用跳转指令代替原本的异常指令,从而避免引发中断所消耗的性能。

说完了linux内核常见的几种HOOK方式,再说个内核安全开发经常会遇到的问题:

先说个锁的定义:一切能够把线程阻塞、并且有办法解除这个阻塞的数据结构、代码逻辑等都可以叫做锁。

在操作系统内核里,基于各种特殊需求,不同的锁有不同的特性。最普的锁,效果就和应用层的锁(比如信号量,互斥体等)户安全一致。但也有些锁,锁定期间会提升当前CPU的中断级别,导致低于这个中断级的中断无法触发,比如自旋锁就是这样。自旋锁锁定期间,中断级会在dpc level,高于缺页中断以及线程调度所在的终端环境的中断级别,导致这些事情都无法做。也就是自旋锁期间,访问分页内存、所有会导致的cpu时间片让出的阻塞(又直译为睡眠)、几乎所有文件IO/网络IO等都是不能做的。

我们在使用这些锁时一定要搞清楚自己的需求,选择恰当的锁类型,否则很容易高中断级的锁打断低中断级的操作从而出现死锁、系统panic等情况。

除此之外,这里再介绍下CPU硬件层面的同步机制。

我们日常的大多数操作都是需要rmw(read,modify,write)三步才能完成的,比如看似简单的a++这行c代码就是如此,这个过程是有可能被打断的。打断后原本的状态就变得不可控。为了解决这个问题,各个硬件平台都有自己的一些特殊指令。

比如x86的带 lock前缀的指令 lock cmpxchg类似这样的,还有ARM64的ldrex等等。区别在于x86的lock指令会锁地址线,它确保一定执行成功、永不失败返回,而ARM64的ldrex指令是类似如下逻辑:

cpu0执行ldrex指令时会先对这个地址标记为私有,且这个标记能记住是哪个cpu标记的,然后进行后续操作。此时cpu1也来操作这个内存地址,也把这个地址重新标记为是自己私有的。然后cpu0做完各种操作后,把数据写回内存前,会检查这个标记是不是自己私有,发现不是,就不会真的做写入操作,而是直接返回一个表示失败的标记。对于cpu1来说,做写入时发现是自己的私有标记,所以写入成功。

所以写程序的人来说,需要循环判断写入成功的标记,直到真的成功为止。这个逻辑有个例子。

全局共有变量 :

volatile int global_var=0;

线程函数:

while(1){

int old_value=global_var;

global_var++;

int new_value=global_var;

If(old_value+1==new_value){

break;

}else{

global_var--;

}

}

这个代码演示的就是刚才说的逻辑过程,只不过cpu里面的成功失败与否的标记是硬件实现的,这里是软件模拟。实际产品代码里我们一般也不会这么写,因为++或者--到第二次赋值期间无法保证原子,后续的判断是不准确的,这里纯粹是为了演示效果。

实际上我们通常都会使用c库包装好的atmoic_xxx等一系列函数,其内部就是利用了cpu这个特性。原子操作属于无锁操作的一种,效率比直接加锁要高的多,但是也有其局限性,比如某些就是需要睡眠、让出cpu执行时间的需求,原子操作是无法完成的。我们一般用原子操作模拟一些无需睡眠、需要抢占资源的场景,其应用场景其实和自旋锁基本一致。

最后,我们用原子操作的方式来实现一个自旋锁结束本节内容:

void* create_lock(){

atomic_t*lock(atomic_t*)malloc(sizeof(atomic_t));

lock.count=0;

return (void*)lock;

 }     

void* free_lock(void* lock){

 free(lock);

 }

void get_lock(void*lock){

while(atomic_cmp_xchg((atomic_t*)lock,0,1)!=0){

sleep(0); //可以思考下这里sleep(0)的意义

   }

 }

 void put_lock(void*lock){

  while(atomic_cmp_xchg((atomic_t*)lock,1,0)!=1){

   sleep(0);//可以思考下这里sleep(0)的意义

  }

 }

END

b732f5947471ee45315055acc34810a5.gif

16e534013fa035bde5d0aaa3271c0ad3.png

扫描关注我们

微信:OPPO安珀实验室

cc6ba840d65c724fd64206a97ef36d39.png

wow64_hook 源码历史更新 --------------------------------------------------------------- 2021/4/16  模块源码1.8.7 更新: 1:重新架构了穿插汇编指令,优化了一些代码和流程 2:在 远程hook64指令_安装()时新增可回调的3个自定义参数值,这些值在回调接口的[寄存器64.自定义值1;2;3]里可获取到该值 3:修复 寻找无效8字节指令地址()中一个重要BUG,此BUG极大可能导致之前版本在 远程hook64指令_安装()时即导致目标程序崩溃的现象 本次更新比较重要 建议使用者更新到此版本使用............ --------------------------------------------------------------- 2021/4/15  模块源码1.8.6 更新: 1:新增3组函数:X64_取模块代码区起始地址(),X64_取模块入口地址(),X64_取模块代码执行段大小() 2:自定义类型:模块信息64,的成员构成新增改动为 以下,在枚举模块中亦可直接取得 成员 模块基址, 长整数型, , , 模块映像的内存地址 也称为句柄 成员 模块长度, 整数型, , , 整个模块文件长度 成员 模块入口, 长整数型, , , 模块入口函数地址 如 Mian/DllMain 成员 模块代码入口, 长整数型, , , 模块代码执行区起始地址 成员 模块代码长度, 整数型, , , 模块代码执行区的长度 成员 模块名称, 文本型, , , 文件名称 成员 模块路径, 文本型, , , 完整的路径地址 3:新消息接口()远程返回_调用回调子程序()优化了代码严谨性,减少hook目标崩溃的可能性 4:寻找无效8字节指令地址()由之前的全模块查找 改动为 在模块代码执行区查找 5:改写模块实列为 一对多的模式 6:模块实列操作控件的方式由变量改为堆内存,避免引起多线程自身崩溃 7:模块实列 对 recv,recvfrom两个函数的hook方式由原先 在回调内 暂停recv-->recv_call-->恢复recv,的方式改为经过特殊改造的 recv_call,这个call经过特殊处理,在recv回调函数内调用,用来取得真实长度,这个调用会绕过hook位置,所以不会触发 recv回调,详见源码 8:修改了一些已知可能出现的问题 --------------------------------------------------------------- 2021/4/12 模块源码 v1.8.2更新 1:修复 x64_远调用函数()在 易语言 主线程调用时造成消息无法回调,导致易语言主线程窗口卡死的问题。      感谢楼下易友发现的BUG,已经第一时间更新 --------------------------------------------------------------- 2021/4/12 模块源码 v1.8.1更新 1:修复 hook全部卸载时的流程写法的一个错误,由于句柄的提前关闭导致多个hook点卸载不干净的问题 2:改写了消息回调时线程传参的代码优化,优化了其他一些小问题 3:  鉴于很多朋友需要,改写了模块自带实列,对TCP,UDP的两组封包函数做了hook实列写法 4:列子中同样增加对x64_远调用函数()的应用写了几个列子,如使用套接字取得本地或远端IP端口API调用的的应用实列 5:本hook模块不支持非模块内存区hook,如申请的动态分配页等,不是不能支持,只是觉得没有任何意义,对这方面有需求的,自行改写模块源码使用 提醒:hook回调函数中尽量减少耗时代码,时间越长返回越慢,回调中谨慎操作控件,如必须要用到可参考源码中实列写法采用线程操作 --------------------------------------------------------------- 2021/3/1   模块源码v1.6更新: 1:修复  x64_远程调用函数()命令,在没有提供 寄存器 参数时,没有返回值的BUG。 --------------------------------------------------------------- 2021/2/28 模块源码v1.5更新: 一:修复win7 64位系统下枚举模块 出现部分模块长度出现负数的问题,从而导致部分win7用户不能使用 :强化 远程hook64指令_安装 的稳定性:        1,穿插代码中增加对标志位的保护,避免hook位置长度下一条指令为跳转时产生跳转错乱的问题,强化了hook任意位置的定位        2,因为穿插代码中会调用AP
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值