从内存中加载 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 函数的流程大致如下:
- 调用 ActivityThread 中 LoadedApk 对象的 makeApplication 方法来创建原始 Dex 文件的 Application 对象
- 替换 ActivityThread 中 mBoundApplication 的 mApplication 属性为原始 Dex 文件的 Application 对象
- 替换 ActivityThread 中的 mInitialApplication 属性为原始 Dex 文件的 Application 对象
- 替换 ActivityThread 中 LoadedApk 的 className 为原始 Dex 文件的 Application的name
- 替换 ActivityThread 中 mBoundApplication 里 appInfo 的 className 为原始 Dex 文件的 Application 的 name 属性
- 替换 ActivityThread 中 mProviderMap 中每个 provider 的 mContext 为原始 Dex 文件的 Application 对象
- 调用原始 Dex 文件的 Application 对象的 onCreate 方法,恢复原始 Dex 文件的组件启动
该函数的加载流程主要完成的是替换的过程,在这一点上和内存落地式没有差异,至于为何要完成替换以及如何理解相应流程,可以查看该篇博文:Android动态替换Application实现,所以关键函数还是在于 extract_file
和 mem_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。
写归纳性的博客还是需要更多的归纳代码的思想和要点。