本工具仅用于安全研究和学习目的,请勿用于非法用途
得益于Frida优秀的Java以及Native层Hook功能,现在基于Frida的脱壳脚本层出不穷,但是对Frida的检测也日益加强,现在都不必说企业壳,免费壳都配备了不少Frida检测。而Fart类基于修改Android源码进行主动调用脱壳的方案虽然非常完美,但是编译,刷机等操作却会对于刷机小白来说门槛较高,而且对源码的修改考虑不周就可能造成无法预知的错误,而且比较难Debug(而且因为需要Linux进行编译,对电脑配置不高使用虚拟机的人来说也会不大友好)
这时Lsposed相对隐蔽的Hook手段和较低的开发成本就会让脱壳这门技术和过检测被划分开来,入门和实操难度也会低很多(但是也有部分厂商是上了比较强的针对lsposed的检测手段的,所以只能应对大多数企业抽取壳情况,而不是全部)
Github仓库地址写在帖子末尾,本项目依赖于Xposed/Lsposed主动调用构造方法进行抽取壳的脱壳,但是并不是非常完善,很多功能还有待完善和优化。如果你对本项目感兴趣,非常欢迎参与讨论、魔改、二次开发、提交 Issue 或分享你的思路
本项目的应用场景:
应对多数未对Lsposed进行有效检测的企业抽取壳和免费壳的Dex加固
工作流程:
首先在Android上,Dex的动态加载大部分都是依赖于ClassLoader的,最常见的是通过DexClassLoader,PathClassLoader和InMemoryClassLoader进行加载。大多数的壳无非就是将Dex进行不同粒度的加密,但是实际交给ART虚拟机解释执行的时候都要进行动态解密。这就是我们脱壳的核心原理,在Dex解密的时候将其从内存中Dump下来。
以下所有源码部分演示均以android-9.0.0_r61作为演示
对于每一个App,都有一个非常核心的类:LoadedApk
它被用于描述该应用在当前进程中的所有运行时信息,其中就有我们非常感兴趣的APP的真实ClassLoader,并且还为我们提供了getClassLoader方法,返回当前LoadedApk中的mClassLoader字段
至于为什么大多数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的方式,所以可能会更稳定一些
因为Xposed对于反射获取字段的封装非常友好,所以我们不需要自己写大量反射,只需要借助Xposed的API:
当然其实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就是
而它们都继承自BaseDexClassLoader,没有使用更上层的ClassLoader是因为在BaseDexClassLoader有很方便的DexPathList用于获取DexFile
其中的dexElements属于Element这个内部类,其中我们就可以遍历所有dexElements的拿到该ClassLoader对应的所有DexFile
可是我们都知道ART虚拟机中的DexFile和我们真正需要的Dex相差甚远,而从Android8系列开始Android源码中去除了DexFile类中的getBytes方法,想拿到我们需要的Dex最好的方法就是拿到Native层的Art::DexFile。而恰好DexFile中有两个比较有趣的字段,是mCookie和mInternalCookie,这两个字段虽然是Object类型,但是其实是long[],内部存储的数据就是指向Art::DexFile绝对地址的指针,所以我们就可以通过将其强转为long[]之后传到Native层去进行读写来Dump(强转之前先做验证)
我们就可以封装这样两个函数来从BaseDexClassLoader中Dump我们需要的Dex文件
64位下Art::DexFile的内存结构大致如下,我们可以使用偏移直接读,但是这里我选择使用dex\n头进行判断,避免Dump下来一大堆无效数据,当然有需求的话可以修改代码重新编译即可,直接通过对应偏移读取
因为我们Native层的读取方式相对Android对开发者的期望来说,可以说是另辟蹊径,所以自然会导致很多问题,其中最重要的问题就是读取到不可读的地址,因为有些地址可能已经被释放了,但是还是被mCookie持有,因为Android本来就没指望有人这样去读取,所以在对一些APP的测试中直接读取就会导致崩溃
所以我们需要在读取前借助pipe进行内核层读写,如果地址不可读,内核会返回 -EFAULT,不会让我们想要脱壳的进程崩溃
此时在Xposed成功注入到对应APP进程后新建一个Thread,sleep一会之后进行Dump就是最初的JDex1了,这也是为什么本项目叫JDex2的原因,因为之前的一版脱壳功能实在不尽人意,对于不少大家可能感兴趣的APP来说,只能脱下来已经触发逻辑的那一些类。我一般分析还是比较喜欢从字符串入手的,这样的话对和我一样的字符串党非常不友好。所以就需要上主动调用之路了
此小节为未实现的探索方案,仅供参考
其实最初的设计是借助一些inlineHook框架对ArtMethod::Invoke解释执行链路上的EnterInterpreterFromEntryPoint函数进行Hook,通过第三个参数ShadowFrame去获取ArtMethod和dex_pc_ptr_,然后return 0中断执行流。
该函数在Native层的实现如下
ShadowFrame的内存结构如下
如果需要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的关系
因为我们在实现Native层的Hook的时候遇到了比较难解决的问题,所以我们主动调用所有方法并且进行方法粒度dump的想法就化为了泡影 QwQ ,那我们只能在Java层通过对原App尽量少的操作进行脱壳。前面提到很多抽取壳都是基于类级别进行抽取的,只要触发类中的任意一个方法就可以了
对原App破坏最小的方法调用就是构造方法了,因为静态方法并不是每个类都有,使用静态方法进行脱壳的话就会很被动,而且调用静态方法由于不可预知的效果,对类的影响扩散可能会直接导致App崩溃,并且仅通过loadClass是无法让大部分抽取壳回填的(父类方法是肯定不行的,不然调用一个Object的方法所有类都解密了还加什么壳)
最初尝试单纯调用构造方法new实例,但是错误的参数可能会导致大量的异常甚至崩溃,并且Activity等系统类实例化是需要Looper和各种各样符合要求的参数的,而我们批量主动调用要获取这些乱七八糟的参数是非常费力的。所以我们可以添加一个Hook,来让方法不实际执行,避免导致的各种不可控的崩溃。
然后在实测中被某些壳检测到了,导致app崩溃了......
但是之前对该壳的免费版有一些了解,该壳会通过检测方法是否被非法转为Native方法从而判断是否被Hook,所以我们需要在newInstance之后解除Hook,以规避这种检测
在我们反射获取的构造方法中,Android为我们提供了一个非隐藏的方法getParameterTypes供我们调用
所以我们可以将它的返回值传递进来进行参数的构造,尽量减少错误的发生(避免在Hook执行执行前遇到的参数检查导致直接异常)
而在主动调用的时候我们要注意接口,注解类,抽象类,枚举类是没有构造函数的,并且本来也没有CodeItem,所以我们直接跳过这些方法避免崩溃
到这里基本上反射脱壳的方案就结束了,只需要对其主动调用后触发回填,然后DumpDex即可了
为什么还要设置这样一个分支呢,因为之前在一些App中碰到了自身一些在Java或Native层进行动态加载的情况,因为并不像整体加壳那样想要加载Activity,Service等组件,所以不会在LoadedApk字段中。我们就需要通过Hook去获取ClassLoader
该方法的弊端也很明显,因为xposed/lsposed系列实现 Java Hook 的原理就是将方法转为Native方法后指向自己的处理函数去处理,所以Hook了不解除就很容易被检测出来,所以该分支一般情况下不推荐使用
这里我们选择Hook的是BaseDexClassLoader的构造方法和这些方法
因为很多壳为了防止Hook ClassLoader导致壳被直接脱下来,以及兼容性问题,所以会选择使用这些方法向DexPathList 中的 dexElements 数组中直接插入DexFile,这样就会避免调用三大ClassLoader的构造方法。
这两个方法在Android源码中的定义如下
此时的thisObject是 DexPathList实例,通过 definingContext字段拿到对应的 ClassLoader,然后仿照之前的反射获取dex即可,这里就不做赘述了
这种情况下为了脱壳完整性也加了反射dumpdex
由于配置文件存在本地,需要读写存储权限
MainActivity中有如下配置,会根据输入将配置写到/sdcard/config.properties路径下,也可以手动编辑,除了目标包名,其它配置多个路径可以通过,间隔
白名单和黑名单只是不做主动调用,但是仍然会进行dump

如果使用的是Lsposed,记得在Lsposed中勾选对应APP
脱壳效果展示: 
只能对抗类级别的方法抽取,而基于方法粒度的抽取则会无法进行,并且无法应对方法执行结束后重新抽取的情况,还有真正开始执行字节码才动态解密的情况
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 1天前
被JSnow编辑
,原因: