首页
论坛
课程
招聘
[原创]百度加固免费版分析及VMP修复
2020-3-3 14:45 27672

[原创]百度加固免费版分析及VMP修复

2020-3-3 14:45
27672

背景

本来计划年后跑路的,不知道是我太菜,还是疫情原因,投简历都没人搭理我。现在又不能出门,只好自己找点事干了。

本文基于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因为重新编译后变了。所以我们只需要把指令根据替换表再改回来就行了。



指令替换函数去掉混淆后的垃圾代码如下




修复vmp方法

思路:

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后缀的文件,反编译就能看到修复后的代码了。


说明

本文的数据结构和修复程序只对当前分析的版本有效。


附件

分析所用的apk和修复代码【baiduprotect.zip

[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~

最后于 2020-3-3 20:21 被卧勒个槽编辑 ,原因: 附件
上传的附件:
收藏
点赞25
打赏
分享
最新回复 (65)
雪    币: 4183
活跃值: 活跃值 (1374)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t 活跃值 2020-3-3 16:08
2
0
不错
雪    币: 5209
活跃值: 活跃值 (2912)
能力值: ( LV10,RANK:175 )
在线值:
发帖
回帖
粉丝
挤蹭菌衣 活跃值 1 2020-3-3 16:16
3
0
支持楼主 学习了
雪    币: 4183
活跃值: 活跃值 (1374)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
D-t 活跃值 2020-3-3 16:30
4
0
 
最后于 2020-3-3 18:07 被D-t编辑 ,原因:
雪    币: 90
活跃值: 活跃值 (439)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
loveqiao 活跃值 2020-3-3 16:41
5
0
雪    币: 10054
活跃值: 活跃值 (2186)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 活跃值 2020-3-3 17:33
6
0
学习。百度加固也vmp了。
Debug.isDebuggerConnected 怎么过?反编译修改然后再编译打包?
最后于 2020-3-3 17:36 被tDasm编辑 ,原因:
雪    币: 3264
活跃值: 活跃值 (226)
能力值: ( LV3,RANK:25 )
在线值:
发帖
回帖
粉丝
yezheyu 活跃值 2020-3-3 18:25
7
0
学习了,感谢楼主
雪    币: 4751
活跃值: 活跃值 (2679)
能力值: ( LV8,RANK:135 )
在线值:
发帖
回帖
粉丝
卧勒个槽 活跃值 3 2020-3-3 19:56
8
0
tDasm 学习。百度加固也vmp了。 Debug.isDebuggerConnected 怎么过?反编译修改然后再编译打包?
我是动态调试的,以调试模式启动app,

ida附加上进程后,在modules窗口搜art.so,

双击找到的模块,在新窗口搜索isDebuggerConnected,

双击找到的函数,在返回处下断,


程序运行后会断在这里,查看返回值x0,当第二次返回1的时候将其改为0就行了。
雪    币: 993
活跃值: 活跃值 (375)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
上海刘一刀 活跃值 2 2020-3-3 21:09
9
0
大佬晚上好 文章中多次提到的 "把垃圾代码删除"  这个干掉 ollvm控制流平坦化的过程或者可以详细说一下。。。大佬写一下的话感觉又能多一个精华文章哪
雪    币: 1495
活跃值: 活跃值 (870)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
my1988 活跃值 1 2020-3-3 21:17
10
0
做个猜测,楼主是将混淆的代码f5之后直接复制到c++工程里面编译调试,通过这种方式删掉混淆的代码吗?
雪    币: 4751
活跃值: 活跃值 (2679)
能力值: ( LV8,RANK:135 )
在线值:
发帖
回帖
粉丝
卧勒个槽 活跃值 3 2020-3-3 22:40
11
1
上海刘一刀 大佬晚上好 文章中多次提到的 "把垃圾代码删除" 这个干掉 ollvm控制流平坦化的过程或者可以详细说一下。。。大佬写一下的话感觉又能多一个精华文章哪
混淆后的代码,有很多判断条件是永远为真或永远为假的。
就像下面这个,一个变量x不需要知道它的值是多少,x*(x-1)永远为偶数,最后一位永远为0,将其和1按位与之后永远为0。
然后根据这些条件就能删掉很大一部分代码了。

雪    币: 4751
活跃值: 活跃值 (2679)
能力值: ( LV8,RANK:135 )
在线值:
发帖
回帖
粉丝
卧勒个槽 活跃值 3 2020-3-3 22:41
12
1
my1988 做个猜测,楼主是将混淆的代码f5之后直接复制到c++工程里面编译调试,通过这种方式删掉混淆的代码吗?
当初还在实习的时候,第一次遇到混淆就是这么干的,一个函数五千行伪代码,调了一两天,
雪    币: 1173
活跃值: 活跃值 (14747)
能力值: ( LV9,RANK:221 )
在线值:
发帖
回帖
粉丝
0x指纹 活跃值 4 2020-3-3 22:53
13
0
膜膜膜膜
雪    币: 1495
活跃值: 活跃值 (870)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
my1988 活跃值 1 2020-3-3 23:12
14
0
卧勒个槽 当初还在实习的时候,第一次遇到混淆就是这么干的,一个函数五千行伪代码,调了一两天,[em_78]
我感觉这个方法挺实用的
雪    币: 993
活跃值: 活跃值 (375)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
上海刘一刀 活跃值 2 2020-3-3 23:58
15
0
卧勒个槽 混淆后的代码,有很多判断条件是永远为真或永远为假的。就像下面这个,一个变量x不需要知道它的值是多少,x*(x-1)永远为偶数,最后一位永远为0,将其和1按位与之后永远为0。然后根据这些条件就能删掉很大 ...
受教了
雪    币: 9678
活跃值: 活跃值 (9558)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-3-4 00:36
16
0
mark,大家辛苦了
雪    币: 3836
活跃值: 活跃值 (384)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
comewhere 活跃值 1 2020-3-4 05:15
17
0
mark 感谢分享
雪    币: 3937
活跃值: 活跃值 (785)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lyghost 活跃值 2020-3-4 07:50
18
0
膜拜大佬,分析的很详细
雪    币: 221
活跃值: 活跃值 (32)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
feikele 活跃值 2020-3-4 15:04
19
0
good 
雪    币: 10054
活跃值: 活跃值 (2186)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
tDasm 活跃值 2020-3-4 16:08
20
0
谢谢。
如果用你修复的libbaiduprotect.so直接替换原来的so,程序能否正常运行?
雪    币: 4751
活跃值: 活跃值 (2679)
能力值: ( LV8,RANK:135 )
在线值:
发帖
回帖
粉丝
卧勒个槽 活跃值 3 2020-3-4 18:35
21
0
tDasm 谢谢。 如果用你修复的libbaiduprotect.so直接替换原来的so,程序能否正常运行?
不能吧,
雪    币: 4
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
卧勒个去 活跃值 2020-3-4 19:33
22
0
膜拜大佬
雪    币: 4
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
卧勒个去 活跃值 2020-3-4 19:33
23
0
 
最后于 2020-3-4 19:34 被卧勒个去编辑 ,原因:
雪    币: 145
活跃值: 活跃值 (574)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
xiaozhu头 活跃值 2020-3-5 10:21
24
0
学习了,感谢楼主
雪    币: 4058
活跃值: 活跃值 (592)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
PanzerT 活跃值 2020-3-5 10:29
25
1
学习了~
游客
登录 | 注册 方可回帖
返回