0x00 前言
在学习android脱壳的时候在想一个点, ART是java虚拟机, 如果要执行dex文件, 需要有一个dex加载到ART虚拟机的过程, 这个加载的函数在加载的时候的dex文件必然是完整正确的dex, 除非是魔改dex的执行过程, 所以只需要找到这个加载的时机就可以把壳脱下来.
0x01 app启动过程
当点击桌面图标时, 桌面这个app会通过binder机制进行跨进程通信给AMS(ActivityManagerService)
AMS通知Zygote fork需要启动的app进程
app执行ActivityThread.main()方法
通过IBinder机制调用AMS的attachApplication函数
app接受system_server进程发来的bindApplication消息
初始化Application并执行相关回调
初始化Activtiy并执行相关回调
这里的Application可以由自己定义, 所以直接从application的加载的地方来看dex文件的加载过程.
0x02 ActivityThread.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public final class ActivityThread extends ClientTransactionHandler implements ActivityThreadInternal { public static void main(String[] args) { ActivityThread thread = new ActivityThread() ; thread.attach(false , startSeq); Looper . loop() ; } private void attach(boolean system, long startSeq) { final IActivityManager mgr = ActivityManager . getService() ; try { mgr.attachApplication(mAppThread , startSeq ) ; } } class H extends Handler { public void handleMessage(Message msg ) { if (DEBUG_MESSAGES) Slog . v(TAG, ">>> handling: " + codeToString(msg .what ) ); switch (msg.what) { case BIND_APPLICATION: Trace . traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "bindApplication" ) ; AppBindData data = (AppBindData)msg.obj; handleBindApplication(data ) ; Trace . traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER) ; break; } } } private void handleBindApplication(AppBindData data ) { try { app = data.info.makeApplicationInner(data .restrictedBackupMode , null ) ; } } }
主线程消息循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public final class Looper { public static void loop () { for (;;) { if (!loopOnce(me, ident, thresholdOverride)) { return ; } } } private static boolean loopOnce (final Looper me, final long ident, final int thresholdOverride) { Message msg = me.mQueue.next(); try { msg.target.dispatchMessage(msg); } } } public class Handler { public void dispatchMessage (@NonNull Message msg) { if (msg.callback != null ) { handleCallback(msg); } else { handleMessage(msg); } } }
0x03 application类加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public final class LoadedApk { private Application makeApplicationInner (boolean forceDefaultAppClass, Instrumentation instrumentation, boolean allowDuplicateInstances) { Application app = null ; final java.lang.ClassLoader cl = getClassLoader(); String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess( myProcessName); if (forceDefaultAppClass || (appClass == null )) { appClass = "android.app.Application" ; } app = mActivityThread.mInstrumentation.newApplication( cl, appClass, appContext); } } public class Instrumentation { public Application newApplication (ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { Application app = getFactory(context.getPackageName()) .instantiateApplication(cl, className); app.attach(context); return app; } } public class AppComponentFactory { public @NonNull Application instantiateApplication (@NonNull ClassLoader cl, @NonNull String className) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return (Application) cl.loadClass(className).newInstance(); } }
分析完上述的application加载过程, 可以清晰起来是使用类加载器来加载dex文件的. 那到这里继续往底层分析类加载器的加载过程.
这里加一点题外话(之所以说题外话是因为本文主要讲脱壳, 而这里是涉及到写壳), 就是这个类加载器的替换:
如果要实现动态dex加载, 就需要自定义类加载器. 在实现自定义类加载器之前, 需要解决一个问题, 那就是替换这里的类加载器为自定义类加载器.
1 2 3 4 5 6 7 8 9 private ClassLoader mClassLoader; public ClassLoader getClassLoader () { synchronized (mLock) { if (mClassLoader == null ) { createOrUpdateClassLoaderLocked(null ); } return mClassLoader; } }
使用java反射机制获取mclassLoader, 然后进行替换
0x04 类加载器
ClassLoader是一个抽象类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public final class LoadedApk { LoadedApk (ActivityThread activityThread) { mDefaultClassLoader = ClassLoader .getSystemClassLoader(); mAppComponentFactory = createAppFactory(mApplicationInfo, mDefaultClassLoader); mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader, new ApplicationInfo (mApplicationInfo)); } } public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader (String dexPath, ClassLoader parent) { super (dexPath, null , null , parent); } } public abstract class ClassLoader { protected Class <?> loadClass(String name, boolean resolve) throws ClassNotFoundException { Class <?> c = findLoadedClass(name); if (c == null ) { try { if (parent != null ) { c = parent.loadClass(name, false ); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { } if (c == null ) { c = findClass(name); } } return c; } }
这里的ClassLoader涉及到双亲委派机制详情请查看看雪大佬写的:动态加载和类加载机制详解
抽象类的ClassLoader为空方法, 需要具体实例加载器去重写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class BaseDexClassLoader extends ClassLoader { protected Class <?> findClass(String name ) throws ClassNotFoundException { // First, check whether the class is present in our shared libraries. if (sharedLibraryLoaders != null ) { for (ClassLoader loader : sharedLibraryLoaders) { try { return loader.loadClass(name ); } catch (ClassNotFoundException ignored) { } } } // Check whether the class in question is present in the dexPath that // this classloader operates on . List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); //进入这里分析, 上下两个load 都是调用的loadClass方法, 本质和上面类加载器的loadClass应该基本上一样 Class c = pathList.findClass(name , suppressedExceptions); if (c != null ) { return c; } // Now, check whether the class is present in the "after" shared libraries. if (sharedLibraryLoadersAfter != null ) { for (ClassLoader loader : sharedLibraryLoadersAfter) { try { return loader.loadClass(name ); } catch (ClassNotFoundException ignored) { } } } return c; } } public final class DexPathList { public Class <?> findClass(String name , List<Throwable> suppressed) { for (Element element : dexElements) { Class <?> clazz = element.findClass(name , definingContext, suppressed); if (clazz != null ) { return clazz; } } return null ; } static class Element { public Class <?> findClass(String name , ClassLoader definingContext, List<Throwable> suppressed) { return dexFile != null ? dexFile.loadClassBinaryName(name , definingContext, suppressed) : null ; } } }
这里就涉及到dexFile类, dexFile从类名上看就知道这个是用来内存存储或者管理dex文件的类, 当ART虚拟机需要的时候直接找dexFile类.加载过程就要浮出水面了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public final class DexFile { 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); } return result; } private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile) }
从这里就基本上到了native, 但是分析着分析着可以发现, 这里只是做了从DexFile加载, 并不是从dex文件映射到内存. 意思就是, 这里是ART虚拟机从内存获取类, 并不是从文件映射到内存的过程. 而指令抽取也是修改的这里来达到动态更改程序的目的.
0x05 dex文件映射到内存
回到加载Application的加载器PathClassLoader的创建过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 public final class LoadedApk { LoadedApk (ActivityThread activityThread ) { mClassLoader = mAppComponentFactory.instantiateClassLoader(mDefaultClassLoader, new ApplicationInfo(mApplicationInfo)); } } private static ClassLoader createSystemClassLoader ( ) { String classPath = System.getProperty("java.class.path" , "." ); String librarySearchPath = System.getProperty("java.library.path" , "" ); return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); } public PathClassLoader (String dexPath, String librarySearchPath, ClassLoader parent ) { super (dexPath, null , librarySearchPath, parent); } public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { this (dexPath, librarySearchPath, parent, null , null , false ); } public BaseDexClassLoader(String dexPath, String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders, ClassLoader[] sharedLibraryLoadersAfter, boolean isTrusted) { this .pathList = new DexPathList(this , dexPath, librarySearchPath, null , isTrusted); } DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { this .dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); } private static List<File> splitDexPath (String path ) { return splitPaths(path, false ); } private static List<File> splitPaths (String searchPath, boolean directoriesOnly ) { List<File> result = new ArrayList<>(); if (searchPath != null ) { for (String path : searchPath.split(File.pathSeparator)) { if (directoriesOnly) { try { StructStat sb = Libcore.os.stat(path); if (!S_ISDIR(sb.st_mode)) { continue ; } } catch (ErrnoException ignored) { continue ; } } result.add(new File(path)); } } return result; } private static Element[] makeDexElements(List<File> files, File optimizedDirectory, List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) { Element[] elements = new Element[files.size()]; int elementsPos = 0 ; for (File file : files) { if (file.isDirectory()) { elements[elementsPos++] = new Element(file); } else if (file.isFile()) { String name = file.getName(); DexFile dex = null ; if (name.endsWith(DEX_SUFFIX)) { try { dex = loadDexFile(file, optimizedDirectory, loader, elements); if (dex != null ) { elements[elementsPos++] = new Element(dex, null ); } } catch (IOException suppressed) { System.logE("Unable to load dex file: " + file, suppressed); suppressedExceptions.add(suppressed); } } else { try { dex = loadDexFile(file, optimizedDirectory, loader, elements); } catch (IOException suppressed) { } if (dex == null ) { elements[elementsPos++] = new Element(file); } else { elements[elementsPos++] = new Element(dex, file); } } if (dex != null && isTrusted) { dex.setTrusted(); } } else { System.logW("ClassLoader referenced unknown path: " + file); } } if (elementsPos != elements.length) { elements = Arrays.copyOf(elements, elementsPos); } return elements; }
到这里文件映射到内存上的过程基本上已经清晰了, 可以继续往下深入进行hook, 也可以就在这里hook得到dex, 这里已经可以得到dex文件了, 如果有魔改, 则需要自己继续往native层分析. 具体的dex文件搜索方法有很多.
0x06 dex文件dump方法
这里从一些脱壳工具的原理讲述dump方法
frida-dexdump
这个脱壳方法是直接去搜索内存, dex文件加载到内存中了之后, 直接使用暴力搜索dex文件头的方法来获取完整dex文件. 可以用来脱360.
反射大师
这是使用的一个cookies的参数, 这个参数就代表了dex文件在内存中的位置, 直接就可以dump出来
frida-hook脱壳
直接hook OpenDexFile函数, 获取返回值就可以得到dex文件
Black*
hook脱壳
如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。 也欢迎您共享此博客,以便更多人可以参与。 如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。 谢谢 !