前言

假期自学计划开启,就以分析 FART 源码开始。

FART 是看雪大牛 hanbingle 提出的一种基于主动调用开发的脱壳工具,工具的具体原理可以阅读帖子 FART:ART环境下基于主动调用的自动化脱壳方案,本文是结合该篇帖子和源码的学习笔记。

源码地址:https://github.com/hanbinglengyue/FART,该代码是基于 Android 6.0 源码基础上修改而成的,加入了主动调用并修复函数的模块。

代码结构

  • art/runtime
    • interpreter
      • interpreter.cc
    • native
      • dalvik_system_DexFile.cc
    • art_method.cc
    • art_method.h
  • frameworks/base/core/java/android/app
    • ActivityThread.java
  • libcore/dalvik/src/main/java/dalvik/system
    • DexFile.java

FART 大体上就修改了源码中的这六个文件,加上 fart.py 这个用于合并 Dex 结构体和 CodeItem 的脚本,共有七个文件。结合作者在帖子中的介绍,大概介绍下每个文件的作用:

  • DexFile.java - 声明了 native 函数 dumpMethodCode,该函数的作用是构造主动调用链,完成方法体的dump。

  • dalvik_system_DexFile.cc - 实现了 dumpMethodCode 函数,该函数会去主动调用 myfartInvoke 函数

  • interpreter.cc - 增加判断,当处于<clinit> 阶段时,调用 dumpDexFileByExecute 来进行 dump

  • art_method.cc - 增加了两个函数 dumpDexFileByExecutedumpArtMethod

  • art_method.h - 声明 get_method_idx,暴露出 dex_method_index_

  • ActivityThread.java - 获得 ClassLoader,获得 mCookie,再利用 mCookie 获得所有的类,然后主动遍历类的所有函数,进而提取出每个函数体的 CodeItem,至于为何采用该类作为入口点,可以参考作者的解释。

    众所周知,对于一个正常的应用来说,最终都要由一个个的Activity来展示应用的界面并和用户完成交互,那么我们就可以选择在ActivityThread中的performLaunchActivity函数作为时机,来获取最终的应用的Classloader。选择该函数还有一个好处在于该函数和应用的最终的application同在ActivityThread类中,可以很方便获取到该类的成员。

CodeItem 抽取

从我对 Android 加固有限的认知来看,目前最为广泛普及的加固技术即为指令抽取,更为高级的 VMP 或者 Dex2C 由于性能问题并不会大范围应用,因此如果能有效地解决指令抽取问题,即可以解决 99% 的问题。

通常情况下,Java 类中的代码有两部分,一些代码只会在初始化阶段执行一次,因此针对 <clinit> 需要进行特判并在执行时直接抽取 CodeItem 部分,而其他部分可以在主动调用过程中进行抽取。

针对这两部分,作者设计了不同的 dump 逻辑,分别如下:

  • <clinit> - Execute => dumpDexFileByExecute
  • 正常函数 - DexFile_dumpMethodCode => myfartInvoke => Invoke => dumpArtMethod

dumpDexFileByExecute 的逻辑如下,可以看到主要还是在执行时立刻保存 artmethod 所对应的 Dex 结构:

extern "C" void dumpDexFileByExecute(ArtMethod * artmethod)
    SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
  
    // 省略部分代码
    if (szProcName[0]) {
        const DexFile *dex_file = artmethod->GetDexFile();
        const uint8_t *begin_ = dex_file->Begin();	// Start of data.
        size_t size_ = dex_file->Size();	// Length of data.
      	// 继续省略代码
        sprintf(dexfilepath,
            "/sdcard/fart/%s/%d_dexfile_execute.dex",
            szProcName, size_int_);
        int dexfilefp = open(dexfilepath, O_RDONLY, 0666);
        if (dexfilefp > 0) {
            close(dexfilefp);
            dexfilefp = 0;
        } else {
            dexfilefp =
                open(dexfilepath, O_CREAT | O_RDWR,
                    0666);
            if (dexfilefp > 0) {
                write(dexfilefp, (void *) begin_,
                        size_);
                fsync(dexfilefp);
                close(dexfilefp);
            }
        }
    }
}

继续看 dumpArtMethod 的逻辑:

extern "C" void dumpArtMethod(ArtMethod * artmethod)
    SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
    // 省略无关代码
    if (szProcName[0]) {
        // 省略代码
        // 保存 Dex 结构
        // 保存函数体
        const DexFile::CodeItem * code_item =
            artmethod->GetCodeItem();
        if (LIKELY(code_item != nullptr)) {
            int code_item_len = 0;
            uint8_t *item = (uint8_t *) code_item;
            if (code_item->tries_size_ > 0) {
                const uint8_t *handler_data =
                    (const uint8_t *) (DexFile::
                                GetTryItems
                                (*code_item,
                            code_item->
                            tries_size_));
                uint8_t *tail =
                    codeitem_end(&handler_data);
                code_item_len =
                    (int) (tail - item);
            } else {
                code_item_len =
                    16 +
                    code_item->
                    insns_size_in_code_units_ * 2;
            }
            // 保存 codeitem 信息 <==> base64 形式
        }
    }
}

在正常保存 Dex 代码外,程序还会将一个函数的 CodeItem 抽取出来并保存。

编译运行

代码是基于 Android 6.0 的修改,因此我们可以自己编译 FART 工具并刷入自己的手机。然后找个带指令抽取的样本进行测试,脱壳结果如下:

root@angler:/sdcard/fart/de.k3b.android.androFotoFinder # ls -al
-rw-rw---- root     sdcard_rw 10139916 1970-01-11 02:22 10139916_dexfile_execute.dex
-rw-rw---- root     sdcard_rw  2499359 1970-01-11 02:25 1835412_5597.bin
-rw-rw---- root     sdcard_rw  1936402 1970-01-11 02:26 1835412_5724.bin
-rw-rw---- root     sdcard_rw  1832365 1970-01-11 02:26 1835412_5732.bin
-rw-rw---- root     sdcard_rw  1835412 1970-01-11 02:24 1835412_dexfile.dex
-rw-rw---- root     sdcard_rw  1835412 1970-01-11 02:22 1835412_dexfile_execute.dex
-rw-rw---- root     sdcard_rw   270916 1970-01-11 02:22 270916_dexfile_execute.dex
-rw-rw---- root     sdcard_rw  4307272 1970-01-11 02:22 4307272_dexfile_execute.dex
-rw-rw---- root     sdcard_rw     4906 1970-01-11 02:25 5109544_5597.bin
-rw-rw---- root     sdcard_rw  5109544 1970-01-11 02:25 5109544_dexfile.dex
-rw-rw---- root     sdcard_rw  5109544 1970-01-11 02:22 5109544_dexfile_execute.dex

然后利用 adb 将文件 pull 出来后,利用官方提供 fart.py 脚本可以尝试合并 dex 文件和相应的函数指令,可以看到相应效果为显示修复前 smali 代码和修复后 smali 代码:

result

如果需要合并成新 dex 的话,需要结合 Dex 结构对脚本进行修改。

参考链接