篇幅有限
完整内容及源码关注公众号:ReverseCode,发送 冲
函数抽取宣告一代壳整体保护的结束,由此进入二代壳的时代,本文将对Dalvik和Art下函数抽取的加固方法原理介绍及实现函数抽取的代码保护方案。
Dalvik
Android中实现「类方法指令抽取方式」加固方案原理解析:对dex结构简单介绍,定位函数指令地址,实现函数抽取壳的demo,对getPwd函数进行指令抽取,类被加载时重填恢复指令。
Android免Root权限通过Hook系统函数修改程序运行时内存指令逻辑:为了修改原有系统对dex加载流程,需要hook系统某些关键函数,自然可以在原有函数逻辑中添加需要自定义的逻辑功能,再添加一些恢复保护函数的填充。
源码分析
实现函数抽取壳需要保证对函数恢复的时机肯定早于函数被调用的时机,不然app逻辑被破坏了,即当函数被调用时指令流必须已经被修复了,否则app逻辑被破坏导致app崩了。再选择时机,Android中实现「类方法指令抽取方式」加固方案原理解析中选择了dexFindClass函数。
对于函数来说在被调用之前,首先dex加载(dexclassloader动态加载),对类加载的函数调用时需要进行一些准备,需要经过装载-链接-初始化,在这些加载过程中有非常多的时机供我们选择,Dalvik源代码编译生成的system/lib/libdvm.so通过ida打开,搜索文中的dexFindClass函数,获取导出的函数名可实现函数抽取。hook的时机点肯定是早于被调用的时机点,当dex被dexclassloader加载完后,需要加载dex其他类(隐式加载+显式加载)。搜索libcore库中loadClass加载一个dex中的类的流程:DexClassLoader->BaseDexClassLoader->ClassLoader的loadClass,完整体现了双亲委派的特性。
1 | 486 protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { // 为了提高加载类效率,使用父节点加载如果已经加载直接返回,否则进行双亲委派的父节点加载 |
对于使用DexClassLoader第一次加载类的parent节点是pathClassLoader或被指定的bootClassLoader,因为类只由当前的classloader加载必然是找不到的,因此进入BaseDexClassLoader的findClass实现中。
1 | 52 protected Class<?> findClass(String name) throws ClassNotFoundException { |
紧接着进入pathList.findClass,其中pathList是在BaseDexClassLoader构造函数中实例化化,跟着进入pathList.findClass,调用了dex.loadClassBinaryName查找类
1 | 317 public Class findClass(String name, List<Throwable> suppressed) { |
跟着进入loadClassBinaryName,其中调用了native层实现defineClassNative
1 | 214 public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { |
dalvik下Full Search搜索defineClassNative,进入/dalvik/vm/native/dalvik_system_DexFile.cpp的Dalvik_dalvik_system_DexFile_defineClassNative方法,如注释中所说从一个dex文件中加载一个类
1 | 349static void Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args, |
进入dvmGetRawDexFileDex,只是取出指针中的pDvmDex
1 | 62INLINE DvmDex* dvmGetRawDexFileDex(RawDexFile* pRawDexFile) { |
1 | 1413ClassObject* dvmDefineClass(DvmDex* pDvmDex, const char* descriptor, |
进入findClassNoInit,首先调用了clazz = dvmLookupClass(descriptor, loader, true);
对当前加载的类名进行计算查询,如没有则返回为空,对于第一次加载必然是没有的,接着进入pClassDef = dexFindClass(pDvmDex->pDexFile, descriptor);
,即Android中实现「类方法指令抽取方式」加固方案原理解析中选择的时机点,并通过hook掉类被加载时的时机,对抽取函数的恢复,实现函数还原,因此必然需要早于函数执行的时机,保证app正常运行。由于有些函数符号没有导出,就不是很容易进行hook,而在libdvm.so中的dexFindClass在IDA中是被导出的,方便hook。以上就是Dalvik下函数抽取壳的简单原理。
ART
Art下实现难点:dex2oat编译流程,dex2oat是可以进行脱壳,dex2oat完成了对抽取的dex进行编译生成了oat文件,后续的函数运行中,从oat中取出函数编译生成的二进制代码来执行,因此函数对dex填充后,如果时机不对,时机在dex2oat后,自然从dex2oat后那么我们动态修改的dex中的smali指令流就不会生效,因为后面app运行调用的真正的代码就会从dex2oat编译生成的oat文件,和以前的dex无关了。因此如果希望填充回去smali指令生效要么禁用dex2oat实现阻止编译,这样对加载到内存中的dex文件进行填充始终会保持生效,要么保持dex2oat编译,但是还原代码时机要早于dex2oat就ok了,保证dex2oat再次对dex编译的时候,dex已经是一个完整dex,不会影响我们填充的代码,但是肯定dex文件存在完整的时候,可以利用dex2oat编译的流程进行脱壳,一般加壳厂商都是牺牲掉app一部分的运行效率,干掉dex2oat的过程,因为google本身提倡dex2oat就是为了提升app运行效率。
禁用dex2oat编译
回到ART下DexClassLoader动态加载dex的流程,在art下搜索GenerateOatFileNoChecks,该函数完成调用dex2oat进行编译
GenerateOatFileNoChecks中调用Dex2Oat函数
1 | 616OatFileAssistant::ResultOfAttemptToUpdate OatFileAssistant::GenerateOatFileNoChecks( |
Dex2Oat最终调用return Exec(argv, error_msg);
,Exec中调用了int status = ExecAndReturnCode(arg_vector, error_msg);
,其中ExecAndReturnCode通过pid_t pid = fork();
进行fork一个子进程,在子进程中调用了execve(program, &args[0], envp);
完成对dex2oat这个二进制程序的调用。整个流程中任意一个环节被打断,dex2oat将会被干掉,无法继续运行。
TurboDex就是干掉dex2oat为了让dex在第一次动态加载时快速加载完成,因为不干掉dex2oat,art虚拟机就会调用dex2oat对当前的dex进行编译,编译过程非常耗时,可以很大地提升dexclassloader加载dex 的效率,该项目就是通过hook了execv方法实现。
众所周知,Android中在Runtime加载一个 未优化的Dex文件 (尤其在 ART 模式)需要花费 很长的时间. 当你在App中使用 插件化框架 的时候, 首次加载插件就需要耗费很长的时间.
TurboDex 就是为了解决这一问题而生, 就像是给AndroidVM开启了上帝模式, 在引入TurboDex后, 无论你加载了多大的Dex文件,都可以在毫秒级别内完成.
接下来通过hook execve实现干掉dex2oat,可以通过爱奇艺xhook的GOT进行表hook干掉dex2oat,也可以用inline库hook。
SecondShell_80项目中:hooklibc中的execve的函数,干掉dexclassloader加载dex过程中dex2oat的流程。
1 | // 系统函数(在libc库中定义)被调用进入替换原始逻辑 |
启动SecondShell_80项目,并授予sdcard权限,查看logcat,检索dex2oat
FART正餐前甜点:ART下几个通用简单高效的dump内存中dex方法中找一个好时机点实现art下二代函数抽取壳对抽空函数的还原,LoadClassMembers完成对要加载的class的准备工作,准备SetSFields
,SetDirectMethodsPtr
,要对当前已经加载的dex内容进行修改某一个被抽空的函数,首先定位到当前函数的CodeItem的地址进行修复,这过程中出现的时机点可以hook掉。LoadMethod设置了CodeItem的偏移,ArtMethod* method = klass->GetDirectMethodUnchecked(i, image_pointer_size_);
初始化了method对象,此时内容还没有被填充,ArtMethod中关键变量dex_code_item_offset,在我们class被加载完后,准备好当前class每个函数对应的ArtMethod对象,禁用掉dex2oat以后,自然类中所有的函数都在解释模式下运行,必然找到当前ArtMethod的CodeItem在内存中的偏移,进行取出一条条smali指令流解释执行。LoadMethod(self, dex_file, it, klass, method);
传入的dex_file为当前的dex对象,method为当前要准备的ArtMethod对象,每个ArtMethod对象都和java层的函数一一对应,这些函数都可以被hook掉,因为都在函数被调用前执行,都能实现对其的填充。
1 | 3305void ClassLinker::LoadMethod(const DexFile& dex_file, |
如果hook了LoadMethod函数,被调用完后ArtMethod对象的CodeItemOffset就完成了设置,且第五个参数就是ArtMethod对象的指针,可以很容易取出ArtMethod参数的CodeItemOffset,定位到了当前要填充的函数的smali指令流在内存中的偏移位置,避免了大量代码解析dex在内存中的映射。
实现函数抽取壳
案例用loadDex.apk和4.dex。
定位函数要抽取的位置com.kanxue.test02.TestClass.testFunc
的函数置空。使用010Editor打开4.dex,方法太多了导出csv
导出的csv用010打开,检索TestClass,出现索引号1028
回过头来再看4.dex中的TestClass的位置
code_item占了32个字节
GDA查看4.dex该方法,右键-Show Hex
对应010中选中部分全部改成0,即完成函数抽取。
dex header本身对dex校验,改完后还需要修改校验头部分
python checksum.py 返回CheckSum = 0x2785815e,则修改校验头部分,使其成为一个合法的dex文件
gda再次打开该dex,完成函数抽取。
接下来完成对testFunc的填充,并正常调用testFunc中的代码。
- 通过hooklibc的execve禁用掉dex2oat
- 选中了LoadMethod函数的hook,原型是oriloadmethod,并添加了自己的逻辑。需要当testFunc被初始化时需要将原有的smali指令流利用myloadmethod填充回去,
1 | void hookART() { |
将修复后的4_chouqu.dex推送到/sdcard中
通过初始化后的code_item的偏移直接定位dex在内存中地址,并进行修正,实现art下函数抽取的解决方案,并没有通过大量代码art中的头文件去解析dex结构信息。