首页
社区
课程
招聘
[原创]对Android加固的学习与总结
发表于: 2026-7-13 20:48 12872

[原创]对Android加固的学习与总结

2026-7-13 20:48
12872

​ 以下是个人搜索资料修改以及使用ai辅助总结的学习笔记,本人小白一枚,如有错误,还请指正,感谢各位读者观看。

原理:在应用打包阶段,将原始的classes.dex文件用加密算法进行整体加密。

加固流程

​ 1)打包新APK:将加密后的DEX文件与一个用于解密的“壳程序”打包成新的APK。

​ 2)壳程序解密:应用启动时,壳程序首先获得控制权,在内存中将加密的DEX文件解密还原。

​ 3)类加载器加载:最后通过自定义的类加载器(DexClassLoaderInMemoryDexClassLoader)将解密后的DEX加载到内存中供程序运行。

加载方式

​ 1)落地加载 (DexClassLoader):解密后的DEX文件会先以文件形式临时存放到设备的私有目录中,然后再通过DexClassLoader加载。

​ 2)不落地加载 (InMemoryDexClassLoader):解密后的DEX文件不会写入磁盘,而是直接在内存中完成加载。

特征:当DEX被解密并加载到内存后,其数据是完整且连续的内存块。

原理:将DEX文件中每个方法的具体实现(Code Item)抽走并加密存储,在原位置置空。

加固流程

​ 1)解析定位:加固厂商在加壳阶段,会解析原始DEX文件,根据类名、方法名和签名等信息,定位到每个方法的指令集起始位置和长度。

​ 2)抽取填充:找到后,使用空指令或全0数据填充到原位置,将真正的字节码抽走并加密保存。
​ 3)修正校验:最后,重新计算并修正DEX文件的校验和与签名。

​ 4)静态结果:在静态分析的DEX文件里,关键方法都变成了空函数。

加载方式:第一次运行某个被抽空的方法时,壳的代码会介入拦截并调用系统加载方法的函数。在方法真正执行前,壳解密出对应的原始字节码,并临时填回内存中该方法对应的区域。填充完成后,虚拟机执行该方法。执行完毕后,再次将字节码抽走,使其在内存中恢复为空状态。

特征

​ 1)内存中无完整DEX。

​ 2)被抽取的代码在内存中以碎片形式存在,仅在方法被调用的短暂瞬间才完整出现。

原理:用一套厂商自研的、非标准的“虚拟机”来替代Android原生虚拟机(Dalvik/ART)执行代码,从“语言”层面进行替换字节码。

加固流程

​ 1)指令抽取与转换:在加壳阶段,将DEX文件中受保护方法的原始Dalvik字节码抽取出来。

​ 2)自定义指令集:将这些标准字节码,按照厂商自定义的规则,转换(或映射)成一套只有自己“虚拟机”才能识别的自定义指令集。

​ 3)原始指令销毁:原始的Dalvik字节码被彻底销毁,不再存在于DEX文件中。

​ 4)内置解释器:在APK的Native层(.so文件)内置一个对应的“解释器”(Handler)。当应用运行到被保护的方法时,会进入这个解释器,由它来读取并执行那些自定义指令。

加载方式:执行自定义指令时,解释器会通过一个巨大的switch-case结构来解析每一条指令的操作码(opcode)和操作数(operand),然后执行相应的处理函数。对于加减乘除等简单逻辑,解释器可直接处理;而对于invokeiget等需要与系统交互的复杂指令,则需通过JNI调用Android原生接口来完成。

特征

​ 1)代码彻底“消失”,在classes.dex中,被保护的方法体被清空,取而代之的是一些无意义的桩代码或Native函数声明。

​ 2)所有函数共享解释器,对于VMP保护的函数,它们的执行都通过同一个解释器,导致其注册地址(在内存中的入口点)非常相似,函数逻辑也高度雷同

原理:将Java/Kotlin编写的核心代码,在编译成APK之前,就提前“编译”成C/C++代码,然后再编译成Native层的.so库文件。其灵感部分来源于Google的DEX2OAT技术(Android运行时将DEX预编译为机器码)。加固厂商将这个“编译”过程提前到了加固阶段。

加固流程

​ 1)代码解析:对DEX文件中的Java字节码进行词法和句法分析。

​ 2)代码转换:将分析后的逻辑等价地转换为C/C++代码。

​ 3)编译成SO:将生成的C/C++代码通过NDK编译成不可读的Native机器码(.so文件)。

特征

​ 1)核心逻辑从Java层完全转移到了Native层。

​ 2)每个函数独立编译。不同于VMP的共享解释器,Java2C为每个被保护的函数都生成了独立的、不同的Native代码。因此,每个函数的注册地址和内部逻辑都是独一无二的

​ 3)不可逆。从编译原理上讲,从C/C++代码到机器码的编译过程是不可逆的,因此从Native代码还原出原始的Java逻辑几乎不可能。

对抗思路:内存DUMP,在DEX被解密并加载到内存的瞬间进行Dump。

常用手段

​ 1)Hook脱壳法:使用Frida或Xposed等框架,Hook与DEX加载相关的关键函数(如dvmDexFileOpenPartialClassLoader.loadClass)。

​ 2)内存搜索Dump法:在App运行时,通过分析进程的内存映射(/proc/[pid]/maps),搜索DEX文件特有的文件头魔数(如dex.035),定位DEX在内存中的位置后进行Dump。

​ 3)动态调试脱壳法:使用IDA Pro等调试器附加到App进程,在dvmDexFileOpenPartial等函数下断点,当断点触发时,通过脚本Dump出内存中的DEX数据。

​ 4)缓存/定制系统脱壳法:对于落地加载的壳,直接从/data/dalvik-cache目录获取优化后的odex文件;或修改Android系统源码,在系统加载DEX的关键函数中加入Dump逻辑。

一代壳的Dump点

​ 1)Android版本

​ 2)厂商

​ 不同加固厂商有自己的“个性化”Dump点,他们会通过各种技术来对抗通用脱壳方法,因此需要针对性地寻找Dump点。

1. 360加固

Dalvik环境下的特殊性:在Dalvik环境下,360加固可能并未调用系统标准的dvmDexFileOpenPartial接口,而是自己实现了从内存中加载DEX的代码,因此难以通过Hook标准系统函数找到Dump点。

ART环境下的Dump点:在ART环境下,360加固的可操作空间相对较小。通常在ClassLinker::DefineClass函数处获取dex_filebeginsize,然后Dump出原始的classes.dex

Hook memcmp的另类思路:在系统校验DEX文件头魔数(Magic Number,如dex.035)时进行Dump。这是一种迂回战术,利用系统在校验DEX完整性时必须读取明文DEX数据这一特点来定位DEX在内存中的位置。

2. 腾讯乐固

Java层Hook ClassLoader:腾讯乐固的一种常用脱壳方式是在Java层Hook ClassLoaderloadClass方法,获得loadClass返回的Class对象,然后通过反射调用getDex方法获取Dex对象,将Dex对象提交给写文件线程,去除重复Dex并写出。

早期版本的文件头加密:腾讯乐固的早期版本仅加密DEX文件头(约0x70字节),运行时解密文件头后即可正常加载。这一特征使得早期版本可以通过修复文件头的方式直接脱壳,而不需要完整的内存Dump。

3. 梆梆加固

调用系统标准接口:梆梆加固的免费版或企业版的某些版本,依然会调用系统的dvmDexFileOpenPartial接口,因此可以直接在该函数处添加Hook进行Dump。

ART下的Dump点:在ART环境下,dvmRawDexFileOpenArray函数是一个关键的Dump点。梆梆的加载流程中,经过多个case之后会调用dvmRawDexFileOpenArray函数,此时可进行Dump。

4. 爱加密

Dalvik下的Dump点:在Dalvik环境下,爱加密常用的Dump点包括dvmDexFileOpenPartialdvmResolveClass等函数。

5. 百度加固

调用DvmDex.cpp中的函数:百度加固常用的Dump点为DvmDexFileOpenFromFd,从文件描述符获取DexFile结构。

6.阿里加固

核心脱壳点dvmDexFileOpenPartial。早期阿里加固的核心逻辑是将DEX入口隐藏,但在加载时仍需调用系统函数dvmDexFileOpenPartial来完成解析。因此,对该函数下断点并dump其addrlen参数,是当时最有效的脱壳方式。

对抗思路:主动调用,迫使解密,精准Dump。

常用手段

​ 1)被动Hook:通过Hook类加载过程中的关键函数,在函数被调用、壳完成方法填充后,立即将内存中的方法体Dump下来。

​ 常见的Hook点包括:

Dalvik环境LoadMethod函数。

ART环境ClassLinker::DefineClass函数

​ 以ART环境为例,可以Hook ClassLinker::DefineClass函数,在该函数被调用时获取DexFile对象的beginsize,进而Dump出DEX文件。如果抽取壳是在ClassLinker::LoadMethod函数调用时才对抽取的函数进行回填,则需要Hook LoadMethod函数。注意,LoadMethod调用有两处——加载直接方法和加载虚方法,两处都需要进行Dump。

​ 被动Hook的缺点在于“被动”。只有当一个类被显式加载(通过Class.forName()ClassLoader.loadClass())或隐式加载(创建实例或访问静态成员)时,才会触发LoadMethod的调用,壳才会对抽取的函数进行回填。

​ 2)主动调用:为了解决被动Hook的局限,产生了“主动调用”的脱壳方案。其核心思路是在App启动后,主动遍历并加载DEX中的所有类,迫使壳为每一个方法都执行一遍“解密-填充”流程。

具体实现原理(以Frida为例子):

第一步:遍历所有ClassLoader:通过Java.enumerateClassLoadersSync()获取应用中的所有ClassLoader

第二步:通过ClassLoader获取所有DEX文件:在Android的ClassLoader体系(如BaseDexClassLoader)中,每个ClassLoader都包含一个DexPathList对象,该对象内部有一个Element[]数组,数组的每一项对应一个DEX文件。

​ 具体反射链路如下:

第三步:枚举所有类名并主动加载:通过DexFile.entries()方法获取该DEX文件中所有的类名,然后调用ClassLoader.loadClass(className)进行主动加载。

第四步:配合Hook点进行Dump:在主动调用的过程中,配合Hook点(如LoadMethodClassLinker::DefineClass)进行Dump,就能获取到一份相对完整的、所有方法体都已填充的DEX文件。需要注意内存中DEX的起始地址和大小,以及正确的脱壳时机Android版本兼容性,因为Android 7.1及之前,Java层有Dex#getBytes方法可直接获取DEX数据;Android 8.0之后该方法被移除,需要通过mCookie字段在Native层获取DexFile

方法一、分析解释器逻辑

​ 首先要了解VMP解释器的基本结构——VMP解释器的核心是经典的 “取指-解码-执行”(Fetch-Decode-Execute)循环

​ 接下来要定位定位VMP入口(vm_entry)。被虚拟化的函数,其内部所有逻辑会变成只跳转到虚拟机入口vm_entry。利用这一特点,可以定位虚拟机入口。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

最后于 2026-7-13 20:53 被ODcat编辑 ,原因:
收藏
免费 3
打赏
分享
最新回复 (3)
雪    币: 341
活跃值: (222)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
先收藏了,有空再看
2026-7-13 21:02
0
雪    币: 666
活跃值: (120)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
陈芸欣 先收藏了,有空再看
收到!
2026-7-13 23:45
0
雪    币: 112
活跃值: (9085)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
tql
2026-7-14 17:16
0
游客
登录 | 注册 方可回帖
返回