0%

题目地址:https://files.buuoj.cn/files/e9761f09f9aac733705d36db1305f015/attachment.exe

使用file查看程序,是32的PE可执行文件,拖进IDA

这是main函数的伪代码:

如果第一次反编译的伪代码很奇怪,可以试着用IDA跑一遍程序,再次反编译的代码会清晰很多

main函数的逻辑很清晰,先是将输入与一个字符串循环异或,然后将异或后的结果传入encrypt函数,encrypt函数返回后将加密后的数据与比较数据进行比较

所以接下来的重点就是分析encrypt函数

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v3; // ecx
int *v4; // eax
int v5; // ecx
int *v6; // eax
char *v7; // esi
int v8; // edi
unsigned int len; // kr00_4
char *v10; // ecx
__int128 *v11; // ecx
int *v12; // ecx
__int128 *v13; // edx
unsigned int v14; // edi
int v15; // eax
int v16; // eax
bool v17; // cf
unsigned __int8 v18; // al
unsigned __int8 v19; // al
unsigned __int8 v20; // al
const char *v21; // edx
int *v22; // eax
char *v23; // eax
int v25; // [esp-14h] [ebp-D8h]
int v26; // [esp-10h] [ebp-D4h]
void *input[4]; // [esp+24h] [ebp-A0h] BYREF
int v28; // [esp+34h] [ebp-90h]
unsigned int v29; // [esp+38h] [ebp-8Ch]
__int128 cipher[2]; // [esp+3Ch] [ebp-88h] BYREF
int v31; // [esp+5Ch] [ebp-68h]
__int128 text; // [esp+60h] [ebp-64h] BYREF
__int128 v33; // [esp+70h] [ebp-54h]
int v34; // [esp+80h] [ebp-44h]
char key[16]; // [esp+84h] [ebp-40h] BYREF
int v36[8]; // [esp+94h] [ebp-30h] BYREF
int v37; // [esp+C0h] [ebp-4h]

v28 = 0;
v29 = 15;
LOBYTE(input[0]) = 0;
v37 = 1;
v4 = printf(v3, "Please input your flag: ");
sub_83050((int)v4);
scanf((int)&dword_B0068, input);
strcpy(key, "SWPU_2019_CTF");
if ( v28 == 32 )
{
v36[4] = -1173078761;
v36[5] = 494076752;
v36[6] = -1811652486;
v36[7] = 688582768;
v8 = 0;
text = 0i64;
v34 = 0;
v33 = 0i64;
len = strlen(key);
do
{
v10 = (char *)input;
if ( v29 >= 0x10 )
v10 = (char *)input[0];
v10[v8] ^= key[v8 % len]; // 与字符串异或
++v8;
}
while ( v8 < 32 );
v11 = (__int128 *)input;
v7 = (char *)input[0];
if ( v29 >= 0x10 )
v11 = (__int128 *)input[0];
v31 = 0;
memset(cipher, 0, sizeof(cipher));
text = *v11;
v33 = v11[1];
encrypt(v25, v26, 256, (unsigned int)&text, (unsigned int)cipher);
v36[0] = -133220429;
v36[1] = 1571732668;
v12 = v36;
v36[2] = -2041750854;
v13 = cipher;
v36[3] = -748513468;
v14 = 28;
v36[4] = 371505743;
v36[5] = 443719435;
v36[6] = 644704357;
v36[7] = 1741188026;
while ( 1 )
{
v15 = *v12;
if ( *v12 != *(_DWORD *)v13 )
break;
++v12;
v13 = (__int128 *)((char *)v13 + 4);
v17 = v14 < 4;
v14 -= 4;
if ( v17 )
{
v16 = 0;
goto LABEL_19;
}
}
v17 = (unsigned __int8)v15 < *(_BYTE *)v13;
if ( (_BYTE)v15 == *(_BYTE *)v13
&& (v18 = *((_BYTE *)v12 + 1), v17 = v18 < *((_BYTE *)v13 + 1), v18 == *((_BYTE *)v13 + 1))
&& (v19 = *((_BYTE *)v12 + 2), v17 = v19 < *((_BYTE *)v13 + 2), v19 == *((_BYTE *)v13 + 2))
&& (v20 = *((_BYTE *)v12 + 3), v17 = v20 < *((_BYTE *)v13 + 3), v20 == *((_BYTE *)v13 + 3)) )
{
v16 = 0;
}
else
{
v16 = v17 ? -1 : 1;
}
LABEL_19:
if ( v16 )
v21 = "Try again!\r\n";
else
v21 = "Congratulations! I always knew you could do it.";
v22 = printf((int)v12, v21);
sub_83050((int)v22);
sub_8ADBE("pause");
}
else
{
v6 = printf(v5, "Try again!\r\n");
sub_83050((int)v6);
sub_8ADBE("pause");
v7 = (char *)input[0];
}
if ( v29 >= 0x10 )
{
v23 = v7;
if ( v29 + 1 >= 0x1000 )
{
v7 = (char *)*((_DWORD *)v7 - 1);
if ( (unsigned int)(v23 - v7 - 4) > 0x1F )
_invalid_parameter_noinfo_noreturn();
}
sub_864DE(v7);
}
return 0;
}

这是encrypt函数的伪代码,远不如main函数清晰,IDA对于函数参数的识别也不尽如人意,勉强可以看出text(即等待加密的数据)被传入了sub_82270这个函数,进入这个函数

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
void __cdecl encrypt(int a1, int a2, int a3, unsigned int a4, unsigned int a5)
{
unsigned __int8 *v5; // ecx
unsigned int v6; // ebx
unsigned __int8 *text; // esi
unsigned int v8; // edi
unsigned int v9; // esi
unsigned int v10; // edx
unsigned int v11; // esi
_DWORD *v12; // ecx
unsigned int v13; // eax
unsigned int v14; // ebx
char *v15; // esi
__m128i v16; // xmm0
__m128i v17; // xmm1
__m128i v18; // xmm0
__m128i v19; // xmm1
__m128i v20; // xmm0
__m128i v21; // xmm1
__m128i v22; // xmm0
__m128i v23; // xmm1
unsigned int v24; // ebx
unsigned int v25; // eax
char *v26; // esi
unsigned int v27; // edi
int v28; // ecx
signed int v29; // [esp+20h] [ebp-20h]
int v30; // [esp+24h] [ebp-1Ch]
_DWORD *Block; // [esp+28h] [ebp-18h]
int v32[4]; // [esp+2Ch] [ebp-14h] BYREF

v6 = a5;
text = v5;
v8 = (unsigned int)(a3 + 31) >> 5;
v30 = 4 * v8;
Block = malloc(4 * v8);
v32[0] = -1839987866;
v32[1] = 120;
v32[2] = -1839987866;
v32[3] = 120;
sub_82270(text, (unsigned __int8 *)v32);
sub_820E0();
sub_82150();
sub_81F80();
v29 = 0;
if ( v8 )
{
do
{
dword_B0D94 = (2 * dword_B0DA4) ^ (unsigned __int16)(dword_B0DBC ^ (2 * dword_B0DA4));
dword_B0D78 = (dword_B0DB8 << 16) | ((unsigned int)dword_B0DAC >> 15);
v9 = (dword_B0D70 << 16) | ((unsigned int)dword_B0D90 >> 15);
dword_B0D8C = (dword_B0D68 << 16) | ((unsigned int)dword_B0D84 >> 15);
dword_B0D7C = v9;
Block[v29] = v9 ^ sub_82150();
sub_81F80();
++v29;
}
while ( v29 < (int)v8 );
v6 = a5;
}
v10 = 0;
if ( v8 )
{
v11 = a4;
if ( v8 < 0x10 || v6 <= a4 + v30 - 4 && v6 + v30 - 4 >= a4 )
{
v12 = Block;
}
else
{
v12 = Block;
if ( v6 > (unsigned int)&Block[v8 - 1] || v6 + v30 - 4 < (unsigned int)Block )
{
v13 = a4 + 16;
v12 = Block;
v14 = v6 + 32;
v15 = (char *)Block - a4;
do
{
v16 = *(__m128i *)(v13 - 16);
v13 += 64;
v14 += 64;
v17 = _mm_xor_si128(*(__m128i *)&Block[v10], v16);
v18 = *(__m128i *)(v13 - 64);
*(__m128i *)(v14 - 96) = v17;
v19 = _mm_xor_si128(*(__m128i *)&v15[v13 - 64], v18);
v20 = *(__m128i *)(v13 - 48);
*(__m128i *)(a5 - a4 + v13 - 64) = v19;
v15 = (char *)Block - a4;
v21 = _mm_xor_si128(*(__m128i *)((char *)Block + v14 - a5 - 64), v20);
v22 = *(__m128i *)(v13 - 32);
*(__m128i *)(v14 - 64) = v21;
v23 = *(__m128i *)&Block[v10 + 12];
v10 += 16;
*(__m128i *)(v14 - 48) = _mm_xor_si128(v23, v22);
}
while ( v10 < (v8 & 0xFFFFFFF0) );
v6 = a5;
v11 = a4;
}
}
if ( v10 < v8 )
{
v24 = v6 - a4;
v25 = v11 + 4 * v10;
v26 = (char *)v12 - a4;
v27 = v8 - v10;
do
{
v28 = *(_DWORD *)&v26[v25];
v25 += 4;
*(_DWORD *)(v24 + v25 - 4) = *(_DWORD *)(v25 - 4) ^ v28;
--v27;
}
while ( v27 );
}
}
free(Block);
}

sub_82270函数的伪代码,更是令人费解,甚至出现了或运算,这个运算一般是不可逆的!

大致看了一下也不像是什么常见的加密算法

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
unsigned int __fastcall sub_82270(unsigned __int8 *a1, unsigned __int8 *a2)
{
unsigned int v2; // esi
unsigned int v3; // ebx
int v4; // edi
int v5; // edi
int v6; // edi
__int16 v7; // cx
unsigned int v8; // edi
unsigned int v9; // edx
unsigned int v10; // ecx
unsigned int v11; // edx
unsigned int v12; // esi
unsigned int v13; // eax
int v14; // ecx
unsigned int v15; // edi
unsigned int result; // eax
int v17; // [esp+Ch] [ebp-38h]
int v18; // [esp+10h] [ebp-34h]
int v19; // [esp+14h] [ebp-30h]
int v20; // [esp+18h] [ebp-2Ch]
int v21; // [esp+1Ch] [ebp-28h]
int v22; // [esp+20h] [ebp-24h]
int v23; // [esp+24h] [ebp-20h]
unsigned int v24; // [esp+28h] [ebp-1Ch]
int v25; // [esp+2Ch] [ebp-18h]
unsigned int v26; // [esp+30h] [ebp-14h]
unsigned int v27; // [esp+34h] [ebp-10h]
int v28; // [esp+38h] [ebp-Ch]
unsigned int v29; // [esp+3Ch] [ebp-8h]
int v30; // [esp+40h] [ebp-4h]

v2 = *a2 | (*a1 << 23) | 0x44D700;
v22 = a2[1] | (a1[1] << 23) | 0x26BC00;
v30 = a2[2] | (a1[2] << 23) | 0x626B00;
v21 = a2[3] | (a1[3] << 23) | 0x135E00;
v3 = a2[4] | (a1[4] << 23) | 0x578900;
v29 = a2[5] | (a1[5] << 23) | 0x35E200;
v20 = a2[6] | (a1[6] << 23) | 0x713500;
v28 = a2[7] | (a1[7] << 23) | 0x9AF00;
v19 = a2[8] | (a1[8] << 23) | 0x4D7800;
v27 = a2[9] | (a1[9] << 23) | 0x2F1300;
v26 = a2[10] | (a1[10] << 23) | 0x6BC400;
v25 = a2[11] | (a1[11] << 23) | 0x1AF100;
v18 = a2[12] | (a1[12] << 23) | 0x5E2600;
v4 = a2[13] | (a1[13] << 23);
dword_B0D9C = 0;
v24 = v4 | 0x3C4D00;
v5 = a2[14] | (a1[14] << 23);
dword_B0DB4 = 0;
v23 = v5 | 0x789A00;
v6 = a1[15];
v7 = v23;
v17 = 32;
v8 = a2[15] | (v6 << 23) | 0x47AC00;
do
{
dword_B0D94 = (2 * v8) ^ (unsigned __int16)(v7 ^ (2 * v8));
dword_B0D78 = (v27 >> 15) | (v25 << 16);
dword_B0D8C = (v29 >> 15) | (v28 << 16);
dword_B0D7C = (v2 >> 15) | (v30 << 16);
v9 = ((v2 + (((v2 << 8) | (v2 >> 23)) & 0x7FFFFFFF)) & 0x7FFFFFFF)
+ ((v2 + (((v2 << 8) | (v2 >> 23)) & 0x7FFFFFFF)) >> 31)
+ (((v3 >> 11) | (v3 << 20)) & 0x7FFFFFFF);
v10 = (v9 & 0x7FFFFFFF) + (v9 >> 31) + (((v26 >> 10) | (v26 << 21)) & 0x7FFFFFFF);
v11 = (v10 & 0x7FFFFFFF) + (v10 >> 31) + (((v24 << 17) | (v24 >> 14)) & 0x7FFFFFFF);
v12 = (v11 & 0x7FFFFFFF) + (v11 >> 31) + (((v8 << 15) | HIWORD(v8)) & 0x7FFFFFFF);
v13 = (v12 & 0x7FFFFFFF) + (v12 >> 31) + ((unsigned int)sub_82150() >> 1);
v2 = v22;
v22 = v30;
dword_B0D88 = v30;
v30 = v21;
dword_B0D70 = v21;
v14 = v3;
v3 = v29;
v21 = v14;
dword_B0DB0 = v14;
v29 = v20;
dword_B0D84 = v20;
v20 = v28;
dword_B0D98 = v28;
v28 = v19;
dword_B0D68 = v19;
v19 = v27;
dword_B0D74 = v27;
v27 = v26;
dword_B0DAC = v26;
v26 = v25;
dword_B0D6C = v25;
v25 = v18;
dword_B0DB8 = v18;
v18 = v24;
dword_B0DA0 = v24;
v24 = v23;
dword_B0DA8 = v23;
v7 = v8;
v23 = v8;
v15 = v13 >> 31;
result = v13 & 0x7FFFFFFF;
v8 = result + v15;
dword_B0DBC = v23;
--v17;
}
while ( v17 );
dword_B0DA4 = v8;
dword_B0D90 = v2;
dword_B0D80 = v3;
return result;
}

像这种看起来很复杂,又不是常见的加密算法的,我们其实只需要关注数据的变化,使用内存硬件断点就可以达到这个目的

先把程序跑起来,输入一串字符,最好有规律一点,这个题目会在输入后判断字符的长度是否为32,所以我们输入的长度要是32

在这个循环中,将输入与一个常量字符串“SWPU_2019_CTF”循环异或

异或后的数据保存在这里,第一个字符b是已经异或过的结果,在b的位置下一个硬件断点,按F2即可

勾选上Hardware,因为是数据,所以下面的复选框中我们勾选Read和Write即可

然后F9运行程序,断在了这里

查看text的内存发现这里的操作是将异或后的数据拷贝到text中,所以在text的内存位置我们也要下一个硬件断点,之前下的那个硬件断点删不删都行

再次F9运行程序,断在了这里

这个循环将[eax-4]中的数据与ecx进行异或,查看内存发现[eax-4]的数据就是text的数据

所以这个循环就是将text异或,然后存入[ebx+eax-4]的地址,在这里我们继续下一个硬件断点

然后F9运行,程序断在这里:

这里已经是最后比较数据的地方了,所以其实输出的数据只是经过了两次异或加密,那些复杂的算式可能只是为了产生异或的数据,有了这个猜测,我们可以在异或的地址处下断点,检查不同的输入是否会产生相同的异或数据

第一次异或的数据是一个常量字符串,我们不用关心,第二次异或的数据我们倒是不清楚,所以在这里下一个断点

然后运行两次,输入不同的数据

查看两次运行的内存发现是一样的,所以异或的数据与我们的输入无关

我们把异或的数据提取出来

[0x86, 0x0C, 0x3E, 0xCA, 0x98, 0xD7, 0xAE, 0x19, 0xE2, 0x77, 0x6B, 0xA6, 0x6A, 0xA1, 0x77, 0xB0, 0x69, 0x91, 0x37, 0x05, 0x7A, 0xF9, 0x7B, 0x30, 0x43, 0x5A, 0x4B, 0x10, 0x86, 0x7D, 0xD4, 0x28]

第一次异或的数据是常量字符串“SWPU_2019_CTF”

比较的数据是:

[0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A, 0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16, 0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B, 0xC8, 0x67]

解密脚本:

1
2
3
4
5
6
7
cmp = [0xB3, 0x37, 0x0F, 0xF8, 0xBC, 0xBC, 0xAE, 0x5D, 0xBA, 0x5A, 0x4D, 0x86, 0x44, 0x97, 0x62, 0xD3, 0x4F, 0xBA, 0x24, 0x16, 0x0B, 0x9F, 0x72, 0x1A, 0x65, 0x68, 0x6D, 0x26, 0xBA, 0x6B, 0xC8, 0x67]
xor_data2 = [0x86, 0x0C, 0x3E, 0xCA, 0x98, 0xD7, 0xAE, 0x19, 0xE2, 0x77, 0x6B, 0xA6, 0x6A, 0xA1, 0x77, 0xB0, 0x69, 0x91, 0x37, 0x05, 0x7A, 0xF9, 0x7B, 0x30, 0x43, 0x5A, 0x4B, 0x10, 0x86, 0x7D, 0xD4, 0x28]
xor_data1 = "SWPU_2019_CTF"
for i in range(len(cmp)):
cmp[i] ^= xor_data2[i]
cmp[i] ^= ord(xor_data1[i % len(xor_data1)])
print(chr(cmp[i]), end='')

小结

当遇到加密函数很复杂而且不是常见的加密算法时,我们可以先使用硬件断点将输入的变化过程分析出来,这可以避免去分析一些与外部变量无关的代码,大大节省解题时间

识别C++程序

C++程序的反汇编代码会呈现一些特殊之处,通过这些特征我们可以识别一个二进制文件是否是由C++编写

频繁使用ecx寄存器(this指针)

在函数调用之前,ecx被赋值为this指针

如果一个函数在使用ecx之前没有初始化他,这个函数可能是一个类成员函数

调用约定

如果一个函数调用完成后,eax被赋值给ecx,紧接着调用另一个函数

前一个函数可能是在实例化一个类,后一个函数可能是该类的构造函数

在C++程序中,虚函数的调用往往是隐式的

STL代码和导入的DLL

在IDA的import窗口可以查看程序导入的DLL,根据名称判断是否是C++程序

STL函数的调用:

类实例布局

一般的类

在C++中实例化一个类:

这个类在内存中的布局是这样的:

为了对齐四字节的地址,最后一个变量的结尾会被填充一个三字节的垃圾数据

含有虚函数的类

如果一个类中包含有虚函数

这个类在内存中的布局会是这样

在布局的最前方会加入一个vfptr,这里保存虚函数表的地址,这个地址指向的内存记录了每个虚函数的地址

单继承的类

如果一个类单继承另一个类

这个类在内存中的布局会是这样

仅仅是将派生类的成员变量添加在父类的后面

多继承的类

这个类在内存中的布局:

与单继承大同小异,子类的成员变量添加在父类后面

另外,子类的虚函数会添加到第一个父类的虚函数表中

识别类

玩逆向半年了,还是菜的发慌,本来想RE和PWN双修,无奈发现精力跟不上,目前的想法是先把RE该学的学了,有空的话PWN的常规知识也要学一下,实话实说PWN有点难,学起来不如RE快

寒假在家闲,最近开始接触虚拟机逆向,从一道简单的虚拟机逆向开始

ida打开程序,在函数窗口搜索main,发现没有main函数,但是在invoke_main中发现了_main,跳过去发现是main函数里面有花指令,修一下就好

nop掉一个花指令,就可以反编译了

main函数伪代码:

这是我修改过后的伪代码,对一些变量的名称进行了修改,使伪代码更加易懂

最外层是一个死循环,死循环中嵌套了一个小的for循环,for循环的作用是读取opcode与option_index进行比较,如果比较相同的话就退出for循环

显然如果循环退出,那么接下来的option[i]调用的函数必定是opcode[vm_eip]对应的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int i; // [esp+10h] [ebp-8h]

sub_4F14F0();
while ( 1 )
{
LABEL_2:
for ( i = 0; ; i += 2 )
{
if ( i >= 0x58 )
goto LABEL_2;
if ( option_index[4 * i] == opcode[vm_eip] )
break;
}
option[i]();
}
}

所以题目的关键就在于opcode以及其对应的option,我们进入option一个一个分析

这里是我分析好的option,随便挑几个说一下

vm_xor_reg

1
2
3
4
5
6
7
8
9
10
11
12
13
int vm_xor_reg()
{
int *v0; // ecx
int reg_index; // eax
int result; // eax

v0 = reg[(unsigned __int8)first[vm_eip]];
reg_index = (unsigned __int8)second[vm_eip];
vm_eip += 3;
result = *reg[reg_index];
*v0 ^= result;
return result;
}

vm_eip不用解释了吧,这是程序的计数器,在调试的时候可以推测出来

那first和second又是什么呢?

先看下面,有一条指令是vm_eip += 3,由此可以推断出一条opcode的长度是三个字节

这些就是opcode,他们的组成是:option_index | first | second

  1. option_index:对应的option的编号
  2. first:前一个操作数
  3. second:后一个操作数

所以first[vm_eip]就相当于取前一个操作数,second同理

reg是一个数组,我们在这里可以把它看成是寄存器数组

变量都解释清楚了,那么这个函数的行为就显而易见了:

以first和second为下标取两个寄存器的值异或,并把结果放到第一个寄存器中

vm_set_zflag

1
2
3
4
5
6
7
8
9
10
void vm_set_zflag()
{
int *v0; // ecx
int reg_index; // eax

v0 = reg[(unsigned __int8)second[vm_eip]];
reg_index = (unsigned __int8)first[vm_eip];
vm_eip += 3;
vm_zflag = *reg[reg_index] - *v0;
}

其他的都解释过了,这个vm_zflag是怎么来的呢?

我们查看vm_zflag的交叉引用,发现他出现在另一个函数中,在这个函数中,vm_zflag被当成了跳转的条件,所以推测这个变量是zflag,用两个数相减来得到zflag的值也合乎情理

vm_jz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int vm_jz()
{
int result; // eax

if ( vm_zflag )
{
vm_eip += 3;
}
else
{
result = 3 * (unsigned __int8)first[vm_eip] - 3;
vm_eip = result;
}
return result;
}

vm_push_reg

1
2
3
4
5
6
7
8
9
void vm_push_reg()
{
int reg_index; // eax

reg_index = (unsigned __int8)first[vm_eip];
++stack_offset;
vm_eip += 3;
stack_base[stack_offset] = *(_BYTE *)reg[reg_index];
}

stack_base也是根据调试推断出来的,有了这个名称,这个函数很容易就可以理解为push_reg

分析这些option的时候,前面会很难懂,但是当你理解了一些变量代表的含义并赋予他们恰当的名字,后面的分析就可以变得很快

一些变量的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *sub_4F14F0()
{
char *v0; // esi

input_len = 0;
yushu = 0;
dword_535BD4 = 0;
vm_zflag = 0;
stack_offset = 0;
dword_535BD0 = 0;
vm_eip = 0;
stack_base = malloc(0x100u);
v0 = (char *)malloc(0x100u);
input = v0;
memset(stack_base, 0, 0x100u);
return memset(v0, 0, 0x100u);
}

现在,我们要做的就是将opcode翻译成这些option,编写一个翻译脚本

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
opcode = [0x01, 0x03, 0x03, 0x05, 0x00, 0x00, 0x11, 0x00, 0x00, 0x01, 0x01, 0x11, 0x0C, 0x00, 0x01, 0x0D, 0x0A, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x02, 0x00, 0x01, 0x00, 0x11, 0x0C, 0x00, 0x02, 0x0D, 0x2B, 0x00, 0x14, 0x00, 0x02, 0x01, 0x01, 0x61, 0x0C, 0x00, 0x01, 0x10, 0x1A, 0x00, 0x01, 0x01, 0x7A, 0x0C, 0x00, 0x01, 0x0F, 0x1A, 0x00, 0x01, 0x01, 0x47, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x06, 0x00, 0x01, 0x0B, 0x24, 0x00, 0x01, 0x01, 0x41, 0x0C, 0x00, 0x01, 0x10, 0x24, 0x00, 0x01, 0x01, 0x5A, 0x0C, 0x00, 0x01, 0x0F, 0x24, 0x00, 0x01, 0x01, 0x4B, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x07, 0x00, 0x01, 0x01, 0x01, 0x10, 0x09, 0x00, 0x01, 0x03, 0x01, 0x00, 0x03, 0x00, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x0B, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x00, 0x00, 0x02, 0x05, 0x00, 0x02, 0x01, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x00, 0x00, 0x02, 0x09, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x03, 0x00, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x05, 0x00, 0x02, 0x03, 0x00, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00, 0x02, 0x07, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0B, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x02, 0x02, 0x00, 0x02, 0x07, 0x00, 0x02, 0x02, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x02, 0x00, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x13, 0x01, 0x02, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x01, 0x0E, 0x5B, 0x00, 0x01, 0x01, 0x22, 0x0C, 0x02, 0x01, 0x0D, 0x59, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x4E, 0x00, 0x01, 0x03, 0x00, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00]
# print(len(opcode))

eip = 0
stack_base = [0] * 0x100
stack_offset = 0
input_len = 0
yushu = 0
zflag = 0
input = ''

for i in range(0x5d):
print('%02d' % (i+1), end=': ')
each_opcode = opcode[eip:eip+3]
if each_opcode[0] == 0:
print('nop')
if each_opcode[0] == 1:
print('mov reg[%d], %d' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 2:
print('push %d' % (each_opcode[1]))
if each_opcode[0] == 3:
print('push reg[%d]' % (each_opcode[1]))
if each_opcode[0] == 4:
print('pop reg[%d]' % (each_opcode[1]))
if each_opcode[0] == 5:
print('print message')
if each_opcode[0] == 6:
print('add reg[%d], reg[%d]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 7:
print('sub reg[%d], reg[%d]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 8:
print('mul reg[%d], reg[%d]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 9:
print('div reg[%d], reg[%d]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 0xa:
print('xor reg[%d], reg[%d]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 0xb:
print('jmp %d' % (each_opcode[1]))
if each_opcode[0] == 0xc:
print('zflag = reg[%d] - reg[%d]' % (each_opcode[1] , each_opcode[2]))
if each_opcode[0] == 0xd:
print('jz %d' % (each_opcode[1]))
if each_opcode[0] == 0xe:
print('jnz %d' % (each_opcode[1]))
if each_opcode[0] == 0xf:
print('jg %d' % (each_opcode[1]))
if each_opcode[0] == 0x10:
print('jl %d' % (each_opcode[1]))
if each_opcode[0] == 0x11:
print('gets input')
if each_opcode[0] == 0x12:
print('memset(input[%d, 0, %d])' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 0x13:
print('load reg[%d], stack[reg[%d]]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 0x14:
print('load reg[%d], input[reg[%d]]' % (each_opcode[1], each_opcode[2]))
if each_opcode[0] == 0xff:
print('exit')
eip += 3

以下是翻译脚本的输出,我分析后添加了一些注释

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
01: mov reg[3], 3
02: print message ;print p;z input:

03: gets input ;put input length into reg[0]

04: mov reg[1], 17
05: zflag = reg[0] - reg[1] ;input length shou be 17
06: jz 10

07: mov reg[3], 1
08: print message ;print wrong

09: exit

10: mov reg[2], 0
11: mov reg[0], 17
12: zflag = reg[0] - reg[2] ;set cycle index to 17
13: jz 43

14: load reg[0], input[reg[2]] ;mov each input byte to reg[0]
15: mov reg[1], 97 ;ascii 'a'
16: zflag = reg[0] - reg[1] ;Determine whether the value is less than charactor 'a'
17: jl 26

18: mov reg[1], 122 ;ascii 'z'
19: zflag = reg[0] - reg[1] ;Determine whether the value is more than charactor 'z'
20: jg 26

21: mov reg[1], 71 ;input char is in 'a' ~ 'z'
22: xor reg[0], reg[1]
23: mov reg[1], 1
24: add reg[0], reg[1] ;input char = (input char ^ 71) + 1
25: jmp 36

26: mov reg[1], 65 ;ascii 'A'
27: zflag = reg[0] - reg[1]
28: jl 36

29: mov reg[1], 90 ;asscii 'Z'
30: zflag = reg[0] - reg[1]
31: jg 36

32: mov reg[1], 75 ;input char is in 'A' ~ 'Z'
33: xor reg[0], reg[1]
34: mov reg[1], 1
35: sub reg[0], reg[1]

36: mov reg[1], 16
37: div reg[0], reg[1] ;input char / 16,
38: push reg[1] ;remainder
39: push reg[0] ;consult
40: mov reg[1], 1
41: add reg[2], reg[1] ;next input char
42: jmp 11

43: push 7 ;compare data
44: push 13
45: push 0
46: push 5
47: push 1
48: push 12
49: push 1
50: push 0
51: push 0
52: push 13
53: push 5
54: push 15
55: push 0
56: push 9
57: push 5
58: push 15
59: push 3
60: push 0
61: push 2
62: push 5
63: push 3
64: push 3
65: push 1
66: push 7
67: push 7
68: push 11
69: push 2
70: push 1
71: push 2
72: push 7
73: push 2
74: push 12
75: push 2
76: push 2

77: mov reg[2], 1
78: load reg[1], stack[reg[2]]
79: pop reg[0]
80: zflag = reg[0] - reg[1] ;compare input char
81: jnz 91

82: mov reg[1], 34
83: zflag = reg[2] - reg[1] ;set cycle index 34
84: jz 89

85: mov reg[1], 1
86: add reg[2], reg[1] ;compare next char
87: jmp 78

88: mov reg[3], 0
89: print message ;print right
90: exit

91: mov reg[3], 1
92: print message ;print wrong
93: exit

根据上面代码的逻辑写出模拟的python代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
compare_data = [2, 2, 12, 2, 7, 2, 1, 2, 11, 7, 7, 1, 3, 3, 5, 2, 0, 3, 15, 5, 9, 0, 15, 5, 13, 0, 0, 1, 12, 1, 5, 0, 13, 7]
flag = input()
result = []
flag = [ord(i) for i in flag]
if len(flag) == 17:
for i in flag:
if i >= ord('a') and i <= ord('z'):
i = ((i ^ 71) + 1) & 0xff
elif i >= ord('A') and i <= ord('Z'):
i = ((i ^ 75) - 1) & 0xff
result.append(i % 16)
result.append(i // 16)
print(result)
if result == compare_data:
print('right')
else:
print('wrong')
else:
print('wrong')

然后再写出解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
compare_data = [2, 2, 12, 2, 7, 2, 1, 2, 11, 7, 7, 1, 3, 3, 5, 2, 0, 3, 15, 5, 9, 0, 15, 5, 13, 0, 0, 1, 12, 1, 5, 0, 13, 7]
flag = ''
for i in range(0, 34, 2):
temp = compare_data[i] + compare_data[i+1] * 16
x = (temp - 1) ^ 71
y = (temp + 1) ^ 75
if x in range(97, 123):
flag += chr(x)
elif y in range(65, 91):
flag += chr(y)
else:
flag += chr(temp)
print(flag)
1
2
yiqiu@LAPTOP-I2IQS5DP ~/C/ezvm> python3 sim.py
flag{Such_A_EZVM}

总结

做完这道题,我对VM逆向有个大概的了解,VM的出题思路大致就是:编写一个指令集,设计一串opcode,实现一个将opcode和指令集联系起来的解释器

所以我们的做题思路就是:逆向指令集,搞清楚每个操作对应的行为,提取opcode,写出一个小的反汇编器,将opcode反汇编成能看懂的文本格式,逆向这个文本格式的代码,写出解题脚本

参考

参考了SYJ学长的文章,附件也是在原文中下载

链接:https://bbs.pediy.com/thread-267670.htm

7.1、编译器驱动程序

编译器驱动程序可以理解为一个软件包,像linux中常用的gcc,它可以自动完成程序的预处理、编译、汇编、链接

先看两个示例程序:

main.c

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

int sum(int *a, int n);

int array[2] = {1, 2};

int main()
{
int val = sum(array, 2);
return val;
}

sum.c

1
2
3
4
5
6
7
8
9
10
int sum(int *a, int n)
{
int i, s = 0;

for (i=0; i<n; i++)
{
s += a[i];
}
return s;
}

在linux环境下使用命令

1
$ gcc -Og -o prog main.c sum.c

运行prog,程序返回3

1
2
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> ./prog
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7 [3]>

prog的生成过程如图所示:

在这里gcc就是一个编译器驱动程序,它自动调用了cpp、cc1、as、ld等程序

7.2、静态链接

由ld完成,链接器主要做两件事:

  1. 符号解析
  2. 重定位

7.3、目标文件

目标文件有三种类型:

  1. 可重定位目标文件,一般以.o结尾
  2. 可执行文件,一般以,out结尾
  3. 共享目标文件,一般以.so结尾

可重定位和共享目标文件由编译器和汇编器生成,可执行目标文件由链接器生成

7.4、可重定位目标文件

一个典型的ELF可重定位目标文件格式如图所示

  1. ELF头:包含可重定位目标文件的大概信息
  2. 节头部表:包含可重定位目标文件的各个节的信息
  3. .text段:代码段
  4. .rodata段:只读数据段
  5. .data段:数据段,存放初始化的全局变量和静态变量
  6. .bss段:数据段,存放未初始化和初始化为零的全局变量和静态变量
  7. .symtab段:符号表,保存全局变量的符号以及函数名称
  8. .rel.text段:代码段的重定位信息
  9. .rel.data段:数据段的重定位信息
  10. .debug段:调试信息
  11. .line段:源代码和机器代码的行号映射关系
  12. .strtab段:字符串表

7.5、符号和符号表

在链接器的上下文中有三种不同的符号:

  1. 由本模块定义并能由其他模块引用的全局符号
  2. 由其他模块定义并能被本模块引用的全局符号
  3. 只被本模块定义和引用的局部符号

static前缀代表该变量是私有的,如果在多个函数中定义同名的static变量,编译器会为其生成不同的符号

  1. name:表示在符号表中的下标
  2. type:函数或者数据
  3. binding:本地或者全局
  4. value:相对于本节区头的偏移

符号表中的每个条目的组成如图所示,使用 readelf -s 可以查看我们之前生成的prog程序的符号表

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
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7 [1]> readelf -s prog

Symbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)

Symbol table '.symtab' contains 65 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000318 0 SECTION LOCAL DEFAULT 1
2: 0000000000000338 0 SECTION LOCAL DEFAULT 2
3: 0000000000000358 0 SECTION LOCAL DEFAULT 3
4: 000000000000037c 0 SECTION LOCAL DEFAULT 4
5: 00000000000003a0 0 SECTION LOCAL DEFAULT 5
6: 00000000000003c8 0 SECTION LOCAL DEFAULT 6
7: 0000000000000458 0 SECTION LOCAL DEFAULT 7
8: 00000000000004d6 0 SECTION LOCAL DEFAULT 8
9: 00000000000004e8 0 SECTION LOCAL DEFAULT 9
10: 0000000000000508 0 SECTION LOCAL DEFAULT 10
11: 0000000000001000 0 SECTION LOCAL DEFAULT 11
12: 0000000000001020 0 SECTION LOCAL DEFAULT 12
13: 0000000000001030 0 SECTION LOCAL DEFAULT 13
14: 0000000000001040 0 SECTION LOCAL DEFAULT 14
15: 00000000000011e8 0 SECTION LOCAL DEFAULT 15
16: 0000000000002000 0 SECTION LOCAL DEFAULT 16
17: 0000000000002004 0 SECTION LOCAL DEFAULT 17
18: 0000000000002048 0 SECTION LOCAL DEFAULT 18
19: 0000000000003df0 0 SECTION LOCAL DEFAULT 19
20: 0000000000003df8 0 SECTION LOCAL DEFAULT 20
21: 0000000000003e00 0 SECTION LOCAL DEFAULT 21
22: 0000000000003fc0 0 SECTION LOCAL DEFAULT 22
23: 0000000000004000 0 SECTION LOCAL DEFAULT 23
24: 0000000000004018 0 SECTION LOCAL DEFAULT 24
25: 0000000000000000 0 SECTION LOCAL DEFAULT 25
26: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
27: 0000000000001070 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
28: 00000000000010a0 0 FUNC LOCAL DEFAULT 14 register_tm_clones
29: 00000000000010e0 0 FUNC LOCAL DEFAULT 14 __do_global_dtors_aux
30: 0000000000004018 1 OBJECT LOCAL DEFAULT 24 completed.8061
31: 0000000000003df8 0 OBJECT LOCAL DEFAULT 20 __do_global_dtors_aux_fin
32: 0000000000001120 0 FUNC LOCAL DEFAULT 14 frame_dummy
33: 0000000000003df0 0 OBJECT LOCAL DEFAULT 19 __frame_dummy_init_array_
34: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
35: 0000000000000000 0 FILE LOCAL DEFAULT ABS sum.c
36: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
37: 0000000000002144 0 OBJECT LOCAL DEFAULT 18 __FRAME_END__
38: 0000000000000000 0 FILE LOCAL DEFAULT ABS
39: 0000000000003df8 0 NOTYPE LOCAL DEFAULT 19 __init_array_end
40: 0000000000003e00 0 OBJECT LOCAL DEFAULT 21 _DYNAMIC
41: 0000000000003df0 0 NOTYPE LOCAL DEFAULT 19 __init_array_start
42: 0000000000002004 0 NOTYPE LOCAL DEFAULT 17 __GNU_EH_FRAME_HDR
43: 0000000000003fc0 0 OBJECT LOCAL DEFAULT 22 _GLOBAL_OFFSET_TABLE_
44: 0000000000001000 0 FUNC LOCAL DEFAULT 11 _init
45: 00000000000011e0 5 FUNC GLOBAL DEFAULT 14 __libc_csu_fini
46: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
47: 0000000000004000 0 NOTYPE WEAK DEFAULT 23 data_start
48: 0000000000004010 8 OBJECT GLOBAL DEFAULT 23 array
49: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 23 _edata
50: 00000000000011e8 0 FUNC GLOBAL HIDDEN 15 _fini
51: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
52: 0000000000004000 0 NOTYPE GLOBAL DEFAULT 23 __data_start
53: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
54: 0000000000004008 0 OBJECT GLOBAL HIDDEN 23 __dso_handle
55: 0000000000001147 32 FUNC GLOBAL DEFAULT 14 sum
56: 0000000000002000 4 OBJECT GLOBAL DEFAULT 16 _IO_stdin_used
57: 0000000000001170 101 FUNC GLOBAL DEFAULT 14 __libc_csu_init
58: 0000000000004020 0 NOTYPE GLOBAL DEFAULT 24 _end
59: 0000000000001040 47 FUNC GLOBAL DEFAULT 14 _start
60: 0000000000004018 0 NOTYPE GLOBAL DEFAULT 24 __bss_start
61: 0000000000001129 30 FUNC GLOBAL DEFAULT 14 main
62: 0000000000004018 0 OBJECT GLOBAL HIDDEN 23 __TMC_END__
63: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
64: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2

COMMON和.bss的区别:

7.6、符号解析

符号解析是链接器的工作,编译器为程序生成符号,链接器通过解析这些符号来确定程序中的引用

对于局部符号,编译器直接生成与原来一样的符号,但是对于全局符号,这个过程就比较复杂,因为在全局符号中可能存在一样的符号

C++中的函数重载就涉及这个问题,两个函数有一样的函数名,但确实是不同的函数,C++的编译器使用了一种叫做符号修饰的方法

7.6.1、链接器如何解析多重定义的全局符号

编译器输出的符号分为强符号和弱符号

函数和已初始化的全局变量是强符号,未初始化的全局变量是弱符号

当一个程序中存在多重定义的全局符号时,链接器遵循以下原则:

7.6.2、与静态库链接

静态库相当于一个模块的集合,使用静态链接是,链接器会将程序中引用的模块从静态库中链接出来,而程序中没有引用的模块,链接器则不做处理

addvec.c

1
2
3
4
5
6
7
8
9
10
11
int addcnt = 0;

void addvec(int *x, int *y, int *z, int n)
{
int i;
addcnt++;
for (i = 0; i < 0; i++)
{
z[i] = x[i] + y[i];
}
}

multvec.c

1
2
3
4
5
6
7
8
9
10
11
int multcnt = 0;

void multvec(int *x, int *y, int *z, int n)
{
int i;
multcnt++;
for (i = 0; i < n; i++)
{
z[i] = x[i] * y[i];
}
}

使用ar将这两个模块合并到一个静态库中:

1
2
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -c addvec.c multvec.c 
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> ar rcs libvector.a addvec.o multvec.o

使用nm查看libvector.a中的符号:

1
2
3
4
5
6
7
8
9
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> nm libvector.a 

addvec.o:
0000000000000000 B addcnt
0000000000000000 T addvec

multvec.o:
0000000000000000 B multcnt
0000000000000000 T multvec

vector.h

1
2
3
void addvec(int *x, int *y, int *z, int n);

void multvec(int *x, int *y, int *z, int n);

在main函数中引用addvec函数,最后静态链接起来

main2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include"vector.h"

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];

int main()
{
addvec(x, y, z, 2);
printf("z = [%d, %d]\n", z[0], z[1]);
return 0;
}
1
2
3
4
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -c main2.c
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -static -o prog2 main2.o ./libvector.a
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> ./prog2
z = [4, 6]

链接过程中,gcc只会将addvec模块链接进程序,而不会理会multvec,这大大减小了程序占用的磁盘空间和运行时的内存

prog2的生成过程:

7.6.2、链接器如何使用静态库来解析引用

需要注意的是:使用编译器驱动程序生成可执行程序时,命令行中文件的顺序是有要求的,符号的引用必须在定义之前

假设main.c引用了libx.a存档中的符号,那么在命令行中,main.c和libx.c的顺序必须是main.c在前,否则会产生报错

7.7、重定位

重定位分为两步:

  1. 重定位节和符号定义
  2. 重定位节中的符号引用

7.7.1、重定位条目

重定位条目是由汇编器生成的,指导链接器完成符号引用的重定位,.text的重定位条目位于.rel.text

重定位条目中的每个元素如图所示

下图是elf文件中最基本的两种重定位类型,第一种可以理解为偏移地址重定位,第二种可以理解为绝对地址重定位

7.7.2、重定位符号引用

重定位算法如图所示:

遍历节区和节区中的重定位条目,然后计算重定位符号引用的地址,然后根据重定位类型进入不同的if语句块

7.8、可执行目标文件

可执行目标文件被映射到内存中时,相同权限的节区会被映射到一个段,上图的文件在内存中的段布局:

一个只读段,一个可读可写段

7.9、加载可执行目标文件

可执行文件加载到内存后,会在内存中生成一个映像,这个映像中不仅有可执行文件的副本,还有诸如堆、栈等运行时所需结构

以及共享库和内核的代码,加载的过程由加载器完成,它将可执行文件的内容复制到内存中并将控制权转交给程序

7.10、动态链接共享库

还记得我们之前使用过的示例程序吗?

这一次我们让其使用共享库

1
2
3
4
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -shared -fpic -o libcvector.so addvec.c multvec.c 
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> gcc -o prog21 main2.c ./libcvector.so
yiqiu@LAPTOP-I2IQS5DP ~/C/c/chapter7> ./prog21
z = [4, 6]

程序的构建和运行过程如图所示:

相比于静态链接,动态链接更节省空间和内存,动态链接库一直保存在内存中,当有程序应用共享库中的模块时,它可以在自己的虚拟空间创建共享库的副本,程序结束后,这个副本也随之消失,就是说内存中只需要保留一份共享库代码就i可以供所有程序使用

如图所示,在形成可执行目标文件时,并不会有libc.so和libvector.so的代码被复制到可执行文件中,链接器只是重定位main2.o中的符号引用

在程序运行时,动态链接器将libc.so和libvector.so的代码复制到本进程的虚拟空间,然后重定位可执行文件中的符号引用,最后将控制权转交给程序

7.11、从应用程序中加载和链接共享库

C和C++提供了几个函数,可以在程序运行过程中加载和链接共享库,并在使用完之后卸载

示例程序:

prog2r.c

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
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>

int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];

int main()
{
void *handle;
void (*addvec)(int *, int *, int*, int);
char *error;

handle = dlopen("/home/yiqiu/Codefile/csapp/chapter7/libcvector.so", RTLD_LAZY); //加载共享库
if(!handle) //判断是否加载成功
{
fprintf(stderr, "%s\n", dlerror());
exit(1);;
}

addvec = dlsym(handle, "addvec"); //引用共享库中的符号
if((error = dlerror()) != NULL)
{
fprintf(stderr, "%s\n", error);
exit(1);
}

addvec(x, y, z, 2); //调用共享库中的函数

if(dlclose(handle) < 0) //卸载共享库
{
fprintf(stderr, "%s\n", dlerror());
exit(1);
}
return 0;
}
1
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [1]> gcc -rdynamic -o prog2r.out prog2r.c -ldl

java调用本地C/C++函数的实现方法:

7.12、位置无关代码

位置无关代码的作用是允许多个进程使用一个共享库代码的副本而不造成内存浪费,在gcc编译时使用-fpic选项指定生成位置无关代码

1、PIC数据引用

在共享库中,数据和代码的之间偏移是固定的,共享库中的代码被动态连接到内存中时,动态链接器还会根据一个全局偏移量表(GOT)来重定位代码中的数据引用

2、PIC函数调用

PIC函数调用通过GOT表、PLT表和延迟绑定机制来实现

函数没有被调用前,GOT表中的地址并不是函数的真正地址,而是对应PIL表中的第二条代码,函数第一次被调用时,经过一系列操作,会将函数的真实地址写入GOT表,以后调用时就可知直接跳转到函数的地址了

第一次调用addvec

  1. call指令调用函数
  2. 跳转到PLT表
  3. 跳转到GOT表指定的位置,即PLT表中的下一条指令
  4. pushq压入addvec函数的序号
  5. 跳转到PLT[0]
  6. 调用动态加载器
  7. 执行函数并将函数的地址写入GOT表

第二次调用addvec

  1. call指令调用函数
  2. 跳转到PLT表
  3. 跳转到GOT表指定的位置,即函数位置

7.13、库打桩机制

按书中的描述,打桩就是hook,将库函数更换为你自己编写的函数

7.13.1、编译时打桩

示例代码:

int.c

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#include<malloc.h>

int main()
{
int *p = malloc(32);
free(p);
return 0;
}

malloc.h

1
2
3
4
5
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)

void* mymalloc(size_t size);
void myfree(void *ptr);

mymalloc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifdef COMPILETIME
#include<stdio.h>
#include<malloc.h>

void* mymalloc(size_t size)
{
void *ptr = malloc(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
return ptr;
}

void myfree(void *ptr)
{
free(ptr);
printf("free(%p)\n", ptr);
}
#endif
1
2
3
4
5
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [1]> gcc -DCOMPILETIME -c mymalloc.c
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> gcc -I. -o intc int.c mymalloc.o
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> ./intc
malloc(32)=0x55848446c2a0
free(0x55848446c2a0)

关键代码在malloc.h中,将malloc函数替换为mymalloc函数,将free函数替换为myfree函数

编译后,调用malloc函数就相当于调用mymalloc函数,free函数同理

7.13.2、链接时打桩

使用–wrap f将f替换为__wrap_f,而__real_f会被替换为f

修改mymalloc.c代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifdef LINKTIME
#include<stdio.h>
#include<malloc.h>

void* __wrap_malloc(size_t size)
{
void *ptr = __real_malloc(size);
printf("malloc(%d)=%p\n", (int)size, ptr);
return ptr;
}

void __wrap_free(void *ptr)
{
__real_free(ptr);
printf("free(%p)\n", ptr);
}
#endif
1
2
3
4
5
6
7
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> gcc -DLINKTIME -c mymalloc.c
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> gcc -c int.c
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> gcc -WL,--wrap,malloc -WL,--wrap,free -o intl int.o mymalloc.o
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [1]> gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> ./intl
malloc(32)=0x55aa635022a0
free(0x55aa635022a0)

7.13.3、运行时打桩

编译时打桩需要源代码,链接时打桩需要目标文件,但是运行时打桩只需要可执行文件

LD_PRELOAD环境变量可以设置可执行程序优先加载的共享库

修改mymalloc.c的代码:

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
#ifdef RUNTIME
#define _GNU_SOURCE
#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>

void* malloc(size_t size)
{
static int calltimes; //默认初始化为0
calltimes++;

void* (*mallocp)(size_t size);
char *error;
mallocp = dlsym(RTLD_NEXT, "malloc");
if((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}
char *ptr = mallocp(size);
if(calltimes == 1) //避免重复调用
printf("malloc(%d)=%p\n", (int)size, ptr);
calltimes = 0;
return ptr;
}


void free(void *ptr)
{
static int calltimes;
calltimes++;
void (*freep)(void *) = NULL;
char *error;

if(!ptr)
return;
freep = dlsym(RTLD_NEXT, "free");
if((error = dlerror()) != NULL)
{
fputs(error, stderr);
exit(1);
}

freep(ptr);
if(calltimes == 1)
printf("free(%p)\n", ptr);
calltimes = 0;
}
#endif
1
2
3
4
5
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp [SIGSEGV]> gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> gcc -o intr int.c
yiqiu@LAPTOP-I2IQS5DP ~/C/csapp> LD_PRELOAD=./mymalloc.so ./intr
malloc(32)=0x55ef433352a0
free(0x55ef433352a0)

7.14、处理目标文件的工具

我们常说程序的入口点是main函数

但是main函数真的是程序开始的地方吗?

答案是:不是,start函数才是,这篇blog旨在分析main函数进行之前,程序都做了哪些事

将示例程序拖进ida,查看start函数的伪代码

1
2
3
4
5
6
7
8
9
10
11
12
void __fastcall __noreturn start(__int64 a1, __int64 a2, void (*a3)(void))
{
__int64 v3; // rax
int v4; // esi
__int64 v5; // [rsp-8h] [rbp-8h] BYREF
char *retaddr; // [rsp+0h] [rbp+0h] BYREF

v4 = v5;
v5 = v3;
_libc_start_main((int (__fastcall *)(int, char **, char **))main, v4, &retaddr, (void (*)(void))init, fini, a3, &v5);
__halt();
}

可以看到:start函数内部调用了libc_start_main函数,这个函数有7个参数

我们只关心其中两个:

第一个参数是main函数的函数指针,有了这个参数,libc_start_main才能调用main函数

第四个参数是init函数的指针,init函数就是我们此次重点关注的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
void __fastcall init(unsigned int a1, __int64 a2, __int64 a3)
{
signed __int64 v4; // rbp
__int64 i; // rbx

init_proc();
v4 = &off_55DFEAA3FD70 - funcs_1E69;
if ( v4 )
{
for ( i = 0LL; i != v4; ++i )
((void (__fastcall *)(_QWORD, __int64, __int64))funcs_1E69[i])(a1, a2, a3);
}
}

init函数中首先调用了init_proc函数,这个函数里面又调用了gmon_start函数,这个函数用来生成profile文件,在逆向中我们不需要关注它

通常,在init_proc函数中还会调用全局构造函数_do_global_ctors_aux

这个函数的源代码如下:

他会循环调用所有全局构造函数

1
2
3
4
5
6
__do_global_ctors_aux (void)
{
func_ptr *p;
for (p = __CTOR_END__ - 1; *p != (func_ptr) -1; p--)
(*p) ();
}
1
2
3
4
5
6
7
8
9
__int64 (**init_proc())(void)
{
__int64 (**result)(void); // rax

result = &_gmon_start__;
if ( &_gmon_start__ )
return (__int64 (**)(void))_gmon_start__();
return result;
}

在init函数中,再往下就是一个循环,首先得到v4的值,然后如果i不等于v4,就依次调用funcs_1E69这个函数指针数组中的函数,这个数组里保存的是用户自定义的函数,这个例题中有两个自定义函数

第一个函数啥也没做,只是返回一个0

第二个函数的伪代码如下:

作用是检查程序是否处于调试环境,如果不是,就将”w0wy0ugot1t”这个字符串粘贴到aHappyhg4me位置

而这个位置保存的是main函数中一个加密算法的密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 sub_55DFEAA3DC27()
{
int fd; // [rsp+4h] [rbp-7Ch]
char *i; // [rsp+8h] [rbp-78h]
char buf[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v4; // [rsp+78h] [rbp-8h]

v4 = __readfsqword(0x28u);
fd = open("/proc/self/status", 0);
read(fd, buf, 0x64uLL);
for ( i = buf; *i != 'T' || i[1] != 'r' || i[2] != 'a' || i[3] != 'c' || i[4] != 'e' || i[5] != 'r'; ++i )
;
if ( !atoi(i + 11) )
strcpy(aHappyhg4me, "w0wy0ugot1t");
return __readfsqword(0x28u) ^ v4;
}

在逆向过程中,这里存放的自定义通常是用来反调试的,建议每次都看看里面有没有什么东西

做完这些事,init函数返回到libc_start_main函数,接着就是由libc_start_main函数调用main函数,在逆向中,主要要注意的就是main函数之前的init函数

里面可能存放着一些关键代码