app加固技术发展

摘录:https://mp.weixin.qq.com/s/j35pPdZyeg_InS9LcPmnkQ

dex保护

代码混淆

  • 混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。ProGuard就是一个混淆代码的开源项目,能够对字节码进行混淆、缩减体积、优化等处理。

动态加载

  • 将需要保护的代码单独编译出来,将其进行加密后在程序运行的过程中对其进行解密,并使用 DexClassLoader 来动态的进行加载

  • DexClassLoader是什么?

    在java环境中,有个概念叫类加载器(ClassLoader),其作用是动态加载class文件,标准的java sdk中,有ClassLoader这个类,可以用来加载想要加载的class文件,每个ClassLoader在初始化的时候必须指定class的路径。

    每个ClassLoader都有一个父ClassLoader,当加载类的时候,子ClassLoader会先请求父ClassLoader去加载class文件,如果父ClassLoader找不到class文件的时候,子ClassLoader才会继续去加载class文件,只是一种安全机制。

    在android中加载的是dex文件,dex事经过优化的class文件。Android中事通过DexClassLoader来加载class的

Native 开发

  • 使用 NDK 编写应用,直接生成 native 代码。

    Java2C,使用 C 语言编写的代码,最后编译成 native 代码更安全。

    为什么更安全,因为 native 代码的二进制分析跟 Java 的二进制分析难度系数不在一个等级上。Java Bytecode 更加利于代码还原,而通过汇编向高级语言的转换需要更多更强大的算法支持,才能得到一个勉强能看的伪代码。

    并且,native 层的保护方式更加的丰富、强大,也更加的受欢迎,所以 Java2C 也是除了 VMP 另一个比较受追捧的硬核保护方式。

    但是 Java2C 的研发难度很高,不仅要涉及很多编译器的相关知识,比如 AST 的转换,还因为是解释性/虚拟机语言与编译型语言的转换,需要关注很多 Java 的特性能否等价转换

Native保护

Obfuscator-LLVM

首先, LLVM 是一套开源的编译器,而 Obfuscator-LLVM 是一个专门为混淆而生的 LLVMOLLVM 通过编写 PASS 来控制中间代码以达到混淆的目的。

安全人员的生活就是如此朴(扑)实(街)无华,拿别人用来做优化的东西,做负优化。

官方版本的 OLLVM 拥有以下三个混淆功能

Bogus Control Flow

翻译为虚假控制流,这个东西可以参见上上上上上上*N 篇文章,借助 IDA Pro,可以完美的消除虚假控制流混淆,这里不再赘述。

值得吐槽的是,编译器辛辛苦苦做的死代码消除,又被一下子就给加回来了一大堆。安全人员果然就是如此的朴(扑)实(街)无华。

Control Flow Flattening

控制流平坦化,这个应该是 OLLVM 中最有挑战性的一项混淆。这个混淆会将原有的控制流进行分割,为每个基本块赋值一个常量 ID,然后通过分发器来决定基本块的真实后继。在 IDA Pro 的伪代码中通常表现为一堆嵌套的 while,看着确实很唬人。

目前公开分析得比较多的反混淆思路是利用符号执行或者仿真执行计算后继,然后再计算汇编进行 Patch。我觉得这个思路基本上是错的,而且效率慢,Patch 难。通过纯静态分析的算法来进行计算才是正道…

控制流平坦化会导致程序运行效率大幅下降,一般只会对关键的重要函数进行混淆。

还是那句话,安全人员果然就是如此的朴实(扑)无华(街)….

Instructions Substitution

指令替换,这个感觉用处不大,就是类似于把 a = b + c 替换成 a = b - (-c)。我觉得这个并没有对逆向分析有多大影响。

HikariObfuscator

除了官方的 OLLVM, 还有第三方修改的版本会有添加一些其他的混淆 PASS,而 Hikari 就是其中比较富有代表性的一个。

Hikari 中不仅包含常规二进制的混淆,还包括针对 iOS 的一些保护,不过这一部分本篇就跳过了。

FunctionCallObfuscate

这个混淆 Pass 是将函数调用指令转换成类似于 Java 中的反射的形态,通过调用 dlopendlsym 函数来进行完成查找函数指针、调用的过程。

好处就是可以消除掉导入表。

FunctionWrapper

函数封装,根据 Wiki 描述,是将函数调用 foo(1) 封装成 DummyA(1)->DummyB(1)->DummyC(1)->foo(1) 的样子。

作用不是很大,有一个好处是让调用的函数不能直观的看到名称。

IndirectBranching

间接跳转,这个也是从古自今用的比较多的一个混淆方法。原理是将原本的立即数跳转转换为寄存器跳转,先将偏移值赋值给寄存器,最后通过寄存器的方式来进行跳转。这会导致很多反汇编器的分析算法无法正确构建 CFG 以及计算函数结尾,导致逆向工具无法正常工作。

StringEncryption

字符串加密,一般是在 .init 段或者 .init_array 段里的函数对字符串进行解密。保证在程序运行之前将字符串解密。

对抗方法也不难,比如可以通过 FRIDA-RPC 远程调用的方法来获取解密后的字符串,然后再对二进制进行 Patch;也可以直接还原算法,解密为明文之后进行 Patch

初代加固

(Dex保护 + 资源保护 + Native保护) = APP加固服务

文件落地加载

何为文件落地加载,就是需要先解密文件,然后写入到另外一个文件当中,然后再调用 DexClassLoader 或者其他加载函数来加载解密后的文件,这里不讨论具体的实现方案,网上就有很多开源的项目。

那么这个缺点就很明显了,既然涉及到文件操作,那完全可以通过动态调试的方式,将解密后的文件截取下来

不落地加载

文件操作太明显,于是整出了一套可以在内存中解密并且直接从内存里加载的方案。方法是调用 libdvm.so 或者 libart.so 等库中的一些私有函数,封装一个自定义的加载器。与初初代主要的不同基本上仅是调用的函数不一样。

但是这同样防不住动态分析,只要内存漫游搜索文件头 dex035,或者在加载时的函数打 hook 、断点照样可以找到解密数据在内存中的指针,然后 Dump 之。

抹掉dex035

加载之后在将内存中的 dex035 抹掉。

这个仅仅是为了防止内存漫游搜索文件头,依然防不住打 Hook 或断点。

初代的保护真正的问题在于,代码数据总是结构完整的存储在一段内存里面,这是一个致命的弱点,一旦反注入、反调试等措施被破解,这个保护就相当于是已经失败了。

于是就有了第二代保护。

二代加固

二代保护谓之代码抽取,核心竞争力在于:真正的代码数据并不与 Dex 的结构数据存储在一起,就算 Dex 被完整的扒下来,也无法看到真正的代码

主动加载 - DexHunter

Dexhunter 是所有二代壳脱壳机的鼻祖,原理是通过主动加载 Dex 中的所有类,然后 Dump 所有方法对应代码区的数据,并将其重建到被抽取之后的 Dex 之中。

此类主动加载脱壳机大概的流程是:

1
遍历Dex中的所有类 -> 模拟加载类的流程(例如调用 dvmFindClass 等系列函数) -> 解析内存中的数据 -> 在 Dex 文件中填

主动调用 - FUPK3\FART

为了对抗 DexHunter, 有的代码抽取方案已经不再类加载时还原代码了,而是在比 DexHunter 更后面的某个时机。因为可以做代码还原的点比较多,所以采用主动调用的方案,可以完全规避掉时机的问题。

原理是对执行方法的入口函数进行插桩,在这个地方判断是否带有主动调用的标志,若属于主动调用则 Dump CodeItem 的数据,然后在进行 Dex 重建。而主动调用放在比较顶层的地方,这样就可以覆盖所有代码还原的时机。

这个方案虽然理论上也可以通过注入和 Hook 来做,但是需要插桩的函数以及一些需要调用的函数有可能没有导出,所以会比较麻烦。

三代加固

DEX 虚拟机保护

DEX 虚拟机保护 == DEX Virtual Machine Protect == DEX VMP

VMP 这个东西源自 PC 平台,DEX VMP 的原理是运行一个定制的解释器来跑经过保护的代码指令。

类似于自己编译一个 dalvik 解释器在 native 中运行,代码执行脱离系统依赖,就算完整 dump 下来也看不懂,唯一的破解方法就是逆向解释器。

当然,理想是好的。一开始的 DEX VMP 可能因为兼容问题或者成本问题,很多都不是真正意义上的虚拟机保护,而是指令替换,约等于把 dalvik 解释器扒下来,改一改 opcode,做个映射就可以了。不过即使只到这种程度,也已经有一定的难度了,想要做到一键破解并不容易。