Эта история начинается с того, что я попытался переустановить Windows на ноутбуке, доставшемся мне вот с такой наклейкой Certificate of Authenticity (COA):
Часть символов серийника видны хорошо, остальные – в большей или меньшей степени угадываются; но несколько попыток ввести серийник «на глаз» успехом не увенчались. Пришлось углубляться в вопрос подробнее.
Достижения в области исследования серийников для Windows сведены в реддит-посте: вкратце, со времён Win98/2000 серийник состоит из 25 символов 24-символьного алфавита BCDFGHJKMPQRTVWXY2346789
и таким образом несёт бит информации. (Начиная с Win8, в серийнике также может быть как максимум одна буква N
, равнозначная B
.) Серийник трактуется как little-endian число в 24-чной системе счисления, и следующим образом (на всем давно известном примере) переводится в 115-битное двоичное число:
>>> decoded = 0
>>> for c in "J3QQ4-H7H2V-2HCH4-M3HK8-6M8VW":
... if '-' != c:
... decoded *= 24
... decoded += "BCDFGHJKMPQRTVWXY2346789".index(c)
...
>>> hex(decoded)
'0x1bd0fab8f909f1935b70c837422c6'
Обратный перевод так же прост:
>>> key = ""
>>> while len(key) < 29:
... if len(key) % 6 == 5:
... key += "-"
... key += "BCDFGHJKMPQRTVWXY2346789"[decoded % 24]
... decoded //= 24
...
>>> key[::-1]
'J3QQ4-H7H2V-2HCH4-M3HK8-6M8VW'
Примеры COA легко и помногу добываются при помощи Google Images. На каждом COA, кроме серийника, напечатан числовой номер; начиная с Win98 SE – он 14-значный, где первый блок из пяти цифр обозначает версию и вариант Windows, а остальные три блока – девятизначный порядковый номер COA. Попробуем расшифровать серийники для старых версий Windows: можно заметить, что числовое значение у них у всех без исключения чётное, т.е. самый младший бит у всех 0. В правую колонку отделены следующие за ним 30 бит, т.е. (decoded & 0x7fffffff) >> 1
Win98 | 731603226 |
|
|
Win98 SE | 00003-891-192-814 |
|
|
Win2000 Pro | 00019-034-701-358 |
|
|
WinME | 00029-066-259-739 |
|
|
WinXP Home | 00043-153-594-618 |
|
|
WinXP Pro | 00045-519-589-488 |
|
|
Win2003 Std | 00085-173-586-932 |
|
|
Vista Bus | 00144-041-819-328 |
|
|
Несложно заметить, что у Win98–XP число в правой колонке – это и есть порядковый номер COA: 731603226, 891192814, 34701358, 66259739, 153594618, 519589488 соответственно. Девятизначное десятичное число гарантированно помещается в 30 бит. Но чем же заняты оставшиеся (старшие) 84 бита серийника? И что произошло, начиная с Win2003 и Vista? – у них в правой колонке (546932909 и 615471428 соответственно) ничего похожего на порядковый номер!
Выгуглившиеся примеры COA для Win98–Vista
Проверка серийника в WinXP (и предположительно в более старых версиях, начиная с Win98) описана у Licenturion: старшие 84 бита серийника расшифровываются публичным ключом Microsoft и сравниваются с хешем порядкового номера. Таким образом, у идущих подряд серийников (например, на рулоне COA-наклеек для OEM) последние семь символов должны почти совпадать – например, за J3QQ4-H7H2V-2HCH4-M3HK8-6M8VW
(0x1bd0fab8f909f1935b70c837422c6
) шёл бы xxxxx-xxxxx-xxxxx-xxxK8-6M8VY
(0xXXXXXXXXXXXXXXXXXXXXXX37422c8
). Важно понимать, что благодаря 24-чному кодированию серийника, изменение старших битов влияет не только на первые символы, а вообще на все: например, обнулив один старший бит (0x0bd0fab8f909f1935b70c837422c6
), получим D7BCK-RYDXP-B4GY6-DMGBT-VCT38
. Это значит, что «беспалевнее» было бы кодировать порядковый номер именно в старших битах серийника; не это ли реализовано в более новых версиях Windows?
Мне не удалось нагуглить рулон COA для Win98–XP (если у вас где-то завалялся такой рулон – буду премного благодарен), зато на этом лоте с китайского базара видны пять идущих подряд серийников для Win7: между числовым значением серийника и номером, напечатанным на COA, не заметно никакой связи (см. таблицу ниже). Вероятно, что начиная с Win2003 и Vista, серийник зашифрован целиком.
Одновременно с изменением устройства серийника произошло ещё одно: в Win98/2000/ME/XP для проверки серийника использовалась библиотека pidgen.dll, в которую встроены подходящие для конкретной версии Windows публичные ключи; тогда как начиная с Vista, код проверки выделили в библиотеку pidgenx.dll, а публичные ключи – в отдельные файлы pkeyconfig*.xrm-ms, которые хранятся в \Windows\System32\spp\tokens\pkeyconfig.
00186-217-764-155 |
|
|
00186-217-764-156 |
|
|
00186-217-764-157 |
|
|
00186-217-764-158 |
|
|
00186-217-764-159 |
|
|
Утёкшие исходники WinXP, включая библиотеку pidgen, каким-то образом оказались у меня – я уже и сам не помню откуда. API библиотеки объявлен следующим образом:
// Original, outdated, interface to PidGen
BOOL STDAPICALLTYPE PIDGenW(
LPWSTR lpstrSecureCdKey, // [IN] 25-character Secure CD-Key (gets U-Cased)
LPCWSTR lpstrMpc, // [IN] 5-character Microsoft Product Code
LPCWSTR lpstrSku, // [IN] Stock Keeping Unit (formatted like 123-12345)
LPCWSTR lpstrOemId, // [IN] 4-character OEM ID or NULL
LPWSTR lpstrLocal24, // [IN] 24-character ordered set to use for decode base conversion or NULL for default set (gets U-Cased)
LPBYTE lpbPublicKey, // [IN] pointer to optional public key or NULL
DWORD dwcbPublicKey, // [IN] byte length of optional public key
DWORD dwKeyIdx, // [IN] key pair index optional public key
BOOL fOem, // [IN] is this an OEM install?
LPWSTR lpstrPid2, // [OUT] PID 2.0, pass in ptr to 24 character array
LPBYTE lpbDigPid, // [IN/OUT] pointer to DigitalPID buffer. First DWORD is the length
LPDWORD lpdwSeq, // [OUT] optional ptr to sequence number (can be NULL)
LPBOOL pfCCP, // [OUT] optional ptr to Compliance Checking flag (can be NULL)
LPBOOL pfPSS); // [OUT] optional ptr to 'PSS Assigned' flag (can be NULL)
// Simplified interface to PidGen
DWORD STDAPICALLTYPE PIDGenSimpW(
LPWSTR lpstrSecureCdKey, // [IN] 25-character Secure CD-Key (gets U-Cased)
LPCWSTR lpstrMpc, // [IN] 5-character Microsoft Product Code
LPCWSTR lpstrSku, // [IN] Stock Keeping Unit (formatted like 123-12345)
LPCWSTR lpstrOemId, // [IN] 4-character OEM ID or NULL
BOOL fOem, // [IN] is this an OEM install?
LPWSTR lpstrPid2, // [OUT] PID 2.0, pass in ptr to 24 character array
LPBYTE lpbDigPid, // [IN/OUT] pointer to DigitalPID buffer. First DWORD is the length
LPDWORD lpdwSeq, // [OUT] optional ptr to sequence number or NULL
LPBOOL pfCCP); // [OUT] ptr to Compliance Checking flag or NULL
Эти исходники подтверждают анализ от Licenturion, и дополняют его некоторыми подробностями. Для хеширования порядкового номера вычисляется 160-битный SHA-1, и затем «ужимается» ~вдвое неочевидным образом: от выполняющей это внутренней функции ShortSigHash
исходники не нашлись. Многочисленные блоки #if defined(WIN32) || defined(_WIN32)
в коде pidgen прибавляют уверенности, что этот код унаследован со времён Win98; алгоритм SHA-1 был опубликован в 1995, и на время разработки Win98 был последним словом техники. Для асимметричного шифрования используется эллиптическая криптография, причём параметры используемой кривой являются частью публичного ключа.
У библиотеки pidgenx API проще, и нашёлся у Ion Bazan: функция PidGenX
принимает серийник, путь к файлу pkeyconfig, несколько необязательных параметров, и указатель на структуру, которая заполнится данными лицензии, если серийник пройдёт проверку. Как я выяснил при помощи отладчика, от этой структуры требуется лишь то, чтобы её первое поле, задающее её размер, равнялось 0x4f8.
Для моей цели – перебор вариантов серийника – удобно было использовать Python-модуль ctypes:
import ctypes
pidgenx = ctypes.WinDLL("pidgenx.dll")
class DigitalProductId(ctypes.Structure):
_fields_ = [("uiSize", ctypes.c_uint), ("data", ctypes.c_byte*0x4f8)]
proto = ctypes.WINFUNCTYPE(ctypes.c_uint, ctypes.c_wchar_p, ctypes.c_wchar_p,
ctypes.c_wchar_p, ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_void_p,
ctypes.POINTER(DigitalProductId))
PidGenX = proto(("PidGenX", pidgenx), ((1, "szProductKey"), (1, "szPKeyConfigPath"),
(1, "szPID"), (1, "OemId"), (1, "szProductId", ctypes.create_unicode_buffer(1024)),
(1, "pDigPid"), (1, "pDigPid4")))
Для проверки попробуем какой-нибудь недействительный ключ:
>>> szProductId = ctypes.create_unicode_buffer(1024)
>>> pid = DigitalProductId(0x4f8)
>>> hex(PidGenX("J3QQ4-H7H2V-2HCH4-M3HK8-6M8VW", "pkeyconfig.xrm-ms", "55041", None,
... szProductId=szProductId, pDigPid=None, pDigPid4=ctypes.byref(pid)))
'0x8a010101'
Легко заметить (даже при попытке вручную подобрать серийник при установке Windows), что для разных серийников проверка занимает разное время: для большинства (~70%) порядка полсекунды на моей системе, для остальных – порядка 15 сек. Это позволяет предположить, что «быстрый отказ» соответствует не поддающейся расшифровке зашифрованной части, тогда как «медленный отказ» – успешной расшифровке и обнаружению несоответствия порядкового номера расшифрованному хешу. Эксперименты с обнулением разных частей серийника позволяют предположить, что шифрование более чувствительно ко младшим битам серийника (соответствующим его последним символам): так, BBBBD-P8T49-4CCD8-6HQKB-42XQH
(0x000001f5b93f3eaca29b682dfe8b5
) и KH2Q7-QVKK4-X8RR2-RKGTR-9WDDH
(0x1dbffff5b93f3eaca29b682dfe8b5
) оба приводят к «медленному отказу», тогда как BBBBD-P8T49-4CCD8-6HQKB-42XQG
(0x000001f5b93f3eaca29b682dfe8b4
) и KH2Q7-QVKK4-X8RR2-RKGTR-9WDDG
(0x1dbffff5b93f3eaca29b682dfe8b4
) – оба к «быстрому». (Два первых, равно как и два вторых, различаются 22 старшими битами; первые от вторых отличаются одним младшим битом.)
Всё, что осталось – реализовать перебиралку серийников по шаблону:
import sys, time, token, tokenize
class Permute:
def __init__(self, options):
self.parts = []
in_sqb = False
for t in tokenize.generate_tokens(lambda:options+"\n"):
match t.exact_type:
case token.NAME | token.NUMBER | token.MINUS:
if in_sqb:
self.parts[-1].extend(list(t.string))
else:
self.parts.append([t.string])
case token.NEWLINE:
return
case token.LSQB:
self.parts.append([])
in_sqb = True
case token.RSQB:
in_sqb = False
def gen(self, prefix, options):
if options:
first, *others = options
for option in first:
yield from self.gen(prefix+option, others)
else:
yield prefix
def __iter__(self):
return self.gen("", self.parts)
start = time.perf_counter()
for i, key in enumerate(Permute("P2[23]JR-J[9G]K[389BDJ][BFKPR]-QB6[EFKPR][EFPR]-[CGQ]W72C-P[CG]VPX")):
print("\r", i, end="")
sys.stdout.flush()
if 0x8a010101!=PidGenX(key,"pkeyconfig.xrm-ms","55041",None,pDigPid=None,pDigPid4=DigitalProductId(0x4f8)):
print("\t\t\t", key)
print(" *", (time.perf_counter()-start)/(i+1), end="")
sys.stdout.flush()
14400 вариантов, подходящих по этому шаблону, успешно перебираются за 19 часов – в среднем по 4.7 сек на вариант. На следующее утро я, счастливый и довольный, уже вводил мой законный серийник, и активировал переустановленную Windows. Для восстановления серийника, чей хозяин обратился на официальный форум поддержки Microsoft, понадобилось перебрать всего лишь 96 вариантов, что заняло <10 мин. Третий пример восстановления COA связан с тем, что на стоках попадаются обрезанные снимки; недостающие там три символа (243=13824 варианта) перебираются за чуть дольше суток. (Перебиралка по шаблону может пригодиться и для множества других целей, не только для восстановления стёршихся COA.)
Теперь посмотрим, какие данные о лицензии PidGenX
выдаёт, когда серийник успешно проходит проверку. Строка szProductId
(кроме первого блока, куда копируется szPID
) – это тот самый номер, который отображается в «свойствах системы»:
szProductId
для OEM COA получается следующим образом:
второй блок – это три буквы "OEM";
две первые цифры третьего блока (подчёркнуты в таблице ниже) – это делённый напополам номер варианта Windows, напечатанный на COA перед порядковым номером, например 000186 → 93;
следующие четыре цифры третьего блока – это старшие цифры порядкового номера;
последняя цифра третьего блока (выделена жирным в таблице ниже) – «контрольная»: она дополняет блок так, чтобы сумма всех цифр в нём делилась на 7;
четвёртый блок – это младшие пять цифр порядкового номера.
Для Retail COA szProductId
формируется как-то иначе, и вдобавок зависит от времени генерации: последние три цифры принимают новое значение при каждом новом вызове PidGenX
. Результаты моих экспериментов сведены в таблицу, где указаны также szEditionType
, szKeyType
и szEULA
из структуры DigitalProductId
00152-102-655-441 | 55041-232-4334205-86xxx | HomePremium | Retail | MSDN |
00174-033-475-083 | 55041-OEM-8703343-75083 | HomePremium | OEM:NONSLP | OEM |
00178-780-401-886 | 55041-OEM-8978046-01886 | Professional | OEM:NONSLP | OEM |
00180-507-523-230 | 55041-OEM-9050752-23230 | Professional | OEM:NONSLP | OEM |
00182-542-249-283 | 55041-067-8995756-86xxx | Ultimate | Retail | MSDN |
00186-217-764-15x | 55041-OEM-9321776-6415x | Professional | OEM:COA | OEM |
00188-029-012-066 | 55041-OEM-9402904-12066 | Ultimate | OEM:COA | OEM |
00188-231-315-699 | 55041-333-1494467-85xxx | Ultimate | Retail | Retail |
00190-079-069-535 | 55041-OEM-9507905-69535 | HomeBasic | OEM:COA | OEM |
00196-148-254-807 | 55041-OEM-9814823-54807 | HomePremium | OEM:COA | OEM |
00212-361-184-637 | 55041-OEM-0636114-84637 | Professional | OEM:NONSLP | OEM |
Извлекаются эти поля из структуры следующим образом:
>>> ctypes.wstring_at(ctypes.byref(pid.data, 276))
'Professional'
>>> ctypes.wstring_at(ctypes.byref(pid.data, 1012))
'OEM:COA'
>>> ctypes.wstring_at(ctypes.byref(pid.data, 1140))
'OEM'
Выгуглившиеся примеры COA для Win7
Соня @sofya_hadasa92 сегодня отмечает шестнадцатеричный юбилей, желаю ей всегда чувствовать себя на 0x20!
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩