本文为看雪论坛优秀文章
看雪论坛作者ID:chinamima
一、概述
本系列会把Android加固一系列的保护原理和思路讲解一遍,仅作为归档整理。
本文主要以Android 9.0代码为蓝本进行研究。
希望把加载隐藏dex的思路和原理讲明白,详细的细节,各个步奏,等等。
二、ClassLoader机制介绍
// DexClassLoader.java
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
// PathClassLoader.java
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
* DexClassLoader和PathClassLoader都可以加载外部dex。
* 两个类的区别在于DexClassLoader可以指定optimizedDirectory,但在android 8.0以后optimizedDirectory已经废弃掉了。
* optimizedDirectory是dex2oat生成的.oat、.odex文件存放位置。
* optimizedDirectory为空,则会在默认位置生成.oat、.odex文件,因此源码里用PathClassLoader作为ClassLoader的实例化类。
//android 8.0
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
}
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
if (reporter != null) {
reportClassLoaderChain();
}
}
BaseDexClassLoader的构造函数并没有把optimizedDirectory传递给DexPathList。
三、初始化ClassLoader
ActivityThread.performLaunchActivity //启动activity
├─ ActivityThread.createBaseContextForActivity //创建ContextImpl,并作为app的Context实现
│ └─ ContextImpl.createActivityContext //从LoadedApk获取ClassLoader,并创建ContextImpl
│ └─ LoadedApk.getClassLoader //判断是否已有ClassLoader,如无则调用createOrUpdateClassLoaderLocked
│ └─ LoadedApk.createOrUpdateClassLoaderLocked
│ └─ ApplicationLoaders.getClassLoader
│ └─ ClassLoaderFactory.createClassLoader
│ └─ new PathClassLoader
└─ ContextImpl.getClassLoader //LoadedApk.getClassLoader已经生成了ClassLoader,并存于ContextImpl的mClassLoader中
package android.app;
public final class ActivityThread extends ClientTransactionHandler {
/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
//...省略,chinamima
// ==========>>> 创建ContextImpl <<<==========
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
//从ContextImpl里获取ClassLoader
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
//...省略,chinamima
} catch (Exception e) {
//...省略,chinamima
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
//...省略,chinamima
if (activity != null) {
//...省略,chinamima
appContext.setOuterContext(activity);
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
//...省略,chinamima
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
//...省略,chinamima
r.activity = activity;
}
mActivities.put(r.token, r);
} catch (SuperNotCalledException e) {
throw e;
} catch (Exception e) {
//...省略,chinamima
}
return activity;
}
}
package android.app;
public final class ActivityThread extends ClientTransactionHandler {
private ContextImpl createBaseContextForActivity(ActivityClientRecord r) {
int displayId = ActivityManager.getService().getActivityDisplayId(r.token);
//...省略,chinamima
// ==========>>> 生成ContextImpl实例,里面会生成一个默认ClassLoader成员变量 <<<==========
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, r.activityInfo, r.token, displayId, r.overrideConfig);
//...省略,chinamima
return appContext;
}
}
package android.app;
class ContextImpl extends Context {
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, ActivityInfo activityInfo, IBinder activityToken, int displayId,
Configuration overrideConfiguration) {
//...省略,chinamima
// ==========>>> 从LoadedApk里获取ClassLoader <<<==========
ClassLoader classLoader = packageInfo.getClassLoader();
//...省略,chinamima
//创建一个ContextImpl实例
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, activityInfo.splitName,
activityToken, null, 0, classLoader);
//...省略,chinamima
return context;
}
}
package android.app;
public final class LoadedApk {
private ClassLoader mClassLoader;
public ClassLoader getClassLoader() {
synchronized (this) {
if (mClassLoader == null) {
// ==========>>> 关键步奏 <<<==========
createOrUpdateClassLoaderLocked(null /*addedPaths*/);
}
return mClassLoader;
}
}
}
调用createOrUpdateClassLoaderLocked。
package android.app;
public final class LoadedApk {
private void createOrUpdateClassLoaderLocked(List<String> addedPaths) {
//...省略,chinamima
// If we're not asked to include code, we construct a classloader that has
// no code path included. We still need to set up the library search paths
// and permitted path because NativeActivity relies on it (it attempts to
// call System.loadLibrary() on a classloader from a LoadedApk with
// mIncludeCode == false).
if (!mIncludeCode) {
if (mClassLoader == null) {
//...省略,chinamima
// ==========>>> 关键步奏 <<<==========
mClassLoader = ApplicationLoaders.getDefault().getClassLoader(
"" /* codePath */, mApplicationInfo.targetSdkVersion, isBundledApp,
librarySearchPath, libraryPermittedPath, mBaseClassLoader,
null /* classLoaderName */);
//...省略,chinamima
}
return;
}
//...省略,chinamima
}
}
调用ApplicationLoaders.getClassLoader获取ClassLoader。
package android.app;
public class ApplicationLoaders {
public static ApplicationLoaders getDefault() {
return gApplicationLoaders;
}
ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
String librarySearchPath, String libraryPermittedPath,
ClassLoader parent, String classLoaderName) {
// For normal usage the cache key used is the same as the zip path.
// ==========>>> 调用另一个getClassLoader <<<==========
return getClassLoader(zip, targetSdkVersion, isBundled, librarySearchPath,
libraryPermittedPath, parent, zip, classLoaderName);
}
private ClassLoader getClassLoader(String zip, int targetSdkVersion, boolean isBundled,
String librarySearchPath, String libraryPermittedPath,
ClassLoader parent, String cacheKey,
String classLoaderName) {
/*
* This is the parent we use if they pass "null" in. In theory
* this should be the "system" class loader; in practice we
* don't use that and can happily (and more efficiently) use the
* bootstrap class loader.
*/
ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();
synchronized (mLoaders) {
if (parent == null) {
parent = baseParent;
}
/*
* If we're one step up from the base class loader, find
* something in our cache. Otherwise, we create a whole
* new ClassLoader for the zip archive.
*/
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(cacheKey);
if (loader != null) {
return loader;
}
//...省略,chinamima
// ==========>>> 工厂模式生成PathClassLoader实例 <<<==========
ClassLoader classloader = ClassLoaderFactory.createClassLoader(
zip, librarySearchPath, libraryPermittedPath, parent,
targetSdkVersion, isBundled, classLoaderName);
//...省略,chinamima
mLoaders.put(cacheKey, classloader);
return classloader;
}
// ==========>>> 工厂模式生成PathClassLoader实例 <<<==========
ClassLoader loader = ClassLoaderFactory.createClassLoader(
zip, null, parent, classLoaderName);
return loader;
}
}
private final ArrayMap<String, ClassLoader> mLoaders = new ArrayMap<>();
private static final ApplicationLoaders gApplicationLoaders = new ApplicationLoaders();
如mLoader已存在,则直接返回。
如mLoader不存在,则调用ClassLoaderFactory.createClassLoader创建。
package com.android.internal.os;
public class ClassLoaderFactory {
public static ClassLoader createClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, String classloaderName) {
if (isPathClassLoaderName(classloaderName)) {
// ==========>>> 新建PathClassLoader实例 <<<==========
return new PathClassLoader(dexPath, librarySearchPath, parent);
} else if (isDelegateLastClassLoaderName(classloaderName)) {
return new DelegateLastClassLoader(dexPath, librarySearchPath, parent);
}
throw new AssertionError("Invalid classLoaderName: " + classloaderName);
}
public static ClassLoader createClassLoader(String dexPath,
String librarySearchPath, String libraryPermittedPath, ClassLoader parent,
int targetSdkVersion, boolean isNamespaceShared, String classloaderName) {
// ==========>>> 调用另一个createClassLoader <<<==========
final ClassLoader classLoader = createClassLoader(dexPath, librarySearchPath, parent,
classloaderName);
//...省略,chinamima
return classLoader;
}
}
新建一个PathClassLoader实例。
四、findClass源码路径
package dalvik.system;
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String librarySearchPath, ClassLoader parent, boolean isTrusted) {
super(parent);
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
//...省略,chinamima
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
// ==========>>> 从DexPathList里找类 <<<==========
Class c = pathList.findClass(name, suppressedExceptions);
//...省略,chinamima
return c;
}
}
package dalvik.system;
final class DexPathList {
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// ==========>>> 从Element里找类 <<<==========
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
//...省略,chinamima
return null;
}
}
static class Element {
private final File path;
private final DexFile dexFile;
public Element(DexFile dexFile, File dexZipPath) {
this.dexFile = dexFile;
this.path = dexZipPath;
}
public Class<?> findClass(String name, ClassLoader definingContext,
List<Throwable> suppressed) {
// ==========>>> 从DexFile里找类 <<<==========
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
: null;
}
}
java/dalvik/system/DexFile.java
public final class DexFile {
private Object mCookie;
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}
private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List<Throwable> suppressed) {
Class result = null;
try {
// ==========>>> 关键步奏 <<<==========
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
} catch (ClassNotFoundException e) {
}
return result;
}
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie,
DexFile dexFile);
}
static jclass DexFile_defineClassNative(JNIEnv* env,
jclass,
jstring javaName,
jobject javaLoader,
jobject cookie,
jobject dexFile) {
std::vector<const DexFile*> dex_files;
const OatFile* oat_file;
if (!ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)) {
//...省略,chinamima
return nullptr;
}
ScopedUtfChars class_name(env, javaName);
//...省略,chinamima
const std::string descriptor(DotToDescriptor(class_name.c_str()));
const size_t hash(ComputeModifiedUtf8Hash(descriptor.c_str()));
for (auto& dex_file : dex_files) {
const DexFile::ClassDef* dex_class_def =
OatDexFile::FindClassDef(*dex_file, descriptor.c_str(), hash);
if (dex_class_def != nullptr) {
ScopedObjectAccess soa(env);
ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
StackHandleScope<1> hs(soa.Self());
Handle<mirror::ClassLoader> class_loader(
hs.NewHandle(soa.Decode<mirror::ClassLoader>(javaLoader)));
ObjPtr<mirror::DexCache> dex_cache =
class_linker->RegisterDexFile(*dex_file, class_loader.Get());
//...省略,chinamima
// ==========>>> 关键步奏 <<<==========
ObjPtr<mirror::Class> result = class_linker->DefineClass(soa.Self(),
descriptor.c_str(),
hash,
class_loader,
*dex_file,
*dex_class_def);
class_linker->InsertDexFileInToClassLoader(soa.Decode<mirror::Object>(dexFile),
class_loader.Get());
if (result != nullptr) {
VLOG(class_linker) << "DexFile_defineClassNative returning " << result
<< " for " << class_name.c_str();
return soa.AddLocalReference<jclass>(result);
}
}
}
VLOG(class_linker) << "Failed to find dex_class_def " << class_name.c_str();
return nullptr;
}
cookie通过ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)转换成DexFile*实例。
再通过class_linker->DefineClass从DexFile*里加载类。
五、替换mCookie
可以看到DexFile_defineClassNative里的ConvertJavaArrayToDexFiles函数把cookie和dex_files、oat_file关联了起来。
/art/runtime/native/dalvik_system_DexFile.cc
static jclass DexFile_defineClassNative(JNIEnv* env,
jclass,
jstring javaName,
jobject javaLoader,
jobject cookie,
jobject dexFile) {
std::vector<const DexFile*> dex_files;
const OatFile* oat_file;
if (!ConvertJavaArrayToDexFiles(env, cookie, /*out*/ dex_files, /*out*/ oat_file)) {
VLOG(class_linker) << "Failed to find dex_file";
DCHECK(env->ExceptionCheck());
return nullptr;
}
//...省略,chinamima
}
从这里可以猜测得出,cookie和dex_files、oat_file是一对一关系的。
因此,只需要把cookie替换成另一个dex的值,就可以加载另一个dex的类。
替换cookie值在native层比较难操作,因此我们把目光放到java层。
java/dalvik/system/DexFile.java
public final class DexFile {
private Object mCookie;
DexFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
mCookie = openDexFile(fileName, null, 0, loader, elements);
mInternalCookie = mCookie;
mFileName = fileName;
}
//...省略,chinamima
}
在DexFile里也有一个mCookie,通过 defineClassNative传参到DexFile_defineClassNative的。
也就是替换mCookie即可达成目的--加载其他dex。
根据DexFile的构造函数可知。
调用DexFile的openDexFile得到一个新的cookie,用于替换原始的mCookie。
4. 思考
openDexFile需要一个dex的路径,也就是dex需要作为文件释放到某处,容易被截获。
art模式下,openDexFile会调用dex2oat生成oat文件,造成一定的性能损耗。
六、替换ClassLoader
由初始化ClassLoader的调用链路可知,LoadedApk保存了ClassLoader的实例,实际上这个实例就是findClass时用到的PathClassLoader。
因此,新new一个ClassLoader并替换掉LoadedApk里的mClassLoader即能实现替换dex效果。
七、添加到DexPathList
package dalvik.system;
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
//...省略,chinamima
}
return c;
}
}
package dalvik.system;
final class DexPathList {
private Element[] dexElements;
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
Class<?> clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
//...省略,chinamima
return null;
}
}
从上面代码可知,ClassLoader是从DexPathList pathList里加载class的。
而DexPathList是由Element[] dexElements组成的,在findClass最先从数组最前面取Element。
因此,我们可以在DexPathList.dexElements的前面插入隐藏dex的Element,从而实现加载隐藏dex。
八、直接加载dex byte
public final class DexFile {
private Object mCookie;
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());
}
}
}
发现DexFile.openInMemoryDexFile可以直接加载buffer,因此可以从内存中加载dex。
DexFile.openInMemoryDexFile
├─ DexFile_createCookieWithDirectBuffer
│ └─ CreateSingleDexFileCookie
│ └─ CreateDexFile
│ └─ ArtDexFileLoader::open
│ └─ ArtDexFileLoader::OpenCommon
└──────── ConvertDexFilesToJavaArray
关键点在于ArtDexFileLoader::open,此处传递了一个kNoOatDexFile参数,明显是无oat文件的意思。
dalvik_system_DexFile.cc
static jobject DexFile_createCookieWithDirectBuffer(JNIEnv* env,
jclass,
jobject buffer,
jint start,
jint end) {
//...省略,chinamima
std::unique_ptr<MemMap> dex_mem_map(AllocateDexMemoryMap(env, start, end));
//...省略,chinamima
//创建单个DexFile实例,并转成cookie
return CreateSingleDexFileCookie(env, std::move(dex_mem_map));
}
static jobject CreateSingleDexFileCookie(JNIEnv* env, std::unique_ptr<MemMap> data) {
//创建DexFile
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));
//转成cookie
return ConvertDexFilesToJavaArray(env, nullptr, dex_files);
}
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;
const ArtDexFileLoader dex_file_loader;
//打开DexFile
std::unique_ptr<const DexFile> dex_file(dex_file_loader.Open(location, //Anonymous-DexFile@0xaabbccdd-0xeeff0011
0,
std::move(dex_mem_map),
/* verify */ true,
/* verify_location */ true,
&error_message));
//...省略,chinamima
return dex_file.release();
}
art_dex_file_loader.cc
static constexpr OatDexFile* kNoOatDexFile = nullptr;
std::unique_ptr<const DexFile> ArtDexFileLoader::Open(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) const {
ScopedTrace trace(std::string("Open dex file from RAM ") + location);
return OpenCommon(base,
size,
/*data_base*/ nullptr,
/*data_size*/ 0u,
location,
location_checksum,
oat_dex_file, //有oat文件
verify,
verify_checksum,
error_msg,
/*container*/ nullptr,
/*verify_result*/ nullptr);
}
std::unique_ptr<const DexFile> ArtDexFileLoader::Open(const std::string& location,
uint32_t location_checksum,
std::unique_ptr<MemMap> map,
bool verify,
bool verify_checksum,
std::string* error_msg) const {
//...省略,chinamima
std::unique_ptr<DexFile> dex_file = OpenCommon(map->Begin(),
map->Size(),
/*data_base*/ nullptr,
/*data_size*/ 0u,
location,
location_checksum,
kNoOatDexFile, //无oat文件!
verify,
verify_checksum,
error_msg,
std::make_unique<MemMapContainer>(std::move(map)),
/*verify_result*/ nullptr);
//...省略,chinamima
return dex_file;
}
看雪ID:chinamima
https://bbs.pediy.com/user-home-633423.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!