本工具仅用于安全研究和学习目的,请勿用于非法用途
JDex2 得益于Frida优秀的Java以及Native层Hook功能,现在基于Frida的脱壳脚本层出不穷,但是对Frida的检测也日益加强,现在都不必说企业壳,免费壳都配备了不少Frida检测。而Fart类基于修改Android源码进行主动调用脱壳的方案虽然非常完美,但是编译,刷机等操作却会对于刷机小白来说门槛较高,而且对源码的修改考虑不周就可能造成无法预知的错误,而且比较难Debug(而且因为需要Linux进行编译,对电脑配置不高使用虚拟机的人来说也会不大友好)
这时Lsposed相对隐蔽的Hook手段和较低的开发成本就会让脱壳 这门技术和过检测 被划分开来,入门和实操难度也会低很多(但是也有部分厂商是上了比较强的针对lsposed的检测手段的,所以只能应对大多数企业抽取壳情况,而不是全部)
Github仓库地址写在帖子末尾,本项目依赖于Xposed/Lsposed主动调用构造方法进行抽取壳的脱壳,但是并不是非常完善,很多功能还有待完善和优化。如果你对本项目感兴趣,非常欢迎参与讨论、魔改、二次开发、提交 Issue 或分享你的思路
本项目的应用场景:
应对多数未对Lsposed进行有效检测的企业抽取壳和免费壳的Dex加固
工作流程:
1 Xposed注入 → 获取真实ClassLoader → 遍历DexFile → 主动调用构造方法触发回填 → Native层Dump
反射脱壳部分 首先在Android上,Dex的动态加载大部分都是依赖于ClassLoader的,最常见的是通过DexClassLoader,PathClassLoader和InMemoryClassLoader进行加载。大多数的壳无非就是将Dex进行不同粒度的加密,但是实际交给ART虚拟机解释执行的时候都要进行动态解密。这就是我们脱壳的核心原理,在Dex解密的时候将其从内存中Dump下来。
以下所有源码部分演示均以android-9.0.0_r61作为演示
寻找真正的ClassLoader 对于每一个App,都有一个非常核心的类:LoadedApk
它被用于描述该应用在当前进程中的所有运行时信息,其中就有我们非常感兴趣的APP的真实ClassLoader,并且还为我们提供了getClassLoader方法,返回当前LoadedApk中的mClassLoader字段
1 2 3 4 5 6 7 8 807 public ClassLoader getClassLoader () {808 synchronized (this ) {809 if (mClassLoader == null ) {810 createOrUpdateClassLoaderLocked(null );811 }812 return mClassLoader;813 }814 }
至于为什么大多数APPLoadedApk中的ClassLoader就是真实的ClassLoader:
系统在启动 Activity 时,不会使用你自定义的 ClassLoader ,而是使用 LoadedApk 中的 mClassLoader。你的 DexClassLoader 加载的类对系统框架层来说是”不可见”的,当App启动自定义的Activity,Service时就会找不到这些类。所以常见的壳会选择将解密后dex的ClassLoader插入到LoadedApk的mClassLoader中,或者在双亲委派链中加入自定义的ClassLoader。而大部分主流加固方案都采用替换 mClassLoader 的方式
因为壳需要解密 DEX 并创建全新的 ClassLoader ,这与”替换”方案天然契合。而”插入委派链”方案无法解决”原始 ClassLoader 不认识解密后 DEX”这一根本问题,且控制力,兼容性和安全性都不如直接替换
获取LoadedApk的方案有很多种,我们这里选择这条反射链获取ClassLoader,因为偶然间在Android源码中看到了这种获取LoadedApk的方式,所以可能会更稳定一些
1 2 3 181 ReflectionHelpers.setField(activityThread, "mBoundApplication" , data);182 183 LoadedApk loadedApk = activityThread.getPackageInfo(applicationInfo, null , Context.CONTEXT_INCLUDE_CODE);
1 2 3 4 ActivityThread.currentActivityThread() → mBoundApplication (AppBindData) → info (LoadedApk) → getClassLoader()
因为Xposed对于反射获取字段的封装非常友好,所以我们不需要自己写大量反射,只需要借助Xposed的API:
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 private ClassLoader getRealClassLoader (XC_LoadPackage.LoadPackageParam lpparam) { try { Class<?> atClass = Class.forName("android.app.ActivityThread" ); Object at = atClass.getMethod("currentActivityThread" ).invoke(null ); if (at == null ) throw new RuntimeException ("ActivityThread is null" ); Object boundApp = XposedHelpers.getObjectField(at, "mBoundApplication" ); if (boundApp == null ) throw new RuntimeException ("mBoundApplication is null" ); Object loadedApk = XposedHelpers.getObjectField(boundApp, "info" ); if (loadedApk == null ) throw new RuntimeException ("LoadedApk is null" ); ClassLoader cl = (ClassLoader) XposedHelpers.callMethod(loadedApk, "getClassLoader" ); if (cl != null ) { Log.e(TAG, "Got real ClassLoader: " + cl); Log.e(TAG, "ClassLoader type: " + cl.getClass().getName()); return cl; } } catch (Throwable t) { Log.e(ErrTAG, "getRealClassLoader failed" , t); } Log.w(TAG, "Fallback to lpparam.classLoader" ); return lpparam.classLoader; }
当然其实loadPackageParam.classLoader也是从LoadedApk中拿ClassLoader,虽然通过两种方式都可以获取,但是这里为了考虑到LoadedApk的ClassLoader可能被替换,而xposed获取的时机不一定是我们想要的时机,所以反射调用获取会更保险
最初想过做一个Frida那样可以枚举ClassLoader的功能:
从当前虚拟机的 Runtime 中获取唯一的 ClassLinker*,在一个 ART 线程中、并在线程被 JVM 暂停的状态下主动调用 ClassLinker::VisitClassLoaders,传入一个始终返回 true 的 visitor 回调。 该 visitor 是 Frida 构造的 native 回调函数指针 ,其签名为 bool visitor(mirror::ClassLoader* loader),(不包含 this 指针)。 ART 在遍历每一个已注册的 mirror::ClassLoader* 时,主动调用该 visitor,并将当前遍历到的 ClassLoader 作为参数传入。 Frida 在回调中将这些 loader 提升为 JNI 全局引用并保存,遍历结束后在 Java 世界中将它们包装为 java.lang.ClassLoader 对象数组返回
但是Xposed实现该功能时间成本较高,而且很多壳并不需要这种方式去脱壳,LoadedApk已经够用,所以这里就暂时不做实现了
从ClassLoader -> Art::DexFile 首先众所周知的三大ClassLoader就是
DexClassLoader
PathClassLoader
InMemoryClassLoader
而它们都继承自BaseDexClassLoader,没有使用更上层的ClassLoader是因为在BaseDexClassLoader有很方便的DexPathList用于获取DexFile
1 46 private final DexPathList pathList;
其中的dexElements属于Element这个内部类,其中我们就可以遍历所有dexElements的拿到该ClassLoader对应的所有DexFile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 64 private Element[] dexElements;static class Element {607 611 private final File path;612 613 private final DexFile dexFile;614 615 private ClassPathURLStreamHandler urlHandler;616 private boolean initialized;......
可是我们都知道ART虚拟机中的DexFile和我们真正需要的Dex相差甚远,而从Android8系列开始Android源码中去除了DexFile类中的getBytes方法,想拿到我们需要的Dex最好的方法就是拿到Native层的Art::DexFile。而恰好DexFile中有两个比较有趣的字段,是mCookie和mInternalCookie,这两个字段虽然是Object类型,但是其实是long[],内部存储的数据就是指向Art::DexFile绝对地址的指针,所以我们就可以通过将其强转为long[]之后传到Native层去进行读写来Dump(强转之前先做验证)
1 2 3 4 5 6 7 8 9 41 public final class DexFile {42 46 @ReachabilitySensitive 47 private Object mCookie;48 49 private Object mInternalCookie;
我们就可以封装这样两个函数来从BaseDexClassLoader中Dump我们需要的Dex文件
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 private List<DexFile> getAllDexFiles (ClassLoader loader) { List<DexFile> dexFiles = new ArrayList <>(); try { if (!(loader instanceof BaseDexClassLoader)) { Log.e(TAG, "Not BaseDexClassLoader: " + loader.getClass().getName()); return dexFiles; } Object pathList = XposedHelpers.getObjectField(loader, "pathList" ); Object[] dexElements = (Object[]) XposedHelpers .getObjectField(pathList, "dexElements" ); Log.e(TAG, "dexElements count: " + dexElements.length); for (Object element : dexElements) { if (element == null ) continue ; Object dexFileObj = XposedHelpers.getObjectField(element, "dexFile" ); if (dexFileObj instanceof DexFile) { dexFiles.add((DexFile) dexFileObj); } } Log.e(TAG, "Valid DexFile count: " + dexFiles.size()); } catch (Throwable t) { Log.e(ErrTAG, "getAllDexFiles failed" , t); } return dexFiles; } private void dumpDex (DexFile dexFile, String outDir) { synchronized (dumpedDexFiles) { if (dumpedDexFiles.contains(dexFile)) return ; dumpedDexFiles.add(dexFile); } try { Object cookie = XposedHelpers.getObjectField(dexFile, "mCookie" ); if (cookie instanceof long []) { long [] c = (long []) cookie; dumpDexByCookie(c, outDir); } } catch (Throwable t) { Log.e(ErrTAG, "dumpDex failed" , t); } }
64位下Art::DexFile的内存结构大致如下,我们可以使用偏移直接读,但是这里我选择使用dex\n头进行判断,避免Dump下来一大堆无效数据,当然有需求的话可以修改代码重新编译即可,直接通过对应偏移读取
1 2 3 4 5 Offset | Size | 成员 ---------------------------------------------- 0x00 8 vptr (隐式, 指向 vtable)0x08 8 begin_ (const uint8_t * const )0x10 8 unused_size_ (size_t )
因为我们Native层的读取方式相对Android对开发者的期望来说,可以说是另辟蹊径,所以自然会导致很多问题,其中最重要的问题就是读取到不可读的地址,因为有些地址可能已经被释放了,但是还是被mCookie持有,因为Android本来就没指望有人这样去读取,所以在对一些APP的测试中直接读取就会导致崩溃
所以我们需要在读取前借助pipe进行内核层读写,如果地址不可读,内核会返回 -EFAULT,不会让我们想要脱壳的进程崩溃
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 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 #include <jni.h> #include <cstdio> #include <cstring> #include <cstdint> #include <sys/stat.h> #include <unistd.h> #include <signal.h> #include <setjmp.h> #include <android/log.h> #include <vector> #define TAG "JDex2 Native" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__) static bool isAddressReadable (const void * addr, size_t len) { if (addr == nullptr ) return false ; if ((uintptr_t )addr < 0x10000 ) return false ; if (len > 4096 ) len = 4096 ; int fd[2 ]; if (pipe (fd) != 0 ) return false ; ssize_t ret = write (fd[1 ], addr, len); close (fd[0 ]); close (fd[1 ]); return ret == (ssize_t )len; } static bool isDexRegionReadable (const uint8_t * begin, uint32_t fileSize) { if (!isAddressReadable (begin, 0x70 )) return false ; if (fileSize > 4 ) { if (!isAddressReadable (begin + fileSize - 4 , 4 )) return false ; } if (fileSize > 0x1000 ) { if (!isAddressReadable (begin + fileSize / 2 , 4 )) return false ; } return true ; } static bool isDexMagic (const uint8_t * data) { return (data[0 ] == 'd' && data[1 ] == 'e' && data[2 ] == 'x' && data[3 ] == '\n' ); } static uint32_t getDexFileSize (const uint8_t * begin) { return *reinterpret_cast <const uint32_t *>(begin + 0x20 ); } static const uint8_t * findBegin (jlong dexFilePtr) { if (!isAddressReadable ((const void *)dexFilePtr, 64 )) { LOGE ("dexFilePtr 0x%llx is not readable" , (unsigned long long )dexFilePtr); return nullptr ; } for (int offset = 0 ; offset < 64 ; offset += sizeof (void *)) { const uint8_t * candidate = *((const uint8_t **)((uint8_t *)dexFilePtr + offset)); if (candidate == nullptr ) continue ; if ((uintptr_t )candidate < 0x10000 ) { LOGE ("offset %d: candidate 0x%llx too small, skip" , offset, (unsigned long long )candidate); continue ; } if (!isAddressReadable (candidate, 0x70 )) { LOGE ("offset %d: candidate 0x%llx not readable, skip" , offset, (unsigned long long )candidate); continue ; } if (isDexMagic (candidate)) { LOGE ("Found begin_ at offset %d, addr=0x%llx" , offset, (unsigned long long )candidate); return candidate; } } return nullptr ; } extern "C" JNIEXPORT jstring JNICALL Java_com_jsnow_jdex2_MainActivity_stringFromJNI (JNIEnv *env, jobject thiz) { return env->NewStringUTF ("Hello from JDex!" ); } extern "C" JNIEXPORT void JNICALL Java_com_jsnow_jdex2_JSHook_dumpDexByCookie (JNIEnv *env, jclass clazz, jlongArray cookie, jstring outDir) { if (cookie == nullptr ) return ; const char * outDirStr = env->GetStringUTFChars (outDir, nullptr ); jsize cookieLen = env->GetArrayLength (cookie); jlong* cookieElements = env->GetLongArrayElements (cookie, nullptr ); mkdir (outDirStr, 0755 ); LOGE ("cookie length = %d" , cookieLen); for (int i = 0 ; i < cookieLen; i++) { jlong dexFilePtr = cookieElements[i]; if (dexFilePtr == 0 ) continue ; LOGE ("Processing cookie[%d] = 0x%llx" , i, (unsigned long long )dexFilePtr); const uint8_t * begin = findBegin (dexFilePtr); if (begin == nullptr ) { LOGE ("cookie[%d]: cannot find valid dex data, skip" , i); continue ; } uint32_t fileSize = getDexFileSize (begin); LOGE ("cookie[%d]: file_size = %u" , i, fileSize); if (fileSize < 0x70 || fileSize > 100 * 1024 * 1024 ) { LOGE ("cookie[%d]: unreasonable file_size=%u, skip" , i, fileSize); continue ; } if (!isDexRegionReadable (begin, fileSize)) { LOGE ("cookie[%d]: dex region not readable, skip" , i); continue ; } uint32_t checksum = *reinterpret_cast <const uint32_t *>(begin + 0x08 ); char outPath[100 ]; snprintf (outPath, sizeof (outPath), "%s%u.dex" , outDirStr, fileSize); FILE* check = fopen (outPath, "r" ); if (check != nullptr ) { fclose (check); LOGE ("Already dumped: %s, skip" , outPath); continue ; } FILE* fp = fopen (outPath, "wb" ); if (fp) { size_t written = 0 ; size_t chunkSize = 4096 ; while (written < fileSize) { size_t toWrite = fileSize - written; if (toWrite > chunkSize) toWrite = chunkSize; size_t ret = fwrite (begin + written, 1 , toWrite, fp); if (ret != toWrite) { LOGE ("cookie[%d]: fwrite error at offset %zu" , i, written); break ; } written += ret; } fclose (fp); LOGE ("Dumped: %s (size=%u, written=%zu)" , outPath, fileSize, written); } else { LOGE ("Failed to write: %s" , outPath); } } env->ReleaseLongArrayElements (cookie, cookieElements, 0 ); env->ReleaseStringUTFChars (outDir, outDirStr); }
此时在Xposed成功注入到对应APP进程后新建一个Thread,sleep一会之后进行Dump就是最初的JDex1了,这也是为什么本项目叫JDex2的原因,因为之前的一版脱壳功能实在不尽人意,对于不少大家可能感兴趣的APP来说,只能脱下来已经触发逻辑的那一些类。我一般分析还是比较喜欢从字符串入手的,这样的话对和我一样的字符串党非常不友好。所以就需要上主动调用 之路了
主动调用 最初的一些设想
此小节为未实现的探索方案,仅供参考
其实最初的设计是借助一些inlineHook框架对ArtMethod::Invoke解释执行链路上的EnterInterpreterFromEntryPoint函数进行Hook,通过第三个参数ShadowFrame去获取ArtMethod和dex_pc_ptr_,然后return 0中断执行流。
该函数在Native层的实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 585 JValue EnterInterpreterFromEntryPoint (Thread* self, const CodeItemDataAccessor& accessor, 586 ShadowFrame* shadow_frame) {587 DCHECK_EQ (self, Thread::Current ());588 bool implicit_check = !Runtime::Current ()->ExplicitStackOverflowChecks ();589 if (UNLIKELY (__builtin_frame_address(0 ) < self->GetStackEndForInterpreter (implicit_check))) {590 ThrowStackOverflowError (self);591 return JValue ();592 }593 594 jit::Jit* jit = Runtime::Current ()->GetJit ();595 if (jit != nullptr ) {596 jit->NotifyCompiledCodeToInterpreterTransition (self, shadow_frame->GetMethod ());597 }598 return Execute (self, accessor, *shadow_frame, JValue ());599 }
ShadowFrame的内存结构如下
1 2 3 4 5 6 7 8 class ShadowFrame { [0x00 ] ShadowFrame* link_; [0x08 ] ArtMethod* method_; [0x10 ] JValue* result_register_; [0x18 ] const uint16_t * dex_pc_ptr_; [0x20 ] const uint16_t * dex_instructions_;
如果需要dump,就需要在之前通过mCookie去DumpDex的时候将所有Dump下来的Dex的begin和size都存起来,在这里通过dex_pc_ptr_指向的地址范围来快速判断方法位于哪个Dex。
这样或许可以避免像frida-fart那样借助自实现的GetObsoleteDexCache去确认方法属于哪个DexFile,同时也能通过dex_pc_ptr_ - begin快速定位offset回填。但是实测了一些inline hook框架,即使hook后不进行任何操作直接调用原函数,也不进行主动调用,也会在Hook之后一段时间之后崩溃,可能是调用约定或是其它原因,所以最后没有实现
此设想因为本项目最终并没有实现,所以也就不做赘述了,大家感兴趣的话可以详细了解该部分Android源码
实现主动调用 在DexFile.java中有这样一个函数,可以帮我们通过cookie获取该DexFile的所有类,从而让我们可以快速得到一个String[],进而进行构造方法的批量调用。这个cookie参数或许也可以引发大家的思考,进而发现cookie和真实Art::DexFile的关系
1 395 private static native String[] getClassNameList(Object cookie);
因为我们在实现Native层的Hook的时候遇到了比较难解决的问题,所以我们主动调用所有方法并且进行方法粒度dump的想法就化为了泡影 QwQ ,那我们只能在Java层通过对原App尽量少的操作进行脱壳。前面提到很多抽取壳都是基于类 级别进行抽取的,只要触发类中的任意一个方法就可以了
对原App破坏最小的方法调用就是构造方法 了,因为静态方法并不是每个类都有,使用静态方法进行脱壳的话就会很被动,而且调用静态方法由于不可预知的效果,对类的影响扩散可能会直接导致App崩溃,并且仅通过loadClass是无法让大部分抽取壳回填的(父类方法是肯定不行的,不然调用一个Object的方法所有类都解密了还加什么壳)
最初尝试单纯调用构造方法new实例,但是错误的参数可能会导致大量的异常甚至崩溃,并且Activity等系统类实例化是需要Looper和各种各样符合要求的参数的,而我们批量主动调用要获取这些乱七八糟的参数是非常费力的。所以我们可以添加一个Hook,来让方法不实际执行,避免导致的各种不可控的崩溃。
然后在实测中被某些壳检测到了,导致app崩溃了……
但是之前对该壳的免费版有一些了解,该壳会通过检测方法是否被非法转为Native方法从而判断是否被Hook,所以我们需要在newInstance之后解除Hook,以规避这种检测
构造参数 在我们反射获取的构造方法中,Android为我们提供了一个非隐藏的方法getParameterTypes供我们调用
1 2 3 4 5 6 7 8 9 136 public Class<?>[] getParameterTypes() {137 138 Class<?>[] paramTypes = super .getParameterTypesInternal();139 if (paramTypes == null ) {140 return EmptyArray.CLASS;141 }142 143 return paramTypes;144 }
所以我们可以将它的返回值传递进来进行参数的构造,尽量减少错误的发生(避免在Hook执行执行前遇到的参数检查导致直接异常)
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 private Object[] makeDefaultArgs(Class<?>[] paramTypes) { Object[] args = new Object [paramTypes.length]; for (int i = 0 ; i < paramTypes.length; i++) { Class<?> t = paramTypes[i]; if (t == boolean .class) args[i] = false ; else if (t == byte .class) args[i] = (byte ) 0 ; else if (t == char .class) args[i] = (char ) 0 ; else if (t == short .class) args[i] = (short ) 0 ; else if (t == int .class) args[i] = 0 ; else if (t == long .class) args[i] = 0L ; else if (t == float .class) args[i] = 0.0f ; else if (t == double .class) args[i] = 0.0 ; else args[i] = null ; } return args; }
主动调用 而在主动调用的时候我们要注意接口,注解类,抽象类,枚举类是没有构造函数的,并且本来也没有CodeItem,所以我们直接跳过这些方法避免崩溃
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 private static final XC_MethodHook BLOCK_CONSTRUCTOR = new XC_MethodHook () { @Override protected void beforeHookedMethod (MethodHookParam param) { param.setResult(null ); } }; private void invokeAllConstructors (String[] names, BaseDexClassLoader loader, List<String> blackList) { for (String name : names) { try { if (matchPrefix(blackList, name)) continue ; Class<?> clazz = XposedHelpers.findClass(name, loader); int mod = clazz.getModifiers(); if (clazz.isInterface() || clazz.isAnnotation() || clazz.isEnum() || Modifier.isAbstract(mod)) continue ; Constructor<?>[] constructors = clazz.getDeclaredConstructors(); if (constructors.length == 0 ) continue ; Constructor<?> target = constructors[0 ]; target.setAccessible(true ); Set<XC_MethodHook.Unhook> unhooks = XposedBridge.hookAllConstructors(clazz, BLOCK_CONSTRUCTOR); try { Object[] args = makeDefaultArgs(target.getParameterTypes()); if (invokeDebugger) Log.e(DebugTag, "Invoke Class: " + clazz); target.newInstance(args); } catch (Throwable ignored) { } finally { for (XC_MethodHook.Unhook unhook : unhooks) { unhook.unhook(); } } } catch (Throwable e) { Log.e(ErrTAG, "Failed: " + name, e); } } }
到这里基本上反射脱壳的方案就结束了,只需要对其主动调用后触发回填,然后DumpDex即可了
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 try { Thread.sleep(10000 ); ensureNativeLoaded(); if (!nativeLoaded) { Log.e(TAG, "Native library not loaded, abort!" ); return ; } ClassLoader realLoader = getRealClassLoader(loadPackageParam); if (!(realLoader instanceof BaseDexClassLoader)) { Log.e(TAG, "realLoader is not BaseDexClassLoader, skip invokeAllMethod" ); return ; } List<DexFile> allDexFile = getAllDexFiles(realLoader); for (DexFile dexFile : allDexFile) { if (dexFile != null ) { String[] names = getAllClassName(dexFile, whiteList, blackList); invokeAllConstructors(names, (BaseDexClassLoader) realLoader,blackList); dumpDex(dexFile, outDir); } } Log.e(TAG, "All Dex dumps completed!" ); } catch (Throwable e) { Log.e(ErrTAG, "Reflect dump failed" , e); }
Hook脱壳部分 为什么还要设置这样一个分支呢,因为之前在一些App中碰到了自身一些在Java或Native层进行动态加载的情况,因为并不像整体加壳那样想要加载Activity,Service等组件,所以不会在LoadedApk字段中。我们就需要通过Hook去获取ClassLoader
该方法的弊端也很明显,因为xposed/lsposed系列实现 Java Hook 的原理就是将方法转为Native方法后指向自己的处理函数去处理,所以Hook了不解除就很容易被检测出来,所以该分支一般情况下不推荐使用
这里我们选择Hook的是BaseDexClassLoader的构造方法和这些方法
1 2 3 4 5 String[] methodNames = { "makeDexElements" , "makePathElements" , "makeInMemoryDexElements" };
因为很多壳为了防止Hook ClassLoader导致壳被直接脱下来,以及兼容性问题,所以会选择使用这些方法向DexPathList 中的 dexElements 数组中直接插入DexFile,这样就会避免调用三大ClassLoader的构造方法。
这两个方法在Android源码中的定义如下
1 2 3 4 5 6 7 8 9 10 11 319 private static Element[] makeDexElements(List<File> files, File optimizedDirectory,320 List<IOException> suppressedExceptions, ClassLoader loader) {321 return makeDexElements(files, optimizedDirectory, suppressedExceptions, loader, false );322 }323 324 325 private static Element[] makeDexElements(List<File> files, File optimizedDirectory,326 List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {327 Element[] elements = new Element [files.size()];328 int elementsPos = 0 ;......
此时的thisObject是 DexPathList实例,通过 definingContext字段拿到对应的 ClassLoader,然后仿照之前的反射获取dex即可,这里就不做赘述了
这种情况下为了脱壳完整性也加了反射dumpdex,脱壳效果:
show
本工具的局限性
只能对抗类级别的方法抽取,而基于方法粒度的抽取则会无法进行,并且无法应对方法执行结束后重新抽取的情况,还有真正开始执行字节码才动态解密的情况
各个方面的便捷性不足,一些崩溃问题可能需要参考崩溃日志进行分析,而且不算很完善
基于Android9.0+开发,未适配Android7系列及以下,Android8稳定性未知
JNI全局引用数量超过最大上限 51200 个导致崩溃
如果类的数量过于庞大,由于每次主动调用每个类都创建一个新的匿名 XC_MethodHook 实例,即使使用了XC_MethodHook共享实例,但是只是减少了 callback 对象的创建,每次 hookAllConstructors 底层仍然会创建全局引用。所以这种情况下要么选择过滤内部类,要么使用黑名单去跳过之前dump过的包 (个人推荐),或者使用白名单每次只dump几个包下的类
高度依赖于Lsposed的隐蔽性,如果Lsposed被检测则会直接闪退无法进行脱壳
对于一些该系统下不存在的类的主动调用实例化可能导致崩溃,需要通过观察invokeDebugger的结束类去将其加入黑名单
UI写的有点草率
更新 后期更新会直接更新在Github中
项目地址 ⭐ 如果本项目对您有帮助,请点个 Star,感谢您的支持!