解决基于VM的CrackMe的问题

来源:岁月联盟 编辑:猪蛋儿 时间:2020-03-16

vm1.exe实现了一个简单的8位虚拟机(VM)来尝试和阻止逆向工程师检索攻击标记。VM的RAM包含加密的标志和一些字节码来解密它。你能弄清楚虚拟机如何工作并编写自己的虚拟机来解密标志吗? ram.bin中提供了VM RAM的副本,该副本与执行前恶意软件VM的RAM内容相同,包含自定义汇编代码和加密标志。
规则是:
1. 你不需要运行vm1.exe,此挑战仅是静态分析。
2. 不要使用调试器或转储程序从内存中检索解密的标志,这是作弊行为。
3. 可以使用免费版本的IDA Pro(无需调试器)进行分析。
在这种情况下,我将使用IDA Pro分析二进制文件,尽管诸如Cutter或Ghidra之类的工具也可以正常工作。我还将使用Python3编写脚本。查看下载的ZIP文件,我们有2个文件: vm1.exe和ram.bin,其中ram.bin包含字节码和加密标志。
对于那些不知道在恶意软件/软件打包程序的情况下虚拟机实际上是什么的人,让我简要地总结一下;样本中的虚拟机是一种混淆实际恶意软件或打包程序正在执行的操作的方法,例如,样本中将包含大量字节码,这些字节码不是有效的程序集,因此无法自行进行分析。此时,样本中将内置一个解释器,负责获取并执行字节码。该字节码可能用在切换情况下,在这种情况下,有大量没有交叉引用的功能,并且通过字节码进行调用,这意味着需要一个解释器来了解示例的工作方式。虚拟机实现的一个极为常见的示例是Python,执行时,脚本将转换为字节码,而python解释器是唯一可以理解并执行该字节码的东西。如果你有兴趣,这是一篇有关用Python编写Python解释器的精彩文章!
在IDA中打开vm1.exe,我们可以看到一些API调用,一些与MD5相关的函数调用以及对sub_4022E0的调用。首先,我对memcpy()调用很感兴趣,因为它将数据从unk_404040复制到新分配的内存区域dword_40423C,然后将其作为参数传递给MD5::DigestString()。因此,让我们看一下unk_404040,看看我们在那里能看到什么。
查看IDA中的数据,在30个空字节之后似乎有25个字节的数据,然后是更多的空字节,然后到达0x40413F处的更多数据,似乎每3个字节具有类似的结构,地址0x40413F依次为0x01、0x1D和0xBD。在这前3个字节之后,分别是0x01、0x05、0x53和0x01。在多次浏览数据后,似乎第一个字节是0x01、0x02、0x03或0x04,其余数据似乎是随机的。此时,我假设unk_404040上的数据是字节码,当我们将其与ram.bin中的数据进行比较时,这一点变得很清楚,它们是相同的。
现在知道了字节码的位置,让我们看一下函数sub_4022E0()。这个函数看起来很复杂,但实际上很简单。首先,将var_1设置为0,并移动到ecx中。指向VM字节码的指针被移动到edx中,然后[edx+ecx+0xFF]的一个字节被移动到eax中。再往下看,我们可以看到var_1在每个循环中增加了3,所以在本例中,var_1显然用作计数器。因此,[edx+ecx+0xFF]基本上是字节码[counter+255],这意味着实际的VM代码从偏移255开始。无论如何,第一个字节被移动到eax中,然后eax又被移动到var_10中。然后var_1增加1,并移动到edx中。VM字节码的地址被移动到eax中,并使用类似的[eax+edx+0xFF],现在edx不但是计数器,更是数据。字节存储在ecx中,ecx被移动到var_C中。以上过程重复一次,计数器var_1增加1,并执行类似的操作,下一个字节存储在var_8中。在循环中最后一次增加var_1,然后将var_8、var_C和var_10作为参数推入sub_402270()。测试它的返回值,然后函数返回或循环。通过分析,我们已经可以为这个函数创建伪代码。
counter = 0
while result:
    byte_1 = bytecode[counter + 255]
    counter += 1
    byte_2 = bytecode[counter + 255]
    counter += 1
    byte_3 = bytecode[counter + 255]
    counter += 1
    decode_and_execute(byte_1, byte_2, byte_3)
我已经将sub_402270() decode_and_execute()命名为sub_402270(),这是基于将字节码传递给sub_402270()并且没有其他函数处理这个字节码这一事实,所以让我们来看看它是如何工作的。首先,它将把byte_1移动到var_4中,var_4在一个值为1、2、3和4的switch语句中使用。此时,我们可以将byte_1定义为操作码,因为它决定了程序的执行,而byte_2和byte_3是操作数(byte_2 = operand1, byte_3 = operand2)。
如果var_4等于1,则将vm_bytecode的地址移入ecx,然后将其添加到byte_2的值中。将byte_3移到dl中,然后将其写入ecx指向的存储区域。然后函数返回,此函数的伪代码非常简单:
bytecode[operand1] = operand2
如果var_4等于2,则将vm_bytecode的地址移入eax,然后将其添加到byte_2中的值。该地址指向的字节被移入cl,然后被移入byte_404240。然后函数返回。同样,此函数的伪代码很简单:
byte_404240 = bytecode[operand1]
最后,如果var_4等于3,则将byte_404240中的值移入edx,并将vm_bytecode的地址移入eax,然后将其加到byte_2中的值。指向该地址的值将移入ecx,并与edx中的值进行异或。然后将其移至vm_bytecode [byte_2]。然后,该函数将返回。该块的伪代码如下:
vm_bytecode[operand1] ^= byte_404240
如果var_4等于其他任何值,则在返回之前将al设置为0。对于所有其他块,将al设置为1。
因此,现在我们首先知道字节码(opcode | operand1 | operand2)的布局以及可以执行的函数(mov,store,xor,exit),因此现在就可以开始编写脚本了!
脚本解释器
查看解析器功能与基于字节码执行的功能之间的区别,可以很清楚地看到ram.bin文件中有2个部分;数据部分([:255])和代码部分([255:])。因此,我将把文件分成2个部分,以防止任何可能的覆盖问题。我还将使用类来包含代码,所以这些都将保存在VM()类中。首先,我们希望创建_init__()函数,因此让我们使用它来初始化vm_data和vm_instructions变量。另外,我们知道每个字节码指令都是3字节长,所以让我们将数据[255:]分割成3字节长的字符串并将其存储在一个列表中。我们还可以将vm_data设置为字节数组,以简化执行过程。我们还将变量xor_byte初始化为0,即我们在IDA中看到的byte_404240。

[1] [2]  下一页