背景
pdd出事之后,我尝试逆向过mw01.bin的还原逻辑,但是花了很多时间也没有头绪。现在既然davinci大佬把脱壳机放出来了,自然要好好理解一下,也为以后遇到的VMP壳做准备。
ManweVmpLoader
1 | public class ManweVmpLoader { |
这部分是脱壳机的起点,代码也比较短,容易理解。
首先将文件内容转化为ManweVmpDataInputStream,这个类支持对文件内容进行不同类型(int,double,UTF等等)的读取,跟核心逻辑关系不大,不再赘述。
之后把输入流导入ManweVmpDex,将文件还原为dex文件,并逐class写回文件系统。因此接下来的关键类就是ManweVmpDex。
ManweVmpDex
1 | public ManweVmpDex(final ManweVmpDataInputStream in) throws IOException { |
在ManweVmpDex的构造函数中,程序首先读取is中的magic number、版本号、headers、常量池(ManweVmpConstantPool),然后再解析其中的class。
常量池开头为两字节,指明常量的数量,之后的每一条都是一个字节指明下一个常量的类型(字节数),然后紧跟着该常量的值。常量的类型包括:
1 | BOOL |
ManweVmpClazz
1 | public ManweVmpClazz(ManweVmpDataInputStream inStream, ManweVmpConstantPool _constantPool, int _version) throws IOException { |
构造常量池之后,开始通过ManweVmpClazz构造class。程序依次读取类名、父类名、接口名、注释所对应的index,并从常量池中取出对应的值进行填充。然后通过readField和readMethod读取成员变量和成员函数的信息。
1 | private Map<String, ManweVmpField> readField(ManweVmpDataInputStream inStream) throws IOException { |
成员变量池和成员函数池的构造类似于常量池,由两字节指明数量,然后是各成员的信息,具体的解析方法在ManweVmpField和ManweVmpMethod中。
读取完成后,返回一个Map,键为变量名/函数签名,值为该成员对应的ManweVmpField/ManweVmpMethod类。
ManweVmpField
1 | public ManweVmpField(ManweVmpDataInputStream inStream, ManweVmpConstantPool _constantPool) throws IOException { |
构造函数从输入流中读取变量名、变量类型、读取权限和注释所对应的index,然后从常量池中取出对应的值。
ManweVmpMethod
1 | public ManweVmpMethod(ManweVmpDataInputStream inStream, ManweVmpConstantPool _constantPool, int version) throws IOException { |
成员方法除了类似于成员变量的方法名、签名、读取权限和注释之外,还包含一个额外的ManweCode类,由长度和内容组成,具体解析方法在ManweCode内。
ManweCode太长了,不放代码了,解析过程大致如下:
两字节指明最大临时变量数,两字节指明最大栈长度(但是这两个参数是final的,感觉意义不明),两字节指明指令数量,接下来逐条解析指令(ManweVmpInstruction),设置其JVM偏移为目前的输出位置;
一字节指明opcode,其中共有以下类型:
1 | OP_WIDE_PREFIX = 0xc4; |
根据opcode类型的不同,在输出流中的处理也不同,以下枚举其中一部分:
1 | case 0: { |
通过这种方式记录指令的操作数,同时将VMP的code写为正常dalvik虚拟机的bytecode之后,还需要对异常处理进行解析
1 | exceptionTableCount = inStream.readUnsignedByte(); |
每个表的四个参数分别为try catch的开始地址,结束地址,异常的处理起始位,异常类名称。
PoolFixer & RuntimeTypeFixer
本来以为到这里解析的部分就结束了,不过仔细一看构造ManweVmpDex的部分最后还有一段儿
1 | Arrays.stream(this.manweVmpClazzes).forEach(RuntimeTypeFixer::parseVmpClazz); |
moveZeroToLast:新建了一个patchedPool,拷贝了常量池的内容,但是把第0个item放在了末尾,并且把所有原本指向0的index改为指向末尾(这是在干嘛?)
addSingleStringToPool:往patchedPool里加了一个CONSTANT_UTF8类型的item,值为”Code”
addStringToPool:往patchedPool里加了很多CONSTANT_UTF8类型的item,其中的值包括:(啊?这些都是哪来的?)
1 | add("android/accounts/Account", "name", DESC_STRING); |
analyzeUtf8ToClazz:将部分opcode中使用的utf-8类型转换为classinfo
patchForFieldAndMethod:将VMP_FIELD_REF和VMP_METHOD_REF类型转换为JVM格式
writeClazzes
1 | public void writeClazzes(String dir) throws IOException { |
将manwe的bin文件解析完毕后,通过ManweVmpDex的writeClazz输出为class文件,详细过程就不再赘述了。
总结
Manwe的文件结构如下图所示:
阅读脱壳机代码的时候总是隐隐感觉像是内部人士写出来的,很多细节不像是单纯逆向能弄得这么清楚的。只能说多行不义必自毙吧。