1. 初步分析

  1. 解包:AIDA64一如既往的使用标准UPX打包,直接使用upx -d aida64.exe即可将其解包为正常exe,接着放入IDA进行分析。
  2. 寻找序列号校验代码:我使用的是AIDA64 Portable版本,可以看到程序相同目录有一个Language目录,找到ulang_cn.txt并搜索序列号,这里会发现序列号的本地化对应为Product Key
  1. 在IDA中搜索Product Key出现的全部位置。搜索到了这些字符串:

其中我们一眼看到The entered product key is invalid,这样我们就定位到了序列号校验窗口的代码,可以反编译并研究检查逻辑了。

2. 校验位分析

首先,按F5进行反编译,往下稍微翻一点可以看到这样的逻辑:

可以看到,如果vars113是1,就说明序列号是无效的。而往上一看,(__int64)&vars113作为sub_108B440的out参数,说明这个函数进行了关键的校验逻辑,第一个参数应该就是序列号了。点开这个函数进行分析:

首先执行了v112 = a1,接着后面的一大串处理函数将字符串分别去除-_和空格、0x00AD0x00C2这几种符号,仅保留25字节,然后进一步规范化为全大写,最终到这个位置:

sub_4166D0(&varsB0, vars120, 1, 24); 把序列号的前24字节提取了出来,紧接着的v17 = sub_108B370(_0, varsB0);计算了一个变种CRC16,具体逻辑详见keygen(可以靠AI逆一下)。比较重要的是,sub_108B0B0(_0, &varsB8, v17 % 0x9987u)为什么是0x9987这个数字?是因为其将CRC结果变成了34进制的3位数来处理,0x9987-1是3位34进制数能表达的最大数,而字母表是一个常量DY14UF3RHWCXLQB6IKJT9N5AGS2PM8VZ7E

后面的

1
2
3
4
sub_4166D0(&varsC0, varsB8, 2, 1);
sub_4166D0(&varsA8, vars120, 25, 1);
v18 = sub_416680(varsC0, varsA8);
*a8 = v18 == 0;

则是将这3字符中的第2个字符作为校验字符提取了出来,与序列号的最后一位对比。

3. 序列号如何保存信息

在校验位后面,程序对输入的序列号搞了一大堆blacklist校验,我们先不看这一段(反正也不会用公开key了)
我们主要看这一段:

sub_108B270主要还是用刚才那个字母表把序列号以34进制转换成整数,其逻辑为:

1
result = Σ digit[i] * 34^(len-1-i)

sub_4166D0(&vars70, vars120, 1, 2); 的后两个参数则是从第几位取几位数字,例如第一行是从23位取2个数字(也就是最后俩数),因为后面的时间运算需要seed,所以这里要先取出来。
具体定义表:

Key 位置 结果
1 - 2 Product ID (也就是判断Business/Expert等)
3 - 4, 5 - 6, 7- 8 不知道是什么,但是影响不大,通过校验即可
9 - 12 序列号
13 - 16 基准日期
17 - 19 到期偏移
20 - 22 版本支持偏移
23 - 24 种子

基准日期算法

先获取13 - 16 这一段

1
2
3
sub_4166D0(&vars48, vars120, 13, 4);
v31 = sub_108B270(_0, vars48);
vars138 = vars144 ^ v31 ^ 0x7CC1;

然后拆出来年月日:

1
2
3
year  = ((vars138 >> 9) & 0x1F) + 2003;
month = (vars138 >> 5) & 0x0F;
day = vars138 & 0x1F;

到期偏移算法

IDA里看起来是这样:

1
2
3
sub_4166D0(&vars40, vars120, 17, 3);
v32 = sub_108B270(_0, vars40);
*a6 = (unsigned __int8)vars144 ^ v32 ^ 0x3FD;

可以比较明显的看出来,逻辑是:

1
expire_delta = (seed & 0xFF) ^ dec34(key[17..19]) ^ 0x3FD

版本支持偏移算法

可见:

1
2
3
sub_4166D0(&vars38, vars120, 20, 3);
v33 = sub_108B270(_0, vars38);
*a7 = (unsigned __int8)vars144 ^ v33 ^ 0x935;

也就是:

1
version_delta = (seed & 0xFF) ^ dec34(key[20..22]) ^ 0x935

那么另外那些字段有什么用呢?后面可以看到一段校验:

1
2
3
4
5
6
7
8
9
if (*v114 != 999
&& vars142 <= 0x64
&& vars146 <= 0x64
&& *v115 != 0xFFFF
&& *a6 < 3653
&& *a7 < 3653)
{
// 错误处理
}

所以我们构造的时候要让:

  • 3..4 != 999
  • 5..6 <= 100
  • 7..8 <= 100
  • 序列号 != 0xFFFF
  • 到期偏移 < 3653
  • 版本支持偏移 < 3653

4. 程序版本校验

核对过前面的序列号后,因为我现在用的是Business,所以程序最后的校验就像这样:

  1. 要求产品ID是1,也就是Business
  2. 要求(unsigned __int8)sub_107FEC0((unsigned int)*v113, (unsigned int)*v114),看起来比较奇怪。前面已经写死了3..4是999,怎么这里还要做校验?其实大概率是为了给版本划区分,如图:

当产品ID是4的时候(不知道4是什么版本),要求3..4不能小于320,然后给了很大一坨候选,比较闲的人可以魔改keygen后自己试一试都有什么区别(
3. 要求sub_1080250(*v113, *v115),这个函数比较小:

1
2
3
4
5
6
7
8
9
10
bool __fastcall sub_1080250(int a1, int a2)
{
int v2; // ecx

v2 = a1 - 1;
if ( v2 && v2 != 3 )
return a2 > 0 && a2 < 777;
else
return a2 > 0 && a2 < 65534;
}

也就是说如果产品ID是1或4,必须 1 <= 序列号 <= 65534,其他ID则必须 1 <= 序列号 < 777
4. 要求sub_1080290(vars142) && sub_10802C0(vars146),这两个函数我就不贴了,其要求1 <= 5..6, 7..8 <= 15,所以这两个数字在1-15之间就可以。


后面还比对了一大堆Blacklist序列号,以及ROG水冷、ROG主板专用序列号的检测,包含CROSSHAIR / MAXIMUS / RAMPAGE / ZENITH的视为ROG主板

5. 实现一个Keygen

AIDA64_8.25.8200_Portable网盘链接: https://www.123865.com/s/EfqLVv-B9ZVh?pwd=52pj#
Keygen:

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
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# By Hitachi_Mako 233!!  hitachimako @52pojie

from __future__ import annotations

import argparse
from datetime import date, datetime, timedelta


ALPHABET = "DY14UF3RHWCXLQB6IKJT9N5AGS2PM8VZ7E"
ALPHABET_INDEX = {char: idx for idx, char in enumerate(ALPHABET)}

MIN_BASE_DATE = date(2004, 1, 1)
MAX_BASE_DATE = date(2034, 12, 31)
MAX_OFFSET_DAYS = 3652
DEFAULT_FIELD_5_6 = 6
DEFAULT_FIELD_7_8 = 1
AUTO_VARIANT_FOR_PRODUCT_1 = 100

ALLOWED_VARIANT_VALUES = (
100,
120,
150,
160,
170,
180,
200,
220,
230,
250,
260,
270,
280,
300,
320,
400,
430,
450,
460,
500,
520,
530,
550,
560,
570,
580,
590,
592,
595,
597,
598,
599,
600,
610,
620,
625,
630,
632,
633,
650,
660,
670,
675,
685,
688,
690,
692,
694,
700,
720,
730,
735,
740,
750,
760,
765,
770,
800,
)


def base34_encode(value: int, width: int) -> str:
limit = 34**width
if not 0 <= value < limit:
raise ValueError(f"value {value} does not fit into {width} base-34 chars")
chars = ["D"] * width
for idx in range(width - 1, -1, -1):
chars[idx] = ALPHABET[value % 34]
value //= 34
return "".join(chars)


def base34_decode(text: str) -> int:
value = 0
for char in text:
value = value * 34 + ALPHABET_INDEX[char]
return value


def crc16_like(text: str) -> int:
value = 0
for char in text:
value ^= ord(char) << 8
for _ in range(8):
if value & 0x8000:
value = ((value << 1) ^ 0x8201) & 0xFFFF
else:
value = (value << 1) & 0xFFFF
return value


def checksum_char(first24: str) -> str:
return base34_encode(crc16_like(first24) % 0x9987, 3)[1]


def normalize_key(text: str) -> str:
key = "".join(char for char in text.upper() if char in ALPHABET)
if len(key) != 25:
raise ValueError("product key must contain exactly 25 alphabet chars after separators are removed")
return key


def group_key(raw_key: str) -> str:
return "-".join(raw_key[idx : idx + 5] for idx in range(0, 25, 5))


def parse_date(text: str) -> date:
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y.%m.%d"):
try:
return datetime.strptime(text.strip(), fmt).date()
except ValueError:
continue
raise ValueError("date must be YYYY-MM-DD, YYYY/MM/DD, or YYYY.MM.DD")


def prompt_value(prompt: str, default: str | None = None) -> str:
suffix = f" [{default}]" if default is not None else ""
value = input(f"{prompt}{suffix}: ").strip()
return value or (default if default is not None else "")


def serial_limit_for_product(product_id: int) -> int:
return 65533 if product_id in (1, 4) else 776


def allowed_variant_code(product_id: int, variant_code: int) -> bool:
if product_id == 4 and variant_code < 320:
return False
return variant_code in ALLOWED_VARIANT_VALUES or 820 <= variant_code <= 999


def minimum_variant_code(product_id: int) -> int:
return 320 if product_id == 4 else ALLOWED_VARIANT_VALUES[0]


def variant_hint(product_id: int) -> str:
values = [value for value in ALLOWED_VARIANT_VALUES if product_id != 4 or value >= 320]
return ", ".join(str(value) for value in values) + ", 820-999"


def pack_base_date(base_date: date) -> int:
if not MIN_BASE_DATE <= base_date <= MAX_BASE_DATE:
raise ValueError(f"base date must be between {MIN_BASE_DATE} and {MAX_BASE_DATE}")
return ((base_date.year - 2003) & 0x1F) << 9 | ((base_date.month & 0xF) << 5) | (base_date.day & 0x1F)


def derive_serial(
product_id: int,
variant_code: int,
packed_date: int,
expire_offset: int,
version_offset: int,
) -> int:
mix = (
product_id * 0x45D
+ variant_code * 0x11
+ packed_date * 3
+ expire_offset * 5
+ version_offset * 7
)
return mix % serial_limit_for_product(product_id) + 1


def derive_seed(
product_id: int,
variant_code: int,
packed_date: int,
expire_offset: int,
version_offset: int,
serial: int,
) -> int:
mix = (
serial
+ product_id * 0x31
+ variant_code * 0x17
+ packed_date
+ expire_offset * 7
+ version_offset * 11
)
return mix % (34 * 34)


def decode_key(raw_key: str) -> dict:
key = normalize_key(raw_key)
seed = base34_decode(key[22:24])
seed_low = seed & 0xFF
product_id = base34_decode(key[0:2]) ^ seed_low ^ 0xBF
variant_code = base34_decode(key[2:4]) ^ seed_low ^ 0xED
field_5_6 = base34_decode(key[4:6]) ^ seed_low ^ 0x77
field_7_8 = base34_decode(key[6:8]) ^ seed_low ^ 0xDF
serial = base34_decode(key[8:12]) ^ seed ^ 0x4755
packed_date = base34_decode(key[12:16]) ^ seed ^ 0x7CC1
base_date = date(((packed_date >> 9) & 0x1F) + 2003, (packed_date >> 5) & 0xF, packed_date & 0x1F)
expire_offset = base34_decode(key[16:19]) ^ seed_low ^ 0x3FD
version_offset = base34_decode(key[19:22]) ^ seed_low ^ 0x935
return {
"product_id": product_id,
"variant_code": variant_code,
"field_5_6": field_5_6,
"field_7_8": field_7_8,
"serial": serial,
"seed": seed,
"base_date": base_date,
"expire_offset": expire_offset,
"expire_date": base_date + timedelta(days=expire_offset),
"version_offset": version_offset,
"version_limit_date": base_date + timedelta(days=version_offset),
"checksum_ok": key[24] == checksum_char(key[:24]),
}


def validate_layout(decoded: dict) -> bool:
try:
pack_base_date(decoded["base_date"])
except ValueError:
return False
return (
decoded["checksum_ok"]
and 1 <= decoded["product_id"] <= 999
and allowed_variant_code(decoded["product_id"], decoded["variant_code"])
and 1 <= decoded["field_5_6"] <= 15
and 1 <= decoded["field_7_8"] <= 15
and 0 <= decoded["expire_offset"] <= MAX_OFFSET_DAYS
and 1 <= decoded["version_offset"] <= MAX_OFFSET_DAYS
and 0 < decoded["serial"] <= serial_limit_for_product(decoded["product_id"])
and 0 <= decoded["seed"] < 34 * 34
)


def validate_current_sample(decoded: dict) -> bool:
return (
validate_layout(decoded)
and decoded["product_id"] == 1
and decoded["variant_code"] != 999
and decoded["field_5_6"] <= 100
and decoded["field_7_8"] <= 100
and decoded["serial"] != 0xFFFF
and decoded["expire_offset"] < 3653
and decoded["version_offset"] < 3653
)


def generate_key(
product_id: int,
variant_code: int,
base_date: date,
expire_offset: int,
version_offset: int,
field_5_6: int = DEFAULT_FIELD_5_6,
field_7_8: int = DEFAULT_FIELD_7_8,
) -> dict:
if not 1 <= product_id <= 999:
raise ValueError("product id must be in 1..999")
if product_id == 1:
variant_code = AUTO_VARIANT_FOR_PRODUCT_1
if not allowed_variant_code(product_id, variant_code):
raise ValueError(f"variant code is invalid for product {product_id}; allowed values: {variant_hint(product_id)}")
if not 1 <= field_5_6 <= 15:
raise ValueError("field_5_6 must be in 1..15")
if not 1 <= field_7_8 <= 15:
raise ValueError("field_7_8 must be in 1..15")
if not 0 <= expire_offset <= MAX_OFFSET_DAYS:
raise ValueError(f"expire offset must be in 0..{MAX_OFFSET_DAYS}")
if not 1 <= version_offset <= MAX_OFFSET_DAYS:
raise ValueError(f"version offset must be in 1..{MAX_OFFSET_DAYS}")

packed_date = pack_base_date(base_date)
serial = derive_serial(product_id, variant_code, packed_date, expire_offset, version_offset)
seed = derive_seed(product_id, variant_code, packed_date, expire_offset, version_offset, serial)
seed_low = seed & 0xFF

first24 = "".join(
[
base34_encode(product_id ^ seed_low ^ 0xBF, 2),
base34_encode(variant_code ^ seed_low ^ 0xED, 2),
base34_encode(field_5_6 ^ seed_low ^ 0x77, 2),
base34_encode(field_7_8 ^ seed_low ^ 0xDF, 2),
base34_encode(serial ^ seed ^ 0x4755, 4),
base34_encode(packed_date ^ seed ^ 0x7CC1, 4),
base34_encode(expire_offset ^ seed_low ^ 0x3FD, 3),
base34_encode(version_offset ^ seed_low ^ 0x935, 3),
base34_encode(seed, 2),
]
)
raw_key = first24 + checksum_char(first24)
decoded = decode_key(raw_key)

if not validate_layout(decoded):
raise RuntimeError("generated key failed the recovered layout checks")

return {
"product_id": product_id,
"variant_code": variant_code,
"field_5_6": field_5_6,
"field_7_8": field_7_8,
"serial": serial,
"seed": seed,
"base_date": base_date,
"expire_offset": expire_offset,
"expire_date": base_date + timedelta(days=expire_offset),
"version_offset": version_offset,
"version_limit_date": base_date + timedelta(days=version_offset),
"raw_key": raw_key,
"grouped_key": group_key(raw_key),
"decoded": decoded,
"current_sample_ok": validate_current_sample(decoded),
}


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Simplified Product Key generator recovered from detailed.md.")
parser.add_argument("--product-id", type=int, help="Recovered product id. The current sample only fully accepts 1.")
parser.add_argument("--variant-code", type=int, help="Recovered variant code. Ignored for product id 1.")
parser.add_argument("--base-date", help=f"Recovered base date ({MIN_BASE_DATE}..{MAX_BASE_DATE}).")
parser.add_argument("--expire-offset", type=int, help=f"Expire offset in days (0..{MAX_OFFSET_DAYS}).")
parser.add_argument("--version-offset", type=int, help=f"Version support offset in days (1..{MAX_OFFSET_DAYS}).")
parser.add_argument("--field-5-6", type=int, default=DEFAULT_FIELD_5_6, help="Field 5..6, valid range 1..15.")
parser.add_argument("--field-7-8", type=int, default=DEFAULT_FIELD_7_8, help="Field 7..8, valid range 1..15.")
return parser


def main() -> None:
parser = build_parser()
args = parser.parse_args()

default_base_date = date.today()
if default_base_date < MIN_BASE_DATE:
default_base_date = MIN_BASE_DATE
if default_base_date > MAX_BASE_DATE:
default_base_date = MAX_BASE_DATE

product_id_text = str(args.product_id) if args.product_id is not None else prompt_value("Product id", "1")
product_id = int(product_id_text)

if product_id == 1:
variant_code = AUTO_VARIANT_FOR_PRODUCT_1
else:
default_variant = minimum_variant_code(product_id)
variant_prompt = f"Variant code ({variant_hint(product_id)})"
variant_text = (
str(args.variant_code)
if args.variant_code is not None
else prompt_value(variant_prompt, str(default_variant))
)
variant_code = int(variant_text)

base_date_text = args.base_date or prompt_value("Base date", default_base_date.isoformat())
expire_offset_text = (
str(args.expire_offset) if args.expire_offset is not None else prompt_value("Expire offset days", "30")
)
version_offset_text = (
str(args.version_offset) if args.version_offset is not None else prompt_value("Version support offset days", "3652")
)

result = generate_key(
product_id=product_id,
variant_code=variant_code,
base_date=parse_date(base_date_text),
expire_offset=int(expire_offset_text),
version_offset=int(version_offset_text),
field_5_6=args.field_5_6,
field_7_8=args.field_7_8,
)

print(f"Product id : {result['product_id']}")
if result["product_id"] == 1:
print(f"Variant code : {result['variant_code']} (auto-selected for product id 1)")
else:
print(f"Variant code : {result['variant_code']}")
print(f"Base date : {result['base_date']}")
print(f"Expire offset : {result['expire_offset']} days")
print(f"Expire date : {result['expire_date']}")
print(f"Version offset : {result['version_offset']} days")
print(f"Version limit : {result['version_limit_date']}")
print(f"Field 5..6 : {result['field_5_6']}")
print(f"Field 7..8 : {result['field_7_8']}")
print(f"Serial : {result['serial']}")
print(f"Seed : {result['seed']}")
print(f"Raw key : {result['raw_key']}")
print(f"Grouped key : {result['grouped_key']}")
print(f"Checksum ok : {result['decoded']['checksum_ok']}")
print(f"Current sample ok: {result['current_sample_ok']}")


if __name__ == "__main__":
main()