一招一式讲Android安全--【加固】运行时加载隐藏dex
2021-04-23 19:00:40 Author: mp.weixin.qq.com(查看原文) 阅读量:292 收藏

本文为看雪论坛优秀文章

看雪论坛作者ID:chinamima

一、概述

1. 背景

本系列会把Android加固一系列的保护原理和思路讲解一遍,仅作为归档整理。


包括dex、so、反调试等。

2. 目的

本文主要以Android 9.0代码为蓝本进行研究。


希望把加载隐藏dex的思路和原理讲明白,详细的细节,各个步奏,等等。

二、ClassLoader机制介绍

1. ClassLoader类关系

2. ClassLoader部分源码

// DexClassLoader.javapublic class DexClassLoader extends BaseDexClassLoader {    public DexClassLoader(String dexPath, String optimizedDirectory,            String libraryPath, ClassLoader parent) {        super(dexPath, new File(optimizedDirectory), libraryPath, parent);    }} // PathClassLoader.javapublic 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.0public 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

1. 调用链路

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中

2. 详细源码

2.1. ActivityThread.performLaunchActivity

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;    }}

2.2. ActivityThread.createBaseContextForActivity

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;    }}

2.3. ContextImpl.createActivityContext

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;    }}

2.4. LoadedApk.getClassLoader

package android.app;public final class LoadedApk {     private ClassLoader mClassLoader;     public ClassLoader getClassLoader() {        synchronized (this) {            if (mClassLoader == null) {                // ==========>>> 关键步奏 <<<==========                createOrUpdateClassLoaderLocked(null /*addedPaths*/);            }            return mClassLoader;        }    }}

调用createOrUpdateClassLoaderLocked。

2.5. LoadedApk.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

2.6. ApplicationLoaders.getClassLoader

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创建。

2.7. 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源码路径

1. BaseDexClassLoader

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;    }    }

2. DexPathList

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;    }}

3. Element

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;    }}

4. DexFile

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);}

5. dalvik_system_DexFile

/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)) {    //...省略,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

1. 关键点

可以看到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的类。

2. 思路

替换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。

3. 实现

根据DexFile的构造函数可知。


调用DexFile的openDexFile得到一个新的cookie,用于替换原始的mCookie。

4. 思考

openDexFile需要一个dex的路径,也就是dex需要作为文件释放到某处,容易被截获。

art模式下,openDexFile会调用dex2oat生成oat文件,造成一定的性能损耗。

六、替换ClassLoader

1. 覆盖ClassLoader成员变量

2. 替换LoadedApkClassLoader

由初始化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。

1. 从内存中加载dex,避免生成oat

(1) 调用链路

DexFile.openInMemoryDexFile ├─ DexFile_createCookieWithDirectBuffer │  └─ CreateSingleDexFileCookie │      └─ CreateDexFile │          └─ ArtDexFileLoader::open │              └─ ArtDexFileLoader::OpenCommon └──────── ConvertDexFilesToJavaArray

关键点在于ArtDexFileLoader::open,此处传递了一个kNoOatDexFile参数,明显是无oat文件的意思。

(2) 详细源码

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;}

- End -

看雪ID:chinamima

https://bbs.pediy.com/user-home-633423.htm

  *本文由看雪论坛 chinamima 原创,转载请注明来自看雪社区。
经验:从事Android行业10年,Android安全7年,网络安全2年。
《安卓高级研修班》2021年6月班火热招生中!

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458383904&idx=1&sn=93f34d0f3bd9ea6392fa0a5836732dc6&chksm=b180c6aa86f74fbc35bc3d7a79e4231e5c035cd9e7cdcc2210d5eda6eac49370d31a10676ed7#rd
如有侵权请联系:admin#unsafe.sh