【Android 逆向】函数拦截实例 ( ③ 刷新 CPU 高速缓存 | ④ 处理拦截函数 | ⑤ 返回特定结果 )

简介: 【Android 逆向】函数拦截实例 ( ③ 刷新 CPU 高速缓存 | ④ 处理拦截函数 | ⑤ 返回特定结果 )

文章目录

前言

一、刷新 CPU 高速缓存

二、处理拦截函数

1、桩函数

2、处理拦截函数

三、返回特定结果

四、相关完整代码

前言

【Android 逆向】函数拦截实例 ( 函数拦截流程 | ① 定位动态库及函数位置 ) 博客中简单介绍了 hook 函数 ( 函数拦截 ) 的流程 , 本系列博客介绍函数拦截实例 ;


拦截 clock_gettime 函数 ;


#include <time.h>
int clock_gettime(clockid_t clk_id,struct timespec *tp);



【Android 逆向】函数拦截实例 ( ② 插桩操作 | 保存实际函数入口 6 字节数据 | 在插桩的函数入口写入跳转指令 | 构造拼接桩函数 ) 博客中进行了插桩操作 ,






一、刷新 CPU 高速缓存


执行 cache_flush 系统调用函数 刷新 CPU 的高速缓存 ; 该步骤 只在 ARM 架构的 CPU 中执行 , x86 架构的 CPU 不需要刷新缓存 ;


x86 不需要执行刷新缓存操作 , 但也可以执行系统调用操作 syscall 来刷新缓存 ;



刷新 CPU 高速缓存 代码示例 : pApi 是实际调用的函数指针 , size 是 6 字节 , 也就是说刷新 (int)pApi 地址到 (int)pApi + size 之间 6 66 字节对应的 CPU 高速缓存即可 ;


/* 清空 CPU 高速缓存 */
#if !defined(__i386__)
  /* 在 arm 架构中必须刷新 CPU 高速缓存 , x86 不需要执行 */
  cacheflush((int)pApi, (int)pApi + size, ICACHE|DCACHE);
#else
  /* x86 下可以执行该系统调用 */
  syscall(0xF002, (int)pApi,(int)pApi + sizeE);






二、处理拦截函数



1、桩函数


在 【Android 逆向】函数拦截实例 ( ② 插桩操作 | 保存实际函数入口 6 字节数据 | 在插桩的函数入口写入跳转指令 | 构造拼接桩函数 ) 三、在插桩的函数入口写入跳转指令 | 构造拼接桩函数 博客章节 , 介绍了拼接装函数 do_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 个字节被覆盖为 跳转指令了 ;



2、处理拦截函数


处理拦截函数 :


当函数执行到 clock_gettime 之后 , 就会执行插入的跳转指令 , 跳转到 dn_clock_gettime 函数中 ;


在该函数中 , 可以调用 do_clock_gettime 函数 , 执行原有的指令 ;


do_clock_gettime 函数执行前后 , 都可以插入自己的业务逻辑 , 监控或修改都可以 ;



处理拦截函数 代码示例 :


/* 拦截函数 , 拦截 clock_gettime 函数后 , 跳转到此处 */
int dn_clock_gettime(clockid_t id, struct timespec* ts) {
  /*if (ts == NULL) {
  return -1;
  }*/
  /* 如果设备实现了系统调用 , 可以通过该代码调用原有的 clock_gettime 函数 */
  //int ret = syscall(__NR_clock_gettime, id, ts);
  /* 
  此处实际上调用的是原有的 clock_gettime 函数 
  如果设备上没有实现系统调用 , 使用如下方法可以调用原有的 clock_gettime 函数 
  */
  do_clock_gettime(id, ts);
  /* clock_gettime 函数执行完后 , 继续执行一些自己实现的部分 */
  /* 
  上面的代码是 hook 住的真实代码 
  我们可以在真实代码 前面 / 后面 执行一些自定义内容 
  */
  if (id > CLOCK_MONOTONIC)return 0;
  if (clock_base[id] == 0.0) {
  clock_base[id] = ts->tv_sec * 1000000000.0 + ts->tv_nsec;
  clock_new[id] = clock_base[id];
  }
  else {
  //mutex.lock();
  double tick = ts->tv_sec * 1000000000.0 + ts->tv_nsec;
  //printf("tick : %f base: %f delta: %f\n", tick, clock_base[id], tick - clock_base[id]);
  if (tick > clock_base[id]) {
    clock_new[id] += (tick - clock_base[id]) * time_scale;
    ts->tv_sec = (time_t)(clock_new[id] / 1000000000.0);
    ts->tv_nsec = (long)(fmod(clock_new[id], 1000000000.0));
    clock_base[id] = tick;
  }
  //mutex.unlock();
  }
  return 0;
}






三、返回特定结果


执行上述 dn_clock_gettime 函数的返回值 , 就是最终的返回结果 ;






四、相关完整代码


下面是相关代码 , 只是逆向代码中的函数拦截部分代码 :


调用代码 :


/* 这是 hook 标准库中的 clock_gettime 函数的入口方法 , 跳转到自定义的 dn_clock_gettime 方法中 */
hook_func((uint8_t*)clock_gettime, (uint8_t*)dn_clock_gettime, (uint8_t*)do_clock_gettime, 6);


函数拦截代码 :


/* hook 函数的完整流程 , 跳转指令 size 是 6 字节*/
/* 这是 hook 标准库中的 clock_gettime 函数的入口方法 , 跳转到自定义的 dn_clock_gettime 方法中 */
/* hook_func((uint8_t*)clock_gettime, (uint8_t*)dn_clock_gettime, (uint8_t*)do_clock_gettime, 6); */
void hook_func(uint8_t* pApi, uint8_t* pUser, uint8_t* pStub, size_t size)
{
  unsigned char code[64] = { 0 };
  /* 插桩前先保存函数的入口 6 字节数据 , 因为之后插桩 , 
  * 会使用跳转代码 0xE9,0,0,0,0 覆盖函数入口内存
  * 该函数最终还是要执行 , 需要拷贝一下 , 供之后实际函数调用使用 
  */
  memcpy(code, pApi, size);
  /* 函数插桩 , 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 方法 , 就相当于调用了
  */
  write_code(size + pStub, size + pApi);
  /* 将复制的 6 字节 代码存放到 pStub 函数中的 0 ~ 6 字节位置 */
  memcpy(pStub, code, size);
  /* 清空 CPU 高速缓存 */
#if !defined(__i386__)
  /* 在 arm 架构中必须刷新 CPU 高速缓存 , x86 不需要执行 */
  cacheflush((int)pApi, (int)pApi + size, ICACHE|DCACHE);
#else
  /* x86 下可以执行该系统调用 */
  syscall(0xF002, (int)pApi,(int)pApi + sizeE);
#endif
}
/*
 * 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;
}
/* 拦截函数 , 拦截 clock_gettime 函数后 , 跳转到此处 */
int dn_clock_gettime(clockid_t id, struct timespec* ts) {
  /*if (ts == NULL) {
  return -1;
  }*/
  /* 如果设备实现了系统调用 , 可以通过该代码调用原有的 clock_gettime 函数 */
  //int ret = syscall(__NR_clock_gettime, id, ts);
  /* 
  此处实际上调用的是原有的 clock_gettime 函数 
  如果设备上没有实现系统调用 , 使用如下方法可以调用原有的 clock_gettime 函数 
  */
  do_clock_gettime(id, ts);
  /* clock_gettime 函数执行完后 , 继续执行一些自己实现的部分 */
  /* 
  上面的代码是 hook 住的真实代码 
  我们可以在真实代码 前面 / 后面 执行一些自定义内容 
  */
  if (id > CLOCK_MONOTONIC)return 0;
  if (clock_base[id] == 0.0) {
  clock_base[id] = ts->tv_sec * 1000000000.0 + ts->tv_nsec;
  clock_new[id] = clock_base[id];
  }
  else {
  //mutex.lock();
  double tick = ts->tv_sec * 1000000000.0 + ts->tv_nsec;
  //printf("tick : %f base: %f delta: %f\n", tick, clock_base[id], tick - clock_base[id]);
  if (tick > clock_base[id]) {
    clock_new[id] += (tick - clock_base[id]) * time_scale;
    ts->tv_sec = (time_t)(clock_new[id] / 1000000000.0);
    ts->tv_nsec = (long)(fmod(clock_new[id], 1000000000.0));
    clock_base[id] = tick;
  }
  //mutex.unlock();
  }
  return 0;
}
int do_clock_gettime(clockid_t which_clock, struct timespec* tp)
{//未使用,内核中该函数实际的代码,反汇编libc.so得到
  /*
  这些指令都不重要 
  都会被覆盖调 , 写的这些指令主要是占坑用的 
  实际上调用的是 clock_gettime 函数 
  下面的汇编代码都会被覆盖为 跳转代码 , 跳转到 clock_gettime 函数 , 
  注意 , clock_gettime 函数 的前 6 字节的指令会被拷贝到函数入口 , 
  执行第 6 字节位置时 , 跳转到 clock_gettime 函数 的第 6 字节位置
  */
  //return syscall(__NR_clock_gettime, which_clock, tp);
  __asm__ __volatile__("push %%ebx"::: "ebx");
  __asm__ __volatile__("push %%ecx\n"::: "ecx");
  __asm__ __volatile__("mov 12(%%esp),%%ebx"::: "ebx");
  __asm__ __volatile__("mov 16(%%esp),%%ecx"::: "ecx");
  __asm__ __volatile__("mov $0x109,%%eax"::: "eax");
  __asm__ __volatile__("int $0x80");
  __asm__ __volatile__("pop %%ecx":::"ecx");
  __asm__ __volatile__("pop %%ebx":::"ebx");
  __asm__ __volatile__("retn");
}


目录
相关文章
|
4月前
|
缓存 安全 Android开发
Android经典实战之用Kotlin泛型实现键值对缓存
本文介绍了Kotlin中泛型的基础知识与实际应用。泛型能提升代码的重用性、类型安全及可读性。文中详细解释了泛型的基本语法、泛型函数、泛型约束以及协变和逆变的概念,并通过一个数据缓存系统的实例展示了泛型的强大功能。
45 2
|
2月前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
80 15
Android 系统缓存扫描与清理方法分析
|
3月前
|
存储 缓存 Android开发
Android RecyclerView 缓存机制深度解析与面试题
本文首发于公众号“AntDream”,详细解析了 `RecyclerView` 的缓存机制,包括多级缓存的原理与流程,并提供了常见面试题及答案。通过本文,你将深入了解 `RecyclerView` 的高性能秘诀,提升列表和网格的开发技能。
77 8
|
4月前
|
Java 调度 Android开发
Android经典实战之Kotlin的delay函数和Java中的Thread.sleep有什么不同?
本文介绍了 Kotlin 中的 `delay` 函数与 Java 中 `Thread.sleep` 方法的区别。两者均可暂停代码执行,但 `delay` 适用于协程,非阻塞且高效;`Thread.sleep` 则阻塞当前线程。理解这些差异有助于提高程序效率与可读性。
85 1
|
4月前
|
Android开发 开发者
Android、Flutter为不同的CPU架构包打包APK(v7a、v8a、x86)
Android、Flutter为不同的CPU架构包打包APK(v7a、v8a、x86)
338 1
|
4月前
|
弹性计算 固态存储 ice
阿里云服务器2核16G、4核32G、8核64G配置不同ECS实例规格收费标准和CPU性能差异
2024年阿里云提供2核16G、4核32G及8核64G等多种服务器配置,用户可根据需求选择不同实例规格如内存型r8i、通用算力型u1等。以华北2(北京)为例,2核16G月费从286.2至385.99元不等;4核32G为572.4至771.97元;8核64G则在1144.8至1543.94元区间。公网带宽与系统盘(如ESSD云盘)亦有多样化选择与价格方案。长期租赁可享折扣,具体价格请访问阿里云官网确认。
170 7
|
5月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
62 8
|
5月前
|
存储 弹性计算 固态存储
阿里云服务器CPU内存配置怎么选?ECS实例规格有啥区别?
阿里云服务器配置选择需考虑ECS实例规格、CPU内存、公网带宽与系统盘。个人开发者或中小企业推荐轻量应用服务器或ECS经济型e实例(2核2G3M带宽,99元/年),适合搭建低流量网站。企业用户应选择企业级独享型如通用算力型u1、计算型c7或通用型g7实例,至少2核4G内存起,推荐5M公网带宽以平衡成本与性能。系统盘推荐ESSD云盘以获得更好的性能。更多详情及链接参见原文。
122 3
|
5月前
|
NoSQL Redis 开发工具
Redis性能优化问题之检查 Redis 实例是否启用了透明大页机制,如何解决
Redis性能优化问题之检查 Redis 实例是否启用了透明大页机制,如何解决
|
4月前
|
开发框架 JavaScript Java
【Azure 应用服务】Azure App Service多实例中,出现某一个实例CPU居高不下的情况,如何重启单个实例呢?
【Azure 应用服务】Azure App Service多实例中,出现某一个实例CPU居高不下的情况,如何重启单个实例呢?