这个题目是鹏程杯预赛里的一道Reverse,是一个比较隐蔽的vm类型题目,赛后战队review被安排到了这个题目,网上的WP都解释的都很简单,正好就以该题目为例,自己做一次vm类型题目的详细记录。

vm类型题目一般是自己写程序来解释执行一些基本的汇编程序指令,这种类型题目的解题方法基本是一样的,就是程序复杂程度的区别,简单的话直接读指令就能猜出来程序逻辑,复杂点的就得自己写一个解释器把程序里指令翻译成自己理解的汇编指令或者其他指令,再去分析,这道题目我是写了一个简单的解释器,具体代码贴在后面,先来看题目。

题目分析

拿到题目,拖进IDA里,找到main函数F5看一下反汇编结果:

发现看不出什么逻辑,只能看出来是c++的代码,有一句输出,点开每个函数简单看看,发本看不出来什么明显的逻辑,但是发现在cout语句前面的几个函数中有两个反调试函数,使得我们gdb调试的时候会直接报错退出,如果想用gdb来动态调试的话,可以把两个反汇编函数用nop给patch掉,操作不难,具体做法这里就不说了,因为我patch掉以后并没有用到动态调试。

接下来就得想办法找到题目的一些关键函数,怎么找呢,一个一个翻太慢了,先运行一下程序看看:

需要我们输入一串字符,然后输出错误提示,看来关键逻辑应该是在我们输入之后,所以要先找到cin函数,直接去ida的import视图下找没找到,就从‘err...’这个字符串入手,根据调用关系向上寻找,找到了一个functiontable:

猜想可能是根据偏移量来调用函数的,再寻找off_205A10的调用关系,终于发现了cin:

那么主要逻辑应该就是在cin之后的几个函数。

一个一个分析,首先是sub_2BE0函数:

分析一下,应该是自己实现的一个str_cpy功能函数,把输入的字符串拷贝到另一个地址中。
然后是sub_2D50函数:

分析一下大概的逻辑是对字符串做了4轮的异或加密,加密逻辑也比较简单,然后将结果赋给另一个变量,分析发现,传入的参数并不是一个简单的字符串,应该是一个结构体,为了方便后面代码的分析,自己在IDA中标一下结构体,这是我标记的字符串结构体:

对于不清楚具体含义的域可以先空着,之后分析用到了再去补充,之后再去看这个函数,就会清晰很多:

接下来是sub_2DD0函数,就是在这个函数中,调用了之前发现的function table:

分析函数里的操作,指针操作有些多,看着是一些赋值操作,猜测应该也是一些结构体变量的初始化,我们先跳过直接看function call调用的function table里的函数:

主要部分是一个while循环,找到了!!VM的主要逻辑就在这里,简单分析一下,循环中的分支路径应该就是不同的操作函数,根据不同的opcode调用function table里的函数作为对应的Handler,直接分析while循环,会发现指针操作,十分混乱,很难理清楚逻辑,需要把相关的变量和结构体分析标注清楚来帮助分析。

VM代码分析

想要分析清楚VM代码的主要逻辑,必须要搞清楚程序中和汇编指令相关的几种变量,首先是程序流,通常是用数组或字符串表示。然后是pc指针,一般在程序中会是一个程序流的index索引,指向要执行的命令,然后opcode,这个比较容易发现,一般会根据opcode来决定调用不同的Handler函数。然后是寄存器regs,涉及到程序中的运算部分,通常会是一个数组表示,程序流中通常直接用数组的索引值来使用寄存器。举个例子,add 1,2 可以理解为regs[1]+=regs[2]。然后是mem和flag,程序会从一段已知的内存中取值到寄存器中,这个已知的数组一般就是mem,和regs使用的方法一样,只不过regs在程序开始前值是未知的,mem的值在程序开始时是已知的,flag通常用来记录比较或者运算的结果,根据flag来决定程序的跳转。

分析出程序的pc,regs,mem,flag等变量,然后对相关的结构体进行标注,程序就会变得十分容易理解,这是我标记的相关结构体,不同题目会有差异,但本质上寻找的变量是一样的:

再回头分析相关函数,就会清晰很多:


之后就是对每一个Handler函数进行分析,这里以只举一个例子,function table里的第一个函数:

这是一个汇编的add命令,操作数是两个寄存器在寄存器数组中的索引,vmcode是程序流,一个word类型的数组,pc指针在每次乘3(其他Handler中也是),说明指令是定长的:opcode、op1、op2。regs从当前程序中去的寄存器状态,计算后返回结果。所以这个函数可以理解为add reg1,reg2。

点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖