抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

img

第一代壳

第一代加固壳简介

DEX 加密(也称落地加载)

第一代壳将整个 apk 文件压缩加密到壳 dex 文件的后面,在壳 dex 文件上写上解压代码,动态加载执行,由于是加密整个 apk,在大型应用中很耗资源,因此这代壳很早就被放弃了但思路还是不变。其中这种加密还可以具体划分为几个方向,如下:

  • Dex 字符串加密
  • 静态 DEX 文件整体加密解密
  • 资源加密(xml 与 arsc 文件加密及十六进制加密)
  • 对抗反编译(针对反编译工具,如 apktool。利用反编译工具本身存在的缺陷,使得反编译失败,以此实现对反编译工具的抵抗)
  • Ptrace 反调试、TracePid 值校验反调试
  • 自定义 DexClassLoader(主要是针对 dex 文件加固、加壳等情况)
  • 落地加载(dex 可以在 apk 目录下看到)

第一代加固壳原理

涉及到三个程序:

  1. 待加壳程序的 APK(源程序 DEX)
  2. (脱)壳程序 APK(负责解密源程序 APK 并加载运行它)
  3. 加壳程序(将源程序 APK 进行加密并与壳程序的 dex 合并成新的 dex)。

主要步骤如下:

首先利用加密算法对源程序 APK 进行加密,然后与脱壳程序 APK 合并得到新的 dex 文件,最后替换脱壳程序中原有的 dex 文件即可。之后运行合并后的程序时,会通过壳程序对源程序进行解密和运行时加载。

具体实现

源程序

MainActivity 中添加一行

1
Log.i("demo", "app:"+getApplicationContext());

文件下添加一个 MyAppliaction 类,并重写一下 OnCreate,输出一下 Log。其实 Application 即使不创建,系统也会帮我们创建的。但是创建自定义 Application 类是为了方便反射获取 Application 类,进而创建源程序的 Application 实例来替换原有的壳程序的 Application 实例。

1
2
3
4
5
6
7
8
public class MyApplication extends Application {
String TAG = "demo";
@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "SourceApk onCreate: " + this);
}
}

加壳程序

主要功能是加密源程序 APK、合并成新的 dex 文件、修正三个字段。常用 python 实现

对于合并后的 dex 文件,需要修改其 dex_header 中的三个字段:

  1. checksum:dex 文件(除 magicchecksum)的校验和(使用 adler32 算法),通过它来判断 dex 文件是否被损坏或篡改。
  2. signature:dex 文件(除 magicchecksumsignature 之外的所有内容)的 SHA-1 签名(哈希),用于对文件进行唯一标识。
  3. file_size:整个 dex 文件的大小。

脱壳程序

由于我们最终需要运行的是我们的源程序,所以我们必须在启动流程调用 ApplicationOnCreate 之前释放出源程序,并替换 Application 为我们的源程序 Application 实例(原来是脱壳程序的 Application 实例)。

工作:

  1. 替换 LoadedApkmClassLoader

    关键点

    • mClassLoader 的作用
      ClassLoader 是用来加载应用程序的类文件的核心组件,通过双亲委托机制寻找和加载类。如果壳程序的 ClassLoader 不包含源程序的类,那么应用将无法正常运行。
    • **为什么需要替换 mClassLoader**: 壳程序本身的类加载器是基于壳的 APK,而源程序的类已经被加密或独立存储(例如在 Source.apk 中)。所以壳程序的 ClassLoader 无法直接加载源程序的类。这就需要创建一个包含源程序类路径的 DexClassLoader 替换壳的 ClassLoader

    替换方式

    三种替换 mClassLoader 的方法:

    1. 替换类加载器:用源程序的 DexClassLoader 完全替换壳的 ClassLoader
    2. 插入类加载器:在壳的 ClassLoader 和其父加载器之间插入源程序的 DexClassLoader,实现动态加载源程序的类。
    3. 合并 dexElements:合并壳和源程序的 dexElements,复用壳的 ClassLoader,既不改变原有加载逻辑,又能访问源程序的类。

    分析

    • 直接替换和插入的方式较简单,但可能引入与系统组件兼容性的问题。
    • 合并 dexElements 更加优雅,也是在插件化、热修复等场景中常用的方法。
  2. 替换 Application

关键点

  • Application 的作用Application 是整个安卓应用程序的上下文和入口点,负责初始化全局状态,并与 ActivityService 等组件交互。壳程序和源程序可能对应不同的 Application 实例,为了正确运行源程序,必须替换掉壳的 Application
  • 替换方法
    • **删除壳的 Application**:从 ActivityThreadmAllApplications 中移除壳的 Application
    • **设置 LoadedApk.mApplicationnull**:清空 mApplication,让系统重新调用 makeApplication() 方法创建源程序的 Application
    • **动态创建源程序的 Application**:通过 meta-data 提供的源程序 Application 类名反射生成对应实例。
    • **替换 ContentProviderApplication**:避免因不同 Application 导致组件交互问题。
    • 运行源程序的 onCreate() 方法:完成初始化,开始应用生命周期。

分析

  • 替换 Application 是必要的,因为壳的 Application 只是个临时过渡,无法维护源程序的全局状态。
  • 关键是要保证在替换前后应用的生命周期保持完整和一致,特别是对于一些较早初始化的组件(如 ContentProvider)。
  1. 选择 Application 作为脱壳的入口点

关键点

  • Application 的特殊性
    • 它是整个应用程序的入口点,生命周期贯穿应用始终。
    • 在系统加载 ActivityService 之前,Application 就已被创建,可以在此时完成解密、加载等操作。
  • 脱壳流程设计
    • attachBaseContext() 中替换壳的 ClassLoader,保证后续加载类时能找到源程序的类。
    • onCreate() 中替换壳的 Application,完成全局状态切换。

分析

  • 脱壳逻辑可以全部在 attachBaseContext() 或 onCreate()中完成,但分步处理更清晰:
    • attachBaseContext() 中修改 ClassLoader,确保后续逻辑能正确加载源程序的类。
    • onCreate() 中完成 Application 替换,既符合逻辑顺序,也避免早期替换影响到 ActivityThread.mInitialApplication

第二代壳

第二代加固壳简介

第一代壳与第二代壳对比

特性 第一代壳 第二代壳
加密对象 整个APK文件 .dex 文件
解密位置 文件系统 内存中
加载方式 ClassLoader 加载文件系统中的 APK 动态加载器直接加载内存中的 .dex 数据
保护层级 纯 Java 层 Java + Native 层,可能配合虚拟化保护
安全性 易逆向,落地解密后容易被提取 难逆向,加载逻辑在 Native 层,避免落地解密
性能 低,涉及文件系统读写和两次加载 高,直接从内存加载,减少不必要的 IO 消耗
适用场景 理论验证或简单防护 生产环境,适用于对安全性要求高的商业应用

在第一代加固壳中,会将源程序APK存储到文件系统中,然后再通过DexClassLoader动态加载。这种落地加载方式存在很大问题,一是容易在文件系统中获取到源程序APK,二是两次加载源程序APK到内存中,效率低。

第二代壳就是为了解决上述问题,做到不落地加载,即不使用文件系统作为中转站,而是直接将内存中的源APK字节码进行加载。首先加密对象就不是整个apk而是变成了apk内的代码文件dex,这个时候第二代壳就体现出了其强大的实用性,如果说第一代壳只是一个理论基础而第二代壳就是可以量产的初号机型了。

第一代壳是将其加密的dex文件存放在dex文件里的,由于java可读性较高,这为逆向人员分析脱壳代码降低了难度,因此第二代壳是将dex代码加密到native层,即so文件。大大增加了逆向难度。

根据各大厂商以这个思路加密的手段,又可以分为:

  • DEX动态加载(分为利用jni和自定义jni,即自定义底层函数)
  • Dex Method代码抽取到外部(类抽取加密按需解密和动态方法修改替换)
  • So加密
  • 反调试,防HOOK
  • 不落地加载 (apk目录下不能看到原始dex,把加密的dex文件加载到内存中,然后解密,从始至终不会以文件形式存在)

第二代加固壳的原理

仅加密 .dex 文件而非整个 APK,以减少解密开销。加密后的 .dex 数据通常存储在壳的资源文件或 Native 层的 so 文件中。

不再将解密后的 .dex 数据存储到文件系统。直接在内存中解密,并通过自定义加载器(如 InMemoryDexClassLoader)加载到虚拟机运行。

解密逻辑隐藏在 Native 层,通过 JNI 与 Java 层交互。加密算法和解密流程使用 Native 实现,避免被 Java 层反编译分析。

按模块对 .dex 文件进行解密和加载,减少内存占用。设置触发条件,在运行时动态加载目标模块。

具体实现

加载内存dex

Android 4.x 及以前的 BaseDexClassLoader 最终调用了带有三个参数的 native 方法 openDexFile()

openDexFileNative 方法

在 Android 5.x ~ 7.x,.dex 文件的加载路径发生了变化。最终调用的 openDexFileNative 方法位于 libart.so 中。

libart.so 中引入了 OpenMemory 函数,用于直接从内存加载 .dex 数据。OpenMemory 接收内存映射的 .dex 数据 dex_mem_map,而不是文件路径。

解密后的 .dex 数据可以通过 MemMap(内存映射)包装,传递给该函数直接加载。

InMemoryDexClassLoader方法

Android 8.x 引入了 InMemoryDexClassLoader,支持从内存直接加载 .dex 字节码。接收一个或多个 ByteBuffer 对象,表示内存中的 .dex 数据。

通过 BaseDexClassLoader 的逻辑,将这些字节码注册到虚拟机中。

不管哪种方法,它们都会返回一个cookie值,它是对dex文件进行操作的唯一凭据,可以简单理解成dex文件的“身份证”。

内存dex中类的加载

在 Android 中,类加载器 ClassLoader 的职责是动态加载类,加载过程可以分为以下步骤:

  1. ClassLoader 调用 **loadClass()**。
  2. BaseDexClassLoader 调用 **findClass()**。
  3. DexPathList 调用 **findClass()**。
  4. DexPathList.Element 调用 **findClass()**。
  5. DexFile 调用 **loadClassBinaryName()**。
  6. 最终调用 defineClassNative() 完成类的加载。

其中:

3~6 的方法主要起中转作用,不包含实际的类加载逻辑。**defineClassNative()** 是最终完成类加载的本地方法。

自定义类加载器的原理

核心思路

  • 重写 BaseDexClassLoaderfindClass() 方法,直接调用底层的 **defineClassNative()**。
  • 使用 cookie 提供 .dex 文件的引用,通过获取 .dex 中的类名来决定加载逻辑。

核心问题

  1. 如何获取 .dex 中的类名?
    • 利用 DexFile.getClassNameList(Object cookie) 本地方法,可以获取 .dex 文件中所有的类名。
  2. 如何调用 defineClassNative()
    • 需要通过反射调用该方法,同时传递合适的参数(包括类名、类加载器、cookie 等)。
  3. 如何处理 cookie
    • cookie.dex 文件的首地址,DexFile 可以通过 cookie 直接映射和操作 .dex 文件。

defineClassNative 方法的分析

  1. Android 7 之前的实现
1
2
3
4
5
static jobject DexFile_defineClassNative(JNIEnv* env,
jclass,
jstring name,
jobject loader,
jobject cookie)

作用:根据类名 name、类加载器 loader.dex 文件的引用 cookie 定义类。

参数说明

  • name:要加载的类名。
  • loader:当前类加载器。
  • cookie.dex 文件的首地址,用于访问 .dex 数据。

主要功能

  • 使用 cookie 解析 .dex 文件。
  • 调用虚拟机的类加载逻辑完成类定义。

Android 7+ 的变化

1
2
3
4
5
6
static jobject DexFile_defineClassNative(JNIEnv* env,
jclass,
jstring name,
jobject loader,
jobject cookie,
jobject dex_file)

**新增参数 dex_file**:

  • DexFile 参数主要用于 class_linker->InsertDexFileInToClassLoader(),将 DexFile 对象关联到类加载器中。

解决方案

  • 如果不需要 InsertDexFileInToClassLoader 的功能,可以直接传入 null,绕过新增的逻辑。
  • 手动实现类加载逻辑,加载过程不会受影响。

获取 .dex 中类名的实现

DexFile 提供了一个本地方法 **getClassNameList(Object cookie)**,可以通过 cookie 获取 .dex 文件中所有的类名:

1
private static native String[] getClassNameList(Object cookie);
  • 关键点:

    • cookie.dex 文件的首地址(可从 DexFile 获取)。
    • 返回值是一个包含类名的字符串数组。
  • 实现示例:

    1
    2
    3
    DexFile dexFile = ... // 获取 DexFile 对象
    Object cookie = getCookie(dexFile); // 通过反射获取 cookie
    String[] classNames = DexFile.getClassNameList(cookie);

自定义类加载器的实现

继承 BaseDexClassLoader,重写 findClass() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CustomClassLoader extends BaseDexClassLoader {
private final Object dexCookie;

public CustomClassLoader(ByteBuffer dexBuffer, ClassLoader parent) {
super(new ByteBuffer[] { dexBuffer }, null, parent);
dexCookie = getCookie(); // 通过反射获取 cookie
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (isDexClass(name)) {
// 调用 defineClassNative
return defineClass(name, dexCookie);
} else {
// 交给父类加载
return super.findClass(name);
}
}

private boolean isDexClass(String name) {
// 判断类是否存在于 dex 文件中
String[] classNames = DexFile.getClassNameList(dexCookie);
return Arrays.asList(classNames).contains(name);
}

private Class<?> defineClass(String name, Object cookie) {
try {
Method defineClassNative = DexFile.class.getDeclaredMethod(
"defineClassNative", String.class, ClassLoader.class, Object.class);
defineClassNative.setAccessible(true);
return (Class<?>) defineClassNative.invoke(null, name, this, cookie);
} catch (Exception e) {
throw new ClassNotFoundException("Failed to define class: " + name, e);
}
}
}

使用方法

  1. 准备 .dex 文件的字节码。
  2. 创建 CustomClassLoader,传入解密后的 .dex 字节码:
1
2
ByteBuffer dexBuffer = ByteBuffer.wrap(decryptedDexData);
CustomClassLoader classLoader = new CustomClassLoader(dexBuffer, parentClassLoader);

3.加载类:

1
Class<?> clazz = classLoader.loadClass("com.example.MyClass");

Native 层实现细节

在 Native 层,cookie.dex 文件的首地址,通过它可以操作 .dex 文件。

1. cookie 的由来

  • 在 Android 5.1 的 DexFile_defineClassNative:

    1
    const DexFile* dex_file = toDexFiles(cookie, env);
    • cookie 被解析为 DexFile 对象。
  • 在 Android 6+:

    1
    std::unique_ptr<std::vector<const DexFile*>> dex_files = ConvertJavaArrayToNative(env, cookie);
    • cookieDexFile 数组的首地址。

2. 直接获取 DexFile 对象

  • 通过反射或其他方式将 cookie 转换为 DexFile 对象,方便调用相关方法。

一些总结

脱壳的关键点

确定内存中 .dex 的起始地址和大小

  • .dex 文件的加载流程中,有多个函数会直接或间接提供 .dex 的内存地址和大小。通过跟踪这些函数,可以准确获取到解密后的 .dex 的内存范围。
  • 常见函数和位置:
    • DexFile::Open(const uint8_t* base, size_t size, ...)
    • OpenMemory(const uint8_t* base, size_t size, ...)
    • DexFileVerifier::Verify(const DexFile* dex_file, const uint8_t* begin, size_t size, ...)
    • 返回 DexFile 对象的函数。
  • 获取方式:
    • Hook 关键函数,拦截参数或返回值。
    • 直接修改源码,打印出对应的地址和大小。

脱壳时机

  • 脱壳时机决定了 dump 的 .dex 文件是否是解密状态。
  • 关键点:
    • 壳程序会先加载加密的 .dex,然后通过解密后再将其交给虚拟机加载。因此,解密后的 .dex 只会存在于内存中,dump 必须在解密后进行。
    • 适合的时机通常是壳完成 .dex的解密并交给系统加载时,例如:
      • DexFile::Open() 函数。
      • defineClassNative() 调用前后。
      • 类加载完成后,通过调用相关函数遍历 DexFile 对象。

Dump 数据

  • 通过拦截上述脱壳点,将 .dex 数据从内存中拷贝出来,并写入文件。
  • 注意事项:
    • 确保获取到 .dex 数据的完整性。
    • .dex 数据的起始地址应对齐,避免损坏文件结构。

脱壳点的选择

在 Android 的加载流程中,以下位置可以作为脱壳点:

1**DexFile::Open() 系列函数**

  • DexFile::Open()和 OpenMemory()是加载 .dex文件的核心函数,会接收 .dex 的起始地址和大小参数:

    1
    DexFile* DexFile::Open(const uint8_t* base, size_t size, ...)
  • Hook 或修改方法:

    • basesize 被传递时,将这段内存直接 dump 出来。
    • 插入代码将内存内容保存到文件中。

2DexFileVerifier::Verify()

  • DexFileVerifier::Verify()验证 .dex 文件是否合法,也会用到 .dex的起始地址和大小:

    1
    bool DexFileVerifier::Verify(const DexFile* dex_file, const uint8_t* begin, size_t size, ...)
  • 作用:

    • 可以在验证阶段拦截到 .dex 数据。
    • 解密后的 .dex 文件通常会经过验证。

3defineClassNative()

  • defineClassNative() 是最终的类定义入口,会加载具体的类。
  • 参数中包含 .dex 文件的 cookie,可以通过 cookie 获取 .dex 数据。

4类加载完成后

  • 可以在加载完成后,通过 DexFile 提供的方法获取所有加载的类和 .dex 数据。
  • 例如,通过反射获取 cookie,再利用 getClassNameList() 遍历类名。

实现脱壳的具体方法

相关脱壳方法

  1. 内存 Dump 法
  2. 缓存脱壳法
  3. 文件监视法
  4. Hook 法
  5. 定制系统法
  6. 动态调试法

1. 通过 Hook 实现

  • 使用工具:
    • Xposed(适合 Java 层 Hook)
    • Frida(适合 Native 层 Hook)
    • Inline Hook 框架(适合底层 Hook,比如 libart.so,使用 Inline Hook 替换 DexFile::Open。)

2.修改源码

在加载流程中直接插入代码,输出 .dex 文件。

源码修改示例:DexFile::Open

找到 DexFile::Open 函数,在解密后插入代码:

1
2
3
4
// Insert this before returning DexFile
FILE* file = fopen("/sdcard/dumped.dex", "wb");
fwrite(base, 1, size, file);
fclose(file);

3. 利用调试器

  • 使用调试工具(如 GDB)设置断点,手动 Dump 数据:
    1. 找到 DexFile::Open 或其他脱壳点。
    2. 设置断点,获取 basesize
    3. 使用调试器保存内存到文件。

评论