第一次接触vm这类的题型。一开始我以为vm和vmp是类似的,但是后来才知道vm指的是自己实现一套指令系统,然后在此基础上运行字节码。类似java的虚拟机。

其实也不算是第一次做vm的题,很久之前在南邮ctf平台上做过的WxyVM这道题也是vm。这题蛮简单的,以至于我一直都不知道它其实也是vm题。

不知道为什么,网上有关虎符ctf逆向的题解不多,vm这道题只找到一份题解,令则师傅的原创-虎符第三题 -vm 虚拟机逆向。令则师傅写得挺清楚的。不过,好歹是我第一次做出这种基于栈的vm题,还是写一下题解温习一下吧。

总述

题目给出了两个文件,

  • vm,解释器,解释字节码并执行,ELF程序
  • code,字节码,十六进制数据

如何运行?

linux终端内键入./vm ./code,回车后输入flag,若错误则输出n

vm的main函数:

1588243222122

VM结构体

上图21~28行的变量可以看作一个结构体,这不是写在程序里的,是等下分析33行的vm函数的代码时总结出来的。

struct VM{
    dword  vm_eip;
    dword  vm_sp;        VMstruct[1]
    qword* code;         VMstruct + 1
    qword* stack;        VMstruct + 2
    qword* c;
    qword* d;
}VMstruct;

vm_eip是表示当前指令运行到哪里了,比如运行code文件的0x100处的指令时,vm_eip = 0x100。

vm_sp表示栈内有几个数据元素,配合下面的stack这个变量,可以表示栈顶元素(stack[sp-1])

code是code文件中的字节码

stack是一个数组,本程序通过数组来模拟了一个栈,所有的数据运算都通过这个模拟的栈进行。

c是用于存储数据的数组,本题中c存储了三组数据,c[i+0x64]存储输入的flag,c[i+0x32]存储预设的一组数据,c[i]存储flag加密后的数据。i取值均为0到0x29,含0x29(最后c[i]和c[i+0x32]相比较,相等则答对flag)

d存储两个循环变量i,j,i = d[0], j=d[1]

为什么上面的结构体c和d两个变量,我没有使用像vm_eip,stack这种明确的变量名?

因为单独分析vm程序的vm函数根本无法分析出c和d的作用。必须结合code文件中的字节码才能分析出其含义。

分析操作码

vm文件的vm函数(sub_400A50):

经典的while-switch结构选择opcode(操作码),不同的操作码模拟不同的操作。

1588244728385

将一些基础的变量改下名之后,代码中的变量和我们总结出的VM结构体有这样的关系:

*(_BYTE *)(code + vm_eip)        
VMstruct.vm_eip。当前eip,程序运行到的地方

*(_BYTE *)(code + vm_eip + 1)    
data。表示操作数。只有单操作数指令才会出现这个变量

(signed int)VMstruct[1]    
VMstruct.vm_sp。表示栈内有几个数据元素。

*((_QWORD *)VMstruct + 1)
VMstruct.code。code字节码地址。

*((_QWORD *)VMstruct + 2)
VMstruct.stack。模拟栈

*((_QWORD *)VMstruct + 3)
VMstruct.c。存储数据的数组。

*((_QWORD *)VMstruct + 4)
VMstruct.d。存储两个循环变量

分析也没啥技巧,就慢慢来,以0xc的操作码为例:

case 0xC:                         // case 0xc : pop a; pop b; b/a;
        v58 = VMstruct[1];                    //VMstruct.vm_sp
        v59 = *((_QWORD *)VMstruct + 2);    //VMstruct.stack
        v60 = v58 - 1;                        
        v58 -= 2;
        VMstruct[1] = v60;                    
        v61 = *(_BYTE *)(v59 + v60);        //pop a
        VMstruct[1] = v58;
        v7 = (_BYTE *)(v58 + v59);            
        vm_eip_1 = (unsigned __int8)*v7;    //pop b
        if ( !v61 )
          return vm_eip_1;
        VMstruct[1] = v60;
        v9 = (unsigned __int16)vm_eip_1 / v61;// temp = b/a
        goto LABEL_8;
//(省略一些中间代码)
LABEL_8:
        *v7 = v9;                            //push temp
LABEL_9:
        code = *((_QWORD *)VMstruct + 1);
        vm_eip_1 = *VMstruct + 1;
        *VMstruct = vm_eip_1;                //eip+=1
        goto LABEL_2;

经过漫长的分析(里面的变量繁杂,代码可读性极低,需要一点一点看),可以整理出下面的指令,我基本上是用伪汇编表示的。

本题中的指令一部分是无操作数(单字节码)的,另一部分是单操作数(双字节码,即一个操作码加上一个操作数)的。如果下面的指令中含有data这个字眼,说明该指令是单操作数的。

1        getchar()
2        pop a; printf(a)
3        vm_eip ++
4        push data
5        push d[data]
6        pop d[data]
7        push c[data]
8        pop c[data]
9        pop a; pop b; push b+a
0xa        pop a; pop b; push b-a
0xb        pop a; pop b; push b*a
0xc        pop a; pop b; push b/a
0xd        pop a; pop b; push b%a
0xe        pop a; pop b; push b^a
0xf        pop a; pop b; push b&a
0x10    pop a; pop b; push b|a
0x11    pop a; push -a;
0x12    pop a; push ~a;
0x13    pop a; pop b; if (b!=a) {vm_eip+=2} else {vm_eip+=data}
0x14    pop a; pop b; if (b==a) {vm_eip+=2} else {vm_eip+=data}
0x15    pop a; pop b; if (b<=a) {vm_eip+=2} else {vm_eip+=data}
0x16    pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=data}
0x17    pop a; pop b; if (b>=a) {vm_eip+=2} else {vm_eip+=data}
0x18    pop a; pop b; if (b>a) {vm_eip+=2} else {vm_eip+=data}
0x19    pop a; push c[a]
0x1a    pop a; pop b; c[a] = b
0x1b    pop a; push d[a]
0x1c    pop a; pop b; d[a] = b
0x1d    vm_eip += data; if (*vm_eip>0x1d) return vm_eip
0x1e    return vm_eip

0x9~0x12是一些简单的算数运算,无操作数,所涉及的数据都是从栈中弹出的。

0x13~0x18是进行判断、实现程序跳转的,类似jnz,jbe等。

0x1d其实就是jmp,注意data是char类型的(*(char *)(code + vm_eip + 1)),有正有负,比如0xe8是负数。

import ctypes
print(ctypes.c_byte(0xe8)).value
#输出-24

0x1e是ret

下面说一些组合指令:

0x4-0x8:向c数组写入一个数据

0x1-0x8:输入一个数据,并写入c数组

0x4-0x6:为循环变量i或j(d[0]或d[1])赋值

等等。。

分析字节码

首先很容易看出前两部分:

一:内置数据的赋值

这部分数据是code文件内含的,用于和加密后的字符串作比较。

第一部分字节码就是把这些数据赋值到c[0x32+i]

def part1(self):
    '''
    验证flag的数据,存储在c[0x32]~c[0x32+0x29]
    0x0 ~ 0xa7 :
    04 66 08 32 04 4E 08 33 04 A9 08 34 04 FD 08 35
    04 3C 08 36 04 55 08 37 04 90 08 38 04 24 08 39
    04 57 08 3A 04 F6 08 3B 04 5D 08 3C 04 B1 08 3D
    04 01 08 3E 04 20 08 3F 04 81 08 40 04 FD 08 41
    04 36 08 42 04 A9 08 43 04 1F 08 44 04 A1 08 45
    04 0E 08 46 04 0D 08 47 04 80 08 48 04 8F 08 49
    04 CE 08 4A 04 77 08 4B 04 E8 08 4C 04 23 08 4D
    04 9E 08 4E 04 27 08 4F 04 60 08 50 04 2F 08 51
    04 A5 08 52 04 CF 08 53 04 1B 08 54 04 BD 08 55
    04 32 08 56 04 DB 08 57 04 FF 08 58 04 28 08 59
    04 A4 08 5A 04 5D 08 5B
    '''
    self.c = [0] * 0x100
    with open(r'code', 'rb')as f:
    self.b = f.read()
    for i in range(0xa8//4):
    group = self.b[i*4:i*4+4]
    data, offset = group[1], group[3]
    self.c[offset] = data

二:输入flag

输入flag,数据保存至c[0x64+i]

def part2(self):
    '''
    输入flag,存储在c[0x64]~c[0x64+0x29]
    0xa8 ~ 0x125 :
    01 08 64 01 08 65 01 08 66 01 08 67 01 08 68 01
    08 69 01 08 6A 01 08 6B 01 08 6C 01 08 6D 01 08
    6E 01 08 6F 01 08 70 01 08 71 01 08 72 01 08 73
    01 08 74 01 08 75 01 08 76 01 08 77 01 08 78 01
    08 79 01 08 7A 01 08 7B 01 08 7C 01 08 7D 01 08
    7E 01 08 7F 01 08 80 01 08 81 01 08 82 01 08 83
    01 08 84 01 08 85 01 08 86 01 08 87 01 08 88 01
    08 89 01 08 8A 01 08 8B 01 08 8C 01 08 8D
    '''
    flag = input('请输入长度为42的字符串:')
    if len(flag) != 42:
    print('字符串长度不合要求')
    else:
    for i in range((0x126-0xa8)//3):
    group = self.b[0xa8+i*3 : 0xa8+i*3+3]
    data, offset = ord(flag[i]), group[2]
    self.c[offset] = data

三:加密部分

翻译字节码脚本

这部分不像前两部分的指令简单,所以我先写了个翻译字节码的脚本:

class Translate():
    def opcode(self):
        '''
        得到一个字节码字典
        '''
        dic = r'''1    getchar()
2    pop a; printf(a)
3    vm_eip ++
4    push data
5    push d[data]
6    pop d[data]
7    push c[data]
8    pop c[data]
9    pop a; pop b; push b+a
0xa    pop a; pop b; push b-a
0xb    pop a; pop b; push b*a
0xc    pop a; pop b; push b/a
0xd    pop a; pop b; push b%a
0xe    pop a; pop b; push b^a
0xf    pop a; pop b; push b&a
0x10    pop a; pop b; push b|a
0x11    pop a; push -a;
0x12    pop a; push ~a;
0x13    pop a; pop b; if (b!=a) {vm_eip+=2} else {vm_eip+=data}
0x14    pop a; pop b; if (b==a) {vm_eip+=2} else {vm_eip+=data}
0x15    pop a; pop b; if (b<=a) {vm_eip+=2} else {vm_eip+=data}
0x16    pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=data}
0x17    pop a; pop b; if (b>=a) {vm_eip+=2} else {vm_eip+=data}
0x18    pop a; pop b; if (b>a) {vm_eip+=2} else {vm_eip+=data}
0x19    pop a; push c[a]
0x1a    pop a; pop b; c[a] = b
0x1b    pop a; push d[a]
0x1c    pop a; pop b; d[a] = b
0x1d    vm_eip += data; if (*vm_eip>0x1d) return vm_eip
0x1e    return vm_eip'''
        self.opcode_dict = {}
        for i in dic.split('\n'):
            num, opcode = int(i.split('\t')[0], 16), i.split('\t')[1]
            self.opcode_dict[num] = opcode


    def translate_part_3_opcode(self):
        '''
        翻译字节码
        '''
        self.opcode()

        with open(r'code', 'rb')as f:
            self.b = f.read()

        eip = 0x126
        while eip < 0x1e8:
            op = self.b[eip]
            if 'data' in self.opcode_dict[op]:
                #将data替换为具体的操作数
                data = self.b[eip+1]
                print('{0:<8}{1:<5}{2:<7}{3}'.format(hex(eip), hex(op), hex(data), self.opcode_dict[op].replace('data', hex(data))))
                eip += 2
            else:
                print('{0:<8}{1:<12}{2}'.format(hex(eip), hex(op), self.opcode_dict[op]))
                eip += 1


    def __init__(self):
        self.opcode()
        self.translate_part_3_opcode()


t = Translate()

本脚本没有写具体跳转的指令翻译(因为本来就是个辅助分析的程序而已)

比如这条输出:

0x178 0x1d 0xbc vm_eip += 0xbc; if (*vm_eip>0x1d) return vm_eip

需要手动计算出跳转的位置,手动替换成下面这条指令:

0x178 0x1d 0xbc jmp 0x134

又比如这条:

0x18c 0x16 0x34 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=0x34}
需要手动翻译成:

0x18c 0x16 0x34 pop a; pop b; cmp a,b; jbe 0x1c0;

分析

最终我们得到了第三部分的伪汇编指令:

0x126   0x4  0x0    push 0x0
0x128   0x6  0x0    pop d[0x0]

0x12a   0x5  0x0    push d[0x0]
0x12c   0x4  0x7    push 0x7
0x12e   0x16 0x56   pop a; pop b; cmp a,b; jbe 0x184;
0x130   0x4  0x0    push 0x0
0x132   0x6  0x1    pop d[0x1]

0x134   0x5  0x1    push d[0x1]
0x136   0x4  0x6    push 0x6
0x138   0x16 0x42   pop a; pop b; cmp a,b; jbe 0x17a;
0x13a   0x5  0x0    push d[0x0]
0x13c   0x4  0x6    push 0x6
0x13e   0xb         pop a; pop b; push b*a
0x13f   0x5  0x1    push d[0x1]
0x141   0x9         pop a; pop b; push b+a
0x142   0x4  0x64   push 0x64
0x144   0x9         pop a; pop b; push b+a
0x145   0x19        pop a; push c[a]
0x146   0x12        pop a; push ~a;
0x147   0x5  0x0    push d[0x0]
0x149   0x5  0x1    push d[0x1]
0x14b   0x4  0x2    push 0x2
0x14d   0x9         pop a; pop b; push b+a
0x14e   0xb         pop a; pop b; push b*a
0x14f   0xf         pop a; pop b; push b&a
0x150   0x4  0x64   push 0x64
0x152   0x5  0x0    push d[0x0]
0x154   0x4  0x6    push 0x6
0x156   0xb         pop a; pop b; push b*a
0x157   0x5  0x1    push d[0x1]
0x159   0x9         pop a; pop b; push b+a
0x15a   0x9         pop a; pop b; push b+a

0x15b   0x19        pop a; push c[a]
0x15c   0x5  0x0    push d[0x0]
0x15e   0x5  0x1    push d[0x1]
0x160   0x4  0x2    push 0x2
0x162   0x9         pop a; pop b; push b+a
0x163   0xb         pop a; pop b; push b*a
0x164   0x12        pop a; push ~a;
0x165   0xf         pop a; pop b; push b&a
0x166   0x10        pop a; pop b; push b|a
0x167   0x5  0x1    push d[0x1]
0x169   0x4  0x7    push 0x7
0x16b   0xb         pop a; pop b; push b*a
0x16c   0x5  0x0    push d[0x0]
0x16e   0x9         pop a; pop b; push b+a
0x16f   0x1a        pop a; pop b; c[a] = b
0x170   0x5  0x1    push d[0x1]
0x172   0x4  0x1    push 0x1
0x174   0x9         pop a; pop b; push b+a
0x175   0x4  0x1    push 0x1
0x177   0x1c        pop a; pop b; d[a] = b
0x178   0x1d 0xbc   jmp 0x134


0x17a   0x5  0x0    push d[0x0]
0x17c   0x4  0x1    push 0x1
0x17e   0x9         pop a; pop b; push b+a
0x17f   0x4  0x0    push 0x0
0x181   0x1c        pop a; pop b; d[a] = b
0x182   0x1d 0xa8   jmp 0x12a                                       
;以上为第一部分,二重循环。设输入为x,求出f(x),存储在c[0]~c[0x29]


;第一个部分循环跳出至此
0x184   0x4  0x1    push 0x1                                        
0x186   0x6  0x0    pop d[0x0]

0x188   0x5  0x0    push d[0x0]
0x18a   0x4  0x2a   push 0x2a
0x18c   0x16 0x34   pop a; pop b; cmp a,b; jbe 0x1c0;
0x18e   0x5  0x0    push d[0x0]
0x190   0x4  0x2    push 0x2
0x192   0xd         pop a; pop b; push b%a
0x193   0x4  0x0    push 0x0
0x195   0x14 0xf    pop a; pop b; cmp a,b; jnz 0x1a4;
0x197   0x5  0x0    push d[0x0]
0x199   0x19        pop a; push c[a]
0x19a   0x5  0x0    push d[0x0]
0x19c   0x4  0x1    push 0x1
0x19e   0xa         pop a; pop b; push b-a
0x19f   0x19        pop a; push c[a]
0x1a0   0x9         pop a; pop b; push b+a
0x1a1   0x5  0x0    push d[0x0]
0x1a3   0x1a        pop a; pop b; c[a] = b


0x1a4   0x5  0x0    push d[0x0]
0x1a6   0x4  0x2    push 0x2
0x1a8   0xd         pop a; pop b; push b%a
0x1a9   0x4  0x1    push 0x1
0x1ab   0x14 0xb    pop a; pop b; cmp a,b; jnz 0x1b6;
0x1ad   0x4  0x6b   push 0x6b
0x1af   0x5  0x0    push d[0x0]
0x1b1   0x19        pop a; push c[a]
0x1b2   0xb         pop a; pop b; push b*a
0x1b3   0x5  0x0    push d[0x0]
0x1b5   0x1a        pop a; pop b; c[a] = b


0x1b6   0x5  0x0    push d[0x0]
0x1b8   0x4  0x1    push 0x1
0x1ba   0x9         pop a; pop b; push b+a
0x1bb   0x4  0x0    push 0x0
0x1bd   0x1c        pop a; pop b; d[a] = b
0x1be   0x1d 0xca   jmp 0x188                                       
;至此为第二部分,按照下标的奇偶,分别将上一步得到的数据进一步加密


;以下为第三部分,判断c[i]是否和c[i+0x32]是否相等,相等输出y,反之输出n
0x1c0   0x4  0x0    push 0x0                                        
0x1c2   0x6  0x0    pop d[0x0]

0x1c4   0x5  0x0    push d[0x0]
0x1c6   0x4  0x29   push 0x29
0x1c8   0x18 0x4    pop a; pop b; cmp a,b; jae 0x1cc;
0x1ca   0x1d 0x1b   jmp 0x1e5                                       
;jmp to printf('y')          


0x1cc   0x5  0x0    push d[0x0]
0x1ce   0x19        pop a; push c[a]
0x1cf   0x4  0x32   push 0x32
0x1d1   0x5  0x0    push d[0x0]
0x1d3   0x9         pop a; pop b; push b+a
0x1d4   0x19        pop a; push c[a]
0x1d5   0x14 0xc    pop a; pop b; cmp a,b; jnz 0x1e1;               
;jmp to printf('n')

0x1d7   0x5  0x0    push d[0x0]
0x1d9   0x4  0x1    push 0x1
0x1db   0x9         pop a; pop b; push b+a
0x1dc   0x4  0x0    push 0x0
0x1de   0x1c        pop a; pop b; d[a] = b
0x1df   0x1d 0xe5   jmp 0x1c4


0x1e1   0x4  0x6e   push 0x6e                                                
;printf('n')
0x1e3   0x2         pop a; printf(a)                                
0x1e4   0x1e        return vm_eip

;printf('y')
0x1e5   0x4  0x79   push 0x79
0x1e7   0x2         pop a; printf(a)                                

然后有需要慢慢分析。我是手动维护了一个栈,然后手动模拟了一遍程序。

最后整理出一下加密逻辑:

def part3(self):
        '''
        加密flag,加密后的数据放在c[0]~c[29]
        '''
        for i in range(7):
            for j in range(6):
                #self.c[j*7+i] = ((~self.c[6*i+j+0x64]) & ((j+2)*i)) | ((self.c[6*i+j+0x64]) & (~((j+2)*i)))
                self.c[j*7+i] = self.c[6*i+j+0x64] ^ ((j+2)*i)
                #因为a ^ b = (~a & b) | (a & ~b),所以上面两行代码等价

        for i in range(1, 0x2a):
            if i % 2 == 0:
                self.c[i] = (self.c[i] + self.c[i-1]) & 0xff
            elif i % 2 == 1:
                self.c[i] = (self.c[i] * 0x6b) & 0xff

        for i in range(0x29+1):
            if self.c[i] != self.c[i+0x32]:
                break
        
        if i == 0x29:
            print('y')
        else:
            print('n')

加密部分有两步,第一步逻辑很复杂,但是可以化简(如果我单独思考,肯定没法化简出来,这个化简是令则大佬在题解中说的)。

第二部根据奇偶分别运算。

特别注意循环的起始和终止条件!!!

特别注意c和python之间变量位数的不同,注意&0xff!!!

逆向

最后逆着写出解密脚本:

class Decode():
    def __init__(self):
        import ctypes
        c_0x32 = [102, 78, 169, 253, 60, 85, 144, 36, 87, 246, 93, 177, 1, 32, 129, 253, 54, 169, 31, 161, 14, 13, 128, 143, 206, 119, 232, 35, 158, 39, 96, 47, 165, 207, 27, 189, 50, 219, 255, 40, 164, 93]
        c = [0] * 0x2a
        c_0x64 = [0] * 0x2a

        c[0] = c_0x32[0]
        for i in range(1,0x2a):
            if i % 2 == 0:
                for j in range(0x100):
                    if c_0x32[i] == (j + c_0x32[i-1]) & 0xff:
                        c[i] = j
            elif i % 2 == 1:
                for j in range(0x100):
                    if c_0x32[i] == (j * 0x6b) & 0xff:
                        c[i] = j
        
        for i in range(7):
            for j in range(6):
                c_0x64[6*i+j] = c[j*7+i] ^ ((j+2)*i)

        print(''.join(map(chr, c_0x64)))
Last modification:April 30th, 2020 at 08:17 pm