本文情节纯属虚构,若有雷同,纯属你想多了。
争春
一键 (Dex保护 + 资源保护 + Native保护) = APP加固服务
当技术开始成熟,再加上逐渐旺盛的需求,那么技术变现将变得水到渠成。
2010 年,梆梆安全成立,这可能是国内最早一家移动安全服务商。
2013 年,爱加密加入战场。
2014 年,娜迦也推出加固产品。
资本力量向来对赚钱的事情都很敏锐,巨头们也希望能抢占到一席之地,360、腾讯、百度、阿里、网易等都纷纷推出了自己的保护产品。
后来者还有顶象,以及很多因为没能发展起来而记不住名字的企业。
13 年至 18 年间,Android 保护的市场犹如战国时代的战场一般,猛烈厮杀,割据地盘。
蕃夏
既然要打仗,那手里必然得持有武器。
毕竟枪杆子里出地盘,谁的黑科技牛逼谁就能多吹几页 PPT,PPT 牛逼了销量就来了,有人买帐就有钱了,有钱了就能招人研究黑科技了,谁的黑科技牛逼谁就能多吹几页 PPT,PPT 牛逼了销量就来了...
......
抢地盘归抢地盘,不过也不耽误研究新的保护技术,于是就发展了好几代保护,值得注意的是,加固一般都是多代保护并行使用,并且持续挖掘每一代保护的潜力,比如至今还有人在研究畸形的初代保护方法。
初代
初代加固的原理,在上一篇已经提到过了,核心就是 DEX 整体加密然后动态加载,这个老掉牙的技术想必无需多加赘述。
不过其中也有部分区别,可以来细分一下。
初初代 - 文件落地加载
何为文件落地加载,就是需要先解密文件,然后写入到另外一个文件当中,然后再调用 DexClassLoader
或者其他加载函数来加载解密后的文件,这里不讨论具体的实现方案,网上就有很多开源的项目。
那么这个缺点就很明显了,既然涉及到文件操作,那完全可以通过动态调试的方式,将解密后的文件截取下来,例如:通过 Hook
截取类似 fwrite
函数的数据;或者屏蔽 unlink
的操作,让解密文件删除失败。诸如此类的方法,太多。
初二代 - 不落地加载
文件操作太明显,于是整出了一套可以在内存中解密并且直接从内存里加载的方案。方法是调用 libdvm.so
或者 libart.so
等库中的一些私有函数,封装一个自定义的加载器。与初初代主要的不同基本上仅是调用的函数不一样,这个网上同样也有很多开源方案可以参考。
但是这同样防不住动态分析,只要内存漫游搜索文件头 dex035
,或者在加载时的函数打 hook
、断点照样可以找到解密数据在内存中的指针,然后 Dump 之。
初三代 - 抹头
加载之后在将内存中的 dex035
抹掉。
这个仅仅是为了防止内存漫游搜索文件头,依然防不住打 Hook
或断点。
不过这个也是有一些方法可以破解的,具体的方法在我的一个仓库 FRIDA-DEXDump
里有体现,实测确实可以对付一些抹头的保护。
初代的保护真正的问题在于,代码数据总是结构完整的存储在一段内存里面,这是一个致命的弱点,一旦反注入、反调试等措施被破解,这个保护就相当于是已经失败了。
于是就有了第二代保护
二代
世界上本没有路,走的人多了就成了路。走一条路不算完,得走出一片新大陆。
当发现利用私有函数可以实现很多黑科技时,人们就开始挖掘他的潜力。
二代保护谓之代码抽取
,核心竞争力在于:真正的代码数据并不与 Dex
的结构数据存储在一起,就算 Dex
被完整的扒下来,也无法看到真正的代码,简单来说就是:你能得到她的身体,可得不到她的心,甚至你扒下她的衣服来,还会发现是个女装大佬。
利用私有函数,通过 Hook
来拦截方法在被调用时的路径,在被真正调用之前,将代码数据填充到对应的代码区里。
这个保护真正杜绝的了一代保护的致命缺陷,同时也宣告手工脱壳的时代结束了。如果有人说要不依靠脚本,全手工单步调试来还原一个代码抽取的保护,那他八成是疯了。
代码抽取虽好,但也带来了一定的兼容性以及性能影响。所以一般来说,正常 APP 是不会将所有代码都进行抽取的,特别是无关紧要的第三方库的代码。
但代码抽取保护也不是无懈可击,目前也有多种方案可以进行脱壳:
主动加载 - DexHunter
Dexhunter
是所有二代壳脱壳机的鼻祖,原理是通过主动加载 Dex
中的所有类,然后 Dump
所有方法对应代码区的数据,并将其重建到被抽取之后的 Dex
之中。
此类主动加载脱壳机大概的流程是:
遍历Dex中的所有类 -> 模拟加载类的流程(例如调用 dvmFindClass 等系列函数) -> 解析内存中的数据 -> 在 Dex 文件中填充数据或者重建结构。
DexHunter
采用直接修改 dvm
或者 art
虚拟机的形式,编译起来较为繁琐,不过根据原理,依赖注入技术也是完全可以实现同样功能。
主动调用 - FUPK3FART
为了对抗 DexHunter
, 有的代码抽取方案已经不再类加载时还原代码了,而是在比 DexHunter
更后面的某个时机。因为可以做代码还原的点比较多,所以采用主动调用的方案,可以完全规避掉时机的问题。
原理是对执行方法的入口函数进行插桩,在这个地方判断是否带有主动调用的标志,若属于主动调用则 Dump CodeItem
的数据,然后在进行 Dex 重建。而主动调用放在比较顶层的地方,这样就可以覆盖所有代码还原的时机。
这个方案虽然理论上也可以通过注入和 Hook 来做,但是需要插桩的函数以及一些需要调用的函数有可能没有导出,所以会比较麻烦。
Anti?
然鹅在 2020 年,代码抽取却依然是加固厂商主流的保护方式,针对上述的方案,虽然没办法从根源上解决问题,但是加固厂商也偷偷留了很多坑来对付叛逆性百万脚本小子,列举俩点:
1. 愿者上钩: 埋一个虚假的钓鱼类,一旦这个类被加载则退出。
2. 面壁者: 监控本进程的文件操作,一旦用文件写出包含 dex035 的数据, kill self.
3. ......
于是乎,二代保护的难度正在逐年下降,因为这些缓解之策,只能对付一些只会用现成工具的人。于是乎,更加硬核的保护方式,来了。
DEX 虚拟机保护
DEX 虚拟机保护 == DEX Virtual Machine Protect == DEX VMP
VMP
这个东西源自 PC 平台,DEX VMP
的原理是运行一个定制的解释器来跑经过保护的代码指令。
类似于自己编译一个 dalvik
解释器在 native
中运行,代码执行脱离系统依赖,就算完整 dump
下来也看不懂,唯一的破解方法就是逆向解释器。
当然,理想是好的。一开始的 DEX VMP
可能因为兼容问题或者成本问题,很多都不是真正意义上的虚拟机保护,而是指令替换,约等于把 dalvik
解释器扒下来,改一改 opcode
,做个映射就可以了。不过即使只到这种程度,也已经有一定的难度了,想要做到一键破解并不容易。
至于现在有没有真正意义上的从 0 设计的虚拟机,我并不太清楚。
Java2C
还记得上一篇里提到的最原始的代码保护方法吗,有一条就是使用 NDK 编写应用。
因为使用 C 语言编写的代码,最后编译成 native
代码更安全。
为什么更安全,因为 native
代码的二进制分析跟 Java
的二进制分析难度系数不在一个等级上。Java Bytecode
更加利于代码还原,而通过汇编向高级语言的转换需要更多更强大的算法支持,才能得到一个勉强能看的伪代码。
并且,native
层的保护方式更加的丰富、强大,也更加的受欢迎,所以 Java2C
也是除了 VMP
另一个比较受追捧的硬核保护方式。
但是 Java2C
的研发难度很高,不仅要涉及很多编译器的相关知识,比如 AST
的转换,还因为是解释性/虚拟机语言与编译型语言的转换,需要关注很多 Java
的特性能否等价转换。
目前有能力提供这个保护服务的,似乎只有两家:360、顶象,但不知效果如何。
Native 保护
说了那么多 DEX 保护,总算来到移动安全真正的核(绝)心(对)领域了。对 Native 代码的保护,才是移动安全真正的分水岭。其实从上面 DEX
几代保护就可以看出来,Java
代码都是不安全的,最后还是得靠 native
老大哥罩着。
反注入、反调试、反 Hook 也都算是 Native
保护中的一部分,然鹅,这不是本篇的重点,因为已经在上上上上上*999 篇讲过了,本篇说说 Native
的代码保护。
关于 Native
的代码保护,可能几百页论文也讲不完,因为这个领域已经发展了几十年,所以我们就挑移动安全上最热门的 Ollvm
简单讲讲。
Obfuscator-LLVM
首先, LLVM
是一套开源的编译器,而 Obfuscator-LLVM
是一个专门为混淆而生的 LLVM
。OLLVM
通过编写 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
中的反射的形态,通过调用 dlopen
、dlsym
函数来进行完成查找函数指针、调用的过程。
好处就是可以消除掉导入表。
FunctionWrapper
函数封装,根据 Wiki 描述,是将函数调用 foo(1)
封装成 DummyA(1)->DummyB(1)->DummyC(1)->foo(1)
的样子。
作用不是很大,有一个好处是让调用的函数不能直观的看到名称。
IndirectBranching
间接跳转,这个也是从古自今用的比较多的一个混淆方法。原理是将原本的立即数跳转转换为寄存器跳转,先将偏移值赋值给寄存器,最后通过寄存器的方式来进行跳转。这会导致很多反汇编器的分析算法无法正确构建 CFG
以及计算函数结尾,导致逆向工具无法正常工作。
StringEncryption
字符串加密,一般是在 .init
段或者 .init_array
段里的函数对字符串进行解密。保证在程序运行之前将字符串解密。
对抗方法也不难,比如可以通过 FRIDA-RPC
远程调用的方法来获取解密后的字符串,然后再对二进制进行 Patch
;也可以直接还原算法,解密为明文之后进行 Patch
。
丰秋
到了现在,APP保护的需求虽然肯定还是持续增长,但天下局势已定,该分地盘的分地盘,该倒闭的倒闭,该转型的转型,该扩张的也扩张了。
秋天到了,开始进入收获的季节,农民工们挥起长长的镰刀,收割着一棵又一棵的金黄韭菜;脸颊上流淌的一滴滴汗水,反照着社会主义核心价值观的耀耀红光。
战事逐渐稳定,大国边疆偶尔发生小摩擦但不再起战事,甚是联姻之事都时有发生;而小国则是各自独守一隅,佛系度日。
安居乐业,兴兴向荣,天下大同,在家隔离,世界和平。
寒冬
略。
前传
系列有续的话应该不再会有如此诡异的文风。
有关阅读
其他阅读
原文始发于微信公众号(秃头的逆向痴想):Android App 保护那些事儿 (二)
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论