狗儿

热爱的话就坚持吧~

0%

破解某游戏修改器时的意外收获

本来是想简单破解一下某国产单机游戏的修改器,没想到还有意外收获。


今天没事干,翻出来一款国产单机游戏,重复刷刷刷太无聊了,于是网上下了个修改器,没想到需要购买激活码才能用。并不是很贵,只要十块几毛钱,在发卡的网站上直接购买即可,推测是支付完就可以获得一个激活码,不需要人工介入。虽然不贵,但是作为一个逆向选手,难得遇到一个锻炼自己的机会,直接开搞喽。

image-20230610232436351

简单一看,是个c#写的修改器,拖进dnspy里面,感觉像是被混淆了:

image-20230610232633619

Decrypt是这样的:

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
private static GCHandle Decrypt(uint[] A_0, uint A_1)
{
uint[] array = new uint[16];
uint[] array2 = new uint[16];
ulong num = (ulong)A_1;
for (int i = 0; i < 16; i++)
{
num = num * num % 339722377UL;
array2[i] = (uint)num;
array[i] = (uint)(num * num % 1145919227UL);
}
array[0] = (array[0] ^ array2[0]) * 2849015165u;
array[1] = (array[1] ^ array2[1] ^ 2786556537u);
array[2] = array[2] * array2[2] * 2849015165u;
array[3] = array[3] * array2[3] + 1807995571u;
array[4] = (array[4] ^ array2[4]) + 1807995571u;
array[5] = (array[5] ^ array2[5]) + 1807995571u;
array[6] = (array[6] * array2[6] ^ 2786556537u);
array[7] = (array[7] + array2[7]) * 2849015165u;
array[8] = array[8] * array2[8] * 2849015165u;
array[9] = (array[9] + array2[9] ^ 2786556537u);
array[10] = array[10] + array2[10] + 1807995571u;
array[11] = array[11] * array2[11] * 2849015165u;
array[12] = (array[12] ^ array2[12]) * 2849015165u;
array[13] = (array[13] + array2[13] ^ 2786556537u);
array[14] = (array[14] ^ array2[14] ^ 2786556537u);
array[15] = (array[15] * array2[15] ^ 2786556537u);
Array.Clear(array2, 0, 16);
byte[] array3 = new byte[A_0.Length << 2];
uint num2 = 0u;
for (int j = 0; j < A_0.Length; j++)
{
uint num3 = A_0[j] ^ array[j & 15];
array[j & 15] = (array[j & 15] ^ num3) + 1037772825u;
array3[(int)((UIntPtr)num2)] = (byte)num3;
array3[(int)((UIntPtr)(num2 + 1u))] = (byte)(num3 >> 8);
array3[(int)((UIntPtr)(num2 + 2u))] = (byte)(num3 >> 16);
array3[(int)((UIntPtr)(num2 + 3u))] = (byte)(num3 >> 24);
num2 += 4u;
}
Array.Clear(array, 0, 16);
byte[] array4 = <Module>.Decompress(array3);
Array.Clear(array3, 0, array3.Length);
GCHandle result = GCHandle.Alloc(array4, GCHandleType.Pinned);
ulong num4 = num % 9067703UL;
for (int k = 0; k < array4.Length; k++)
{
byte[] array5 = array4;
int num5 = k;
array5[num5] ^= (byte)num;
if ((k & 255) == 0)
{
num = num * num % 9067703UL;
}
}
return result;
}

Decompress是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// <Module>
// Token: 0x06000004 RID: 4 RVA: 0x00016364 File Offset: 0x00014564
internal static byte[] Decompress(byte[] A_0)
{
MemoryStream memoryStream = new MemoryStream(A_0);
<Module>.LzmaDecoder lzmaDecoder = new <Module>.LzmaDecoder();
byte[] array = new byte[5];
memoryStream.Read(array, 0, 5);
lzmaDecoder.SetDecoderProperties(array);
long num = 0L;
for (int i = 0; i < 8; i++)
{
int num2 = memoryStream.ReadByte();
num |= (long)((long)((ulong)((byte)num2)) << 8 * i);
}
byte[] array2 = new byte[(int)num];
MemoryStream memoryStream2 = new MemoryStream(array2, true);
long num3 = memoryStream.Length - 13L;
lzmaDecoder.Code(memoryStream, memoryStream2, num3, num);
return array2;
}

<Module>.Main是这样的:

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
private static int Main(string[] A_0)
{
uint[] array = new uint[]
{
2433042623u,
3134412526u,
156519932u,
2628545700u,
// 此后省略,共20244个uint
};
Assembly executingAssembly = Assembly.GetExecutingAssembly();
Module manifestModule = executingAssembly.ManifestModule;
GCHandle gchandle = <Module>.Decrypt(array, 4159999126u);
byte[] array2 = (byte[])gchandle.Target;
Module module = executingAssembly.LoadModule("koi", array2);
Array.Clear(array2, 0, array2.Length);
gchandle.Free();
Array.Clear(array, 0, array.Length);
<Module>.key = manifestModule.ResolveSignature(285212673);
AppDomain.CurrentDomain.AssemblyResolve += <Module>.Resolve;
module.GetTypes();
MethodBase methodBase = module.ResolveMethod((int)<Module>.key[0] | (int)<Module>.key[1] << 8 | (int)<Module>.key[2] << 16 | (int)<Module>.key[3] << 24);
object[] array3 = new object[methodBase.GetParameters().Length];
if (array3.Length != 0)
{
array3[0] = A_0;
}
object obj = methodBase.Invoke(null, array3);
if (obj is int)
{
return (int)obj;
}
return 0;
}

Main中的array里面有两万多个uint数据,肯定是混淆无疑了。

13行的<Module>.Decrypt(array, 4159999126u)解密得到gchandle:

image-20230610233333205

15行把gchandle.Target加载成module,其ScopeName为koi:

image-20230610233508122

然后再通过后续几行代码的数据变化,最后于28行,通过反射,调用处理后的数据所形成的函数/方法:

image-20230610233912172

不到万不得已,我真的不想手动处理混淆。先换条路试试看。

掏出fiddler抓包,没抓到,所以激活码验证时大概率使用的不是http(s)协议。

进而掏出wireshark抓包,这次抓到了。随即大吃一惊,居然是tds协议:

image-20230610235339255

tds协议是微软sql server通信的协议。这个修改器居然在本地客户端使用这个协议,那数据库的用户名和密码也必然存在于本地客户端中。

这里我简单介绍一下上图的各个数据包。

第一个数据包是客户端发往数据库的服务端,传输一些预登录的信息,比如版本号等:

image-20230610235644537

第二个数据包是服务端针对第一个数据包的响应。

后面的四个数据包是tls包,进行数据库登录操作。注意,这个tls包的架构是ip->tcp->tds->tls,tls的数据是作为tds的负载而存在的、tds的数据是作为tcp的负载而存在的;而非我们常见的ip->tcp->tls:

image-20230610235941094

然后通过一个tls数据包加密传输数据库的账号和密码,注意这个数据包是真正的ip->tcp->tls的数据包,不是tds数据包,所以过滤条件是tds的话看不到这个包,需要追踪tcp流才能看到:

image-20230611001756330

image-20230611001832691

另外,我在检索资料时发现,有的sql server数据库登录其实没有使用tls,就直接是账号+密码,比如:

image

(sa是sql server的管理员用户名)

我们回到正题。随后的那个Response是一些登录成功后的相关信息,是tds数据包,明文形式的:

image-20230611002024814

image-20230611000155784

image-20230611000207686

image-20230611000216104

最后四个数据包,两两组合,分别是数据库查询和对应的查询结果:

image-20230611002209046

image-20230611000318023

image-20230611000328231

image-20230611000416956

image-20230611002228479

分析到这里,我们有了意外收获:数据库的账号和密码都存在于这个修改器的本地客户端中。

它可能会被混淆了,被加壳了,或是其他怎么样了让我们难以找到,但必然存在某一时刻,它是存在于程序中的,不然是不可能通过tds协议连接上远程的microsoft sql server的。

走到这里,我们的目标很明确,不必去绞尽脑汁地在本地程序中绕过激活码,只需要拿到数据库的账号和密码,远程连接上数据库,拿到数据库里的激活码即可,这简直比我们预期想要的成果高到不知道哪里去了。

所以,怎么拿到数据库的账号和密码呢?两个思考的角度:流量中;程序中。

流量中

虽然账号和密码是通过变种tls(指ip->tcp->tds->tls)加密传输的,但是有没有解密的方法呢?

我们先来看下,正常的tls的加密流量如何解密呢?有且只有两种方案:

  1. 服务端也是我们的,我们有服务端的私钥,因此可以解密加密流量(但这是几乎不可能的)
  2. 在tls客户端中”插桩”,导出预主密钥(比如chrome和firefox都会将相关信息写入环境变量 SSLKEYLOGFILE的值对应的文件中)

具体可参考:

解密TLS协议全记录之利用wireshark解密 (PS:单纯依靠wireshark中的信息是不可能解密tls的,必须从外部引入额外的信息才有可能)

HTTPS 篇之 SSL 握手过程详解 | 渡渡岛

密钥交换算法 - 廖雪峰的官方网站

但是经过尝试,c#中的tls实现(更具体一点是基于tds的tls),并没有像chrome那些会把tls密钥相关的信息记录到相应的文件中。此路不通。

程序中

没办法,只能处理一下混淆了,关键就是要恢复处理后的那些数据。

随手复制了一行代码,检索了一下,居然是现有的混淆方案,名字是ConfuserEx。我去,早知道我还费那么大劲抓包干嘛。。。

这里放一篇检索到的使用了该混淆方案的病毒样本的分析报告:Sophisticated Mutli-stage Malware (hosted on pussyhunters.ru) – Malware Analysis

顺藤摸瓜很容易就找到了一个对应的脱壳脚本:hackovh/ConfuserEx-Unpacker-2 (github.com),同样也是c#写的,编译后直接脚本.exe 目标.exe,然后等一会就处理好了。可能是脚本比较老,比较是五年前的了,所以脱壳后的程序不能运行,只能静态分析。

脱壳后大概是这样的:

image-20230610234730192

其中在HT.Activate中,我们发现了数据库的密码。。。

image-20230611004322023

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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
using System;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using System.Management;

namespace HT
{
// Token: 0x02000004 RID: 4
public class Activate
{
// Token: 0x0600000D RID: 13 RVA: 0x000020CF File Offset: 0x000002CF
internal static bool GetActivateState()
{
return Activate.activateState;
}

// Token: 0x0600000E RID: 14 RVA: 0x0000264C File Offset: 0x0000084C
internal static bool RunActivate(string path, string key)
{
bool result;
try
{
string cpuInfo = Activate.GetCpuInfo();
string cmdText = string.Concat(new string[]
{
"UPDATE GuiGuBaHuangKey SET Code='",
cpuInfo,
"' WHERE [Key]='",
key,
"' AND (Code IS NULL OR Code='",
cpuInfo,
"')"
});
using (SqlConnection sqlConnection = new SqlConnection(Activate.connstr))
{
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
using (SqlCommand sqlCommand = new SqlCommand(cmdText, sqlConnection))
{
if (sqlCommand.ExecuteNonQuery() > 0)
{
Activate.activateState = true;
Activate.CreateKeyFile(path, key);
result = true;
}
else
{
Activate.activateState = false;
result = false;
}
}
}
}
catch
{
throw;
}
return result;
}

// Token: 0x0600000F RID: 15 RVA: 0x00002720 File Offset: 0x00000920
internal static bool IsActivate(string path)
{
bool result;
try
{
string key = Activate.GetKey(path);
string cpuInfo = Activate.GetCpuInfo();
string cmdText = "SELECT Code FROM GuiGuBaHuangKey WHERE [Key] = '" + key + "'";
using (SqlConnection sqlConnection = new SqlConnection(Activate.connstr))
{
using (SqlCommand sqlCommand = new SqlCommand(cmdText, sqlConnection))
{
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
if (sqlDataReader.Read() && string.Equals(cpuInfo, sqlDataReader.GetString(0)))
{
Activate.activateState = true;
result = true;
}
else
{
Activate.activateState = false;
result = false;
}
}
}
}
}
catch
{
throw;
}
return result;
}

// Token: 0x06000010 RID: 16 RVA: 0x00002804 File Offset: 0x00000A04
internal static string GetClientVer()
{
string result;
try
{
string cmdText = "SELECT ClientVer FROM GuiGuBaHuangVer WHERE ID = '1'";
using (SqlConnection sqlConnection = new SqlConnection(Activate.connstr))
{
using (SqlCommand sqlCommand = new SqlCommand(cmdText, sqlConnection))
{
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
if (sqlDataReader.Read())
{
result = sqlDataReader.GetString(0);
}
else
{
result = string.Empty;
}
}
}
}
}
catch
{
throw;
}
return result;
}

// Token: 0x06000011 RID: 17 RVA: 0x000028B4 File Offset: 0x00000AB4
internal static string GetServerVer()
{
string result;
try
{
string cmdText = "SELECT ServerVer FROM GuiGuBaHuangVer WHERE ID = '1'";
using (SqlConnection sqlConnection = new SqlConnection(Activate.connstr))
{
using (SqlCommand sqlCommand = new SqlCommand(cmdText, sqlConnection))
{
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
if (sqlDataReader.Read())
{
result = sqlDataReader.GetString(0);
}
else
{
result = string.Empty;
}
}
}
}
}
catch
{
throw;
}
return result;
}

// Token: 0x06000012 RID: 18 RVA: 0x00002964 File Offset: 0x00000B64
internal static string GetUrl()
{
string result;
try
{
string cmdText = "SELECT Url FROM GuiGuBaHuangUrl WHERE ID = '1'";
using (SqlConnection sqlConnection = new SqlConnection(Activate.connstr))
{
using (SqlCommand sqlCommand = new SqlCommand(cmdText, sqlConnection))
{
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
if (sqlDataReader.Read())
{
result = sqlDataReader.GetString(0);
}
else
{
result = string.Empty;
}
}
}
}
}
catch
{
throw;
}
return result;
}

// Token: 0x06000013 RID: 19 RVA: 0x00002A14 File Offset: 0x00000C14
internal static string GetUpdateUrl()
{
string result;
try
{
string cmdText = "SELECT UpdateUrl FROM GuiGuBaHuangUrl WHERE ID = '1'";
using (SqlConnection sqlConnection = new SqlConnection(Activate.connstr))
{
using (SqlCommand sqlCommand = new SqlCommand(cmdText, sqlConnection))
{
if (sqlConnection.State != ConnectionState.Open)
{
sqlConnection.Open();
}
using (SqlDataReader sqlDataReader = sqlCommand.ExecuteReader())
{
if (sqlDataReader.Read())
{
result = sqlDataReader.GetString(0);
}
else
{
result = string.Empty;
}
}
}
}
}
catch
{
throw;
}
return result;
}

// Token: 0x06000014 RID: 20 RVA: 0x00002AC4 File Offset: 0x00000CC4
private static string GetKey(string path)
{
string result;
try
{
using (FileStream fileStream = new FileStream(path + Activate.keyName, FileMode.Open, FileAccess.Read))
{
using (StreamReader streamReader = new StreamReader(fileStream))
{
result = streamReader.ReadLine();
}
}
}
catch
{
result = string.Empty;
}
return result;
}

// Token: 0x06000015 RID: 21 RVA: 0x00002B3C File Offset: 0x00000D3C
private static void CreateKeyFile(string path, string key)
{
using (FileStream fileStream = new FileStream(path + Activate.keyName, FileMode.Create, FileAccess.ReadWrite))
{
using (StreamWriter streamWriter = new StreamWriter(fileStream))
{
streamWriter.Write(key);
streamWriter.Flush();
}
}
}

// Token: 0x06000016 RID: 22 RVA: 0x00002BA4 File Offset: 0x00000DA4
private static string GetCpuInfo()
{
string result;
try
{
string text = "";
using (ManagementClass managementClass = new ManagementClass("Win32_Processor"))
{
ManagementObjectCollection instances = managementClass.GetInstances();
using (ManagementObjectCollection.ManagementObjectEnumerator enumerator = instances.GetEnumerator())
{
if (enumerator.MoveNext())
{
text = ((ManagementObject)enumerator.Current).Properties["ProcessorId"].Value.ToString();
}
}
instances.Dispose();
}
result = text.ToString();
}
catch
{
throw;
}
return result;
}

// Token: 0x04000003 RID: 3
private static bool activateState = false;

// Token: 0x04000004 RID: 4
private static string keyName = "key.dat";

// Token: 0x04000005 RID: 5
private static string connstr = "Data Source=***.my3w.com;Initial Catalog=b***4_db;User ID=b***4;Password=***";
}
}

有了数据库密码,直接登录:

image-20230611005808265

查询版本:

image-20230611010246226

查询url:

image-20230611010146316

最激动人心的时刻到喽,查询激活码:

image-20230611010416591

Key就是激活码,Code是通过cpu等信息计算出来的。如果Code为Null,说明这个激活码还没使用过,如果不为Null,说明这个激活码已经被使用过了。

我们来验证一下。首先选择要给Code不为Null的激活码,验证失败:

image-20230611010652109

然后选一个Code为Null的,直接激活成功。激活成功后会在目录下写入一个key.dat,内容就是激活码:

image-20230611010802088

尾声

拿下数据库,我们再大胆一点,看下能不能拿下这台远程主机。然而扫描发现ssh和3389都没开:

image-20230611011110320

最后很可惜,通过域名***.my3w.com以及对应ip属地的查询,我们可以看出这个是阿里云北京的一台虚拟主机,是不能远程登录的。

image-20230611005215322

此外,my3w.com是阿里云的域名,***.my3w.com是万网提供的临时域名。