本来计划年后跑路的,不知道是我太菜,还是疫情原因,投简历都没人搭理我。现在又不能出门,只好自己找点事干了。
本文基于Android8.1分析。如果不想看分析过程,可以直接跳到最后的总结。
自己随便写个app,上传到百度开发者平台去加固。
加固后反编译看下。包名com.example.test下原来的类都没了,多了个com.baidu.protect,assets下面多了几个文件,lib下面多了一个so。猜测是通过libbaiduprotect.so将assets下的文件解密出dex,然后加载。
通过AndroidManifest.xml知道最先执行的class为com.baidu.protect.StubApplication
找到attachBaseContext方法,可以看到先通过Debug.isDebuggerConnected检测是否被调试,如果被调试就不会加载so,直接进行application替换,这样肯定是不行的,因为原来的class都没有被加载进来,程序直接就会崩溃,所以要调试的话需要把这里过掉。
然后看loadLibrary方法,可以看到直接System.loadLibrary(AppInfo.LIBNAME)加载so,AppInfo.LIBNAME就是baiduprotect。
将libbaiduprotect.so用ida打开。可以看到有.init_array,这个数组包含好几个函数,而其他很多函数,包括JNI_OnLoad都被加密了。
现在详细分析.init_array中的每一个函数。
先看第一个函数sub_88060,一看就是被混淆过的。
先跳过第一个函数,看下后面的。进到函数sub_6FC4,看下c代码。没有被加密,也没有被混淆,但是其中调用的loc_3E73C被加密了。
现在只能回去分析第一个函数sub_88060了,把垃圾代码删除之后,分析出调用流程,发现全是一些字符操作,应该是在解密,没有发现有反调试的地方。
所以直接动态调试,当第一个函数sub_88060执行完后,dump解密后的so。将其用ida打开,可以看到函数都已经被解密了。
继续分析.init_array中的函数,通过分析,从sub_6FC4到sub_7578这8个函数都在做一件事情,就是向一个列表添加函数,每个函数添加的时候会指定一个数字(索引),将其通过索引排序插入列表。后面的过程中会按顺序调用列表中所有函数。
这8个函数这里就分析其中的sub_6FC4,其余的都类似,有兴趣的可以自己去看。
为了防止静态分析,so中所有字符串都被加密的,每个字符串的解密函数都不一样,但是原理都是一样的,只是解密用的key不一样。这里sub_28C28就是一个将字符串解密的函数,然后通过函数sub_3E590初始化一个调用对象,这里的第三个参数(1)就是索引,off_BB5E0是一个指针,里面存着调用的函数地址,qword_BE7B8就是函数列表,函数sub_3E73C用于将刚刚构造的调用对象添加到列表中。
最后分析得到一个索引(即调用顺序)和函数的映射关系。
.init_array中最后两个函数sub_76BC和sub_7744是初始化tls和mutex相关的,就不看了。
.init_array分析完了,就该JNI_OnLoad了。可以看到它也被混淆了。
将垃圾代码删除后,得到真实流程。开始我以为会通过时间来判断是否被调试,其实这里获取时间只是统计信息上报。
这里的函数sub_3E628就是调用队列中的所有函数,它的第2个参数会被作为参数传到函数中,判断函数是否该执行。
所以,首先用1作为参数调用队列中的所有函数,通过分析只有sub_B3B4会执行。该函数通过dlsym将libc中的符号获取保存下来,之后调用这些函数都通过指针调用。
继续回到JNI_OnLoad,执行完函数队列中的函数,就该执行函数sub_91E4了,该函数是注册com.baidu.protect.A中的部分本地函数,n001,n002,n003,分别对应的函数为sub_9318,sub_94E4,sub_9564
继续回到JNI_OnLoad,注册函数后,失败就直接返回了,成功则以2作为参数再次调用队列中的所有函数。通过分析,只有sub_3E29C会执行。可以看到,该函数可以接受2和3,现在我们只看参数为2的情况。
现在进入sub_3E36C,其主要就是调用sub_3E3F0,而sub_3E3F0就是通过读取/proc/self/maps文件,通过加载的虚拟机文件,判断虚拟机类型。
至此,libbaiduprotect.so的加载流程就执行完了。
现在回到java层,调用com.baidu.protect.A.n001方法,通过前面分析可以,该方法对应的本地函数为sub_9318
sub_9318中将参数保存起来,然后最主要就是调用了sub_781C,
sub_781C也是被混淆的,但是代码很少,稍微看下,就知道只做了一件事,就是用3作为参数调用函数队列中的所有函数。
通过分析,会有sub_3E29C、sub_40CF8、sub_3DFC4、sub_11F5C、sub_45964这几个函数执行。
先看sub_3E29C,这个函数之前执行过参数为2的部分,现在来看参数为3的部分。先执行sub_13880,通过分析,该函数是获取apk包的签名,然后计算签名的MD5。然后sub_66064将签名的MD5值进行扩展,变为176字节。然后将签名的MD5和扩展后的内容存放在qword_BE690。
再看sub_40CF8,该函数可以接受3和4作为参数,现在先看3,
调用sub_409E0,它先注册com.baidu.protect.CrashHandler的本地函数,然后调用init方法。然后用sigaction设置信号4、6、7、8、11的回调sub_40BD0。
再看sub_3DFC4,这个函数最主要就是sub_3D6AC,通过解析apk中assets,生成各种路径,然后通过qword_BE2F0生成目录,qword_BE2F0就是之前从libc中获取的mkdir的指针。
再看sub_11F5C,该函数只是调用了sub_BC60,sub_BC60也被混淆了,删除垃圾代码后,流程如下。因为我用的手机是8.1的,所以只看了sdk大于26的,
先看sub_188AC,该函数也是被混淆的,删除垃圾代码后流程如下,
sub_4029C读取/proc/self/maps文件,查找对应的so,修改内存页属性
sub_3FF9C是对函数进行hook,通过动态节,找到重定位节和got,然后替换指定标签的地址。这里分别hook了__android_log_print和mmap,__android_log_print被替换为sub_1B044,这是个空函数,禁用log。mmap被替换为sub_1B070,在加载dex的时候有用,稍后分析。
再看sub_11AB0,最主要的是下面的这个循环,将assets下所有的jar解密为dex,然后通过InMemoryDexClassLoader加载,然后提取DexPathList$Element添加到源classloader中。
对于每个assets/baiduprotect*.jar,它由以下几部分组成,
先看sub_3BA90,该函数用于解密dex,其中sub_9B2C用于获取apk包中的所有文件目录,sub_A104用于检查apk包中是否存在assets/baiduprotect*.jar,sub_A23C用于获取该文件的信息(压缩前后大小,时间,crc等),sub_A60C用于将文件解压出来,sub_65980用之前的签名信息将前0x1000字节解密,sub_1C43C将附加数据1复制到附加数据头中指定的位置,用签名信息解密,再用附加数据2修复class_data_off_,然后重新计算校验和。
由此可知,如果重新打包,签名不正确的话就会解密失败。
dex脱壳:当sub_3BA90执行返回就可以dump解密的dex了,如果不想动态调试手动搞,也可以写个xposed模块,在后面一步InMemoryDexClassLoader加载dex的时候获取。
sub_18880保存当前dex的大小,后面加载dex的时候要用到。
sub_12100用InMemoryDexClassLoader类调用NewObjectV加载dex,InMemoryDexClassLoader内部会使用mmap分配内存存放dex,通过前面分析,我们知道mmap被hook替换为sub_1B070,所以现在来看下sub_1B070,可以看到,先将原始方法调用了一遍,然后检查是否为刚才加载dex所需的那块内存,是则将其保存。
回到刚才加载dex的调用之后,通过sub_1217C分配一个新的DexPathList$Element数组,将原来系统的类加载器和刚才的InMemoryDexClassLoader中的classLoader.pathList.dexElements合并成一个数组,然后替换原来系统中的类加载器的dexElements。sub_1889C获取刚才mmap的hook函数保存的dex地址。sub_3DDC8解析出dex中的各种数组指针
最后一个函数sub_45964,主要就两步。
第一步调用sub_42C08注册com.baidu.protect.A中剩余的本地函数,用于vmp代理,可以看到,一共10个代理,每个对应一个返回值类型。每个函数对应的本地函数都是一样的,均为sub_42598。
第二步就是对每个dex调用sub_42D8C,去解析附加数据3,通过分析,其数据结构如下,其中有用的字段为方法数组和指令替换表。
其中方法的数据结构如下,
至此,com.baidu.protect.A.n001方法的调用过程就分析完了。
现在,回到attachBaseContext,剩下的就只是替换程序原来的application了。
然后就是onCreate,它调用了com.baidu.protect.A.n002
通过前面分析可以,该方法对应的本地函数为sub_94E4,该函数主要就是用4作为参数调用函数队列中的所有函数。
通过分析,会有sub_ 40CF8、sub_ 3E96C、sub_ 42388这几个函数执行。这部分就不详细写了,通过分析知sub_ 40CF8调用CrashHandler.asynRun方法,向https://apkprotect.baidu.com/apklog发送统计信息。sub_ 3E96C assets/baiduprotect.m检查dex的完整性,该文件中存有加密的dex MD5。sub_ 42388注册com.baidu.xshield.jni.Asc和com.baidu.xshield.utility.KeyUtil的本地函数,调用com.baidu.xshield.ac.XH.init方法。
至此启动流程分析完毕。
接下来看看dump出来的dex文件。可以看到onCreate方法被改了,调用了没有返回值那个代理函数。推测0xAB000000是其id,暂且称为vmp_method_id
由前面分析可知,所有代理绑定的本地函数都为sub_42598,该函数也被混淆了,删除垃圾代码后如下,由此可知,vmp_method_id值的最高字节没有用,第16至24位为dex编号,通过该值找到对应dex解析后的信息。
sub_4A458通过分析,可知,vmp_method_id的低16位为附加数据3中method的索引,通过该id找到对应的method结构,然后分配寄存器空间。将参数值放入寄存器中,然后检查指令对应的函数数组有没有初始化,没有初始化则通过附加数据3最后的指令替换表,将原始的指令数组映射。然后开始通过解释器执行指令。
随便找个vmp化后的方法指令,然后和未加固前的指令对比,可以看出只是将指令第一个字节替换了(第一个字节表示哪条指令,后面的都是操作数)。还有就是有些id因为重新编译后变了。所以我们只需要把指令根据替换表再改回来就行了。
指令替换函数去掉混淆后的垃圾代码如下
思路:
1.将附加数据3解析出来,构造成DexCode添加到dex文件的后面,然后将class_data中的code_off修改为新构造的DexCode。
2.因为code_off是uleb128类型的值,所以新的值和旧的值占用空间可能不一样,所以当空间占用相同的情况下,则在原来的地方直接修改,不相同的话还得重新构造一个class_data放在所有添加的DexCode之后,然后将class_def中的class_data_off更新。
3.如何判断dex中的方法和附加数据中方法的关系?通过dex中方法id(暂且称为dex_method_id)和调用vmp代理时使用的vmp_method_id进行关联,如下图所示。
4.当method_ids_map为空的时候,修复程序将所有方法和它的id输出到文件,然后手动在文件中去找到对应方法dex_method_id,添加到method_ids_map中,再次运行修复程序就会将map所有指定的方法修复
修复过程:
首先将dex读取进内存
当map为空的时候,将文件添加个后缀,然后将所有方法信息写入。
然后通过搜索找到方法dex_method_id,然后将方法dex_method_id和vmp调用时的vmp_method_id添加到map中。
再次执行,现在因为map不为空,开始修复。
首先,解析附加数据3构造出所有的DexCode。
然后遍历class的方法,修复code_off
将修复后的内容写入文件。
修复完成后,将dex反编译,可以看到已经能够看到原来的代码了。
1.直接在InMemoryDexClassLoader构造函数处获取dex。
2.反编译获取到的dex,找到所有vmp方法的vmp_method_id,如下图所示。
3.运行修复程序,将str_dex_path改为要修复的文件路径,method_ids_map内容置空,如下图所示。
4.第一次运行完后,在dex同目录下,有一个同名.methods.txt后缀的文件,打开找到对应方法的dex_method_id,如下图所示 。
5.第二次运行修复程序,将方法对应的两个id添加到method_ids_map,如下图所示。注意顺序不要搞反了。
6. 第二次运行完毕后,在dex同目录下,有一个同名.new.dex后缀的文件,反编译就能看到修复后的代码了。
本文的数据结构和修复程序只对当前分析的版本有效。
[招生]科锐逆向工程师培训(3月6日远程教学报名特惠, 第37期)
最后于 21小时前 被卧勒个槽编辑 ,原因: 附件