0x00 声明
相信授人以鱼不如授人以渔。
0x01 前言
作为App安全逆向分析的第一步,针对加固手段衍生的脱壳技术正在蓬勃发展,由于本人所学知识还未能进入VMP阶段,因此本帖就基于目前常见的函数抽取壳而展开。
学术界有了不少有意思的脱壳系统。 AppSpear 搜集内存中的运行时 Dalvik 数据结构 (Dalvik Data Structure ,以下简称 DDS) 来重新组装一个正常的 Dex 文件。一般而言,一个应用程序只能通过某些固定的系统服务完成转换。因此, AppSpear 就选择通过监控某些 JNI 接口并确定何时开始收集 Dalvik 数据结构。其次,无论加固厂商加密原始数据有多复杂, Dalvik 都很少修改原始字节码的语义。 Dex 加载过程结束后,从 DDS 中就可观察到原 App 的字节码的准确内容。最后取出 anti-analysis 代码并进一步综合 Dex 文件、清单文件和其他资源文件然后重打包并进行解包分析。
有很多研究者开始对整个运行环境进行监控从而观察内存读写操作来抓住解包时机。Lei X等人提出了一种新的自适应方法,并开发了一个名为PackerGrind的新系统来脱壳应用程序。 PackerGrind是取得了一定的效果,但是如果打包的应用程序将不同的代码加载到相同的内存中,并在不同的条件下执行它们,PackerGrind就因为缺乏语义信息从而无法判断哪些代码是真实的; DROIDUNPACK 是Duan Y 等人提出的一种脱壳工具,基于整个系统仿真监视程序执行和内存操作执行。 由于DROIDUNPACK是建立在整个系统仿真之上,实施反仿真技术将不可避免地破坏后续的分析而且成本相对后面要讲到的脱壳工具来说较大。
看雪论坛中出现了不少前辈们的作品。 DexHunter是在Android系统代码调用函数dvmDefineClass(Dalvik下的函数)进行类加载之前,主动地一次性加载并初始化Dex文件所有的类。DexHunter主要关注如何在内存中定位和转储Dex。 DexHunter脱壳过程中所使用的location和fileName是其技术人员通过人工分析得到的,通过对每个加固服务的加固方式进行详细地分析和研究,选择固定的特征字符串作为脱壳开始的标志。采用这样的方法有一个明显的限制:如果加固厂商针对不同的App有不同特征的加固方法,那么DexHunter就不能再准确地定位出脱壳起点。
Fupk3是由美团大佬写的一个Android半自动脱壳机,首先遍历gDvm中的dvmUserDexFiles结构,获取所有cookie; 其次对内存中的Dex文件,遍历触发函数,并通过在解析器处插桩,截取解密后的code_item,获取code_item后直接返回不执行该函数。然后对内存中截取出来的数据进行重组,生成Dex文件。最后利用修改过的smali/baksmali对dump下来的dex文件进行修复。Fupk3基于Android 系统 KTU84P (4.4.4_r1)开发,可以对没被虚拟化的Java层函数进行转储。然而, Fupk3还是基于Dalvik环境的自动化脱壳方案,其中Fupk3首次将脱壳粒度细化到函数级别。DexHunter和Fupk3都可以应对函数抽取型加固壳。但很可惜的是,两个优秀方案以及前文提及的AppSpear由于环境的限制无法应对Android8.0以及更高版本 ART 的系统变化。
时间点来到2019年,FART横空出世,结合了DexHunter和Fupk3的优点(主动调用链不影响正常调用的构造和 CodeItem结尾计算方法 ),开始着力于ART高版本环境下函数粒度的主动调用。FART告诉我们的一个道理是:无论函数抽取壳做得有多么复杂,一定存在 某一个时刻,Dex的部分数据肯定是还原的状态。通过三种组件的配合使用可以达到一种“输入->输出”自动化的流程,并且寒冰老师也在他的文章中提及三个组件可以分离使用,这种组件独立性为结合Frida提供了无限的可能性。稍微多嘴讲一下FART的一些优秀特征:
关于Fart的脱壳点:
有两点,一个点是Execute函数,另一个点就是送到主动调用链的时候。
①初始化函数<clinit> - Execute => dumpDexFileByExecute
②其他正常函数=> DexFile_dumpMethodCode => myfartInvoke => Invoke => dumpArtMethod
关于主动调用链:
①启动fart线程-(getClassloader来获取ClassLoader)>
②fartwithClassLoader-(反射获取mCookie)>
③loadClassAndInvoke-(dumpMethodCode将各种函数转化成ArtMethod类型并送入我们的fake_Invoke参数包装)>
④送入系统的Invoke-(调用dumpArtMethod实现第二个脱壳点)。
Fart主动调用前提:
①获取appClassLoader;
②通过ClassLoader加载到所有类 ;
③通过每个类获取到该类下的所有方法【包括构造函数和普通函数】。
时间再次推移到2020年。 一款针对Dex整体加固和应对各式各样Dex抽取的脱壳机——Youpk于看雪论坛诞生。作者在参考链接中明确表示是结合了Fupk3和FART二者的优点,其特色是 着重于加深主动调用流程至Switch解释器的每条汇编指令执行前回调 。Youpk在回调中仅仅做了一项操作:对CodeItem的直接转储。当然,Youpk作者在文中提及,针对某些厂商的抽取, 可以等待几条指令真正被执行且CodeItem解密后再进行dump 。
从Youpk最关键的回调过程来分析, beforeInstructionExecute这个方法的 传参依靠ins_count即指令数就可以判断执行了多少条指令。比如ins_count等于5时代表已经执行了5条指令。不过, 我们可以通过一些加固壳中的反编译内容发现有些指令是参差不齐的,有可能关键解密相关函数一次在3条以内,一次在5条以外,如果来来回回查看个数似乎略失自动化的意义。
因此我们通过接下来结合对Youpk的分析,强化FART。
0x02 实验
Youpk在文中写到构造主动调用链时,检索Dex文件中所有的类时类的数目大小通过 NumClassDefs ()来获得,坐标在 /art/runtime/dex_file.h
而FART中调用原生方法 getClassNameList获取当前dexfile对象下的所有类名并存储为数组便于进行类的遍历,Youpk在遍历类的时候是在 unpacker.cc中进行,说明它此时已经进入了Native层,FART的遍历还是在Java层,这一区别可能涉及到Java和C++中循环开销这里先暂且不涉及太多。Youpk选择类似forName的方式 解析并初始化Class,因为初始化会帮我们链接并设置好非常多的信息。
FART中我们的Java层传递过来的method是使用反射方法加载的,但我们并没有对它真正进行调用,而是通过dumpMethodCode函数把它转为ArtMethod函数并传递给了Invoke,这流程是一种模拟调用【FART中 dumpMethodCode 这个函数就是模拟了CallxxxxxMethod的流程 】。
因此根据Youpk的解析并初始化思路,我选择在jobject2ArtMethod 中进行一些修改。因为这里是Method转ArtMethod的关键之处。 我们可以用Java到Native的方式去类似地实现一个: 观察GetMethodID函数源码实现,最终调用的 FindMethodID是以“初始化—为ArtMethod赋值—加密ArtMethod使其成为jmethodID类型”为流程进行的。不过,现在 我们需要的并不是让其加密,因为我们不是真正地去使用CallxxxxMethod,而是将其在构造后直接送入ArtMethod::Invoke。现在 目的明确且为了符合Youpk思路,我们只需要完成最重要的事情——初始化 ,然后再返回即可。
主要修改art/runtime/native/j ava_lang_reflect_Method.cc
对其他壳还好,但是在实际某款App的测试中发现这个初始化会引起一些问题( 经研究发现这个壳当看到如果 对这些activities中的关键类进行初始化就对程序进行了一种退出保护 )。
在这种情况下Youpk可能就失去了后续所有步骤的进行,因此初始化这里还需要借鉴FART的策略: 通过壳最后的ClassLoader,经过反射思路使用不带初始化过程的loadClass加载类 ,并且这里要在Java层中进行,方便将类进行传递和储存。
================================根据Youpk的思路,构造完整的主动调用链=================================
从LinkCode源码中可以知道,无论一个类方法是通过解释器执行,还是直接以本地机器指令执行,均可以通过ArtMethod类的成员函数GetEntryPointFromCompiledCode获得其入口点,并且该入口不为NULL。不过,Invoke并没有直接调用该入口点,而是通过Stub来间接调用。 这是因为ART需要设置一些特殊的寄存器。同时,ArtMethod::Invoke会在其中判断需要执行的函数时运行在什么模式的,①解释模式②Quick模式③JNI函数
我们要恢复的函数肯定是会在解释模式下执行的【这个原因我会在后面讲到】,因此我们只关注这一部分区域
首先判断是否是Native化的函数,如果是则主动调用没有意义,反之,则进入主动调用连。
可以看到,进行了静态函数和非静态函数的判断,静态函数的参数数量会比非静态少一个,因为非静态需要一个对象。 构造“完整”的主动调用链首先 一定要把握好一个重要原则“不真实执行” ,一些参数可以设置为空,但关键的参数还是得构造,比如这里我们将receiver的位置置空,由于其他位置不能是nullptr,所以都需要我们根据能执行程度而构造,例如self是当前线程、ArtMethod的位置则是this以及参数个数的位置是args和args+1。而且在构造的同时,在函数的最前方需要加入 识别主动调用链到来的标志——5201314。 我们将继续进入解释模式下的EnterInterpreterFromInvoke函数,并且当主动调用链完成调用之后我们直接return,不让它继续真实执行。
EnterInterpreterInvoke 这个函数前面部分都在做参数压栈操作,最后几行才真正进入主题。
①如果不是Native,那么调用Execute执行, Execute就是ART的解释器入口代码, Dex的字节码是通过ArtMethod::GetCodeItem函数获得,由Execute逐条执行。
②Native函数则调用InterpreterJni。 InterpreterJni通过GetEntryPointFromJni来获得native的函数的入口点,跳转并执行。
显而易见,我们想要搞定的函数肯定不是Native,因此走①。
构造思路:
在经过构造失败多次后发现,EnterInterpreterInvoke这个函数在它的逻辑中有个不得不提的操作就是它会通过SetVRegReference去追溯参数内容,因此如果参数不合法,会造成异常。 在构造这个函数内部逻辑的时候,首先想到 由于我们自定义传递的参数有一些是Null(或者因无法实际获取其真实的内容而假设构造的参数存在),因此不 让其指向 o.Ptr和receiver.Ptr 的指针 。我们选择使用SetVReg来替代,因为这是一个ART源码中默认使用的方法。 可以发现 EnterInterpreterInvoke中Switch 语句default中使用SetVReg函数来进行传参,且在最末尾明确看到它是不会去解引用参数的内容的。 SetVRegLong则与SetVReg同理:
271 void SetVReg(size_t i, int32_t val) {
272 DCHECK_LT(i, NumberOfVRegs());
273 uint32_t* vreg = &vregs_[i];
274 *reinterpret_cast<int32_t*>(vreg) = val;
275 // This is needed for moving collectors since these can update the vreg references if they
276 // happen to agree with references in the reference array.
277 if (kMovingCollector && HasReferenceArray()) {
278 References()[i].Clear();
279 }
280 }
原理是源码在非解引用的情况中均使用SetVReg和SetVRegLong来进行参数的传递,而不去使用SetVReference解引用我们构造的参数 。前面也讲到,主动调用链来的函数并非Native函数,我们最终是会进入Execute函数,因此在Execute前面的函数我们都需要保留并完整地执行完成。当真正执行完Execute之后保存结果寄存器的值,然后抛出栈帧并返回。
Execute函数中 如果有 jit,并且 jit 编译出了对应 method 的 quick code,那么选择通过 ArtInterpreterToCompiledCodeBridge 这个去执行对应的 quick code。 如果这些条件不满足,那么根据 kInterpreterImplKind 选择 Mterp 或者 Switch 类型的解释器实现来解释执行对应的 Dalvik 字节码。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-6-12 09:33
被一颗金柚子编辑
,原因: 改错字