FART 主动调用组件设计和源码分析

版权归作者所有,如有转发,请注明文章出处:https://cyrus-studio.github.io/blog/

现有脱壳方法存在的问题

脱壳粒度集中在 DexFile 整体,当前对 apk 保护的粒度在函数粒度,这就导致了脱壳与加固的不对等,无法应对函数粒度的加固保护。

Dalvik 下的基于主动调用的自动化脱壳方案,首次将粒度下降到函数粒度

存在问题:

1、Dalvik 慢慢淡出视野,无法应对 ART 环境

2、当前没有一个好的脱壳修复框架,无法应用到 VMP 函数的修复

主动调用相关概念

1、被动调用

指 app 正常运行过程中发生的调用,该过程只对 dex 中部分的类完成了加载,同时也只是对 dex 中的部分函数完成了调用。

2、主动调用

通过构造虚拟调用,从而达到欺骗 "壳" ,让壳误以为 app 在执行正常的函数调用流程从而达成对 dex 中所有类函数的虚拟调用。

被动调用也可以用来完成函数粒度的修复。如当前通过正常运行 app ,待 app 将 dex 中的类正常加载并完成相关的函数的正常调用后再进行 dex 的 dump 的脱壳方法。

被动调用脱壳的缺点: 存在修复函数不全的问题。由于测试用例无法覆盖 dex 中所有的函数,导致代码覆盖率低,只能对 app 运行过程中调用过的函数的修复。

主动调用的优点: 能够覆盖 dex 中所有的函数,从而完成更彻底的函数粒度的修复。同时,函数的修复准确度同主动调用链的构造深度有关。

FART 中要解决的三个问题

1、如何构造主动调用链并让每一个函数都到达主动调用过程,但是又不影响 app 的正常运行?

2、如何根据 ArtMethod 定位内存中对应的 CodeItem 的起始地址?

3、如何遍历 dex 中的所有函数并完成主动调用?

如何构造主动调用链?

标准 Java 函数调用示例代码:

复制代码
// 1. 查找 Java 类
jclass clazz = env->FindClass("com/cyrus/example/jniexample/JNIExample");
if (clazz == nullptr) {
    LOGI("Class not found");
    return env->NewStringUTF("Class not found");
}

// 2. 获取 Java 方法 ID
jmethodID methodId = env->GetStaticMethodID(clazz, "helloFromJava", "()Ljava/lang/String;");
if (methodId == nullptr) {
    LOGI("Method not found");
    return env->NewStringUTF("Method not found");
}

// 3. 调用 Java 方法
jstring resultStr = (jstring) env->CallStaticObjectMethod(clazz, methodId);

标准流程:

  1. 通过 FindClass 得到 jclass

  2. 通过 GetStaticMethodID / GetMethodId 得到 jmethodID

  3. 通过一系列 Call 开头的函数完成调用(比如这里的 CallStaticObjectMethod)

FindClass

FindClass 的作用: 通过 ClassLinker 的 FindClass 取得目标类的 jclass

复制代码
  static jclass FindClass(JNIEnv* env, const char* name) {
    CHECK_NON_NULL_ARGUMENT(name);
    Runtime* runtime = Runtime::Current();
    ClassLinker* class_linker = runtime->GetClassLinker();
    std::string descriptor(NormalizeJniClassDescriptor(name));
    ScopedObjectAccess soa(env);
    ObjPtr<mirror::Class> c = nullptr;
    if (runtime->IsStarted()) {
      StackHandleScope<1> hs(soa.Self());
      Handle<mirror::ClassLoader> class_loader(hs.NewHandle(GetClassLoader(soa)));
      c = class_linker->FindClass(soa.Self(), descriptor.c_str(), class_loader);
    } else {
      c = class_linker->FindSystemClass(soa.Self(), descriptor.c_str());
    }
    return soa.AddLocalReference<jclass>(c);
  }

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=654

GetMethodID / GetStaticMethodID 的作用:根据 jclass 对象、方法名和签名信息取得对应的 ArtMethod 用于下一步的调用

复制代码
 static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig) {
    CHECK_NON_NULL_ARGUMENT(java_class);
    CHECK_NON_NULL_ARGUMENT(name);
    CHECK_NON_NULL_ARGUMENT(sig);
    ScopedObjectAccess soa(env);
    return FindMethodID(soa, java_class, name, sig, false);
  }

  static jmethodID GetStaticMethodID(JNIEnv* env, jclass java_class, const char* name,
                                     const char* sig) {
    CHECK_NON_NULL_ARGUMENT(java_class);
    CHECK_NON_NULL_ARGUMENT(name);
    CHECK_NON_NULL_ARGUMENT(sig);
    ScopedObjectAccess soa(env);
    return FindMethodID(soa, java_class, name, sig, true);
  }

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=980

FindMethodID

GetStaticMethodID 和 GetMethodID 最终都是走到 FindMethodID ,只是传参 is_static 不同

复制代码
static jmethodID FindMethodID(ScopedObjectAccess& soa, jclass jni_class,
                              const char* name, const char* sig, bool is_static)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  ObjPtr<mirror::Class> c = EnsureInitialized(soa.Self(), soa.Decode<mirror::Class>(jni_class));
  if (c == nullptr) {
    return nullptr;
  }
  ArtMethod* method = nullptr;
  auto pointer_size = Runtime::Current()->GetClassLinker()->GetImagePointerSize();
  if (c->IsInterface()) {
    method = c->FindInterfaceMethod(name, sig, pointer_size);
  } else {
    method = c->FindClassMethod(name, sig, pointer_size);
  }
  if (method != nullptr && ShouldDenyAccessToMember(method, soa.Self())) {
    method = nullptr;
  }
  if (method == nullptr || method->IsStatic() != is_static) {
    ThrowNoSuchMethodError(soa, c, name, sig, is_static ? "static" : "non-static");
    return nullptr;
  }
  return jni::EncodeArtMethod(method);
}

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=430

最后返回的是 jni::EncodeArtMethod(method),这是 ART 中将 ArtMethod* 转换为 jmethodID 的封装过程。

复制代码
ALWAYS_INLINE
static inline jmethodID EncodeArtMethod(ArtMethod* art_method) {
  return reinterpret_cast<jmethodID>(art_method);
}

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.h;l=55

jmethodID 在 ART 中其实就是一个 ArtMethod*,而 EncodeArtMethod 就是把 ArtMethod* 安全地封装成 jmethodID。

CallObjectMethod

CallObjectMethod / CallStaticObjectMethod 函数源码如下:

复制代码
  static jobject CallObjectMethod(JNIEnv* env, jobject obj, jmethodID mid, ...) {
    va_list ap;
    va_start(ap, mid);
    ScopedVAArgs free_args_later(&ap);
    CHECK_NON_NULL_ARGUMENT(obj);
    CHECK_NON_NULL_ARGUMENT(mid);
    ScopedObjectAccess soa(env);
    JValue result(InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, ap));
    return soa.AddLocalReference<jobject>(result.GetL());
  }
  
static jobject CallStaticObjectMethod(JNIEnv* env, jclass, jmethodID mid, ...) {
    va_list ap;
    va_start(ap, mid);
    ScopedVAArgs free_args_later(&ap);
    CHECK_NON_NULL_ARGUMENT(mid);
    ScopedObjectAccess soa(env);
    JValue result(InvokeWithVarArgs(soa, nullptr, mid, ap));
    jobject local_result = soa.AddLocalReference<jobject>(result.GetL());
    return local_result;
  }

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/jni/jni_internal.cc;l=997

InvokeVirtualOrInterfaceWithVarArgs 与 InvokeWithVarArgs

复制代码
JValue InvokeVirtualOrInterfaceWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,
                                           jobject obj, jmethodID mid, va_list args) {
  // We want to make sure that the stack is not within a small distance from the
  // protected region in case we are calling into a leaf function whose stack
  // check has been elided.
  if (UNLIKELY(__builtin_frame_address(0) < soa.Self()->GetStackEnd())) {
    ThrowStackOverflowError(soa.Self());
    return JValue();
  }

  ObjPtr<mirror::Object> receiver = soa.Decode<mirror::Object>(obj);
  ArtMethod* method = FindVirtualMethod(receiver, jni::DecodeArtMethod(mid));
  bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();
  if (is_string_init) {
    // Replace calls to String.<init> with equivalent StringFactory call.
    method = WellKnownClasses::StringInitToStringFactory(method);
    receiver = nullptr;
  }
  uint32_t shorty_len = 0;
  const char* shorty =
      method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetShorty(&shorty_len);
  JValue result;
  ArgArray arg_array(shorty, shorty_len);
  arg_array.BuildArgArrayFromVarArgs(soa, receiver, args);
  InvokeWithArgArray(soa, method, &arg_array, &result, shorty);
  if (is_string_init) {
    // For string init, remap original receiver to StringFactory result.
    UpdateReference(soa.Self(), obj, result.GetL());
  }
  return result;
}

JValue InvokeWithVarArgs(const ScopedObjectAccessAlreadyRunnable& soa, jobject obj, jmethodID mid,
                         va_list args)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  // We want to make sure that the stack is not within a small distance from the
  // protected region in case we are calling into a leaf function whose stack
  // check has been elided.
  if (UNLIKELY(__builtin_frame_address(0) < soa.Self()->GetStackEnd())) {
    ThrowStackOverflowError(soa.Self());
    return JValue();
  }

  ArtMethod* method = jni::DecodeArtMethod(mid);
  bool is_string_init = method->GetDeclaringClass()->IsStringClass() && method->IsConstructor();
  if (is_string_init) {
    // Replace calls to String.<init> with equivalent StringFactory call.
    method = WellKnownClasses::StringInitToStringFactory(method);
  }
  ObjPtr<mirror::Object> receiver = method->IsStatic() ? nullptr : soa.Decode<mirror::Object>(obj);
  uint32_t shorty_len = 0;
  const char* shorty =
      method->GetInterfaceMethodIfProxy(kRuntimePointerSize)->GetShorty(&shorty_len);
  JValue result;
  ArgArray arg_array(shorty, shorty_len);
  arg_array.BuildArgArrayFromVarArgs(soa, receiver, args);
  InvokeWithArgArray(soa, method, &arg_array, &result, shorty);
  if (is_string_init) {
    // For string init, remap original receiver to StringFactory result.
    UpdateReference(soa.Self(), obj, result.GetL());
  }
  return result;
}

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/reflection.cc;l=616

所以,无论是静态方法还是非静态方法最终都会调用到 InvokeWithArgArray

复制代码
void InvokeWithArgArray(const ScopedObjectAccessAlreadyRunnable& soa,
                               ArtMethod* method, ArgArray* arg_array, JValue* result,
                               const char* shorty)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  uint32_t* args = arg_array->GetArray();
  if (UNLIKELY(soa.Env()->IsCheckJniEnabled())) {
    CheckMethodArguments(soa.Vm(), method->GetInterfaceMethodIfProxy(kRuntimePointerSize), args);
  }
  method->Invoke(soa.Self(), args, arg_array->GetNumBytes(), result, shorty);
}

https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/reflection.cc;l=450

结论:对于 Java 函数的调用,最终由该函数对应的 ArtMethod 对象的 Invoke 函数完成。

如何 dump CodeItem?

dumpMethodCode

DexFIle 类添加 native 函数 dumpMethodCode,用于提取指定 ArtMethod 对应的 Dex 字节码(CodeItem)。

复制代码
static void DexFile_dumpMethodCode(JNIEnv* env, jclass, jobject method) {
  // 将当前线程从 kNative 状态切换到 kRunnable,表示线程可以安全访问 Java 对象(如 jobject)。
  ScopedFastNativeObjectAccess soa(env);

  // 如果 Java 传入的 Method 不为 null
  if(method != nullptr){
     // 将 java.lang.reflect.Method 转换为 ART 内部的 ArtMethod 指针
     ArtMethod* artmethod = ArtMethod::FromReflectedMethod(soa, method);

     // 调用 myfartInvoke
     myfartInvoke(artmethod);
  }
  
  return;
}

libcore/dalvik/src/main/java/dalvik/system/DexFile.java

复制代码
public final class DexFile {
    private static native void dumpMethodCode(Object m);
}

myfartInvoke 方法中主动调用 ArtMethod 的 Invoke 方法

复制代码
extern "C" void myfartInvoke(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {
    JValue *result = nullptr;
    Thread *self = nullptr;
    uint32_t temp = 6;
    uint32_t *args = &temp;
    uint32_t args_size = 6;
    artmethod->Invoke(self, args, args_size, result, "fart");
}

在 ArtMethod::Invoke 方法中,检测到 self 是 nullptr,也就是 myfartInvoke 的主动调用时,调用 dumpArtMethod 把解密后的函数 dump 下来

复制代码
void ArtMethod::Invoke(Thread * self, uint32_t * args, uint32_t args_size, JValue * result,
              const char *shorty) {
                  
    if (self == nullptr) {
       dumpArtMethod(this);
       return;
    }
    
    ...        
}

art/runtime/art_method.cc

获取 CodeItem 起始地址

CodeItem 起始地址就在 ArtMethod 的 dex_code_item_offset_ 字段。

不同 Android 版本略有不同


https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/art_method.h;l=755

通过 GetCodeItemOffset() 方法拿到 CodeItem 的起始地址


https://cs.android.com/android/platform/superproject/+/android10-release:art/runtime/art_method.h;l=378

CodeItem 结构定义

Android 源码中 CodeItem 的结构定义如下:

复制代码
struct CodeItem : public dex::CodeItem {
  // DEX 字节码必须按照 4 字节对齐
  static constexpr size_t kAlignment = 4;

private:
  // 方法使用的虚拟寄存器数量(包括本地变量和参数)
  uint16_t registers_size_;

  // 方法的入参占用的寄存器数量
  uint16_t ins_size_;

  // 方法调用其他方法时所需的最大出参寄存器数量(即调用其他方法时的参数空间)
  uint16_t outs_size_;

  // try-catch 块的数量。如果不为 0,则在 insns_ 后紧跟 try_item 和 catch_handler。
  uint16_t tries_size_;

  // 调试信息在 DEX 文件中的偏移,指向 debug_info 结构
  // 包括局部变量名、源码行号映射等
  uint32_t debug_info_off_;

  // 指令(insns_)数组长度,单位是 2 字节(code units)
  // 每条指令通常是 2 字节对齐,有些指令占用多个 code unit
  uint32_t insns_size_in_code_units_;

  // 指令数组(实际大小是可变的,柔性数组)
  // 存放 DEX 字节码指令,insns_size_in_code_units_ 表示其长度
  uint16_t insns_[1];
};

https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/standard_dex_file.h;l=35

https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/compact_dex_file.h;l=87

CodeItem 前 16 字节是固定结构

字节偏移 字段名 含义说明 大小(字节)
0x00 registers_size_ 方法使用的寄存器数(本地变量 + 参数) 2
0x02 ins_size_ 方法参数占用的寄存器数(入参) 2
0x04 outs_size_ 调用其他方法所需的最大出参寄存器数(临时参数空间) 2
0x06 tries_size_ try-catch 块数量,非 0 时表示有异常处理结构 2
0x08 debug_info_off_ 调试信息在 DEX 文件中的偏移 4
0x0C insns_size_in_code_units_ 指令数组长度(单位为 2 字节 code unit) 4
共计 16 字节

CodeItem 前 16 字节是方法的执行元信息,后面的 insns_ 是变长的字节码数组,长度由 insns_size_in_code_units_ 决定,之后可能还有异常处理相关结构(try_items 和 catch_handlers)。

TryItem

TryItem 结构体源码如下 :

复制代码
// Raw try_item.
struct TryItem {
  uint32_t start_addr_;
  uint16_t insn_count_;
  uint16_t handler_off_;
};

https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/dex_file_structs.h;l=196

字段名 类型 说明
start_addr_ uint32_t 指向 code_item->insns[] 的某个偏移,表示 try 块的起始位置(单位为 16-bit 指令)
insn_count_ uint16_t try 块中包含多少条指令(单位为 16-bit 指令)
handler_off_ uint16_t 是 hander 结构的偏移,这个偏移是从 insns_ + sizeof(TryItem) * tries_size_ 开始的,用于描述 catch/finally 的处理逻辑

完整示例:带各种 TryItem 情况的 Kotlin 类

复制代码
package com.cyrus.example.shell

class TryItemExample {

    // ✅ 0. 没有 try-catch(不会生成 TryItem)
    fun noTryCatch(): Int {
        val a = 1
        val b = 2
        return a + b
    }

    // ✅ 1. 简单 try-catch(一个 TryItem)
    fun simpleTryCatch(): String {
        return try {
            val x = 10 / 2
            "Result: $x"
        } catch (e: Exception) {
            "Caught Exception"
        }
    }

    // ✅ 2. 多个 catch 分支(一个 TryItem,多个 handler entry)
    fun multiCatch(input: String?): Int {
        return try {
            input!!.length
        } catch (e: NullPointerException) {
            -1
        } catch (e: Exception) {
            -2
        }
    }

    // ✅ 3. try-catch-finally(一个 TryItem + finally handler)
    fun tryCatchFinally(): Int {
        return try {
            1 / 0
        } catch (e: ArithmeticException) {
            -100
        } finally {
            println("finally block executed")
        }
    }

    // ✅ 4. 嵌套 try-catch(两个 TryItem,嵌套结构)
    fun nestedTryCatch(): String {
        return try {
            try {
                val data = "123".toInt()
                "Parsed: $data"
            } catch (e: NumberFormatException) {
                "Inner Catch"
            }
        } catch (e: Exception) {
            "Outer Catch"
        }
    }

    // ✅ 5. 只有 finally,无 catch(一个 TryItem,无 handler entry)
    fun onlyFinally(): Int {
        try {
            val x = 1 + 1
        } finally {
            println("executing finally without catch")
        }
        return 0
    }

    // ✅ 6. 多个独立 try 块(多个 TryItem,非嵌套)
    fun multipleTryBlocks(): Int {
        try {
            val a = 10 / 2
        } catch (e: Exception) {
            println("First catch")
        }

        try {
            val b = "abc".toInt()
        } catch (e: NumberFormatException) {
            println("Second catch")
        }

        return 0
    }
}

编译运行得到 dex 文件

通过 GDA 查看 dex 中 multipleTryBlocks 函数的 TryItem 信息,可以看到 0004 到 0005 就是第一个 try 块的开始和结束地址

在 010Editor 中打开 dex ,在如下路径

复制代码
dex_class_defs/class_def[17]/class_def[11] public final com.cyrus.example.shell.TryItemExample/class_data/virtual_methods/method[1] public final int com.cyrus.example.shell.TryItemExample.multipleTryBlocks()/code/tries[2]

可以看到 TryItem 数据

CatchHandlerItem

在 DEX 文件中,TryItem 表示一个异常捕获区域,其对应的异常处理器(handler)由 remaining_count_ 指示 catch/finally 块的数量和类型:

  • 如果 remaining_count_ <= 0 表示最后一个是 finally 块。

  • 如果 remaining_count_ > 0 表示只有 catch 块。

  • remaining_count_ 的绝对值表示 catch 块的个数。

  • 如果 remaining_count_ == 0 表示没有 catch 块,只有 finally 块。

CatchHandlerItem:表示一个 catch 分支(即一个异常类型和对应的处理地址)。

复制代码
class CatchHandlerIterator {
  ...
  struct CatchHandlerItem {
      dex::TypeIndex type_idx_;  // 捕获异常的类型索引(指向 Dex 文件中的 type_ids 表)
      uint32_t address_;         // 异常处理器的代码地址(从 code_item.insns 的偏移量)
  } handler_;
  const uint8_t* current_data_;  // the current handler in dex file.
  int32_t remaining_count_;   // number of handlers not read.
  bool catch_all_;            // is there a handler that will catch all exceptions in case
                              // that all typed handler does not match.
};

https://cs.android.com/android/platform/superproject/+/android10-release:art/libdexfile/dex/dex_file_exception_helpers.h;l=67

在 010Editor 中打开 dex ,在如下路径

复制代码
dex_class_defs/class_def[17]/class_def[11] public final com.cyrus.example.shell.TryItemExample/class_data/virtual_methods/method[1] public final int com.cyrus.example.shell.TryItemExample.multipleTryBlocks()/code/handlers

可以看到 CatchHandlerItem 数据

一个 TryItem 对应一个 CatchHandlerIterator ,CatchHandlerIterator 中有一个或多个 CatchHandlerItem,共同组成 try catch/finally 块。

CodeItem 的长度计算和 dump

CodeItem 的长度计算和 dump,两种情况:

  1. 无异常处理

  2. 有异常处理

具体实现代码在 FART 项目 art/runtime/art_method.cc 的 dumpArtMethod 函数里

复制代码
extern "C" void dumpArtMethod(ArtMethod * artmethod) SHARED_LOCKS_REQUIRED(Locks::mutator_lock_) {

    // 分配临时路径缓冲区
    char *dexfilepath = (char *) malloc(sizeof(char) * 2000);
    if (dexfilepath == nullptr) {
        LOG(INFO) << "ArtMethod::dumpArtMethod invoked, methodname: "
                  << PrettyMethod(artmethod).c_str()
                  << " malloc 2000 byte failed";
        return;
    }

    // 获取进程名(用于保存路径)
    int fcmdline = -1;
    char szCmdline[64] = { 0 };
    char szProcName[256] = { 0 };
    int procid = getpid();
    sprintf(szCmdline, "/proc/%d/cmdline", procid);
    fcmdline = open(szCmdline, O_RDONLY, 0644);
    if (fcmdline > 0) {
        read(fcmdline, szProcName, 256);
        close(fcmdline);
    }

    // 如果成功获取进程名
    if (szProcName[0]) {
        // 获取方法所属的 DexFile
        const DexFile *dex_file = artmethod->GetDexFile();
        const char *methodname = PrettyMethod(artmethod).c_str();
        const uint8_t *begin_ = dex_file->Begin();  // dex 数据起始地址
        size_t size_ = dex_file->Size();            // dex 大小

        // 创建 fart 路径
        memset(dexfilepath, 0, 2000);
        sprintf(dexfilepath, "%s", "/sdcard/fart");
        mkdir(dexfilepath, 0777);

        // 创建 fart/进程名 路径
        memset(dexfilepath, 0, 2000);
        sprintf(dexfilepath, "/sdcard/fart/%s", szProcName);
        mkdir(dexfilepath, 0777);

        // 保存 dex 文件到 /sdcard/fart/进程名/xxx_dexfile.dex
        memset(dexfilepath, 0, 2000);
        sprintf(dexfilepath, "/sdcard/fart/%s/%d_dexfile.dex", szProcName, (int)size_);
        int dexfilefp = open(dexfilepath, O_RDONLY, 0666);
        if (dexfilefp > 0) {
            // 如果文件已存在,不再写入
            close(dexfilefp);
            dexfilefp = 0;
        } else {
            // 写入完整 dex 数据
            dexfilefp = open(dexfilepath, O_CREAT | O_RDWR, 0666);
            if (dexfilefp > 0) {
                write(dexfilefp, (void *) begin_, size_);
                fsync(dexfilefp);
                close(dexfilefp);
            }
        }

        // 获取该方法的 CodeItem(即字节码结构体)
        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_));
                // 解析 handler 数据结构,返回 handler 数据结束的指针位置
                uint8_t *tail = codeitem_end(&handler_data);
                code_item_len = (int) (tail - item);
            } else {
                // 没有 try 块,长度 = 16字节头部 + 指令长度(每个指令2字节)
                code_item_len = 16 + code_item->insns_size_in_code_units_ * 2;
            }

            // 组合保存路径
            memset(dexfilepath, 0, 2000);
            int size_int = (int) dex_file->Size();
            uint32_t method_idx = artmethod->get_method_idx();
            sprintf(dexfilepath, "/sdcard/fart/%s/%d_%ld.bin", szProcName, size_int, gettidv1());

            // 打开保存 CodeItem 信息的文件
            int fp2 = open(dexfilepath, O_CREAT | O_APPEND | O_RDWR, 0666);
            if (fp2 > 0) {
                lseek(fp2, 0, SEEK_END);

                // 写入方法信息(name、method_idx、offset、code_item_len)
                memset(dexfilepath, 0, 2000);
                int offset = (int)(item - begin_);
                sprintf(dexfilepath,
                        "{name:%s,method_idx:%d,offset:%d,code_item_len:%d,ins:",
                        methodname, method_idx, offset, code_item_len);
                int contentlength = strlen(dexfilepath);
                write(fp2, (void *) dexfilepath, contentlength);

                // base64 编码指令数据并写入
                long outlen = 0;
                char *base64result = base64_encode((char *) item, (long)code_item_len, &outlen);
                write(fp2, base64result, outlen);
                write(fp2, "};", 2);

                fsync(fp2);
                close(fp2);

                // 清理内存
                if (base64result != nullptr) {
                    free(base64result);
                    base64result = nullptr;
                }
            }
        }
    }

    // 释放路径缓冲区
    if (dexfilepath != nullptr) {
        free(dexfilepath);
        dexfilepath = nullptr;
    }
}

解析 CodeItem 的异常处理数据部分,返回 handler 数据结束的指针位置

复制代码
uint8_t *codeitem_end(const uint8_t **pData) {
    // 读取 handler 列表的数量(即有多少个 try 块)
    uint32_t num_of_list = DecodeUnsignedLeb128(pData);

    // 遍历每个 try 块的 handler 列表
    for (; num_of_list > 0; num_of_list--) {
        // 读取当前 handler 列表中 handler 的数量,可能为负值,表示存在 catch-all handler
        int32_t num_of_handlers = DecodeSignedLeb128(pData);

        // 取绝对值,得到实际的 type-handler 对数量
        int num = num_of_handlers;
        if (num_of_handlers <= 0) {
            num = -num_of_handlers;  // catch-all handler 也算在内,但单独处理
        }

        // 读取每个 handler 的 type_idx 和 address
        for (; num > 0; num--) {
            DecodeUnsignedLeb128(pData);  // type_idx
            DecodeUnsignedLeb128(pData);  // address
        }

        // 如果存在 catch-all handler,再额外读取一个地址
        if (num_of_handlers <= 0) {
            DecodeUnsignedLeb128(pData);  // catch-all address
        }
    }

    // 此时 *pData 已指向异常处理部分结束的位置
    return (uint8_t *)(*pData);
}

如何实现主动调用?

fart 函数中遍历当前 App 的 ClassLoader,拿到 ClassLoader 需要判断不是系统的 BootClassLoader,不然会把系统框架的 dex dump 下来

复制代码
public static void fart() {
    ClassLoader appClassloader = getClassloader();
    if(appClassloader == null){
        Log.e("ActivityThread", "appClassloader is null");
        return;
    }
    
    if(appClassloader.toString().indexOf("java.lang.BootClassLoader") == -1){
        fartWithClassLoader(appClassloader);
    }
    
    ClassLoader tmpClassloader = appClassloader;
    ClassLoader parentClassloader = appClassloader.getParent();
    while(parentClassloader != null){
        if(parentClassloader.toString().indexOf("java.lang.BootClassLoader") == -1){
            fartWithClassLoader(parentClassloader);
        }
        tmpClassloader = parentClassloader;
        parentClassloader = parentClassloader.getParent();
    }
}

fartWithClassLoader 函数中遍历 ClassLoader 中的所有 DexFile,获取所有类名,并调用 native 方法 dumpMethodCode。

复制代码
public static void fartWithClassLoader(ClassLoader appClassloader) {
    Log.i("ActivityThread", "fartWithClassLoader " + appClassloader.toString());
    
    // 用于存放获取到的 dexFile 对象
    List<Object> dexFilesArray = new ArrayList<Object>();

    // 获取 DexPathList 对象实例
    Object pathList_object = getFieldOjbect("dalvik.system.BaseDexClassLoader", appClassloader, "pathList");

    // 获取 dexElements 字段,类型为 DexPathList$Element[],每个 element 封装了 dexFile
    Object[] ElementsArray = (Object[]) getFieldOjbect("dalvik.system.DexPathList", pathList_object, "dexElements");

    // 声明 dexElements 中的 dexFile 字段
    Field dexFile_fileField = null;
    try {
        dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element", "dexFile");
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 通过类加载器反射获取 dalvik.system.DexFile 类
    Class DexFileClazz = null;
    try {
        DexFileClazz = appClassloader.loadClass("dalvik.system.DexFile");
    } catch (Exception e) {
        e.printStackTrace();
    }

    // 要调用的 native 方法
    Method getClassNameList_method = null;
    Method defineClass_method = null;
    Method dumpMethodCode_method = null;

    // 遍历 DexFile 类中的方法,设置需要的 native 方法强制可访问
    for (Method field : DexFileClazz.getDeclaredMethods()) {
        if (field.getName().equals("getClassNameList")) {
            getClassNameList_method = field;
            getClassNameList_method.setAccessible(true);
        }
        if (field.getName().equals("defineClassNative")) {
            defineClass_method = field;
            defineClass_method.setAccessible(true);
        }
        if (field.getName().equals("dumpMethodCode")) {
            dumpMethodCode_method = field;
            dumpMethodCode_method.setAccessible(true);
        }
    }

    // 获取 mCookie 字段(DexFile 中指向 native Dex 的句柄)
    Field mCookiefield = getClassField(appClassloader, "dalvik.system.DexFile", "mCookie");

    // 遍历每一个 dex element(即每个 dex 文件)
    for (int j = 0; j < ElementsArray.length; j++) {
        Object element = ElementsArray[j];
        Object dexfile = null;
        try {
            // 通过反射获取 DexFile 对象
            dexfile = (Object) dexFile_fileField.get(element);
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (dexfile == null) {
            continue;
        }

        // 加入列表
        dexFilesArray.add(dexfile);

        // 获取 native mCookie 对象
        Object mcookie = getClassFieldObject(appClassloader, "dalvik.system.DexFile", dexfile, "mCookie");
        if (mcookie == null) {
            continue;
        }

        // 调用 native 方法 getClassNameList(mcookie) 获取 dex 中包含的类名数组
        String[] classnames = null;
        try {
            classnames = (String[]) getClassNameList_method.invoke(dexfile, mcookie);
        } catch (Exception e) {
            e.printStackTrace();
            continue;
        } catch (Error e) {
            e.printStackTrace();
            continue;
        }

        // 遍历所有类名,对每个类调用 dumpMethodCode
        if (classnames != null) {
            for (String eachclassname : classnames) {
                loadClassAndInvoke(appClassloader, eachclassname, dumpMethodCode_method);
            }
        }
    }

    return;
}

主动加载 dex 中的所有类需要对 dex 文件解析,获取 dex 中的所有类列表,两种解决方案:

  1. 手动解析 dex 文件

  2. 直接调用 aosp 源码中已有的 api:getClassNameList 即可(需要通过反射一步步获取当前 ClassLoader 当中的 mCookie)

拿到 ClassLoader 的 dexElements ,迭代 dexElements 通过反射拿到 DexFile 的 mCookie


https://cs.android.com/android/platform/superproject/+/android10-release:libcore/dalvik/src/main/java/dalvik/system/DexPathList.java;l=69

调用 DexFile.getClassNameList 传递 mCookie 得到 dex 的 class list


https://cs.android.com/android/platform/superproject/+/android10-release:libcore/dalvik/src/main/java/dalvik/system/DexFile.java;l=432

拿到 ClassNameList 后,调用 loadClassAndInvoke 进行主动加载;

LoadClassAndInvoke 中对类进行加载,遍历类中构造函数和普通函数,进行主动调用。

复制代码
public static void loadClassAndInvoke(ClassLoader appClassloader, String eachclassname, Method dumpMethodCode_method) {
    Log.i("ActivityThread", "go into loadClassAndInvoke->" + "classname:" + eachclassname);

    Class resultclass = null;
    try {
        // 使用给定的 ClassLoader 加载指定类名的类
        resultclass = appClassloader.loadClass(eachclassname);
    } catch (Exception e) {
        e.printStackTrace();
        return;
    } catch (Error e) {
        e.printStackTrace();
        return;
    }

    // 如果类加载成功
    if (resultclass != null) {
        try {
            // 获取该类的所有构造方法(包括 private 的)
            Constructor<?> cons[] = resultclass.getDeclaredConstructors();
            for (Constructor<?> constructor : cons) {
                if (dumpMethodCode_method != null) {
                    try {
                        // 调用 native 方法 dumpMethodCode 传入构造方法对象
                        // 发起主动调用 并 dump method
                        dumpMethodCode_method.invoke(null, constructor);
                    } catch (Exception e) {
                        e.printStackTrace();
                        continue;
                    } catch (Error e) {
                        e.printStackTrace();
                        continue;
                    }
                } else {
                    Log.e("ActivityThread", "dumpMethodCode_method is null ");
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }

        try {
            // 获取该类的所有方法(包括 private 的)
            Method[] methods = resultclass.getDeclaredMethods();
            if (methods != null) {
                for (Method m : methods) {
                    if (dumpMethodCode_method != null) {
                        try {
                            // 调用 native 方法 dumpMethodCode
                            dumpMethodCode_method.invoke(null, m);
                        } catch (Exception e) {
                            e.printStackTrace();
                            continue;
                        } catch (Error e) {
                            e.printStackTrace();
                            continue;
                        }
                    } else {
                        Log.e("ActivityThread", "dumpMethodCode_method is null ");
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } catch (Error e) {
            e.printStackTrace();
        }
    }
}

通过 loadClass 加载类,为什么不用 Class.forName?因为 Class.forName 会调用类的初始化方法,有一些对抗手段会在类初始化中执行,所以用 loadClass,loadClass 不会调用类的初始化方法。

复制代码
resultclass = appClassloader.loadClass(eachclassname);

进入到 ArtMethod 的 Invoke 方法后,识别出是我们自己发起的主动调用后,执行 dumpArtMethod

复制代码
void ArtMethod::Invoke(Thread* self, uint32_t* args,
                       uint32_t args_size, JValue* result,
                       const char* shorty) {

   // 若 self 为空,直接 dump 当前 ArtMethod
   if (self == nullptr) {
      dumpArtMethod(this);
      return;
   }

    ...
}
相关推荐
南国樗里疾26 分钟前
Android 14 解决打开app出现不兼容弹窗的问题
android
IT小码哥丶29 分钟前
HarmonyOS实战:自定义时间选择器
android·harmonyos
liulangrenaaa29 分钟前
C语言实现android/linux按键模拟
android·linux·c语言
LB211242 分钟前
黑马 javaweb Day07 MySQL --DQL(查询)语句
android·数据库·mysql
CYRUS STUDIO1 小时前
FART 自动化脱壳框架简介与脱壳点的选择
android·驱动开发·自动化·逆向·源码阅读·脱壳
zimoyin8 小时前
kotlin Android AccessibilityService 无障碍入门
android·开发语言·kotlin
韩仔搭建19 小时前
第二章:安卓端启动流程详解与疑难杂症调试手册
android·ui·娱乐
A-花开堪折19 小时前
Android7 Input(七)App与input系统服务建立连接
android
冰糖葫芦三剑客19 小时前
Android 自定义悬浮拖动吸附按钮
android