虚拟机分析

有关虚拟机分析部分, 我们以一道简单的crackme来进行讲解.

对应的crackme可以点击此处下载: FuelVM.exearrow-up-right

对应的keygenme可以点击此处下载: fuelvm_keygen.pyarrow-up-right

对应的IDA数据库可以点击此处下载: FuelVM.idbarrow-up-right

本题作者设计了一个具有多种指令的简单虚拟机. 我们使用IDA来进行分析. 并为了方便讲解, 我对反汇编出的一些变量重新进行了命名.

运行程序

我们运行程序 FuelVM.exe. 界面如下所示

start.png

在这个界面中, 我们看到右两个输入框, 一个用于输入用户名Name, 另一个则用于输入密钥Key. 还有两个按钮, Go用于提交输入, 而Exit则用于退出程序.

获取用户输入

那么我们就可以从这里入手. 程序想获取用户输入, 需要调用的一个API是GetDlgItemTextA()

获取的输入字符串会保存在lpString里. 那么我们就可以打开IDA查找有交叉引用GetDlgItemTextA()的地方.

如上, IDA只有这里调用过GetDlgItemTextA并且调用了两次分别获取inputNameinputKey. 随后初始化了一个变量为0, 因为还不明白这个变量的作用, 因此先重命名为var_a. 之后进行了一次函数调用并jmp跳转. 因为jmp跳转位置的代码是一些退出程序的代码, 因此我们可以断定上面的这个call, 是在调用处理用户输入的函数. 因此将jmp的位置重命名为locExit, 函数则重命名为process_input.

处理用户输入

我们进入process_input函数, 该函数仅仅对输入字符串进行了很简单的处理.

首先是这个strlength()函数. 函数使用cld; repne scasb; not ecx; dec ecx来计算字符串长度并将结果保存在ecx里. 是汇编基础知识就不多介绍. 所以我们将该函数重命名为strlength

而在IDA生成的伪C代码处有v1v2, 我对其进行了注解, 可以看汇编, 里面是使用ecx7进行比较, 而ecx是字符串的长度, 于是我们可以知道, 这里对输入的要求是: inputName 和 inputKey 的长度均不少于 7

inputNameinputKey长度均不少于7时, 那么就可以对输入进行简单的变换. 以下是一个循环

对应的python代码即

函数之后对一些变量进行了赋值(这些并不重要, 就忽略不讲了.)

注册SEH

initVM完成的是一些虚拟机启动前的初始化工作(其实就是对一些寄存器和相关的部分赋初值), 我们之后来讨论. 这里我们关注的是SEH部分. 这里注册了一个SEH句柄, 异常处理函数我重命名为seh_handler, 并之后使用int 3手动触发异常. 而在seh_handler位置, IDA并未正确识别出对应的代码

我们可以点击相应位置按下c键, 将这些数据转换成代码进行识别. (我们需要按下多次c键进行转换), 得到如下代码.

如下, 在seh_handler位置, 又用类似的方法注册了一个位于401306h的异常处理函数, 并通过xor ecx,ecx; div ecx手动触发了一个除0异常. 而在loc_401301位置, 这是一个反调试技巧, jmp loc_401301+2会使得EIP转向一条指令中间, 使得无法继续调试. 所以我们可以将00401301~00401306部分的代码nop掉, 然后在00401306位置创建一个新函数seh_handler2

类似的, 还有401330h重命名为seh_handler3, 而40135Eh是最后一个注册的异常处理函数, 我们可以推测这才是虚拟机真正的main函数, 因此我们将40135Eh重命名为vm_main. (有关SEH和反调试的部分, 可以推荐大家自己去动态调试一番弄清楚)

恢复堆栈平衡

我们创建了一个vm_main函数(重命名后还需要创建函数, IDA才能识别), 然后按下F5提示失败, 失败的原因则是由于堆栈不平衡导致的. 因此我们可以点击IDA菜单项Options->General在右侧勾选stack pointer. 这样就会显示出对应的栈指针.

我们来到最下显示不平衡的位置. 最上的jmp vm_main表明虚拟机内在执行一个循环. 而MessageBoxA的调用则是显示最后弹出的错误信息. 而在locret_40180A位置处, 经过多次leave堆栈严重不平衡, 因此我们需要手动恢复堆栈平衡.

这里也很简单, 在0040180A位置已经堆栈平衡了(000), 因此我们只需要将这一句leave修改为retn就可以了. 如下这样

然后你就可以发现vm_main可以F5生成伪C代码了.

虚拟机指令分析

说实话, 虚拟机的分析部分是一个比较枯燥的还原过程, 你需要比对各个小部分的操作来判断这是一个怎样的指令, 使用的是哪些寄存器. 像这个crackme中, vm进行的是一个取指-译码-执行的循环. 译码过程可给予我们的信息最多, 不同的指令都会在这里, 根据它们各自的opcode, 使用if-else if-else分支进行区分. 实际的还原过程并不复杂, 但有可能会因为虚拟机实现的指令数量而显得有些乏味.

最后分析出的结果如下:

opcode
value

push

0x0a

pop

0x0b

mov

0x0c

cmp

0x0d

inc

0x0e

dec

0x0f

and

0x1b

or

0x1c

xor

0x1d

check

0xff

我们再来看分析后的initVM函数

这里有4个通用寄存器(r1/r2/r3/r4), 1个sp指针和1个pc指针, 标志zfsf. 先前我们不知道的var_a也被重命名为cur_index, 指向的是inputName当前正在处理的字符索引.

对于VM实现的多个指令我们就不再多说, 重点来看下check部分的操作.

如果r1中的值跟inputKey[cur_index]相等, 那么会继续判断是否已经检查完了整个inputName, 如果没有出错且比对结束, 那么就会弹出Good job! Now write a keygen.的消息框. 否则会继续initVM进入下一轮循环.(出错了当然是弹出消息框提示错误了. )

cur_index会在initVM中自增1, 那么还记得之前在process_input里有执行2次initVM吗. 因为有执行2次initVM, 所以我们的inputKey的前2位可以是任意字符.

故而我们分析完了整个虚拟机, 便可以开始着手编写Keygen.

对应的keygenme可以点击此处下载: fuelvm_keygen.pyarrow-up-right

对应的IDA数据库可以点击此处下载: FuelVM.idbarrow-up-right

Last updated