android study 10 so load

Author Avatar
Xzhah 7月 09, 2022
  • 在其它设备中阅读本文章

android so加固

1. so加固简介

​ 因为so逆向的难度比dex逆向难度更大,所以开发者常常会把核心算法(加解密,协议)放在native实现。这时候就需要对so进行加固保护。

​ so加固分为有源so加固和无源so加固。有源的so加固:自解密,混淆,源码VMP等。无源保护则是对so加壳,vmp保护等。

​ 有源保护中的自解密就是先把要加密的函数给加密了,然后加载so文件,在运行的时候把加密的数据给解密。还有一种比较常见的是ollvm,通过指令膨胀,虚假指令,控制流平坦化等方式可以混淆代码达到保护的目的。而vmp就是自己写一个解释器,对自定义的指令进行解释执行,不过对性能影响较大。

​ 无源码保护的加壳,其核心是自定义linker。在壳被执行后还原出原来的so文件,最后把控制流交还给被保护的so文件,该方法成本低,对性能影响小,但是也相对容易攻破。

2. so加载原理

​ so有执行视图和链接视图。链接视图以Section为单位访问各段数据,编译链接过程会用section信息去读取相关数据。执行视图则是程序加载执行过程中访问的方式。so加固则是根据so执行流程以及所依赖的数据结构进行拆解,加密和重组的。

​ so加载流程如下:

​ 1)dl_open会调用find_library -> find_loaded_library_by_soname,从内存中查找已经加载的so文件,如果有已经加载的直接返回handler。

​ 2)如果没有已加载的so,则执行加载,并且新建soinfo结构体,将so加载到内存中。

​ 3)执行预加载prelink_image,读取so文件中dynamic字段的内容,初始化重定位需要的数据。

​ 4) 执行link_image,修复内存中的数据,完成重定位,调用call_constructors构造函数

​ 5) 执行so的init和init_array中的函数

​ 从以上流程可以知道,在重定位完成以前,so中的代码是不会被执行的,代码都在linker中执行。以及so中最先执行的函数是init, 其次是init_array。所以有很多加固会利用init,init_array中的函数来进行解密。或者利用JNI_OnLoad函数,但这种方式只适用于Android下特殊的so。

3.so的加固

​ UPX加固方法:只加固代码段。数据段和重定位的相关结构都保留在文件中,利用插入的init节对代码进行解密。本质上和Section加密类似,只是放大了加密范围,并添加修改init节的操作。

​ 自实现linker方法:重定位过程需要自己实现,带来的好处是可以破坏so的结构或者自定义so的结构。

4.ollvm混淆

​ llvm分为三段式:Frontend、Optimizer、backend。前端负责词法分析,语法分析,语义分析,生成中间代码等。然后进入optimizer优化环节,比如死代码去除,编译器内联等操作。最后就是bankend后端环节,核心是根据中间语言ir生成和CPU匹配的机器码。

​ 混淆这个操作就是在optimizer这个环节进行的:

​ 1. 字符串加密,逐行扫面ir中的字符串,对这些字符串进行逐字节异或。

2. 指令膨胀,会把简单的指令膨胀为功能相同,但是逻辑上更复杂的指令。我看网上对这个反混淆的思路有人把二进制反过来提升为llvm ir指令,然后用llvm自己的优化把这种指令膨胀的混淆和不可达路径给干掉。。。
 3. 虚假控制流,加入额外的条件跳转和不可达基本块,达到干扰控制流分析的目的。解决思路的话,可以考虑用符号执行/模拟执行遍历出所有的真实块,或者对跳转条件做分析?把恒定跳转的分支给定下来
 4. 控制流平坦化,ollvm会添加一个用于控制跳转的状态变量和分发器,当真实块执行了过后就更新这个状态变量。然后分发器根据这个状态变量跳转到下一个真实块。如果原本的执行流程中存在条件跳转,则会在各条件下对状态变量设置不同的值,再回到分发器进行检查和跳转。就相当于做了一个大范围的switch。

4.2 去控制流平坦化

​ 首先我的观点是,ollvm的混淆其实都没啥很好的办法可以通用的自动化去混淆。原因是混淆指令和开发者指令之间没有明显边界,特别是编译器优化过后(比如把大switch优化成二分查找的if), 真的要调试还不如用脚本给所有真实块第一条指令下个断点来调试。

​ 网上的帖子用符号执行/模拟执行来自动去控制流平坦化,其实大多都是需要对着样本特地写的,没法泛化使用,核心思想就是1)通过规则识别真实块,2)获取真实块之间的前后关系,3)patch二进制程序,重新连边。

识别真实块

1)函数的开始地址为序言的地址
2)序言的后继为主分发器
3)后继为主分发器的块为预处理器
4)后继为预处理器的块为真实块
5)无后继的块为retn块
6)剩下的为无用块

识别真实块前后关系

​ 主要需要处理的就是分支语句,这里就是把cmov涉及的状态设为true/false,分别获取两次的分支地址。

patch程序

​ 把无用块置为nop,没有分支的则直接jmp导向下一个真实块,分支的则把cmovz这样的改为jz,然后在后面新增jmp指向另一分支。

references

https://f5.pm/go-87511.html