Straw's B1og.

vm--插曲

字数统计: 2.2k阅读时长: 11 min
2024/07/17

VM

简介

一个虚拟机指令集,相当于自己写了个虚拟机,执行自己写的指令,来实现程序的运行,我觉得有点套中套的味道了

要求

分析代码能力,扎实的汇编基础,chatgpt的熟练使用

例子

简单学习一下vm,以WcyVM(nctf2018)为例

1

这就是一个经典的vm题目,下面的一堆case就分别代表不同的指令集,上面的v4,v5就代表不同的寄存器,用来存储和操作数据,那么我们首先需要做什么呢?

opcode

2

opcode也就是操作码,用来判断这个程序是根据什么顺序运行的,有些题的opcode是只有操作的,但是有些题的opcode还会有带上参数什么的,这题就是

寄存器

3

操作分析

case 0x8

4

5

传入R和数据,a2值传入a1,相当于mov指令

case 0x9

1
sub_40096D(v4[*(_DWORD *)(v5[0] + 4) - 1], &v4[4], v5);
1
2
3
4
5
6
7
8
9
_QWORD *__fastcall sub_40096D(_DWORD *a1, _DWORD **a2, _QWORD *a3)
{
_QWORD *result; // rax

*a1 = *(*a2)++;
result = a3;
*a3 += 8LL;
return result;
}

传入R和栈,a2传入a1,a2++,相当于pop指令

case 0xa

1
sub_400927(v4[*(_DWORD *)(v5[0] + 4) - 1], &v4[4], v5);
1
2
3
4
5
6
7
8
9
10
_QWORD *__fastcall sub_400927(_DWORD *a1, _QWORD *a2, _QWORD *a3)
{
_QWORD *result; // rax

*a2 -= 4LL;
*(_DWORD *)*a2 = *a1;
result = a3;
*a3 += 8LL;
return result;
}

传入R和栈,a2-4,a1传入a2,相当于push指令

case 0xb,0xc

1
sub_4009B3(v4[0], v5);
1
2
3
4
5
6
7
8
9
_QWORD *__fastcall sub_4009B3(int *a1, _QWORD *a2)
{
_QWORD *result; // rax

*a1 = getchar();
result = a2;
*a2 += 4LL;
return result;
}
1
2
3
4
5
6
7
8
9
_QWORD *__fastcall sub_4009E5(int *a1, _QWORD *a2)
{
_QWORD *result; // rax

putchar(*a1);
result = a2;
*a2 += 4LL;
return result;
}

传入R0,R0=getchar(),R0=putchar()操作

case 0xd

1
sub_400B5D(&v1, v4[*(_DWORD *)(v5[0] + 4) - 1], v4[*(_DWORD *)(v5[0] + 8) - 1], v5);
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
_QWORD *__fastcall sub_400B5D(_DWORD *a1, _DWORD *a2, _DWORD *a3, _QWORD *a4)
{
int v4; // eax
int v5; // eax
int v6; // eax
_QWORD *result; // rax

*a1 = 0;
if ( *a2 == *a3 )
v4 = 0x80;
else
v4 = 0;
*a1 |= v4;
if ( *a3 >= *a2 )
v5 = 0;
else
v5 = 64;
*a1 |= v5;
if ( *a2 >= *a3 )
v6 = 0;
else
v6 = 32;
*a1 |= v6;
result = a4;
*a4 += 12LL;
return result;
}

比较难的部分,但不是很关键的部分

传入了两个R和v1,进行了比较,只有相等了才能使v1=80然后接下来下面的操作

case 0xe

1
sub_400A34(v5, *(unsigned int *)(v5[0] + 4), dest);
1
2
3
4
5
6
7
8
_QWORD *__fastcall sub_400A34(_QWORD *a1, int a2, __int64 a3)
{
_QWORD *result; // rax

result = a1;
*a1 = a3 + 4LL * a2;
return result;
}

传入了数据和dest,使v5发生改变,相当于是一个跳转指令,jmp

case 0xf,0x10

1
sub_400AAF(v1, v5, *(unsigned int *)(v5[0] + 4), dest);
1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 *__fastcall sub_400AAF(char a1, __int64 *a2, unsigned int a3, __int64 a4)
{
__int64 v4; // rdx
__int64 *result; // rax

if ( (a1 & 0x80) != 0 )
v4 = *a2 + 8;
else
v4 = a4 + 4LL * a3;
result = a2;
*a2 = v4;
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 *__fastcall sub_400A61(char a1, __int64 *a2, unsigned int a3, __int64 a4)
{
__int64 v4; // rdx
__int64 *result; // rax

if ( (a1 & 0x80) != 0 )
v4 = a4 + 4LL * a3;
else
v4 = *a2 + 8;
result = a2;
*a2 = v4;
return result;
}

传入数据和dest和v1,两个代码的if和else反一下,其实就是一个是jz,一个是jnz

case 0x11,0x12

1
sub_400AFD(v4[*(_DWORD *)(v5[0] + 4) - 1], v5);
1
2
3
4
5
6
7
8
9
_QWORD *__fastcall sub_400AFD(_DWORD *a1, _QWORD *a2)
{
_QWORD *result; // rax

++*a1;
result = a2;
*a2 += 8LL;
return result;
}
1
2
3
4
5
6
7
8
9
_QWORD *__fastcall sub_400B2D(_DWORD *a1, _QWORD *a2)
{
_QWORD *result; // rax

--*a1;
result = a2;
*a2 += 8LL;
return result;
}

只传入了R,很简单的+1,-1操作,inc,dec

case 0x13,0x14,0x15,0x16,0x17,0x1D

1
sub_400C0E((_DWORD *)v4[*(_DWORD *)(v5[0] + 4) - 1], *(_DWORD *)(v5[0] + 8), v5);
1
sub_400C43((_DWORD *)v4[*(_DWORD *)(v5[0] + 4) - 1], (_DWORD *)v4[*(_DWORD *)(v5[0] + 8) - 1], v5);

很简单的+,-,&,|,*操作,看好是传入数据还是R就行

case 0x19,0x1A,0x1B,0x1C

1
sub_400889((_DWORD *)v4[*(_DWORD *)(v5[0] + 4) - 1], v4[*(_DWORD *)(v5[0] + 8) - 1], v5);

各种类型的mov操作,地址啥的

opcode——汇编还原

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
opcode = [
0x00000008, 0x00000001, 0x00000000,
0x00000008, 0x00000003, 0x00000046,
0x0000000E, 0x00000015,
0x0000000A, 0x00000001,
0x00000009, 0x00000002,
0x0000000B,
0x0000000A, 0x00000001,
0x0000000A, 0x00000002,
0x00000009, 0x00000001,
0x00000011, 0x00000001,
0x0000000D, 0x00000001, 0x00000003,
0x0000000F, 0x00000008,
0x00000008, 0x00000001, 0x00000000,
0x00000008, 0x00000003, 0x00000047,
0x0000000E, 0x00000046,
0x0000000A, 0x00000001,
0x0000001A, 0x00000002, 0x00000006,
0x0000001D, 0x00000001, 0x00000004,
0x00000014, 0x00000002, 0x00000001,
0x00000019, 0x00000001, 0x00000002,
0x0000001B, 0x00000001, 0x00000001,
0x0000001D, 0x00000001, 0x0000006E,
0x00000013, 0x00000001, 0x00000063,
0x00000015, 0x00000001, 0x00000074,
0x00000013, 0x00000001, 0x00000066,
0x0000001C, 0x00000002, 0x00000001,
0x00000009, 0x00000001,
0x00000011, 0x00000001,
0x0000000D, 0x00000001, 0x00000003,
0x0000000F, 0x00000022, 0x00000064
]

def disasm(i, c):
if c == 8:
print ("_%02X:\t" % i + "mov R{} {}".format(opcode[i+1]-1,opcode[i+2]))
i=i+3
elif c == 9:
print ("_%02X:\t" % i + "pop R{}".format(opcode[i+1]-1))
i=i+2
elif c == 0xa:
print ("_%02X:\t" % i + "push R{}".format(opcode[i+1]-1))
i=i+2
elif c == 0xb:
print ("_%02X:\t" % i + "R0=getchar()")
i=i+1
elif c == 0xc:
print ("_%02X:\t" % i + "R0=putchar()")
i=i+1
elif c == 0xd:
print("_%02X:\t" % i + "cmp R{} R{}".format(opcode[i+1]-1,opcode[i+2]-1))
print(" jnz {}".format(i+3))
print(" mov a, 80")
i=i+3
elif c == 0xe:
print ("_%02X:\t" % i + "jmp {}".format(opcode[i+1]))
i=i+2
elif c == 0xf:
print("_%02X:\t" % i +"and a, 80")
print(" test a a")
print(" jnz {}".format(opcode[i+1]))
i+=2
elif c == 0x10:
print("_%02X:\t" % i +"and a, 80")
print(" test a a")
print(" jz {}".format(opcode[i+1]))
i+=2
elif c == 0x11:
print ("_%02X:\t" % i + "inc R{}".format(opcode[i+1]-1))
i=i+2
elif c == 0x12:
print ("_%02X:\t" % i + "dec R{}".format(opcode[i+1]-1))
i=i+2
elif c == 0x13:
print ("_%02X:\t" % i + "add R{} {}".format(opcode[i+1]-1,opcode[i+2]))
i=i+3
elif c == 0x14:
print ("_%02X:\t" % i + "sub R{} R{}".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x15:
print ("_%02X:\t" % i + "xor R{} {}".format(opcode[i+1]-1,opcode[i+2]))
i=i+3
elif c == 0x16:
print ("_%02X:\t" % i + "and R{} R{}".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x17:
print("_%02X:\t" % i + "or R{} R{}".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x19:
print("_%02X:\t" % i + "mov R{} R{}".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x1A:
print("_%02X:\t" % i + "mov R{} R{}".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x1B:
print("_%02X:\t" % i + "mov R{} [R{}]".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x1C:
print("_%02X:\t" % i + "mov [R{}] R{}".format(opcode[i+1]-1,opcode[i+2]-1))
i=i+3
elif c == 0x1D:
print ("_%02X:\t" % i + "mul R{} {}".format(opcode[i+1]-1,opcode[i+2]))
i=i+3
return i
i=0
while(i<len(opcode)-1):
i=disasm(i,opcode[i])
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
_00:    mov R0 0
_03: mov R2 70
_06: jmp 21

_08: push R0
_0A: pop R1
_0C: R0=getchar()
_0D: push R0
_0F: push R1
_11: pop R0
_13: inc R0


_15: cmp R0 R2
jnz 24
mov a, 80
_18: and a, 80
test a a
jnz 8
_1A: mov R0 0
_1D: mov R2 71
_20: jmp 70

_22: push R0
_24: mov R1 R5
_27: mul R0 4
_2A: sub R1 R0
_2D: mov R0 R1
_30: mov R0 [R0]
_33: mul R0 110
_36: add R0 99
_39: xor R0 116
_3C: add R0 102
_3F: mov [R1] R0
_42: pop R0
_44: inc R0

_46: cmp R0 R2
jnz 73
mov a, 80
_49: and a, 80
test a a
jnz 34

因为比较长,看不懂的话直接交给gpt处理,当然其实重要的代码就这么点

分析

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
R0 = 0         // 初始化 R0 为 0
R2 = 70 // 初始化 R2 为 70
跳转到 标签1

标签0:
压栈(R0) // 将 R0 的值压入栈
R1 = 弹栈() // 从栈中弹出一个值到 R1
R0 = 获取字符() // 从输入中读取一个字符到 R0
压栈(R0) // 将 R0 的值压入栈
压栈(R1) // 将 R1 的值压入栈
R0 = 弹栈() // 从栈中弹出一个值到 R0
R0 = R0 + 1 // 将 R0 增加 1

标签1:
如果 (R0 != R2) { // 比较 R0 和 R2
a = 80 // 如果不相等,将 a 设置为 80
}
a = a & 80 // 执行 a 和 80 的按位与操作
如果 (a != 0) { // 测试 a 是否为非零
跳转到 标签0 // 如果非零,跳转到 标签0
}

R0 = 0 // 初始化 R0 为 0
R2 = 71 // 初始化 R2 为 71
跳转到 标签2

标签3:
压栈(R0) // 将 R0 的值压入栈
R1 = 5 // 设置 R1 为 5
R0 = R0 * 4 // 将 R0 乘以 4
R1 = R1 - R0 // 将 R0 从 R1 中减去
R0 = R1 // 将结果赋给 R0
R0 = 内存[R0] // 访问地址为 R0 的内存
R0 = R0 * 110 // 将 R0 乘以 110
R0 = R0 + 99 // 将 R0 加 99
R0 = R0 ^ 116 // 将 R0 与 116 进行异或操作
R0 = R0 + 102 // 将 R0 加 102
内存[R1] = R0 // 将结果存储回地址为 R1 的内存
R0 = 弹栈() // 从栈中弹出一个值到 R0
R0 = R0 + 1 // 将 R0 增加 1

标签2:
如果 (R0 != R2) { // 比较 R0 和 R2
a = 80 // 如果不相等,将 a 设置为 80
}
a = a & 80 // 执行 a 和 80 的按位与操作
如果 (a != 0) { // 测试 a 是否为非零
跳转到 标签3 // 如果非零,跳转到 标签3
}

最重要的加密就在label3里面

找到密文就可以写解密脚本了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
int s[]={
0x36D3, 0x2AFF, 0x2ACB, 0x2B95, 0x2B95, 0x2B95, 0x169F, 0x186D,
0x18D7, 0x1611, 0x18D7, 0x2B95, 0x2C23, 0x2CA9, 0x1611, 0x1611,
0x18D7, 0x2AFF, 0x1849, 0x18FB, 0x2ACB, 0x2A71, 0x1735, 0x18D7,
0x1611, 0x2ACB, 0x15DD, 0x18D7, 0x2C23, 0x169F, 0x15DD, 0x2B95,
0x169F, 0x156B, 0x186D, 0x2AFF, 0x1611, 0x1611, 0x15DD, 0x2AFF,
0x2C23, 0x2ACB, 0x15DD, 0x15DD, 0x186D, 0x1849, 0x2B95, 0x156B,
0x1735, 0x18FB, 0x18FB, 0x2A71, 0x2AFF, 0x1735, 0x2C23, 0x15DD,
0x18D7, 0x2A71, 0x18D7, 0x18D7, 0x2C23, 0x2AFF, 0x156B, 0x2C23,
0x169F, 0x35AF, 0x2CA9, 0x32B5, 0x2AFF, 0x3039
};
int c;
int main(){
for(int i=69;i>=0;i--){
c=(((s[i]-102)^116)-99)/110;
printf("%c",c);
}
}
//nctf{3e1ce77b70e4cb9941d6800aec022c813d03e70a274ba96c722fed72783dddac}
CATALOG
  1. 1. VM
    1. 1.1. 简介
    2. 1.2. 要求
    3. 1.3. 例子
      1. 1.3.1. opcode
      2. 1.3.2. 寄存器
      3. 1.3.3. 操作分析
        1. 1.3.3.1. case 0x8
        2. 1.3.3.2. case 0x9
        3. 1.3.3.3. case 0xa
        4. 1.3.3.4. case 0xb,0xc
        5. 1.3.3.5. case 0xd
        6. 1.3.3.6. case 0xe
        7. 1.3.3.7. case 0xf,0x10
        8. 1.3.3.8. case 0x11,0x12
        9. 1.3.3.9. case 0x13,0x14,0x15,0x16,0x17,0x1D
        10. 1.3.3.10. case 0x19,0x1A,0x1B,0x1C
      4. 1.3.4. opcode——汇编还原
      5. 1.3.5. 分析