文章目录
- 前言
- 一、函数拦截需要的几个参数
- 二、插桩前先保存实际函数入口 6 字节数据
- 三、在插桩的函数入口写入跳转指令 | 构造拼接桩函数
前言
【Android 逆向】函数拦截实例 ( 函数拦截流程 | ① 定位动态库及函数位置 ) 博客中简单介绍了 hook 函数 ( 函数拦截 ) 的流程 , 本篇博客开始介绍函数拦截实例 ;
拦截 clock_gettime
函数 ;
#include <time.h>
int clock_gettime(clockid_t clk_id,struct timespec *tp);
一、函数拦截需要的几个参数
定义 hook_func
函数 , 执行 C/C 函数的 hook 操作 ;
void hook_func(uint8_t* pApi, uint8_t* pUser, uint8_t* pStub, size_t size)
上述函数的
个参数含义如下 :
uint8_t* pApi
参数 : 要拦截的实际函数 ,int clock_gettime(clockid_t clk_id,struct timespec *tp);
函数 ;uint8_t* pUser
参数 : 拦截函数后 , 跳转到的dn_clock_gettime
函数 ;uint8_t* pStub
参数 : 定义的do_clock_gettime
桩代码 , 将pApi
函数的前 6 字节拷贝到该pStub
函数入口 , 然后跳转到pApi
函数的第
字节开始执行 , 相当于调用了 uint8_t* pApi
参数对应的实际函数 , 即 int clock_gettime(clockid_t clk_id,struct timespec *tp);
函数 ;
size_t size
参数 : 跳转指令占0xE9,0,0,0,0
字节 , 这里 将函数入口前
字节保存下来 ;
函数调用实例 :
代码语言:javascript复制/* 这是 hook 标准库中的 clock_gettime 函数的入口方法 , 跳转到自定义的 dn_clock_gettime 方法中 */
hook_func((uint8_t*)clock_gettime, (uint8_t*)dn_clock_gettime, (uint8_t*)do_clock_gettime, 6);
二、插桩前先保存实际函数入口 6 字节数据
插桩前先 保存函数的入口 6 字节数据 , 因为之后插桩 , 会使用跳转代码 0xE9,0,0,0,0
覆盖函数入口内存 , 被破坏的实际函数 最终还是要执行 , 需要拷贝一下 , 供之后实际函数调用使用 ;
unsigned char code[64] = { 0 };
/* 插桩前先保存函数的入口 6 字节数据 , 因为之后插桩 ,
* 会使用跳转代码 0xE9,0,0,0,0 覆盖函数入口内存
* 该函数最终还是要执行 , 需要拷贝一下 , 供之后实际函数调用使用
*/
memcpy(code, pApi, size);
三、在插桩的函数入口写入跳转指令 | 构造拼接桩函数
这里执行了
次插桩操作 :
- 第一次是实际函数跳转 : 函数插桩 , pApi 是实际函数 , pUser 是插桩后跳转到的拦截函数 ; 该情况是在
clock_gettime
函数的入口处插入跳转代码 , 跳转到dn_clock_gettime
函数位置 ; - 第二次是构造桩函数 ( 构造拼接桩函数 ) : 在自定义的
dn_clock_gettime
函数中 , 需要调用实际的clock_gettime
函数 , 这里将do_clock_gettime
函数构造成clock_gettime
函数 ;
构造拼接桩函数 : 前 6 字节是保存下来的 clock_gettime
函数的前 6 字节指令 , 执行到第 6 字节时 , 直接跳转到 clock_gettime
函数 执行 , 这样执行拼接的函数 等同于执行 clock_gettime
函数 ;
将 do_clock_gettime
函数构造成 clock_gettime
函数流程 : 执行 do_clock_gettime
方法的第 6 字节的指令时 , 跳转到 clock_gettime
函数的第 6 字节指令位置 , do_clock_gettime
的 0 ~ 6 字节指令是 clock_gettime
实际函数的前 6 字节 , 之所以这么定义 , 是因为 clock_gettime
的前 6 个字节被覆盖为 跳转指令了 ;
调用 do_clock_gettime 方法 , 就相当于调用了 clock_gettime 方法 ;
代码语言:javascript复制 /* 函数插桩 , pApi 是实际函数 , pUser 是插桩后跳转到的拦截函数 */
write_code(pApi, pUser);
/*
执行 size pStub 位置的指令时 , 直接跳转到 size pApi 位置
如 : 执行 do_clock_gettime 方法的第 6 字节的指令时 , 跳转到 clock_gettime 函数的第 6 字节指令位置
do_clock_gettime 的 0 ~ 6 字节指令是 clock_gettime 实际函数的前 6 字节 ,
之所以这么定义 , 是因为 clock_gettime 的前 6 个字节被覆盖为 跳转指令了
调用 do_clock_gettime 方法 , 就相当于调用了 clock_gettime 方法 ;
*/
write_code(size pStub, size pApi);
/* 将复制的 6 字节 代码存放到 pStub 函数中的 0 ~ 6 字节位置 */
memcpy(pStub, code, size);
函数插桩的具体细节在之前的
- 【Android 逆向】函数拦截 ( 修改内存页属性 | x86 架构插桩拦截 )
- 【Android 逆向】函数拦截 ( ARM 架构下的插桩拦截 | 完整代码示例 )
博客中有详细的说明 , 先修改内存页属性 , 然后直接修改内存 , 写入跳转汇编指令对应的二进制机器码数据 ;
代码示例 :
代码语言:javascript复制/*
* unsigned char* pFunc
* unsigned char* pStub
* 上述两个参数分别是两个函数指针
*
* 注意 : 写完之后要刷新 CPU 高速缓存 , 调用 cache_flush 系统调用函数
*/
int write_code(unsigned char* pFunc, unsigned char* pStub) {
/* 获取 pFunc 函数入口 , 先获取该函数所在内存页地址 */
void* pBase = (void*)(0xFFFFF000 & (int)pFunc);
/* 修改整个内存页属性 , 修改为 可读 | 可写 | 可执行 ,
* 避免因为内存访问权限问题导致操作失败
* mprotect 函数只能对整个页内存的属性进行修改
* 每个 内存页 大小都是 4KB
*/
int ret = mprotect(pBase, 0x1000, PROT_WRITE | PROT_READ | PROT_EXEC);
/* 修改内存页属性失败的情况 */
if (ret == -1) {
perror("mprotect:");
return -1;
}
#if defined(__i386__) // arm 情况处理
/* E9 是 JMP 无条件跳转指令 , 后面 4 字节是跳转的地址 */
unsigned char code[] = { 0xE9,0,0,0,0 };
/* 计算 pStub 函数跳转地址 , 目标函数 pStub 地址 - 当前函数 pFunc 地址 - 5
* 跳转指令 跳转的是 偏移量 , 不是绝对地址值
*/
*(unsigned*)(code 1) = pStub - pFunc - 5;
/* 将跳转代码拷贝到 pFunc 地址处 , 这是 pFunc 函数的入口地址 */
memcpy(pFunc, code, sizeof(code));
#else // arm 情况处理
/* B 无条件跳转指令 */
unsigned char code[] = { 0x04,0xF0,0x1F,0xE5,0x00,0x00,0x00,0x00 };
/* arm 的跳转是绝对地址跳转 , 传入 pStub 函数指针即可 */
*(unsigned*)(code 4) = (unsigned)pStub;
/* 将机器码复制到函数开始位置 */
memcpy(pFunc, code, sizeof(code));
#endif
return 0;
}