夫天地者,万物之逆旅;光阴者,百代之过客。

0%

虎符ctf之vm(虚拟机)逆向

第一次接触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函数的代码时总结出来的。

1
2
3
4
5
6
7
8
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结构体有这样的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*(_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的操作码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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是负数。

1
2
3
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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

三:加密部分

翻译字节码脚本

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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;

分析

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
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)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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!!!

逆向

最后逆着写出解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)))