Эта история начинается с того, что я попытался переустановить Windows на ноутбуке, доставшемся мне вот с такой наклейкой Certificate of Authenticity (COA):

Часть символов серийника видны хорошо, остальные – в большей или меньшей степени угадываются; но несколько попыток ввести серийник «на глаз» успехом не увенчались. Пришлось углубляться в вопрос подробнее.

Достижения в области исследования серийников для Windows сведены в реддит-посте: вкратце, со времён Win98/2000 серийник состоит из 25 символов 24-символьного алфавита BCDFGHJKMPQRTVWXY2346789 и таким образом несёт \left\lceil{\log_224^{25}}\right\rceil =115 бит информации. (Начиная с 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

0x0a033d38032d650f79cc25736c234

0x2b9b611a

Win98 SE

00003-891-192-814

0x3ebced6e3eddfaaae57fe6a3d0bdc

0x351e85ee

Win2000 Pro

00019-034-701-358

0x32525e95ad6ae564808480423005c

0x0211802e

WinME

00029-066-259-739

0x26d52384b3ba0715493b987e61636

0x03f30b1b

WinXP Home

00043-153-594-618

0x07891e46afcb92d661b26924f55f4

0x0927aafa

WinXP Pro

00045-519-589-488

0x1e0dd45951870ff31dd7ebdf09ce0

0x1ef84e70

Win2003 Std

00085-173-586-932

0x0828781c442ea431cfdd14133115a

0x209988ad

Vista Bus

00144-041-819-328

0x0c36ca51f9c20a32d6256495eb288

0x24af5944

Несложно заметить, что у Win98–XP число в правой колонке – это и есть порядковый номер COA: 731603226, 891192814, 34701358, 66259739, 153594618, 519589488 соответственно. Девятизначное десятичное число гарантированно помещается в 30 бит. Но чем же заняты оставшиеся (старшие) 84 бита серийника? И что произошло, начиная с Win2003 и Vista? – у них в правой колонке (546932909 и 615471428 соответственно) ничего похожего на порядковый номер!

Выгуглившиеся примеры COA для Win98–Vista
731603226
731603226
00003-891-192-814
00003-891-192-814
00019-034-701-358
00019-034-701-358
00029-066-259-739
00029-066-259-739
00043-153-594-618
00043-153-594-618
00045-519-589-488
00045-519-589-488
00085-173-586-932
00085-173-586-932
00144-041-819-328
00144-041-819-328

Проверка серийника в 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?

00186-217-764-15x
00186-217-764-15x

Мне не удалось нагуглить рулон 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

GWY9Q-6FYRK-P2JHW-48676-H64WP

0x12f54851aa9d31976feee724fb419

00186-217-764-156

BDFR2-3XDK6-KY8Y3-26R8B-P84YX

0x005e0d147f1049463779e65ea604f

00186-217-764-157

9BM39-MKPRP-8KVMT-2WCWB-GWG2G

0x5e9717dda260601772c3c16ef3e9c

00186-217-764-158

RGWBQ-4KQ2C-F7W7D-RXPCM-X76MJ

0x2dfe6a15422ab4b12bcdaa2e08bc6

00186-217-764-159

2VMGP-BYPMM-CBHQG-YW3FJ-VMMBD

0x48271855e00c525f8bd80ac089202

Утёкшие исходники 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.)

00180-507-523-230
00180-507-523-230
00178-780-401-886
00178-780-401-886

Теперь посмотрим, какие данные о лицензии PidGenX выдаёт, когда серийник успешно проходит проверку. Строка szProductId (кроме первого блока, куда копируется szPID) – это тот самый номер, который отображается в «свойствах системы»:

00186-224-466-285
00186-224-466-285

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
00152-102-655-441
00152-102-655-441
00174-033-475-083
00174-033-475-083
00182-542-249-283
00182-542-249-283
00188-029-012-066
00188-029-012-066
00188-231-315-699
00188-231-315-699
00190-079-069-535
00190-079-069-535
00196-148-254-807
00196-148-254-807
00212-361-184-637
00212-361-184-637

Соня @sofya_hadasa92 сегодня отмечает шестнадцатеричный юбилей, желаю ей всегда чувствовать себя на 0x20!

Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud  в нашем Telegram-канале 

Перейти ↩

? Читайте также:

Комментарии (1)


  1. MasterMentor
    14.10.2024 09:52

    Тёмыч, твою бы энергию - да в мирное русло. Сколько на трекерах так нужного Родине софта, без лекарств томится. Непаханная нива для таких исследований!