本人小白一枚,想尝试学习一些加固,在身边朋友的推荐下选择了这个壳子.恰巧最近论坛上也有很多大佬发布了自己的分析贴(比如乐子人大佬的帖子),只是复现下来后发现后续的dex vmp相关帖子比较少(很不幸只找到了爱吃菠菜大佬的这篇帖子,不过因为年代久远甚至指令结构都不同了),于是写篇帖子记录一下.
新人发帖,如有错误还望大佬指出.
帖子使用的样本不方便说出来,不过是今年的.(因为据说这个样本保护项开得多)

可以看到被vmp保护的方法调用了Lcom/fort/andjni/JniLib;类的一个native方法

JniLib类中有很多种不同的native函数,显然是通过不同的函数签名来处理不同返回值的跳板函数. 既然这个类加载了libdexjni.so,那么这些native函数大概率就是被这个so注册的.
通过BinaryNinja加载so(这里把so基址设为了0x12000000),尝试在函数表中搜索Java_来定位,发现没有结果.
扫了一眼导出表

虽然有些明显的名称混淆,但JNI_OnLoad这些关键函数还在,导出表应该是没有被保护的.
既然静态注册搜不到,那么这些native函数必然是动态注册的了.
直接来到JNI_OnLoad:

发现函数不可解析,应该是被保护了.
往上查找.init.array:


对应的地址完全是空的,那么只可能是在更早执行的.init进行解密了.
先从dynamic里找到init地址


sub_125617dc分配了一块内存并复制了arg2的数据,数据大小被存储在了arg1 + 0x28.
而sub_1256150c主要是进行了数据解压(疑似还有其他操作,但我这个样本并没有执行到,就没去分析):

解压算法是nrv2d,以上数据均来自init函数最开始的参数0x12561b48:

具体逻辑搞清楚了,修复就简单了.可选择:
1.直接使用支持nrv2d的库对0x120096d8处大小0x33dfd的数据进行解压;
2.在init函数运行完后直接dump下0x120096d8处大小0x7e500的数据.
得到解压数据后直接塞回so文件对应位置即可.
这里需要补充一段,完成这步之后BinaryNinja已经可以正常解析函数了,但是后面在使用ida分析so时发现虽然导入表和导出表正确,但ida未能将符号对应的函数解析完整:

简单分析发现这是因为这个so的section被动了手脚:

节中将一个很大的区域标记成SHT_NOBITS扰乱了ida的分析,只需把type改成SHT_NULL(0)或者SHT_PROGBITS(1)即可使ida正常解析.
解压数据回填后,JNI_OnLoad已经可以正常反编译了,映入眼帘的便是三个函数调用.

挨个翻了一下,发现前两个函数都有很明显的控制流:

而第三个函数看上去非常清晰,并且一眼就能看到很多关键数据(为了方便直观这里稍微分析了一下):

既然前两个函数不太好看,那么就先分析第三个函数好了.
这个函数很简单,首先sub_12010028初始化了一些全局对象:

初始化完成后就注册了JniLib类的native函数:

既然要找的函数已经在这了,不妨直接进去看看吧:

sub_12017adc就是虚拟机的入口了,这里命名为vm_entrance.
由上图不难看出,这个函数的签名大概是(JNIEnv* env, jobjectArray args, void* result)
到了这里,先回顾一下函数的入参:

jobjectArray第0位是jclass,若为实例方法则第1位是实例的jobject,最后一位jint是vm函数的idx.

入口函数先获取了jobjectArray最后一位的Integer,然后调用了intValue函数读到jint传给j__lSSO05lI$I5IllOl_0lS$_lllIII__l$IOlI55$O550IS__lS5$:

而这个函数通过vm_idx去查了表data_1255ff48,因此这个表里一定存储着所有vm方法的数据.
交叉引用可以看到另一个函数对这个表进行了很多操作,应该就是初始化了:

至于这个函数,我们在上面的流程里其实已经见过了,正是JNI_OnLoad调用的第一个函数.
既然终究是躲不过的话,就只好顶着控制流简单看一下喽:

这个表里的结构体大小是0x28,通过一些函数读取数据,读取格式是MessagePack:

这里偷懒用frida在这个函数初始化完后去遍历表data_1255ff48(这里只处理了libDexHelper.so的检测,libdexjni.so应该是没有检测的),根据打印出的数据,大概猜测结构体结构:
*注:这里的code_item是我先入为主瞎取的名字,并不是真正的标准code_item,因为这个vmp的字节码很像code_item的结构,具体长这样:

而code_item_size则是这段字节码数据的size.
后面会对其进行详细分析,这个命名我感觉还是比较贴合的,就是有点容易混淆,因此提一嘴.
重新回到vm_entrance函数,可以看到在栈上重新排了一个结构体:


最终传入了函数sub_12018544.
sub_12018544大概扫了眼,应该是整理vm方法的参数,给包装类拆箱:

这里创建了一个大小0x48的结构体,与寄存器有关,偏移0x8处的列表用于存储寄存器


最终来到函数j__lIl_0ll5OIlIlISl00lI_lI5lIll$I$IIlIl00I$lIIIl5S0S5$

跳到这个函数后binaryninja就有点不好使了,加载了半天也没加载好,控制流混淆得很严重:

并且函数体量奇大,根据以上种种,不难猜到这个就是解释器函数了.
面对这一望无际的工作量,死磕明显不太现实.
尝试过写反混淆插件,但只能处理些简单的,效果不尽人意,若要兼容更多情况,插件工程量也相当大.
反混淆无果后,决定依靠trace. 试了下frida trace感觉不太稳定,经常会闪退,最后选择了unidbg trace.
简单看了一下,这个so的依赖相当少,甚至没有对libDexHepler.so的依赖:

因此unidbg应该不难运行起来,事实也的确如此:
整个运行流程非常清晰,可以依据这个来补sub_12010028中的结构体.
这里我为了方便选择调用0号函数(这张图好像是第三次出现了233)

因为函数简单(毕竟类都叫BlankActivity了),没有复杂逻辑,只补了几个简单函数就跑完了,整个流程被顺利trace下来.
毕竟是vmp,接下来的首要任务是先找到insns.
这里通过定位我命名的那个code_item结构然后偏移0x10即为insns,而code_item又在j__lSSO05lI$I5IllOl_0lS$_lllIII__l$IOlI55$O550IS__lS5$函数返回的结构体中:


在trace中搜索0x12018194:
得到结构体内存地址0x12893030,再查找其偏移0x8处的code_item读取:

得到code_item地址0x128ac000,偏移0x10得到0x128ac010即为要找的insns地址.
同时也在这个0x12018228处下断点,先把code_item的具体数据获取到,看看长什么样子
对比一下code_item的标准结构(这里不去关注后面的try_item)
前0x10字节的确非常符合code_item的结构,并且可以验证偏移0xc处的0x30正是insns_size(这段数据没有try_item,长度共0x70,符合0x10+0x30*2),因此后面0x60字节即为insns.
查找对0x128ac010处的读取:

读出了0x00009c2e四个字节,低16位被拿去查表0x12098a30,结果是0x12098a30 + (0x8d6c << 3)) = 0x120e6ba0
然后从这个位置读出了一个地址,最后br跳过去了

至于这个目标地址...依然在解释器函数之内:

(0x12029578-0x1206e068,这函数实在太大了)
而高16位则是
通过高16位查询了一个表并写入了一个值.
低16位的查表跳转,很像是在解释指令;而高16位的查表写入,则像是对寄存器的操作.
但是标准的opcode明明是uint8_t,这里却是uint16_t,难不成魔改了?
简单翻了一下表0x12098a30,发现其中元素大多为0,但非0元素全部指向解释器函数内部,加大了uint16_t的vmcode的可能性.
为了验证,直接查一下第0xffff位的元素地址,看看这个表是否真的有这么大:
0x12098a30 + (0xffff << 3) = 0x12118a28

结果非常神奇,恰好达到了一个数据分界处,0x12118a28之上的部分即为跳转表,之下的部分则是其他数据,所以这个表的确有0xffff个元素,涵盖了整个uint16_t范围.
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!