不知不觉做到了虚拟机逆向了,曾经我也只是听说,还未曾想也能自己做出。

静态分析文件

exe文件,先查壳,没有壳直接ida打开。

main函数逻辑还是比较简单的,先拷贝一串内存给v4,v4再作为第一个参数给vm_operad函数,第二个参数是114。

那么我们先提取它拷贝的内存,至于这段内存如何使用那就进去分析vm_operad函数了。很明显,这个第一个参数是int类型的,并且也没有对a1做强制转换之类的关系,那么很明显这个内存是一个int数组,那么用提取成char数组之后再用int 类型去输出它就能得到对应的int数组。

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
#include<stdio.h>
#include<bits/stdc++.h>
unsigned char s[] =
{
0x0A, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x05, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x21, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x51, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x04, 0x00,
0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x0C, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0B, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x02, 0x00,
0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x08, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x41, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x25, 0x00, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00,
0x08, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x20, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
0x41, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x0C, 0x00,
0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x22, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x3F, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x34, 0x00, 0x00, 0x00,
0x07, 0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x72, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x33, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x18, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0xA7, 0xFF, 0xFF, 0xFF,
0x07, 0x00, 0x00, 0x00, 0x31, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0xF1, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00,
0x28, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x84, 0xFF,
0xFF, 0xFF, 0x07, 0x00, 0x00, 0x00, 0xC1, 0xFF, 0xFF, 0xFF,
0x07, 0x00, 0x00, 0x00, 0x1E, 0x00, 0x00, 0x00, 0x07, 0x00,
0x00, 0x00, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00
};
int a[]={10,4,16,8,3,5,1,4,32,8,5,3,1,3,2,8,11,1,12,8,4,4,1,5,3,8,3,33,1,11,8,11,1,4,9,8,3,32,1,2,81,8,4,36,1,12,8,11,1,5,2,8,2,37,1,2,54,8,4,65,1,2,32,8,5,1,1,5,3,8,2,37,1,4,9,8,3,32,1,2,65,8,12,1,7,34,7,63,7,52,7,50,7,114,7,51,7,24,7,-89,7,49,7,-15,7,40,7,-124,7,-63,7,30,7,122};//运行结果
int main(){

int *p=(int *)s;
for(int i=0;i<sizeof(s)/4;i++){
printf("%d,",*(p+i));
}

return 0;
}

可以很明显发现,这个数组刚好114。vm逆向基本是自己定义了指令集做运算的,所以这个不可能是flag,就应该是指令,那么至于指令集应该在这个函数有给解析,接着往下看。

分析指令

这里看了很久,里面有一个str char数组和一个char变量v4,然后其它的都是int变量了,一开始对4给int变量都初始化0了,大概率是做一个下标的。然后char数组应该是输入的flag,至于v4变量猜测可能是做一个中间数的,就相当于寄存器一样。

接下来看看这五个int变量干嘛的。

先是v9,发现它在每一个case当中都有+1或者+2。就很像我们的ip寄存器,每执行一次指令都往后移,然后这里+1+2应该是某些指令有操作数,导致指令宽度为2,那么v9就是指向当前指令的。而且循环退出条件就是v9>=a2,a2是整个指令的长度。

其次v8,v8在所有指令中基本都是做str数组的下标,并且做下标的时候,str[v8]都是做源操作数,并且只在1号指令中有自增操作。

然后v7,这个很简单,只在7号指令中存在,并且做str[100+v7]这样的下标,然后和后一个操作数作比较,如果不等那么退出,那这个就应该是比较指针,7号指令应该是作比较的指令。大概率会做一个运算,运算结果就是str[100]往后的位置的变量。

接下来看看v6,只在1号指令中出现,并且是以str[100+v6]这样的下标,作为目的操作数。那这个大概率就是做一个变换然后运算结果保存到str[100]开始往后的位置。

最后一个是v5,只在8号指令出现,作用是取出v4的值保存在str[v5]中。这个v4我们前面分析就大概率是一个普通的寄存器,这里相当于就是取寄存器中的数还给内存一样的。

那么这些都了解之后就可以很轻松地写出指令的作用了,如下。

接下来就手动解析指令,这个指令编码比较简单,无非就是从头开始,遇到2,3,4,5,7就取出后一个数一起与他编码,否则单独作为一个指令编码。

这个其实可以做程序算的,但是为了一开始理解方便,我也还是手算了。可以发现比较指令都集中在最后了,那么我们从头开始分析。

1
2
3
4
5
6
7
8
9
10      //读取数据
4,16 //取出str[0]^16--->rax
8 //rax--->str[0]
//str[0]^=16;
3,5 //取出str[0]-5--->rax
1 //rax--->str[100]
//str[100]=str[0]-5;
//根据后面的比较str[100]=34,所以(str[0]^16)-5==34
//str[0]=55='7'

很容易算出来了str[0]='7'。剩下的同理,因为位之间比较独立,并且它有一定规律,手撸还是很块就出来了的。

下面给出我做这个用的草稿。

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
10      //读取数据
4,16 //取出str[0]^16--->rax
8 //rax--->str[0]
//str[0]^=16;
3,5 //取出str[0]-5--->rax
1 //rax--->str[100]
//str[100]=str[0]-5;
//根据后面的比较str[100]=34,所以(str[0]^16)-5==34
//str[0]=55='7'
4,32 //str[1]^32--->rax
8 //rax--->str[1]
//str[1]^=32;
5,3 //str[1]*3--->rax
1 //rax--->str[1]
//str[101]=(str[1]^32)*3
//str[101]=63
//str[1]=53='5'
3,2
8 //str[2]-=2;
11
1 //str[102]=str[2]-1;
//str[2]-3=52
//str[2]=55='7'
12
8 //str[3]+=1;
4,4
1 //str[103]=str[3]^4;
//(str[3]+1)^4=50
//str[3]=55='5'
5,3
8 //str[4]*=3;
3,33
1 //str[104]=str[4]-33;
//(str[4]*3)-33=114
//str(4)=49='1'
11
8
11
1
//str[5]-2=51
//str[5]=53='5'
4,9
8 //str[6]^=9;
3,32
1 //str[6]-=32;
//(str[6]^9)-32=24
//str[6]=49='1'
2,81
8 //str[7]+=81;
4,36
1 //str[7]^=36;
//(str[7]+81)^36=-89;
//str[7]=50='2'
12
8 str[8]+=1;
11
1 str[8]-=1;
str[8]=49='1'
5,2
8 //str[9]*=2;
2,37
1 //str[9]+=37;
//str[9]*2+37=-15
//str[9]=102='f'
2,54
8 //str[10]+=54;
4,65
1 //str[10]^=65;
//(str[10]+54)^65=40
//str[10]=51='3'
2,32
8 //str[11]+=32;
5,1
1 //
//str[11]+32=-124
//str[11]=100='d'
5,3
8 //str[12]*=3;
2,37
1 //str[12]+=37;
//str[12]*3+37=-63
//str[12]=52='4'
4,9 //str[13]^=9;
8
3,32 //str[13]-=32;
1
//(str[13]^9)-32=30;
//str[13]=53='7'
2,65
8 //str[14]+=65;
12
1 //str[14]++;

//str[14]=56='8'
比较:
7,34,7,63,7,52,7,50
7,114,7,51,7,24
7,-89,7,49,7,-15
7,40,7,-124,7,-63
7,30,7,122

最后得出这个15位的key就是757515121f3d478 。这个程序基本用不到动态调试,最后就算要跑也只是检验我们的flag是否正确而已,那么我们跑一遍吧。

最终提交的flag就是flag{757515121f3d478}

这题目在vmre中还是比较简单的,推荐入坑虚拟机逆向的师傅一定做做这个,然后自己粗心居然把11和12指令搞反了一开始,导致算出来的就一直不对,这个毛病还是得改改(捂脸