River5tone

虚拟化保护 VMP

字数统计: 4.7k阅读时长: 22 min
2019/11/12 Share

——虚拟机指令分析 Virtual Machine Command Analysis

In code obfuscation, a virtual machine is a mechanism used to execute a different instruction set than the one used by the machine that runs the program.

原理

基本原理

虚拟机保护技术是指,将程序代码转换为自定义的中间操作码OpCode(当操作码只占一个字节可称作ByteCode),通过一种解释执行系统或者模拟器(Emulator)解释执行,实现代码基本功能。

逆向这种程序,一般需要对emulator结构进行逆向,结合opcode进行分析,得到各个操作码对应的基本操作,从而理解程序功能,整个程序逆向工程将会十分繁琐。(图片来自:https://mp.weixin.qq.com/s/4Nfso1OuHeQgCTGYv2IF5Q)

这种虚拟化的思想,广泛用于计算机科学领域。从某种程度上来说,解释执行的脚本语言都需要有一个虚拟机,例如LuaVM,PythonInterpreter。静态语言类似Java通过JVM实现了平台无关性,每个安装JVM的机器都可以执行Java程序。这些虚拟机可以提供一种平台无关的编程环境,将源程序翻译为平台无关的中间码,然后翻译执行。JVM虚拟机的基本架构(图片来自geeksforgeeks)

VMP虚拟机

VMP虚拟机保护技术指的是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,达到保护原有指令不被轻易逆向和修改的目的。从本质上来说,就是创建一套虚拟指令系统对原本的x86汇编指令系统进行一次封装,将原本的汇编指令转换为另一种表现形式。

虚拟指令有自己的机器码,但和原本的x86汇编机器码完全不一样,而且常常是一堆无意义的代码,他们只能由VM虚拟解释器(Dispatcher)来解释并执行。我们在逆向时看到的汇编代码其实不是x86汇编代码,而是字节码(伪指令),它是由指令执行系统定义的一套指令和数据组成的一串数据流,所以虚拟机的脱壳很难写出一个通用的脱壳机,原则上只要虚拟指令集一变动,原本的伪指令的解释就会发生变化。

VMProtect 壳

VMProtect 是一款纯虚拟机保护软件,效果很好,但也有缺点,就是会影响程序速度,因此在一些对速度要求很高的场合就不适合用了。

分析方法

在目前的CTF比赛中,虚拟机题目常常有两种考法:

  • 给可执行程序和 opcode,逆向 emulator,结合 opcode 文件,推出 flag
  • 只给可执行程序,逆向 emulator,构造 opcode,读取 flag

题目

RCTF2018 simple-vm

这个题目的指令分析,我真是吐了,又多又恶心,谁看谁知道。复制大部分程序,跑出来指令分析。

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#include <bits/stdc++.h>
#include <windows.h>

BYTE d[400];

inline void print(BYTE *dat, long sz)
{
for(int i = 0; i < sz; ++i) {
if(i % 16 == 0) printf("\n");
printf("%2x ", dat[i]);
}
}

inline unsigned int solve(long len)
{
int i, j;
BYTE *data, *v15;
int v4, v5, v6, v7, v8, v9, v10, v11, v13, v14, v16, v17, v18;
char v12;
int R1, R2, cnt = 0;

i = 0LL;
data = d;
while ( 1 )
{//printf("i: %d op: %d :: \n", i, data[i]);

if(i == 182) {
//print(data, len);
}

j = i + 1;
switch ( data[i] )
{
case 0:
printf("%s", data[j]);
return *(unsigned int *)&data[j];
case 1: // op e1 ;v2 = v1[v2]
printf("\top e1 ;v2 = v1[v2]\n");
goto LABEL_35;
case 2: // op e1, e2 ;v1[e1] = e2
printf("\top e1, e2 ;v1[e1] = e2\n");
v4 = j;
j = i + 9;
data[*(signed int *)&data[v4]] = *(DWORD *)&data[(signed int)i + 5];
break;
case 3: // op e1 ;A0 = v1[e1]
printf("\top e1 ;A0 = v1[e1]\n");
v5 = j;
j += 4;
v6 = *(signed int *)&data[v5];
goto LABEL_27;
case 4: // op e1 ;v1[e1] = A0
printf("\top e1 ;v1[e1] = A0\n");
v7 = j;
j += 4;
v8 = *(signed int *)&data[v7];
goto LABEL_31;
case 5: // op e1 ;A4 = v1[e1]
printf("\top e1 ;A4 = v1[e1]\n");
v9 = j;
j += 4;
v10 = (char)data[*(signed int *)&data[v9]];
goto LABEL_21;
case 6: // op e1 ;v1[e1] = A4
printf("\top e1 ;v1[e1] = A4\n");
v11 = j;
v12 = R2;
j += 4;
v8 = *(signed int *)&data[v11];
goto LABEL_9;
case 7: // op ;A0 += A4
printf("\top ;A0 += A4\n");
v13 = R2;
goto LABEL_23;
case 8: // op ;A0 = ~(A0 & A4)
printf("\top ;A0 = ~(A0 & A4)\n");
v14 = ~(R2 & R1);
goto LABEL_12;
case 0xA: // op ;A0 = getchar()
printf("\top ;A0 = getchar()\n");
printf("input_addr: %d\n", R2);
v14 = getchar();
if(++cnt == 32) print(data, len);
goto LABEL_12;
case 0xB: // op ;putchar(A0)
printf("\top ;putchar(A0)\n");
putchar(R1);
break;
case 0xC: // if(v1[e1]) --v1[e1], jmp v1[e2];
printf("\tif(v1[e1]) --v1[e1], jmp v1[e2];\n");
v15 = &data[*(signed int *)&data[j]];
if ( *v15 )
{
j = *(DWORD *)&data[j + 4];
--*v15;
}
else
{
j += 8;
}
break;
case 0xD: // op ;++A0
printf("\top ;++A0\n");
++R1;
break;
case 0xE: // op ;++A4
printf("\top ;++A4\n");
++R2;
break;
case 0xF: // op ;A0 = A4
printf("\top ;A0 = A4\n");
v14 = R2;
goto LABEL_12;
case 0x10: // op ;A4 = A0
printf("\top ;A4 = A0\n");
v10 = R1;
goto LABEL_21;
case 0x11: // op e1 ;A0 += e1
printf("\top e1 ;A0 += e1\n");
v16 = j;
j += 4;
v13 = *(DWORD *)&data[v16];
LABEL_23:
R1 += v13;
break;
case 0x12: // op ;A0 = v1[A4]
printf("\top ;A0 = v1[A4]\n");
v6 = R2;
goto LABEL_27;
case 0x13: // op ;A0 = v1[A0]
printf("\top ;A0 = v1[A0]\n");
v6 = R1;
LABEL_27:
v14 = (char)data[v6];
goto LABEL_12;
case 0x14: // op e1 ;A0 = e1
printf("\top e1 ;A0 = e1\n");
v17 = j;
j += 4;
v14 = *(DWORD *)&data[v17];
goto LABEL_12;
case 0x15: // op e1 ;A4 = e1
printf("\top e1 ;A4 = e1\n");
v18 = j;
j += 4;
v10 = *(DWORD *)&data[v18];
LABEL_21:
R2 = v10;
break;
case 0x16: // op ;v1[A4] = A0
printf("\top ;v1[A4] = A0\n");
printf("\tR: A4: %d A0 %d\n", R2, R1);
v8 = R2;
LABEL_31:
v12 = R1;
LABEL_9:
data[v8] = v12;
break;
case 0x17: // op ;A0 -= A4
printf("\top ;A0 -= A4\n");
v14 = R1 - R2;
LABEL_12:
R1 = v14;
break;
case 0x18: // op e1 ;if(A0) jmp v1[e1]
printf("\top e1 ;if(A0) jmp v1[e1]\n");
if ( R1 )
LABEL_35:
j = *(DWORD *)&data[j];
else
j = i + 5;
break;
default:
break;
}//printf("R:%d %d\n", R1, R2);
if ( j >= len )
return 0LL;
i = j;
}
}

int main()
{
FILE *fp, *temp;
long sz, len;

fp = fopen("./p.bin", "rb");
//if(!fp) printf("error\n");
temp = fp;
fseek(fp, 0LL, SEEK_END);
sz = ftell(temp);
fseek(temp, 0LL, 0);
printf("sz: %ld\n", sz);

//d = malloc(sz);
len = fread(d, 1uLL, sz, temp);
printf("len: %ld\n", len);

int i = 0, j, r1, r2, v14;

solve(sz);

return 0;
}

具体过程大部分手写在草稿纸上了,我是原始人。总之最后分析出来了逻辑,其实就是 x^y,我写麻烦了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enc = [
true0x10, 0x18, 0x43, 0x14, 0x15, 0x47, 0x40, 0x17,
true0x10, 0x1d, 0x4b, 0x12, 0x1f, 0x49, 0x48, 0x18,
true0x53, 0x54, 0x01, 0x57, 0x51, 0x53, 0x05, 0x56,
true0x5a, 0x08, 0x58, 0x5f, 0x0a, 0x0c, 0x58, 0x09
]

flag = [0 for i in range(32)]
flagstr = ""

for i in range(32):
truex = 0x20 + i
truefor j in range(128):
truetruey = j
truetruemid1 = ~(x & y)
truetruemid2 = ~(x & mid1)
truetruemid3 = ~(y & mid1)
truetruemid4 = ~(mid2 & mid3)
truetrueif(mid4 == enc[i]):
truetruetrueflag[i] = j
truetruetrueflagstr += chr(j)
truetruetruebreak
print(flag)
print(flagstr)

解得 flag

有个fseekftell的使用,fseek定位光标,组合使用可以得到文件字节长度。

DDCTF2018 黑盒破解

(不要想东西,你的脑子不是用来想东西的)

没错这个题目就是构造 opcode 读取 flag 的。

IDA64位打开,main函数很清晰:

sub_401E98sub_4016BD是虚拟机保护部分的指令初始代码;*(v4 + 16)读入字节码指令,前4个字符不是exit,调用judge_603F00并使其为1的是sub_40133D函数。

sub_4016BD函数的返回值应是1才会进入到正确分支。

静态调试 & 动态调试分析一下该函数做了什么:

(下面是巨丑无比的草稿)

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
base = (a1 + 8)

base (unit 1) | byte_6030E0
...
base + 0x100

base + 0x110 (unit 8) | *base - *a1

9 ops:
base + 0x120 (unit 4) | byte_6030E0 + 0xFF = byte_6031E0
... (9 unit)
base + 0x148

base + 0x190 (unit 1) | byte_603700
...
base + 0x290 | next_op or reg

base + 0x2A0 - 8 | func_reg

func:
base + 0x2A0 (unit 8) | sub_400DC1 + pre(dword_603200)
...(0xFF unit)
base + 0xA98

------------

base + 0x2A0 [byte_6031E0[l]] = off_603840[l]

byte_603600(byte_603900)[n] == byte_603800[3*l]
then byte_603900[n] == 0

------------

[a1 + 0x120] ;reg0
[a1 + 0x298] ;nxtop/regop
[a1 + 0x299] ;tmp

以上为虚拟机初始化各种解析的东西,,

可以看到sub_401A48是将输入作为虚拟机指令解析,执行对应操作函数:

一共有 9 种操作,输入 op,遍历 9 种操作查看与哪个匹配,b600[op] == a1 + 0x198 + [a1 + 4*(j + 0x48LL)+ 8],那么 handler 在a1 + 0x2A0中存放为a1 + 8*(p + 0x54) + 8a1 + 0x298为下一个指令。

idc 脚本跑出 opcode 和 handler,注意要在动态调试时跑,数据有变动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <idc.idc>

static main()
{
auto i, j, p, func;
auto b900 = 0x603900, b700 = 0x603700, a1 = 0x17DC8C0;
for(j = 0; j <= 8; j++) {
p = Byte(a1 + 4*(j + 0x48) + 8);
func = Dword(a1 + 8*(p + 0x54) + 8);
for(i = 0; i < 256; i++) {
if(Byte(b900 + i) == Byte(a1 + p + 0x198)) {
Message("%x %c %x\n", i, i, func);
break;
}
}
}
}

idapython 脚本:

1
//坑中

结果如下:

1
2
3
4
5
6
7
8
9
24 $ 400dc1		// tmp = base[reg0];
38 8 400e7a // base[reg0] = tmp;
43 C 400f3a // tmp = tmp + nxtop - 0x21;
74 t 401064 // tmp = tmp - nxtop + 0x21;
30 0 4011c9 // ++reg0;
45 E 40133d // if nxtop == 's' then puts(s = base[reg0+(0,20)]);
75 u 4012f3 // --reg0;
23 # 4014b9 // base[reg0] = input[reg0 + nxtop - 0x30] - 0x31;
3b ; 400cf1 // reg0 += nxtop; base[reg0] = input[reg0 + nxtop - 0x30] - 0x31;

可以看到比较关键的函数 sub_40133D ,当下一个指令为s时,进入 if 分支,judge_603F00为1,即主函数中成功输出 flag 文件。具体的判断函数分析有、、复杂,但我们可以看到主函数的提示信息\n\nPlease Input Your Passcode,If You See print the \"Binggo\" string,Congratulations,You Win. Good luck!,所以我们需要让这个函数中的s输出为Binggo\x00

构造指令,注意最后\00的构造,因为有判断不能为 0 ,所以不能延续之前的构造方式,考虑用#指令对应的函数#B1构造得:

1
Cc80CH80C&80t(8080C)80#B1uuuuuuEs

还有师傅倒着构造好机智。

最后跑下脚本连接服务器得 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

r = remote('49.234.62.86', 10100)
context(os='linux',arch='i386',log_level='debug')

password = '48ee204317'
opcode = 'Cc80CH80C&80t(8080C)80#B1uuuuuuEs'
r.sendline(password)
r.sendline(opcode)

r.interactive()

# flag{VMP_is_Too_h@rd!}
RCTF2018 magic

这是一个(我觉得很)难的综合题,涉及动态调试程序入口点、暴力破解、程序复用思想、rc4、虚拟机指令分析等知识点。

寻找程序入口点

第一步关键函数定位,运行程序得到如下结果

查找字符串,IDA分析出的main函数有点儿假,动态调试什么时候输出字符串,得到sub_402357中调用的sub_402218输出了字符串,2357运行中才运行到的函数,是关键函数,静态状态下交叉引用无结果。大体逻辑如下,先经过了一个sub_402268check函数。

爆破时间验证

根据上面关键函数的内容,我们可知dword_4099D0[0]不能为0,所以需要使程序进入if流程,即需要使 v4 == 0x700

下面异或的随机数的生成也有当前时间决定,所以应该是有一个确定的时间。

timeoff即time64返回值的范围大约是$ 2^{17} $,可以考虑暴力。

table数组会异或生成随机数,参与执行sub_4027ED,改变v4v3的值。

爆破时间的思路主要就是程序的复用,而复用方式有很多种

  1. 官方wp 用 multiasm 写 shellcode 调用程序自身代码

    有机会填下坑 (咕咕咕)

  2. 吾爱破解的一个师傅优秀的LoadLibrary

    然而我真的跑不出来 orz

  3. CTFtime wp 原始复制IDA反编译程序代码稍作修改

    真的麻烦

    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
    #include <cstdio>
    #include <windows.h>
    #include <cstring>
    #include <bits/stdc++.h>

    char table[256] = {
    0x58, 0x71, 0x8F, 0x32, 0x05, 0x06, 0x51, 0xC7, 0xA7, 0xF8, 0x3A, 0xE1, 0x06, 0x48, 0x82, 0x09,
    0xA1, 0x12, 0x9F, 0x7C, 0xB8, 0x2A, 0x6F, 0x95, 0xFD, 0xD0, 0x67, 0xC8, 0xE3, 0xCE, 0xAB, 0x12,
    0x1F, 0x98, 0x6B, 0x14, 0xEA, 0x89, 0x90, 0x21, 0x2D, 0xFD, 0x9A, 0xBB, 0x47, 0xCC, 0xEA, 0x9C,
    0xD7, 0x50, 0x27, 0xAF, 0xB9, 0x77, 0xDF, 0xC5, 0xE9, 0xE1, 0x50, 0xD3, 0x38, 0x89, 0xEF, 0x2D,
    0x72, 0xC2, 0xDF, 0xF3, 0x7D, 0x7D, 0x65, 0x95, 0xED, 0x13, 0x00, 0x1C, 0xA3, 0x3C, 0xE3, 0x57,
    0xE3, 0xF7, 0xF7, 0x2C, 0x73, 0x88, 0x34, 0xB1, 0x62, 0xD3, 0x37, 0x19, 0x26, 0xBE, 0xB2, 0x33,
    0x20, 0x3F, 0x60, 0x39, 0x87, 0xA6, 0x65, 0xAD, 0x73, 0x1A, 0x6D, 0x49, 0x33, 0x49, 0xC0, 0x56,
    0x00, 0xBE, 0x0A, 0xCF, 0x28, 0x7E, 0x8E, 0x69, 0x87, 0xE1, 0x05, 0x88, 0xDA, 0x54, 0x3E, 0x3C,
    0x0E, 0xA9, 0xFA, 0xD7, 0x7F, 0x4E, 0x44, 0xC6, 0x9A, 0x0A, 0xD2, 0x98, 0x6A, 0xA4, 0x19, 0x6D,
    0x8C, 0xE1, 0xF9, 0x30, 0xE5, 0xFF, 0x33, 0x4A, 0xA9, 0x52, 0x3A, 0x0D, 0x67, 0x20, 0x1D, 0xBF,
    0x36, 0x3E, 0xE8, 0x56, 0xBF, 0x5A, 0x88, 0xA8, 0x69, 0xD6, 0xAB, 0x52, 0xF1, 0x14, 0xF2, 0xD7,
    0xEF, 0x92, 0xF7, 0xA0, 0x70, 0xA1, 0xEF, 0xE3, 0x1F, 0x66, 0x2B, 0x97, 0xF6, 0x2B, 0x30, 0x0F,
    0xB0, 0xB4, 0xC0, 0xFE, 0xA6, 0x62, 0xFD, 0xE6, 0x4C, 0x39, 0xCF, 0x20, 0xB3, 0x10, 0x60, 0x9F,
    0x34, 0xBE, 0xB2, 0x1C, 0x3B, 0x6B, 0x1D, 0xDF, 0x53, 0x72, 0xF2, 0xFA, 0xB1, 0x51, 0x82, 0x04,
    0x30, 0x56, 0x1F, 0x37, 0x72, 0x7A, 0x97, 0x50, 0x29, 0x86, 0x4A, 0x09, 0x3C, 0x59, 0xC4, 0x41,
    0x71, 0xF8, 0x1A, 0xD2, 0x30, 0x88, 0x63, 0xFF, 0x85, 0xDE, 0x24, 0x8C, 0xC3, 0x37, 0x14, 0xC7
    };
    char new_table[256];

    struct vstruct{
    int8_t a;
    int32_t b, c;
    }vars0[256];

    vstruct * sub_402690(signed int idx, vstruct * v_head)
    {
    vstruct *result; // rax

    if ( idx >= 0 && idx <= 255 )
    result = &v_head[idx];
    else
    result = 0LL;
    return result;
    }

    void sub_4026D0(int num, vstruct * v_head)
    {
    vstruct *temp1; // rax
    vstruct *temp2; // rax
    vstruct *temp3;

    temp3 = sub_402690(num, v_head);
    if ( temp3 )
    {
    if ( num & 0xF )
    temp1 = sub_402690(num - 1, v_head);
    else
    temp1 = 0;
    if ( num + 15 <= 0x1E )
    temp2 = 0;
    else
    temp2 = sub_402690(num - 16, v_head);

    if ( temp1 || temp2 )
    {
    if ( temp1 )
    {
    temp3->b = temp3->a + temp1->b;
    temp3->c = 2 * temp1->c;
    }
    if ( temp2 )
    {
    int d = temp3->a + temp2->b;
    if ( d < temp3->b )
    {
    temp3->b = d;
    temp3->c = 2 * temp2->c | 1;
    }
    }
    }
    else
    {
    temp3->b = temp3->a;
    }
    }
    }

    int main()
    {
    for(unsigned int timeoff = 0x5AFFE790; timeoff <= 0x5B028A8F; ++timeoff) {
    srand(timeoff);
    for(int i = 0; i < 256; ++i)
    new_table[i] = table[i] ^ rand();
    for(int i = 0; i < 256; ++i) {
    vars0[i].a = new_table[i];
    vars0[i].b = 0x7FFFFFFF;
    vars0[i].c = 0;
    sub_4026D0(i, vars0);
    }
    if(vars0[255].b == 0x700 && vars0[255].c != 0)
    printf("[*] Find time = %x\n[*] %x\n", timeoff, vars0[255].c); // [*] Find time = 5b00e398 [*] 322ce7a4
    }

    return 0;
    }

根据上面跑出的结果,时间应是0x5b00e398dword_4099D0[0]0x322ce7a4

时间验证通过方法

  1. Patch call time64mov eax, 5B00E398h

    将补丁应用到原有的二进制文件上的步骤为Edit->Patch Program/Apply patchs to input file

  2. mov [rbp+timeoff], eax 打断点,每次手动更改为0x5b00e398

分析输入验证主函数

调到这里让我好找1551,接下来运行有了正常的输入输出但是没找到在哪里,随便输入试了下,跳到了这里输出错误,交叉引用发现sub_4023B1中读入大小为 26 的字符串,是主要逻辑函数。

执行过程其实是:key_func``sub_402357在执行完成之后回到函数sub_4032A0,经过几次跳转来到函数sub_403180,发现onexit()函数,这个函数的作用是注册一个函数,使得程序在exit()的时候自动调用这个被注册的函数,所以在假main函数exit()时,执行到了sub_403260,调用了sub_4023B1, 即main_func

主函数用了时间验证中得到的dword_4099D0[0],即time_data

输出了输入提示信息,用time_data作为 key 加密过的亚子;

读入长度为 26 的字符串,某加密算法(其实就是rc4)加密;

if 条件句判断函数是循环指令码的感觉,虚拟机指令;

判断函数返回值为 0 则进入错误分支,反之,某加密算法解密,因为相同可知为对称加密算法,可以考虑程序复用,(不太清楚sub_401FFB进行了什么操作,)然后 putssomething 输出相关正确分支内容。

程序运行:vm(rc4(flag))

逆向思路:vm -> mid_str -> rc4 -> flag

虚拟机指令分析

先上该部分代码

虚拟机实现用到了setjmp|longjmp,参考资料(http://www.cs.cmu.edu/afs/cs/academic/class/15213-s02/www/applications/recitation/recitation7/B/r07-B.pdf),我是无脑~~翻译~~谷歌翻译搬运工:

1
2
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

易于处理异常和中断。

  • setjmp 将环境(例如寄存器)保存在env中,供longjmp稍后使用
  • longjmp 完成后,调用后程序开始,对于setjmp,就像setjmp返回了val

应用实例:

  1. 考虑如下情景,编写一个项目,当它出现段错误的时候就会失败。相反,只需优雅地重新启动它即可。

    如何做到?catch SIGSEGV 错误,使用setjmp|longjmp跳转回到开始。

  2. 一个应用setjmp|longjmp的常见 bug:不能从一个调用setjmp的函数 return 。

还用到了signal函数,设置一个函数来处理信号

1
void (*signal(int sig, void (*func)(int)))(int)

这个地方很巧妙地,先将input_flag拷贝到Count,然后用signal设置一个函数。

汇编更清晰一点

设置函数如下

接下来看取指令过程

op = setjmp(Count) 返回值一开始为 0,到如下分支

sub_404716中调用longjmp,设置 v5op = setjmp(Count) 返回值,即 op = opcode[i]。故 byte_405340opcode数组。

在各个 handler 中有一个如下操作,除法,除数为 0 时会引起异常,会到 sign_func 执行操作,执行完又到了循环中的 setjmp 执行再次取指令等操作。所以这一部分其实是执行的 sign_func 中的运算操作。

dword_409040作为虚拟机寄存器reg

静态/动态调试分析可得出如下指令集

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
before while(1):
truereg[1] = &match_str
truereg[2] = &rc4(input_flag)
AB 03 00:
truereg[3] = 0;
AB 04 1A:
truereg[4] = 0x1A;( = len(rc4(input_flag)))
AB 00 66:
truereg[0] = 0x66;
loop:
AA 05 02:
truereg[5] = reg[2];( = &rc4(input_flag))
A9 53:
truereg[5] += reg[3];( = &rc4(input_flag)[i])
A0 05:
truereg[5] = [reg[5]];( = rc4(input_flag)[i])
AB 06 CC:
truereg[6] = 0xCC;
A9 56:
truereg[5] += reg[6];( = rc4(input_flag)[i] + 0xCC)
AB 06 FF:
truereg[6] = 0xFF;
AC 56:
truereg[5] &= reg[6];( = (rc4(input_flag)[i] + 0xCC) & 0xFF)
AE 50:
truereg[5] ^= reg[0];( = ((rc4(input_flag)[i] + 0xCC) & 0xFF) ^ (i & 1 ? 0x99 : 0x66))
AD 00:
truereg[0] = ~reg[0];
AA 06 05:
truereg[6] = reg[5];
AA 05 01:
truereg[5] = reg[1];( = &match_str)
A9 53:
truereg[5] += reg[3];( = &match_str[i])
A0 05:
truereg[5] = [reg[5]];( = match_str[i])
AF 56 00:
truereg[5] = (reg[5] == reg[6]);
A7 01:
trueif(reg[5]) reg_ip += 1;
CC:
trueret = reg[5]; exit;
A9 35:
truereg[3] += reg[5];
AA 05 03:
truereg[5] = reg[3];
AF 54 00:
truereg[5] = (reg[5] == reg[4]);
A6 D1:
trueif(!reg[5]) reg_ip += 0XD1;(reg_ip -= 0x2F => goto loop)
CC:
trueret = reg[5]; exit;

动态调试 got match_str :

根据分析出来的程序写脚本,逆过程得到rc4加密过的flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enc = [
true0x89, 0xC1, 0xEC, 0x50, 0x97, 0x3A, 0x57, 0x59,
true0xE4, 0xE6, 0xE4, 0x42, 0xCB, 0xD9, 0x08, 0x22,
true0xAE, 0x9D, 0x7C, 0x07, 0x80, 0x8F, 0x1B, 0x45,
true0x04, 0xE8
]

en_flag = [0 for i in range(0x1A)]
en_str = ""

for i in range(0x1A):
trueif i & 1:
truetruetemp = 0x99
trueelse:
truetruetemp = 0x66
trueenc[i] ^= temp
trueenc[i] -= 0xCC
trueen_flag[i] = enc[i] & 0xFF
trueen_str += hex(en_flag[i])
trueen_str += ' '

print(en_str)

encode_flag_str :

1
0x23 0x8c 0xbe 0xfd 0x25 0xd7 0x65 0xf4 0xb6 0xb3 0xb6 0xf 0xe1 0x74 0xa2 0xef 0xfc 0x38 0x4e 0xd2 0x1a 0x4a 0xb1 0x10 0x96 0xa5
rc4 解得 flag

有两种方法:

  1. 将上述字符串作为输入串,用原函数跑(因为是对称加密)

    Patch 修改16进制数据,跑出来

    看吾爱师傅用了脚本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    from idaapi import *
    def PatchArr(dest, str):
    for i in range(len(str)):
    c = str[i]
    idc.PatchByte(dest+i, ord(c));
    print 'ok'

    input = "\x23\x8c\xbe\xfd\x25\xd7\x65\xf4\xb6\xb3\xb6\x0f\xe1\x74\xa2\xef\xfc\x38\x4e\xd2\x1a\x4a\xb1\x10\x96\xa5"
    PatchArr(0x0060FD54, input)
  2. 代码解得

    1
    2
    input = "\x23\x8c\xbe\xfd\x25\xd7\x65\xf4\xb6\xb3\xb6\x0f\xe1\x74\xa2\xef\xfc\x38\x4e\xd2\x1a\x4a\xb1\x10\x96\xa5"
    # 没错在坑
1
@ck_For_fun_02508iO2_2iOR}

运行程序,输入上述字符串,得到字符画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
the part of flag was protected by a magic spell!
@ck_For_fun_02508iO2_2iOR}
.843fFDCb52bc573DA7e336b4BCC97C6E.
.1adC4b19FEBA1Bf9D182FAe8Eac1AeBF.
.CB7EEFeD2B2D6dd76f bE D0 ec92.
.DD1C36EDBaf56 63b6 ad83 f5D a60D.
.28CCE56eaBbcF 0Bb9 ed7F 669 aff7.
. dC 83 4 bf a01 .
. DAB 2a0 CBD eB74 9eF6 0De 1Bf .
. E15 d55A276 7A4c fA7 eE72 dc7 .
. afB bE0fa2e 7Bf9 Eb14 6A5 891 .
. DCf c907BF9 aFBB 28eA 4dE aB1 .
. B25 c5B 16d d90f 0cb0 D78 Edd .
. aEA7 eDaD 07 743A 935 27d .
.D38f5b1FacEaBDeFBEEcbA4 0b9D0A0f.
.ce1A5DFCe012a0a62A5e2D8 8e38C9A.
.CC1b26fF12fC01f8aeB7cAC06c65FCbe.
.e663471A878EcE289bee7c11d7f8CF7b.
.--------------------------------.
@ck_For_fun_02508iO2_2iOR}
.--------------------------------.

(真难看),其实就是rctf{h

所以 flag 就是

1
rctf{h@ck_For_fun_02508iO2_2iOR}

参考

[1] 虚拟化保护合集|wyx’s blog

[2] 虚拟机保护逆向入门

CATALOG
  1. 1. ——虚拟机指令分析 Virtual Machine Command Analysis
  2. 2. 原理
    1. 2.1. 基本原理
    2. 2.2. VMP虚拟机
    3. 2.3. VMProtect 壳
  3. 3. 分析方法
  4. 4. 题目
    1. 4.1. RCTF2018 simple-vm
    2. 4.2. DDCTF2018 黑盒破解
    3. 4.3. RCTF2018 magic
      1. 4.3.1. 寻找程序入口点
      2. 4.3.2. 爆破时间验证
      3. 4.3.3. 分析输入验证主函数
      4. 4.3.4. 虚拟机指令分析
      5. 4.3.5. rc4 解得 flag
  5. 5. 参考