之前分享过:[原创]Android-ARM64的VMP分析和还原,里面的样本比较简单,IDA按F5可以直接看到伪代码。
金罡大佬分享了复杂的样本:[原创]用魔法打败魔法:互联网大厂虚拟机分析还原,F5没办法直接看到伪代码,文章里面对vm_entry进行patch,让IDA顺利进行反编译。
简单的简单大佬分享了[原创] VMP攻略笔记,里面更是进一步对最复杂vm_interpreter进行patch,大大提升了分析的效率。
可惜的是,两位大佬分享的更多是VMP的分析结论,VMP的分析思路较少。而这篇文章主要补充两点:1、结合两位大佬的文章,猜测一下大佬对VMP的分析思路;2、进一步优化patch,在IDA上能直观看到:只有一个主分发器switch,并且case直接对应handle。
先找到VMP的入口:vm_entry
正常写代码:caller(调用者)和callee(被调用者)
因为同一个函数可能被多个函数调用,当callee(被调用者)被虚拟化,一般不会修改多个caller(调用者),而只是修改一个callee(被调用者)本身。
换句话说,被虚拟化的函数,内部所有逻辑变成只跳转到虚拟机入口vm_entry。
利用这个特点,可以定位虚拟机入口:vm_entry
1、只有一个函数调用
2、调用完恢复栈帧
3、紧接着直接RET
此外,对于大型一些的apk,同一个vm_entry很可能被多个地方调用,因此加上规则:
4、调用vm_entry次数多
按照上面的规则,可以写对应的脚本:
看下调用的地方,确实类似:vm_entry(vinsts, args);
进入sub_1313F0,会发现不是一个正常的函数。
明明调用sub_1313F0的地方有传递多个参数,但sub_1313F0内部没看到参数。
这就意味着ida的反编译识别有问题,在这里被加了对抗手段。
看下sub_1313F0具体的汇编
sub_25D00的核心作用正是根据传入的索引值,动态修改返回地址,实现跳转。
用公式表达就是:目标地址 = 返回地址 + *(返回地址 + 索引 × 4)
简单说,这里通过间接跳转的方式,让ida反编译失效。
对应的,我们可以写一个脚本,让间接跳转变成直接跳转:
效果如下:
但是这个时候按F5还是反编译出错
需要把patch的结果保存到so,然后重新加载so到IDA里面进行完整分析:
具体步骤:工具栏:Edit -> Patch program -> Apply patches to input file ...


退出当前ida,重新加载so,就可看到正常的vm_entry:sub_1313F0

而这个函数的逻辑比较简单
封装可变参数:pVA,得到返回结果pResult,然后传给sub_131680

进入sub_131680,IDA能正常反编译,但这个函数比较大,以结果为导向,直接看pResult是怎么处理的。
从截图可以看出,pResult没有做额外处理,而是直接传给sub_138518,并且直接返回。所以sub_131680这个函数是vm_ready:只是做一些准备,还没真正进行虚拟代码的解释运行。

进入sub_138518,提示13CEF4不是有效的基本块

跳到13CEF4,可以看到13CEF4是一个数据,而不是基本块,并且被引用 XREF

查看13CEF4引用,发现是在.data.rel.ro,这里一般存放的是函数指针或者虚函数表而ida根据这个规则,会对里面的地址识别成指向可以执行代码的基本块但13CEF4实际上是数据块,导致ida进行F5反编译的时候出错

而修改的办法就是把指向13CEF4改为指向一个新地址,让13CEF4不会被识别成代码,这里直接上简单的简单大佬的脚本:
修改之后,效果如下:

而sub_138518就可以按F5进行反编译,结果如下:

伪代码可以看出这个函数比较简单,只有两个case。但是从CFG图上看这个函数,会发现实际上很复杂。

进一步跟踪switch和case的逻辑,从汇编可以看出这里是特殊的二级跳转表switch,不是普通的一级跳转表switch。普通的一级跳转表switch,对应的case是从0、1、2...连续的。而特殊的两级跳转表switch一般是为了解决稀疏的 case 值,也就是case间隔比较大的情况。
并且这个二级跳转表还不是固定同一个位置,而是分散开被多处引用

因此,这里遇到两个麻烦的问题:
对于这个问题,简单的简单大佬的解决思路如下:
第3步类似inline hook,构建一段新的ifelse基本块,然后原地址JMP过去因为第2步拦截多个地址,并且对应的case有多个,导致ifelse基本块也要多个
这个思路确实解决了上面的问题1,让IDA反编译出完整的函数。但问题2还是存在,毕竟是多个地方加上各种跳转。
为了方便后续的分析,可以进一步优化,更解决问题2。对应的思路如下:
简单来说就是:
可以看出,case不是连续的
缺失的case默认指向0
模拟一级跳转表switch的汇编,创建一个新的分发器:
其中
等价
从下往上找到:BR->LDR->LDR->ADD
把一开始的case存入X16,然后两条LDR都NOP掉,之后直接跳到分发器
参考前面说的,保存patch到so,退出重新加载so。会发现这个时候IDA依旧反编译有问题,需要手动调整一下。
进入0x144220,改成成数组:



进入0x1443A0,手动设置成switch:

[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 1天前
被GhHei编辑
,原因: 微调