从内存中加载 Dex 是近年来一个比较热门的需求,无论是 Android 加固的内存不落地实现,还是 Android 插件化、热修复等技术等的应用,都需要实现一个 ClassLoader 能主动从内存中加载 Dex 文件。而之前通用的做法大多数是通过自定义 ClassLoader + Hook ART/Dalvik 虚拟机实现,但在 Android 8.0 后,谷歌官方推出了可以从内存中加载 Dex 的 InMemoryDexClassLoader 来实现这一目标。本文旨在深入分析这两者的异同 XD

PS:写到一半的途中发现由于贴了过多的代码导致文章显得非常冗长,阅读可能需要一定耐心 (・_・;

InMemoryDexClassLoader

首先直接看源代码(android-8.0.0_r1),可以看到 InMemoryDexClassLoader 是 BaseDexClassLoader 的子类:

public final class InMemoryDexClassLoader extends BaseDexClassLoader {
    public InMemoryDexClassLoader(@NonNull ByteBuffer @NonNull [] dexBuffers,
            @Nullable String librarySearchPath, @Nullable ClassLoader parent) {
        super(dexBuffers, librarySearchPath, parent);
    }

    public InMemoryDexClassLoader(@NonNull ByteBuffer @NonNull [] dexBuffers,
            @Nullable ClassLoader parent) {
        this(dexBuffers, null, parent);
    }

    public InMemoryDexClassLoader(@NonNull ByteBuffer dexBuffer, @Nullable ClassLoader parent) {
        this(new ByteBuffer[] { dexBuffer }, parent);
    }
}

Emmm,看来是直接丢给父类处理了,继续跟进查看父类的定义:

public class BaseDexClassLoader extends ClassLoader {
		// …… 省略其他
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
}

可以看到 DexPathList 这个类也相应跟进了使用 ByteBuffer 数组来创建内存中的 Dex,继续跟进该函数:

public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
  if (definingContext == null) {
    throw new NullPointerException("definingContext == null");
  }
  if (dexFiles == null) {
    throw new NullPointerException("dexFiles == null");
  }
  if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
    throw new NullPointerException("dexFiles contains a null Buffer!");
  }

  this.definingContext = definingContext;
  // TODO It might be useful to let in-memory dex-paths have native libraries.
  this.nativeLibraryDirectories = Collections.emptyList();
  this.systemNativeLibraryDirectories =
    splitPaths(System.getProperty("java.library.path"), true);
  this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);

  ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
  this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
  if (suppressedExceptions.size() > 0) {
    this.dexElementsSuppressedExceptions =
      suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
  } else {
    dexElementsSuppressedExceptions = null;
  }
}

那么很明显就是 makeInMemoryDexElements 函数了,继续查看该函数实现:

private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
                                                 List<IOException> suppressedExceptions) {
  Element[] elements = new Element[dexFiles.length];
  int elementPos = 0;
  for (ByteBuffer buf : dexFiles) {
    try {
      DexFile dex = new DexFile(buf);
      elements[elementPos++] = new Element(dex);
    } catch (IOException suppressed) {
      System.logE("Unable to load dex file: " + buf, suppressed);
      suppressedExceptions.add(suppressed);
    }
  }
  if (elementPos != elements.length) {
    elements = Arrays.copyOf(elements, elementPos);
  }
  return elements;
}

直接调用了 DexFile 的构造函数:

DexFile(ByteBuffer buf) throws IOException {
  mCookie = openInMemoryDexFile(buf);
  mInternalCookie = mCookie;
  mFileName = null;
}

private static Object openInMemoryDexFile(ByteBuffer buf) throws IOException {
  if (buf.isDirect()) {
    return createCookieWithDirectBuffer(buf, buf.position(), buf.limit());
  } else {
    return createCookieWithArray(buf.array(), buf.position(), buf.limit());
  }
}

private static native Object createCookieWithDirectBuffer(ByteBuffer buf, int start, int end);
private static native Object createCookieWithArray(byte[] buf, int start, int end);

可以看到这里程序就进入了 native 逻辑,相应实现在 art/runtime/native/dalvik_system_DexFile.cc:

static jobject DexFile_createCookieWithDirectBuffer(JNIEnv* env,
                                                    jclass,
                                                    jobject buffer,
                                                    jint start,
                                                    jint end) {
  uint8_t* base_address = reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(buffer));
  if (base_address == nullptr) {
    ScopedObjectAccess soa(env);
    ThrowWrappedIOException("dexFileBuffer not direct");
    return 0;
  }

  std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
  if (dex_mem_map == nullptr) {
    DCHECK(Thread::Current()->IsExceptionPending());
    return 0;
  }

  size_t length = static_cast<size_t>(end - start);
  memcpy(dex_mem_map->Begin(), base_address, length);
  return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
}

static jobject DexFile_createCookieWithArray(JNIEnv* env,
                                             jclass,
                                             jbyteArray buffer,
                                             jint start,
                                             jint end) {
  std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
  if (dex_mem_map == nullptr) {
    DCHECK(Thread::Current()->IsExceptionPending());
    return 0;
  }

  auto destination = reinterpret_cast<jbyte*>(dex_mem_map.get()->Begin());
  env->GetByteArrayRegion(buffer, start, end - start, destination);
  return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
}

这两个函数最终的目的都是将 java 中的 byte 数组转为 native 层的 MemoryMap,然后通过 CreateSingleDexFileCookie 获得相应的 Cookie:

static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data) {
  std::unique_ptr<const DexFile> dex_file(CreateDexFile(env, std::move(data)));
  if (dex_file.get() == nullptr) {
    DCHECK(env->ExceptionCheck());
    return nullptr;
  }
  std::vector<std::unique_ptr<const DexFile>> dex_files;
  dex_files.push_back(std::move(dex_file));
  return ConvertDexFilesToJavaArray(env, nullptr, dex_files);
}

最终 Cookie 由 ConvertDexFilesToJavaArray 生成(没错,这个版本的 cookie 就是 jlongArray 类型):

static jlongArray ConvertDexFilesToJavaArray(JNIEnv* env,
                                             const OatFile* oat_file,
                                             std::vector<std::unique_ptr<const DexFile>>& vec) {
  // Add one for the oat file.
  jlongArray long_array = env->NewLongArray(static_cast<jsize>(kDexFileIndexStart + vec.size()));
  if (env->ExceptionCheck() == JNI_TRUE) {
    return nullptr;
  }

  jboolean is_long_data_copied;
  jlong* long_data = env->GetLongArrayElements(long_array, &is_long_data_copied);
  if (env->ExceptionCheck() == JNI_TRUE) {
    return nullptr;
  }

  long_data[kOatFileIndex] = reinterpret_cast<uintptr_t>(oat_file);
  for (size_t i = 0; i < vec.size(); ++i) {
    long_data[kDexFileIndexStart + i] = reinterpret_cast<uintptr_t>(vec[i].get());
  }

  env->ReleaseLongArrayElements(long_array, long_data, 0);
  if (env->ExceptionCheck() == JNI_TRUE) {
    return nullptr;
  }

  // Now release all the unique_ptrs.
  for (auto& dex_file : vec) {
    dex_file.release();
  }

  return long_array;
}

最后看一下是如何生存 Dex 结构的,可以看到 DexFile::Open 允许传入之前生成的MemoryMap结构: dex_mem_map,代码如下。

// art/runtime/native/dalvik_system_DexFile.cc
static const DexFile* CreateDexFile(JNIEnv* env, std::unique_ptr<MemMap> dex_mem_map) {
  std::string location = StringPrintf("Anonymous-DexFile@%p-%p",
                                      dex_mem_map->Begin(),
                                      dex_mem_map->End());
  std::string error_message;
  std::unique_ptr<const DexFile> dex_file(DexFile::Open(location,
                                                        0,
                                                        std::move(dex_mem_map),
                                                        /* verify */ true,
                                                        /* verify_location */ true,
                                                        &error_message));
  if (dex_file == nullptr) {
    ScopedObjectAccess soa(env);
    ThrowWrappedIOException("%s", error_message.c_str());
    return nullptr;
  }

  if (!dex_file->DisableWrite()) {
    ScopedObjectAccess soa(env);
    ThrowWrappedIOException("Failed to make dex file read-only");
    return nullptr;
  }

  return dex_file.release();
}

// art/runtime/dex_file.cc
std::unique_ptr<const DexFile> DexFile::Open(const std::string& location,
                                             uint32_t location_checksum,
                                             std::unique_ptr<MemMap> map,
                                             bool verify,
                                             bool verify_checksum,
                                             std::string* error_msg) {
  ScopedTrace trace(std::string("Open dex file from mapped-memory ") + location);
  CHECK(map.get() != nullptr);

  if (map->Size() < sizeof(DexFile::Header)) {
    *error_msg = StringPrintf(
        "DexFile: failed to open dex file '%s' that is too short to have a header",
        location.c_str());
    return nullptr;
  }

  std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
                                                 map->Size(),
                                                 location,
                                                 location_checksum,
                                                 kNoOatDexFile,
                                                 verify,
                                                 verify_checksum,
                                                 error_msg);
  if (dex_file != nullptr) {
    dex_file->mem_map_.reset(map.release());
  }
  return dex_file;
}

std::unique_ptr<DexFile> DexFile::OpenCommon(const uint8_t* base,
                                             size_t size,
                                             const std::string& location,
                                             uint32_t location_checksum,
                                             const OatDexFile* oat_dex_file,
                                             bool verify,
                                             bool verify_checksum,
                                             std::string* error_msg,
                                             VerifyResult* verify_result) {
  if (verify_result != nullptr) {
    *verify_result = VerifyResult::kVerifyNotAttempted;
  }
  std::unique_ptr<DexFile> dex_file(new DexFile(base,
                                                size,
                                                location,
                                                location_checksum,
                                                oat_dex_file));
  if (dex_file == nullptr) {
    *error_msg = StringPrintf("Failed to open dex file '%s' from memory: %s", location.c_str(),
                              error_msg->c_str());
    return nullptr;
  }
  if (!dex_file->Init(error_msg)) {
    dex_file.reset();
    return nullptr;
  }
  if (verify && !DexFileVerifier::Verify(dex_file.get(),
                                         dex_file->Begin(),
                                         dex_file->Size(),
                                         location.c_str(),
                                         verify_checksum,
                                         error_msg)) {
    if (verify_result != nullptr) {
      *verify_result = VerifyResult::kVerifyFailed;
    }
    return nullptr;
  }
  if (verify_result != nullptr) {
    *verify_result = VerifyResult::kVerifySucceeded;
  }
  return dex_file;
}

最终调用了 DexFile 的构造函数:

DexFile::DexFile(const uint8_t* base,
                 size_t size,
                 const std::string& location,
                 uint32_t location_checksum,
                 const OatDexFile* oat_dex_file)
    : begin_(base),
      size_(size),
      location_(location),
      location_checksum_(location_checksum),
      header_(reinterpret_cast<const Header*>(base)),
      string_ids_(reinterpret_cast<const StringId*>(base + header_->string_ids_off_)),
      type_ids_(reinterpret_cast<const TypeId*>(base + header_->type_ids_off_)),
      field_ids_(reinterpret_cast<const FieldId*>(base + header_->field_ids_off_)),
      method_ids_(reinterpret_cast<const MethodId*>(base + header_->method_ids_off_)),
      proto_ids_(reinterpret_cast<const ProtoId*>(base + header_->proto_ids_off_)),
      class_defs_(reinterpret_cast<const ClassDef*>(base + header_->class_defs_off_)),
      method_handles_(nullptr),
      num_method_handles_(0),
      call_site_ids_(nullptr),
      num_call_site_ids_(0),
      oat_dex_file_(oat_dex_file) {
  CHECK(begin_ != nullptr) << GetLocation();
  CHECK_GT(size_, 0U) << GetLocation();
  // Check base (=header) alignment.
  // Must be 4-byte aligned to avoid undefined behavior when accessing
  // any of the sections via a pointer.
  CHECK_ALIGNED(begin_, alignof(Header));

  InitializeSectionsFromMapList();
}

该方案和原有 DexClassLoader 实现的差异,可以参考文章 InMemoryDexClassLoader探究,这里不再赘叙。

非官方内存不落地实现

在 Android 8.0 之前,InMemoryClassLoader 并不存在,那么用什么方式来实现加载内存中的 Dex?

用于参考的项目地址:https://github.com/woxihuannisja/Bangcle

以下是作者的描述:

原理

Dalvik下的动态加载方法使用的是我这篇文章的方案 https://bbs.pediy.com/thread-215078.htm, Art 下提供了2种方案, 方案一是 call libart下的OpenMemory函数,如何将java的mCookie和c层的cookie联系起来是一个难点, 方案二是 使用elf Hook来实现 ,由于在Nougat+上dlsym openMemory失败,还有dex_location和dex_cache_location的路径检查,使用方案一有些问题。

首先来看最关键的两个函数 https://github.com/woxihuannisja/Bangcle/blob/master/jni/packer.cpp

  • native_attachBaseContext
  • native_onCreate

这两个函数主要实现了 attachBaseContext 和 onCreate 的逻辑。按函数的执行顺序,先看 native_attachBaseContext

void native_attachBaseContext(JNIEnv *env, jobject thiz, jobject ctx)
{
#if defined(__arm__)
    LOGD("[+]Running arm libdexload");
#elif defined(__aarch64__)
    LOGD("[+]Running aarch64 libdexload");

#endif

    jclass ApplicationClass = env->GetObjectClass(ctx);
    jmethodID getFilesDir = env->GetMethodID(ApplicationClass, "getFilesDir", "()Ljava/io/File;");
    jobject File_obj = env->CallObjectMethod(ctx, getFilesDir);
    jclass FileClass = env->GetObjectClass(File_obj);

    jmethodID getAbsolutePath = env->GetMethodID(FileClass, "getAbsolutePath", "()Ljava/lang/String;");
    jstring data_file_dir = static_cast<jstring>(env->CallObjectMethod(File_obj, getAbsolutePath));

    g_file_dir = env->GetStringUTFChars(data_file_dir, NULL);
    //g_file_dir_backup=g_file_dir;
    LOGD("[+]FilesDir:%s", g_file_dir);
    env->DeleteLocalRef(data_file_dir);
    env->DeleteLocalRef(File_obj);
    env->DeleteLocalRef(FileClass);

    // NativeLibraryDir
    jmethodID getApplicationInfo = env->GetMethodID(ApplicationClass, "getApplicationInfo",
                                                    "()Landroid/content/pm/ApplicationInfo;");
    jobject ApplicationInfo_obj = env->CallObjectMethod(ctx, getApplicationInfo);
    jclass ApplicationInfoClass = env->GetObjectClass(ApplicationInfo_obj);
    jfieldID nativeLibraryDir_field = env->GetFieldID(ApplicationInfoClass, "nativeLibraryDir", "Ljava/lang/String;");
    jstring nativeLibraryDir = (jstring)(env->GetObjectField(ApplicationInfo_obj, nativeLibraryDir_field));

    g_NativeLibDir = env->GetStringUTFChars(nativeLibraryDir, NULL);
    LOGD("[+]NativeLibDir:%s", g_NativeLibDir);

    env->DeleteLocalRef(nativeLibraryDir);
    env->DeleteLocalRef(ApplicationInfoClass);
    env->DeleteLocalRef(ApplicationInfo_obj);

    jmethodID getPackageResourcePath = env->GetMethodID(ApplicationClass, "getPackageResourcePath", "()Ljava/lang/String;");

    jstring mPackageFilePath = static_cast<jstring>(env->CallObjectMethod(ctx, getPackageResourcePath));
    const char *cmPackageFilePath = env->GetStringUTFChars(mPackageFilePath, NULL);
    g_PackageResourcePath = const_cast<char *>(cmPackageFilePath);
    LOGD("[+]PackageResourcePath:%s", g_PackageResourcePath);
    env->DeleteLocalRef(mPackageFilePath);

    jmethodID getPackageName = env->GetMethodID(ApplicationClass, "getPackageName", "()Ljava/lang/String;");
    jstring PackageName = static_cast<jstring>(env->CallObjectMethod(ctx, getPackageName));
    const char *packagename = env->GetStringUTFChars(PackageName, NULL);
    g_pkgName = (char *)packagename;
    LOGD("[+]g_pkgName :%s", g_pkgName);
    env->DeleteLocalRef(PackageName);

    char jiaguPath[256] = {0}; // 加密dex的存储路径

    sprintf(g_jiagu_dir, "%s/%s", g_file_dir, PACKER_MAGIC);
    sprintf(jiaguPath, "%s/%s", g_jiagu_dir, JIAMI_MAGIC);
    LOGD("[+]g_jiagu_dir:%s,jiaguPath:%s", g_jiagu_dir, jiaguPath);
    if (access(g_jiagu_dir, F_OK) != 0)
    {
        if (mkdir(g_jiagu_dir, 0755) == -1)
        {
            LOGE("[-]mkdir %s error:%s", g_jiagu_dir, strerror(errno));
        }
    }
    //从assets目录提取加密dex
    extract_file(env, ctx, jiaguPath, JIAMI_MAGIC);
    mem_loadDex(env, ctx, jiaguPath);
} // native_attachBaseContext

可以看到大部分操作都是为了获得程序路径等关键属性,主要执行的函数为 extract_file(env, ctx, jiaguPath, JIAMI_MAGIC);mem_loadDex(env, ctx, jiaguPath); ,分别是提取 dex 文件以及在内存中加载 dex 文件。

下面看 native_onCreate 函数,对 Android 加固有研究的可以看到,这段代码主要实现的就是利用 JNI 实现 Application 和 Context 的替换:

void native_onCreate(JNIEnv *env, jobject thiz, jobject instance)
{
    LOGD("[+]native onCreate is called");
    jclass ProxyApplicationClass = env->GetObjectClass(instance);
    jmethodID getPackageManager = env->GetMethodID(ProxyApplicationClass, "getPackageManager", "()Landroid/content/pm/PackageManager;");
    if (env->ExceptionCheck())
    {
        LOGE("[-]find getPackageManager methodID failed");
        return;
    }
    jobject packageManager = env->CallObjectMethod(instance, getPackageManager);
    if (env->ExceptionCheck())
    {
        LOGE("[-]call getPackageManager method failed");
        return;
    }
    jmethodID pmGetApplicationInfo = env->GetMethodID(env->GetObjectClass(packageManager), "getApplicationInfo", "(Ljava/lang/String;I)Landroid/content/pm/ApplicationInfo;");
    jmethodID getPackageName = env->GetMethodID(ProxyApplicationClass, "getPackageName", "()Ljava/lang/String;");
    jobject pmAppInfo = env->CallObjectMethod(packageManager, pmGetApplicationInfo, env->CallObjectMethod(instance, getPackageName), 128);

    jclass PackageItemInfoClass = env->FindClass("android/content/pm/PackageItemInfo");

    jfieldID metaDataField = env->GetFieldID(PackageItemInfoClass, "metaData", "Landroid/os/Bundle;");
    jobject metaData = env->GetObjectField(pmAppInfo, metaDataField);
    if (metaData == NULL)
    {
        LOGE("[-]not found meta Bundle");
        return;
    }

    jmethodID bundleGetString = env->GetMethodID(env->GetObjectClass(metaData), "getString", "(Ljava/lang/String;)Ljava/lang/String;");
    //found orignal ApplicationName
    jstring originApplicationName = (jstring)env->CallObjectMethod(metaData, bundleGetString, env->NewStringUTF("APP_NAME"));
    if (originApplicationName == NULL)
    {
        LOGE("[-]not found original Application Name");
        return;
    }
    LOGD("[+]original Application Name : %s", env->GetStringUTFChars(originApplicationName, NULL));

    //将LoadedApk中的mApplication对象替换
    jclass ActivityThreadClass = env->FindClass("android/app/ActivityThread");
    jmethodID currentActivityThread = env->GetStaticMethodID(ActivityThreadClass, "currentActivityThread", "()Landroid/app/ActivityThread;");
    jobject activityThread = env->CallStaticObjectMethod(ActivityThreadClass, currentActivityThread);
    LOGE("get ActivityThreadClass");
    //得到AppBindData
    jfieldID mBoundApplicationField = env->GetFieldID(ActivityThreadClass, "mBoundApplication", "Landroid/app/ActivityThread$AppBindData;");
    jobject mBoundApplication = env->GetObjectField(activityThread, mBoundApplicationField);
    LOGE("get AppBindData");
    //得到LoadedApk
    jfieldID infoField = env->GetFieldID(env->GetObjectClass(mBoundApplication), "info", "Landroid/app/LoadedApk;");
    jobject info = env->GetObjectField(mBoundApplication, infoField);
    LOGE("get LoadedApk");
    //把LoadedApk中的成员变量private Application mApplication;置空
    jfieldID mApplicationField = env->GetFieldID(env->GetObjectClass(info), "mApplication", "Landroid/app/Application;");
    env->SetObjectField(info, mApplicationField, NULL);
    LOGE("mApplication set null");
    //得到壳Application
    jfieldID mInitialApplicationField = env->GetFieldID(ActivityThreadClass, "mInitialApplication", "Landroid/app/Application;");
    jobject mInitialApplication = env->GetObjectField(activityThread, mInitialApplicationField);
    LOGE("get packer Application");

    //将壳Application移除
    jfieldID mAllApplicationsField = env->GetFieldID(ActivityThreadClass, "mAllApplications", "Ljava/util/ArrayList;");
    jobject mAllApplications = env->GetObjectField(activityThread, mAllApplicationsField);
    jmethodID remove = env->GetMethodID(env->GetObjectClass(mAllApplications), "remove", "(Ljava/lang/Object;)Z");
    env->CallBooleanMethod(mAllApplications, remove, mInitialApplication);
    LOGE("remove packer Application");
    //得到AppBindData中的ApplicationInfo
    jfieldID appInfoField = env->GetFieldID(env->GetObjectClass(mBoundApplication), "appInfo", "Landroid/content/pm/ApplicationInfo;");
    jobject appInfo = env->GetObjectField(mBoundApplication, appInfoField);
    LOGE("get AppBindData's ApplicationInfo");
    //得到LoadedApk中的ApplicationInfo
    jfieldID mApplicationInfoField = env->GetFieldID(env->GetObjectClass(info), "mApplicationInfo", "Landroid/content/pm/ApplicationInfo;");
    jobject mApplicationInfo = env->GetObjectField(info, mApplicationInfoField);
    LOGE("get LoadedApk's ApplicationInfo");
    //替换掉ApplicationInfo中的className
    jfieldID classNameField = env->GetFieldID(env->GetObjectClass(appInfo), "className", "Ljava/lang/String;");
    env->SetObjectField(appInfo, classNameField, originApplicationName);
    env->SetObjectField(mApplicationInfo, classNameField, originApplicationName);
    LOGE("replace ApplicationInfo's className");
    //创建新的Application
    jmethodID makeApplication = env->GetMethodID(env->GetObjectClass(info), "makeApplication", "(ZLandroid/app/Instrumentation;)Landroid/app/Application;");
    //这里调用原始app的attacheBaseContext
    jobject originApp = env->CallObjectMethod(info, makeApplication, false, NULL);
    LOGE("create new Application");
    //将句柄赋值到mInitialApplicationField
    env->SetObjectField(activityThread, mInitialApplicationField, originApp);
    LOGE("set object mInitialApplicationField");
    jfieldID mProviderMapField;
    if (g_sdk_int < 19)
    {
        mProviderMapField = env->GetFieldID(ActivityThreadClass, "mProviderMap", "Ljava/util/HashMap;");
    }
    else
    {
        mProviderMapField = env->GetFieldID(ActivityThreadClass, "mProviderMap", "Landroid/util/ArrayMap;");
    }
    if (mProviderMapField == NULL)
    {
        LOGE("not found mProviderMapField");
        return;
    }
    LOGE("found mProviderMapField");
    jobject mProviderMap = env->GetObjectField(activityThread, mProviderMapField);
    LOGE("found mProviderMap");
    jmethodID values = env->GetMethodID(env->GetObjectClass(mProviderMap), "values", "()Ljava/util/Collection;");
    jobject collections = env->CallObjectMethod(mProviderMap, values);
    jmethodID iterator = env->GetMethodID(env->GetObjectClass(collections), "iterator", "()Ljava/util/Iterator;");
    jobject mIterator = env->CallObjectMethod(collections, iterator);
    jmethodID hasNext = env->GetMethodID(env->GetObjectClass(mIterator), "hasNext", "()Z");
    jmethodID next = env->GetMethodID(env->GetObjectClass(mIterator), "next", "()Ljava/lang/Object;");

    //替换所有ContentProvider中的Context
    LOGE("ready replace all ContentProvider's context");
    while (env->CallBooleanMethod(mIterator, hasNext))
    {
        jobject providerClientRecord = env->CallObjectMethod(mIterator, next);
        if (providerClientRecord == NULL)
        {
            LOGE("providerClientRecord = NULL");
            continue;
        }
        jclass ProviderClientRecordClass = env->FindClass("android/app/ActivityThread$ProviderClientRecord");
        jfieldID mLocalProviderField = env->GetFieldID(ProviderClientRecordClass, "mLocalProvider", "Landroid/content/ContentProvider;");
        if (mLocalProviderField == NULL)
        {
            LOGE("mLocalProviderField not found");
            continue;
        }
        jobject mLocalProvider = env->GetObjectField(providerClientRecord, mLocalProviderField);
        if (mLocalProvider == NULL)
        {
            LOGE("mLocalProvider is NULL");
            continue;
        }
        jfieldID mContextField = env->GetFieldID(env->GetObjectClass(mLocalProvider), "mContext", "Landroid/content/Context;");
        if (mContextField == NULL)
        {
            LOGE("mContextField not found");
            continue;
        }
        env->SetObjectField(mLocalProvider, mContextField, originApp);
    }

    //执行originApp的onCreate
    jmethodID onCreate = env->GetMethodID(env->GetObjectClass(originApp), "onCreate", "()V");
    env->CallVoidMethod(originApp, onCreate);
    LOGD("Packer is done");
}

onCreate 函数的流程大致如下:

  1. 调用 ActivityThread 中 LoadedApk 对象的 makeApplication 方法来创建原始 Dex 文件的 Application 对象
  2. 替换 ActivityThread 中 mBoundApplication 的 mApplication 属性为原始 Dex 文件的 Application 对象
  3. 替换 ActivityThread 中的 mInitialApplication 属性为原始 Dex 文件的 Application 对象
  4. 替换 ActivityThread 中 LoadedApk 的 className 为原始 Dex 文件的 Application的name
  5. 替换 ActivityThread 中 mBoundApplication 里 appInfo 的 className 为原始 Dex 文件的 Application 的 name 属性
  6. 替换 ActivityThread 中 mProviderMap 中每个 provider 的 mContext 为原始 Dex 文件的 Application 对象
  7. 调用原始 Dex 文件的 Application 对象的 onCreate 方法,恢复原始 Dex 文件的组件启动

该函数的加载流程主要完成的是替换的过程,在这一点上和内存落地式没有差异,至于为何要完成替换以及如何理解相应流程,可以查看该篇博文:Android动态替换Application实现,所以关键函数还是在于 extract_filemem_loadDex,下面继续来看以下两个函数:

extract_file 定义在 https://github.com/woxihuannisja/Bangcle/blob/master/jni/utils.cpp,可以看到逻辑非常简单,从 Assets 中提取相应文件并保存到一定目录下:

int extract_file(JNIEnv *env, jobject ctx, const char *szDexPath, const char *fileName)
{
    if (access(szDexPath, F_OK) == 0)
    {
        LOGD("[+]File %s have existed", szDexPath);
        return 0;
    }
    // jiami.dat不存在,开始提取
    else
    {
        AAssetManager *mgr;
        jclass ApplicationClass = env->GetObjectClass(ctx);
        jmethodID getAssets =
            env->GetMethodID(ApplicationClass, "getAssets", "()Landroid/content/res/AssetManager;");
        jobject Assets_obj = env->CallObjectMethod(ctx, getAssets);
        mgr = AAssetManager_fromJava(env, Assets_obj);
        if (mgr == NULL)
        {
            LOGE("[-]getAAssetManager failed");
            return 0;
        }
        AAsset *asset = AAssetManager_open(mgr, fileName, AASSET_MODE_STREAMING);
        FILE *file = fopen(szDexPath, "wb");
        int bufferSize = AAsset_getLength(asset);
        LOGD("[+]Asset FileName:%s,extract path:%s,size:%d\n", fileName, szDexPath, bufferSize);
        void *buffer = malloc(4096);
        while (true)
        {
            int numBytesRead = AAsset_read(asset, buffer, 4096);
            if (numBytesRead <= 0)
            {
                break;
            }
            fwrite(buffer, numBytesRead, 1, file);
        }
        free(buffer);
        fclose(file);
        AAsset_close(asset);
        chmod(szDexPath, 493);
    }
} // extract_file

那么看来最核心的逻辑在于 mem_loadDex 函数:

void mem_loadDex(JNIEnv *env, jobject ctx, const char *dex_path)
{
    char inPath[256] = {0};
    char outPath[256] = {0};
    jobject mini_dex_obj = NULL;
    void *c_dex_cookie = NULL;

    jclass ApplicationClass = env->GetObjectClass(ctx);
    jmethodID getClassLoader = env->GetMethodID(ApplicationClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
    jobject classLoader = env->CallObjectMethod(ctx, getClassLoader);

    char szDexPath[256] = {0};

    sprintf((char *)szDexPath, dex_path, strlen(dex_path));
    LOGD("[+]Dex Path:%s", szDexPath);
    //读取加密dex
    int encrypt_size;
    char *encrypt_buffer = parse_file(szDexPath, encrypt_size);
    //check dex size
    if (!encrypt_size)
    {
        LOGE("[-]encrypt_size is 0");
        exit(-1);
    }
    else if (encrypt_size % 16)
    {
        LOGE("[-]encrypt_size is not mutiple 16");
        exit(-1);
    }

    int zero = open("/dev/zero", PROT_WRITE);
    g_decrypt_base = mmap(0, encrypt_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, zero, 0);
    close(zero);
    if (g_decrypt_base == MAP_FAILED)
    {
        LOGE("[-]ANONYMOUS mmap failed:%s", strerror(errno));
        exit(-1);
    }
    //LOGD("[+]ANONYMOUS mmap addr:%p", g_decrypt_base);
    char decrypt_path[256] = {0};
    sprintf((char *)decrypt_path, "%s/decrypt.dat", g_jiagu_dir);
    //char *decrypt_buffer = aes_decrypt_cbc(encrypt_buffer, encrypt_size, &g_dex_size);
    char *decrypt_buffer = tiny_aes_decrypt_cbc(encrypt_buffer, encrypt_size, &g_dex_size);
    if (!decrypt_buffer)
    {
        LOGE("[-]aes_decrypt_cbc decrypt dex failed");
        exit(-1);
    }

    memcpy(g_decrypt_base, decrypt_buffer, g_dex_size);
    g_page_size = PAGE_END(g_dex_size);
    free(decrypt_buffer);

    LOGD("[+]After decrypt dex magic:0x%x,size:%d,page_size:%d", *(int *)g_decrypt_base, g_dex_size, g_page_size);

    if (!g_isArt)
    {
        jint mCookie = mem_loadDex_dvm(env, (char *)szDexPath);
        LOGD("[+]Dalvik dex cookie :0x%x", mCookie);
        jclass DexFileClass = env->FindClass("dalvik/system/DexFile");
        jfieldID cookie_field = env->GetFieldID(DexFileClass, "mCookie", "I");
        //replace cookie
        env->SetIntField(mini_dex_obj, cookie_field, mCookie);
        make_dex_elements(env, classLoader, mini_dex_obj);
        return;
    }
    else
    {
        sprintf(inPath, "%s/mini.dex", g_jiagu_dir);
        sprintf(outPath, "%s/mini.oat", g_jiagu_dir);
        write_mix_dex(inPath);
        g_ArtHandle = get_lib_handle(LIB_ART_PATH);
        if (g_ArtHandle)
        {
            c_dex_cookie = openmemory_load_dex(g_ArtHandle, (char *)g_decrypt_base, (size_t)g_dex_size, g_sdk_int);
            LOGD("[+]sdk_int :%d,c_dex_cookie:%p", g_sdk_int, c_dex_cookie);
            if (c_dex_cookie)
            {
                // 加载mini.dex
                mini_dex_obj = load_dex_fromfile(env, inPath, outPath);
                replace_cookie(env, mini_dex_obj, c_dex_cookie, g_sdk_int);
                make_dex_elements(env, classLoader, mini_dex_obj);
                LOGD("[+]using fast plan load dex finished");
            }
            else
            {
                // 执行方案二
                LOGD("[-]get c_dex_cookie failed! Try second plan");
                goto label;
            }
        }
        else
        {
            LOGD("[-]get art handle failed! Try second plan");
        label:
            // get_path_frommaps(g_pkgName, (char *)g_fake_dex_path, (char *)".dex", (char *)".odex");
            // pkgName
            sprintf((char*)g_fake_dex_magic,"%s/mini.dex",PACKER_MAGIC);
            LOGD("[+]g_faked_dex_magic:%s",(char*)g_fake_dex_magic);
            // 加载fake_dex
            mini_dex_obj = hook_load_dex_internally(env, (const char *)LIB_ART_PATH, (char *)inPath, outPath);
            
            make_dex_elements(env, classLoader, mini_dex_obj);
            LOGD("[+]using second plan load dex finished");
        }

        if (g_ArtHandle)
        {
            dlclose(g_ArtHandle);
        }
        return;
    }
}

可以看到这段代码也比较长,但总的来说可以分成以下几个步骤:

  • 读取加密文件并在内存中解密,存入一个 buffer 中

  • 根据虚拟机版本(art / dvm)进入不同逻辑

  • dvm 的逻辑参考作者文章 阿里早期加固代码还原4.4-6.0

    用mmap将dex映射内存,使用openDexFile加载dex,就会得到cookie,然后用这个cookie设置Application类中的某些成员

    这里主要还是使用的是 dvm 虚拟机自带的 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 函数来实现加载字节码,可以继续参考文章 对抗静态分析——dex不落地加载

  • Art 的逻辑参考作者的另一篇文章 Android第二代加固(support 4.4-8.1)

    Art 下提供了2种方案, 方案一是 call libart下的OpenMemory函数,如何将java的mCookie和c层的cookie联系起来是一个难点, 方案二是 使用elf Hook来实现 ,由于在Nougat+上dlsym openMemory失败,还有dex_location和dex_cache_location的路径检查,使用方案一有些问题

  • 调用 make_dex_elements 函数,作用大致上就是模拟 DexClassLoader 加载过程中对 DexPathList 对象的 dexElements 进行赋值,将部分的 Java 逻辑用 JNI 实现。

总结

写完之后才感觉总结的并不恰当,花了很多时间和片段在分析代码逻辑上,导致最后读起来非常冗长,但既然已经写了这么多最后还是不舍得删了233。

写归纳性的博客还是需要更多的归纳代码的思想和要点。

参考链接