'Cerberus Shield' keygenning practice.
  ======================================


  1. Введение
  ~~~~~~~~~~~
  
    В  этой статье  я хочу  рассмотреть защиту  под гордым  названием 'Cerberus Shield' и написание кейгена  для нее же.
  Сразу хочу заметить, что если у вас есть время и желание, то  лучше  поковыряйтесь и сделайте все самостоятельно, т.к.
  это того стоит. Хотя особой оригинальности  в данном продукте  и не наблюдается, но все же намного интереснее дойти до
  всего самому. По крайней мере мне так кажется.

    Что касается непосредственно 'Cerberus Shield' - я так и не понял кто его производитель.При беглом осмотре интернета
  ничего толкового (например, сайт разработчиков)  не  нашлось. Возможно  это самописный продукт Parallaxis? Тогда каким
  образом он появился у AusLogics? Одна контора под разными названиями? :) В общем ладно, это не есть главное для нас.

    Итак, в поле моего зрения попало несколько программ:

    - Parallaxis Boost-It 2.2.9.360 (Cerberus DE)
    - Parallaxis iAlbum 1.0.0.81 (Cerberus DE)
    - Parallaxis Winclip 3.0.0.14 (Cerberus DE)
    - AusLogics BoostSpeed 3.0.2.451 (Cerberus DE 1.1)
    - Parallaxis Cuckoo Clock (Cerberus 2)

    В скобках  указаны версии защиты, на которые я разделил их лично для себя. Cerberus DE - это  самая простая  из всех
  перечисленных. Написана на дельфях (DE - Delphi Edition :) и  ничего сложного  там нет. Поэтому  свой рассказ я  начну
  именно  с  нее.  Cerberus DE 1.1  -  это  "усовершенствованная" версия  предыдущей. Слово я взял в кавычки потому, что
  изменения  -  это несколько  приписанных  инструкций ассемблера. И  наконец Cerberus 2 - основная тема моего рассказа.
  Написана на С++ и по сравнению с предыдущими двумя выглядит более значимо. На ней мы остановимся более подробно.

    В комплекте с каждой из перечисленных программ идет файл cerberus.dll, в котором и содержится интересующий нас  код.
  Он упакован UPX`м, но этот факт нас остановить не может и не должен. Можно даже не утруждать себя и не набивать лишний
  раз "upx -d", т.к. это особой роли не играет.

    Что понадобится в работе:

    1) OllyDbg (http://home.t-online.de/home/Ollydbg/)
  2) IDA (http://reversing.kulichki.net/files/dizas/dizas.htm - на момент написания статьи  там был живой  линк на
     версию 4.3.0.740. Более новые ищите сами)
    3) masm32 (http://www.masm32.com/)
    4) Ну и конечно же правильно настроенный девайс "руки" (также известный как "hands.sys")

    Ах да, чуть не забыл. Ссылки на софт, который будем исследовать:

    1) Parallaxis (http://www.pxcompany.com/)
    2) AusLogics BoostSpeed (http://www.boost-speed.com/)

    Вот теперь можно приступать.


  2. Cerberus DE
  ~~~~~~~~~~~~~~
    Будем рассматривать Parallaxis iAlbum. Почему именно его? Потому что я первым его скачал =) Техника же для остальных
  программ ничуть не меняется. Все различия (кроме смещений конечно) я постарался указать явно в тексте.
  
    Как и говорилось раньше, у этой версии алгоритм не такой уж и сложный. В общем случае его можно записать  так (далее
  при исследовании рассмотрим все это подробнее):

  1) Проверка длины имени и ключа
  2) Проверка первой части ключа (входят ли символы в определенную группу) и формирование дворда на их основе
  3) Получение хэша на основе имени
  4) Сравнение дворда и хэша имени
  5) Формирование дворда из второй части ключа
  6) Получение хэша на основе дворда
  7) Сравнение хэша с константой

    Пока  IDA  дизасмит  iAlbum.exe, мы  загрузим  его  в  Олю и найдем  то место,  где собственно начинается реализация
  приведенного алгоритма. Для этого перейдем на OEP (если вы не знаете как это сделать - читайте туториалы по распаковке
  UPX`а), поставим бряк на MessageBoxA, запустим программу по F9,  введем левые регистрационные данные и нажмем кнопочку
  Register. После этой незамысловатой процедуры Оля приземлится тут:

  0049D936   E8 896FF6FF      CALL iAlbum.004048C4
  0049D93B   50               PUSH EAX
  0049D93C   53               PUSH EBX
  0049D93D   E8 7A9CF6FF      CALL iAlbum.004075BC                     ; <-- Мы тут
  0049D942   8BD8             MOV EBX,EAX
  0049D944   33C0             XOR EAX,EAX

    Смотрим чуть выше и находим начало данной процедуры:

  0049D8FC   55               PUSH EBP                                 ; <-- Сюда бряк
  0049D8FD   8BEC             MOV EBP,ESP
  0049D8FF   51               PUSH ECX
  0049D900   53               PUSH EBX

    Удаляем старый бряк и ставим новый в указанном месте (0x0049D8FC). Отпускаем программу и снова жмем  Register. После
  этого смотрим на текущий образ стека и находим там такую строчку:

  0012F338  |004EDC65  RETURN to iAlbum.004EDC65 from iAlbum.0049D8A8

    Это то место, куда вернется программа, если мы вводим неправильные данные регистрации. Попросим Олю перенести  нас в
  это место (Ctrl+G и введем адрес 0x004EDC65). Покрутим листинг чуть вверх и увидим желанные строчки:

  004EDBF5   50               PUSH EAX
  004EDBF6   E8 CDF9FFFF      CALL iAlbum.004ED5C8                     ; JMP to OFFSET cerberus.regVerifyKey
  004EDBFB   84C0             TEST AL,AL
  004EDBFD   74 57            JE SHORT iAlbum.004EDC56

    Вот  именно  функция  regVerifyKey  нам и  понадобится. Кстати, для  большей наглядности можете заглянуть в экспорты
  cerberus.dll (при  помощи  PE Tools например)  и  посмотреть  на функции,  которые  там  имеются. Все  их имена  несут
  достаточную смысловую нагрузку для того, чтобы  понять что именно они делают. Для осознания всех тонкостей этой защиты
  можно поковырять и их ;)

    Итак, заходим внутрь процедуры. Не удивляйтесь, что она такая маленькая  -  это всего лишь  "переходник". Нам  нужен
  единственный  call (0x0034CD9D),  который и является  функцией  проверки. Трейсим по F7 до ее начала и, как говорится,
  понеслась...

    Первые строки можно смело пропускать - здесь идет всякая никому не нужная дельфовая хрень.  Наш алгоритм  начинается
  по адресу 0x0034CAF4. И первое - это проверка длины имени пользователя:

  0034CAF4   8B45 E4          MOV EAX,DWORD PTR SS:[EBP-1C]            ; eax = имя
  0034CAF7   E8 C074FEFF      CALL cerberus.00333FBC                   ; получить длину строки
  0034CAFC   83F8 03          CMP EAX,3
  0034CAFF   0F8C 52020000    JL cerberus.0034CD57

    Как  видно  из приведенного  куска,  имя должно быть >= 3 символов. Следует об этом помнить. Длина ключа проверяется
  таким  же  образом чуть ниже.  Кусок,  который проверяет присутствие двух символов '-' в ключе мне не совсем понятен в
  данной ситуации.  Зачем это  делать,  если в передаваемой функции строке они есть ВСЕГДА? Возможно cerberus.dll писали
  все-таки сторонние производители? :) 

    Следующий шаг - это проверка первой части серийника и формирование дворда. Сначала приведу кусок кода, потом объясню
  что за проверки и как все происходит.

  0034CB6F   8A40 08          MOV AL,BYTE PTR DS:[EAX+8]                ; al = символ ключа
  0034CB72   E8 89FEFFFF      CALL cerberus.0034CA00                    ; проверка и преобразование символа
  0034CB77   85C0             TEST EAX,EAX
  0034CB79   0F8C D8010000    JL cerberus.0034CD57
  0034CB7F   C1E6 04          SHL ESI,4                                 ; esi = формируемый дворд
  0034CB82   0BF0             OR ESI,EAX                                ; добавляем преобразованный символ к дворду

    Если  кто еще не догадался, ключ в памяти выглядит так: "XXXX-XXXX-XXXXXXXX". Первая часть  -  это "XXXX-XXXX". Суть
  работы  такая:  берется  символ  из  этой  части (начиная с _КОНЦА_!)  и вызывается функция cerberus.0034CA00, которая
  преобразует символ. Что есть преобразование? Все очень просто. Имеется массив, состоящий из 16 элементов. Функция ищет
  переданный  ей символ  в данном  массиве и  возвращает  порядковый  номер в нем, который в последствии и добавляется к
  дворду.  Если переданного  символа в  нем  нет  - это значит, что введенные данные уже не верны. Массив для iAlbum.exe
  выглядит так: "23456789ARTFGNSM", причем от программы к программе он остается постоянным.

    Шаг  номер  3  -  получение  хэша на основе имени. Тут все совсем просто. Имеется единая  функция генерации, которая
  используется  во  всей процедуре проверки. И если присмотреться повнимательнее к коду, то... это есть ни что иное, как
  алгоритм CRC32! В edx ей передается указатель на тот участок памяти, с которого хотят получить crc; в ecx - количество
  байт. А выглядит эта функция так:

  0034B968   09D2             OR EDX,EDX
  0034B96A   74 18            JE SHORT cerberus.0034B984
  0034B96C   E3 16            JECXZ SHORT cerberus.0034B984
  0034B96E   53               PUSH EBX
  0034B96F   31DB             XOR EBX,EBX
  0034B971   88C3             MOV BL,AL
  0034B973   321A             XOR BL,BYTE PTR DS:[EDX]
  0034B975   C1E8 08          SHR EAX,8
  0034B978   33049D A45D3500  XOR EAX,DWORD PTR DS:[EBX*4Ю]
  0034B97F   42               INC EDX
  0034B980   49               DEC ECX
  0034B981  ^75 EC            JNZ SHORT cerberus.0034B96F
  0034B983   5B               POP EBX
  0034B984   C3               RETN


    Я  думаю  все понятно  без  дополнительных  объяснений. СтОит наверное  только отметить то, что инструкция по адресу
  0x0034B978 ксорит значение в eax с числом,  которое берется  из массива. Этот массив состоит из 256 элементов и размер
  каждого из них, как вы наверное могли уже заметить, есть дворд.

    И шаг номер 4 завершает первый кусок алгоритма инструкцией:

  0034CC46   3BF0             CMP ESI,EAX

    В которой просто сравнивается полученный дворд из серийника и хэш имени. Если чуть подумать и  прокрутить алгоритм в
  обратном направлении, то в голове уже рождается первая часть кейгена. Нам нужно только подсчитать CRC введенного имени
  и преобразовать его в строчку, используя упоминавшийся массив из 16 элементов. Проще по-моему некуда.

    Вторая  часть  алгоритма чуть  более  сложная. Объясню почему. Вас должно смущать слово "константа"  в шаге номер 8.
  Константа есть числовое значение. Нам же нужно вводить текст, чтобы функция смогла ее преобразовать и сравнить. Отсюда
  делаем вывод, что  автор  программы знает  эту строчку  и  прописывает ее заранее. И т.к. он врятли подскажет ее нам в
  интимной переписке по мылу, придется все это угадывать самим. А здесь поможет только брутфорс...

    Но обо  всем по  порядку.  Следующий шаг - это формирование дворда из второй  части ключа. Здесь все так  же как и с
  частью номер раз. Опять же, алгоритм начинается с последнего символа и движется к началу. Получившийся дворд находится
  в edx.

    Получение хэша на основе дворда - это то же  самое, что и  получение хэша  имени. Функции подсчета  CRC32 передается
  указатель на память (в данном конкретном случае - это указатель на получившийся дворд). Количество байтов равно 4.

  0034CD30   8955 F0          MOV DWORD PTR SS:[EBP-10],EDX            ; в edx наш дворд
  0034CD33   8D55 F0          LEA EDX,DWORD PTR SS:[EBP-10]            ; edx = указатель на память
  0034CD36   B9 04000000      MOV ECX,4                                ; кол-во байт
  0034CD3B   33C0             XOR EAX,EAX
  0034CD3D   E8 26ECFFFF      CALL cerberus.0034B968                   ; CRC32
  0034CD42   8B15 B4613500    MOV EDX,DWORD PTR DS:[3561B4]
  0034CD48   3B42 14          CMP EAX,DWORD PTR DS:[EDX+14]            ; [edx+14] = дворд-константа

    Последняя  из  вышеприведенных  инструкций  сравнивает  результат  работы  функции с  тем самым константным двордом,
  "строковое" значение которого мы в скором времени получим. В iAlbum.exe  он  равен 0x6E3DF5BB.  И это значение во всех
  программах РАЗНОЕ!

    Вот  в общем-то  и все. Вторая часть будущего кейгена ничего толком  не делает. Только  выводит "строковое" значение
  константы.

    Алгоритм работы брутфорса по-моему должен быть понятен всем.Нет?! Ну ладно, объясняю :) Все, что нам нужно сделать -
  это найти правильное числовое значение для второй части ключа. Вторая часть выглядит так: "XXXXXXXX", т.е. 8 символов.
  Все было бы хорошо, если бы эти символы были последовательны, т.е.,  например, "ABCDE..." и т.д. Но ведь это не так. В
  нашем случае символы берутся  из массива,  о котором  я говорил выше. И вот там символы совсем уж непоследовательны...
  Поэтому стандартный брутфорс немного усложняется.

    Суть же его работы такая:

    1) Сформировать дворд из сточки (читай - куска ключа)
    2) Подсчитать CRC32 этого дворда
    3) Сравнить его с забитой в программе константой
    4) Если они равны - ура, мы нашли то, что искали.
    5) Если нет, то мы должны заменить символ(ы) в нашей строке и перейти к шагу 1.

    Проблему тут может представлять только шаг под номером 5.Я выбрал такой способ (ниже идет собственно код брутфорса):

  ;---------------------------------------------------------------------------------------------------------------------
  .386
  .model flat, stdcall
  option casemap :none

  include c:\masm32\include\windows.inc
  include c:\masm32\include\kernel32.inc
  include c:\masm32\include\user32.inc
  include c:\masm32\include\shlwapi.inc

  includelib c:\masm32\lib\kernel32.lib
  includelib c:\masm32\lib\user32.lib
  includelib c:\masm32\lib\shlwapi.lib

  .data
    o_str db '23456789ARTFGNSM', 0    ; Наш исходный массив с символами
    f_str db '22222222', 0      ; Строка, на основе которой считаем дворд

    str_ptr dd 7          ; Указатель на символы в предыдущей строке

    array dd 8 dup (0)        ; Массив содержит текущее "состояние" строки,
                ; т.е. каждый элемент есть указатель на символ
                ; в o_str.
    arr_ptr dd ?          ; Указатель на элементы в предыдущем массиве
    num dd ?          ; Наш дворд

    notfound db 'No combinations found....wtf?!', 0

                ; А это та самая таблица из 256 значений, которая
                ; используется при получении CRC32
  tabl dd 0, 77073096h, 0EE0E612Ch, 990951BAh, 76DC419h, 706AF48Fh
  dd 0E963A535h, 9E6495A3h, 0EDB8832h, 79DCB8A4h, 0E0D5E91Eh
  dd 97D2D988h, 9B64C2Bh, 7EB17CBDh, 0E7B82D07h, 90BF1D91h
  dd 1DB71064h, 6AB020F2h, 0F3B97148h, 84BE41DEh, 1ADAD47Dh
  dd 6DDDE4EBh, 0F4D4B551h, 83D385C7h, 136C9856h, 646BA8C0h
  dd 0FD62F97Ah, 8A65C9ECh, 14015C4Fh, 63066CD9h, 0FA0F3D63h
  dd 8D080DF5h, 3B6E20C8h, 4C69105Eh, 0D56041E4h, 0A2677172h
  dd 3C03E4D1h, 4B04D447h, 0D20D85FDh, 0A50AB56Bh, 35B5A8FAh
  dd 42B2986Ch, 0DBBBC9D6h, 0ACBCF940h, 32D86CE3h, 45DF5C75h
  dd 0DCD60DCFh, 0ABD13D59h, 26D930ACh, 51DE003Ah, 0C8D75180h
  dd 0BFD06116h, 21B4F4B5h, 56B3C423h, 0CFBA9599h, 0B8BDA50Fh
  dd 2802B89Eh, 5F058808h, 0C60CD9B2h, 0B10BE924h, 2F6F7C87h
  dd 58684C11h, 0C1611DABh, 0B6662D3Dh, 76DC4190h, 1DB7106h
  dd 98D220BCh, 0EFD5102Ah, 71B18589h, 6B6B51Fh, 9FBFE4A5h
  dd 0E8B8D433h, 7807C9A2h, 0F00F934h, 9609A88Eh, 0E10E9818h
  dd 7F6A0DBBh, 86D3D2Dh, 91646C97h, 0E6635C01h, 6B6B51F4h
  dd 1C6C6162h, 856530D8h, 0F262004Eh, 6C0695EDh, 1B01A57Bh
  dd 8208F4C1h, 0F50FC457h, 65B0D9C6h, 12B7E950h, 8BBEB8EAh
  dd 0FCB9887Ch, 62DD1DDFh, 15DA2D49h, 8CD37CF3h, 0FBD44C65h
  dd 4DB26158h, 3AB551CEh, 0A3BC0074h, 0D4BB30E2h, 4ADFA541h
  dd 3DD895D7h, 0A4D1C46Dh, 0D3D6F4FBh, 4369E96Ah, 346ED9FCh
  dd 0AD678846h, 0DA60B8D0h, 44042D73h, 33031DE5h, 0AA0A4C5Fh
  dd 0DD0D7CC9h, 5005713Ch, 270241AAh, 0BE0B1010h, 0C90C2086h
  dd 5768B525h, 206F85B3h, 0B966D409h, 0CE61E49Fh, 5EDEF90Eh
  dd 29D9C998h, 0B0D09822h, 0C7D7A8B4h, 59B33D17h, 2EB40D81h
  dd 0B7BD5C3Bh, 0C0BA6CADh, 0EDB88320h, 9ABFB3B6h, 3B6E20Ch
  dd 74B1D29Ah, 0EAD54739h, 9DD277AFh, 4DB2615h, 73DC1683h
  dd 0E3630B12h, 94643B84h, 0D6D6A3Eh, 7A6A5AA8h, 0E40ECF0Bh
  dd 9309FF9Dh, 0A00AE27h, 7D079EB1h, 0F00F9344h, 8708A3D2h
  dd 1E01F268h, 6906C2FEh, 0F762575Dh, 806567CBh, 196C3671h
  dd 6E6B06E7h, 0FED41B76h, 89D32BE0h, 10DA7A5Ah, 67DD4ACCh
  dd 0F9B9DF6Fh, 8EBEEFF9h, 17B7BE43h, 60B08ED5h, 0D6D6A3E8h
  dd 0A1D1937Eh, 38D8C2C4h, 4FDFF252h, 0D1BB67F1h, 0A6BC5767h
  dd 3FB506DDh, 48B2364Bh, 0D80D2BDAh, 0AF0A1B4Ch, 36034AF6h
  dd 41047A60h, 0DF60EFC3h, 0A867DF55h, 316E8EEFh, 4669BE79h
  dd 0CB61B38Ch, 0BC66831Ah, 256FD2A0h, 5268E236h, 0CC0C7795h
  dd 0BB0B4703h, 220216B9h, 5505262Fh, 0C5BA3BBEh, 0B2BD0B28h
  dd 2BB45A92h, 5CB36A04h, 0C2D7FFA7h, 0B5D0CF31h, 2CD99E8Bh
  dd 5BDEAE1Dh, 9B64C2B0h, 0EC63F226h, 756AA39Ch, 26D930Ah
  dd 9C0906A9h, 0EB0E363Fh, 72076785h, 5005713h, 95BF4A82h
  dd 0E2B87A14h, 7BB12BAEh, 0CB61B38h, 92D28E9Bh, 0E5D5BE0Dh
  dd 7CDCEFB7h, 0BDBDF21h, 86D3D2D4h, 0F1D4E242h, 68DDB3F8h
  dd 1FDA836Eh, 81BE16CDh, 0F6B9265Bh, 6FB077E1h, 18B74777h
  dd 88085AE6h, 0FF0F6A70h, 66063BCAh, 11010B5Ch, 8F659EFFh
  dd 0F862AE69h, 616BFFD3h, 166CCF45h, 0A00AE278h, 0D70DD2EEh
  dd 4E048354h, 3903B3C2h, 0A7672661h, 0D06016F7h, 4969474Dh
  dd 3E6E77DBh, 0AED16A4Ah, 0D9D65ADCh, 40DF0B66h, 37D83BF0h
  dd 0A9BCAE53h, 0DEBB9EC5h, 47B2CF7Fh, 30B5FFE9h, 0BDBDF21Ch
  dd 0CABAC28Ah, 53B39330h, 24B4A3A6h, 0BAD03605h, 0CDD70693h
  dd 54DE5729h, 23D967BFh, 0B3667A2Eh, 0C4614AB8h, 5D681B02h
  dd 2A6F2B94h, 0B40BBE37h, 0C30C8EA1h, 5A05DF1Bh, 2D02EF8Dh

  .code
  start:
      push  offset arr_ptr-4      ; Инициализация
      pop arr_ptr

      mov str_ptr, 7

      mov ecx, 8
      mov ebx, ecx
      dec ebx
      xor edx, edx

  _GetNum:                ; Получение дворда
      mov eax, array[ebx*4]
      shl edx, 4
      or  edx, eax
      dec ebx
      loop  _GetNum

      mov num, edx
      lea edx, num
      mov ecx, 4
      xor eax, eax

  CRC32:                ; Генерация хэша
      xor ebx, ebx
      mov bl, al
      xor bl, byte ptr [edx]
      shr eax, 8
      xor eax, dword ptr [ebx*4+tabl]
      inc edx
      dec ecx
      jnz CRC32

      cmp eax, 6e3df5bbh        ; Сравнение с константой
      jz  YEAH

      cmp num, -1         ; Если ВСЕ комбинации проверены
      je  NotFound

                  ; Изменяемый символ последний в массиве?
      mov edi, arr_ptr
      inc dword ptr [edi]
      mov esi, [edi]
      cmp esi, 15
      jg  FixArray        ; Да, фиксим строчку

      movzx eax, byte ptr o_str[esi]    ; Нет, просто заменяем на следующий
      mov [f_str+7], al
      jmp start

  FixArray:
      mov ecx, 8

  FixArray_loop:                ; В цикле фиксим всю строку
      and dword ptr [edi], 0
      mov [f_str+ecx-1], '2'

      sub edi, 4
      inc dword ptr [edi]
      cmp dword ptr [edi], 16
      je  FixNum

      mov esi, [edi]
      movzx eax, byte ptr o_str[esi]
      mov ebx, str_ptr
      dec ebx
      mov f_str[ebx], al
      jmp start

  FixNum:
      dec str_ptr
      sub arr_ptr, 4
      loop  FixArray_loop

  YEAH:
      invoke  MessageBox, NULL, offset f_str, NULL, 0
      jmp _exit

  NotFound:
      invoke  MessageBox, NULL, offset notfound, NULL, 0

  _exit:
      invoke  ExitProcess, 0

  end start
  end
  ;---------------------------------------------------------------------------------------------------------------------

    Вот такой вот может быть и не совсем оптимизированный способ. Но это первое,  что пришло мне  в голову. А если брать
  в расчет то, что количество комбинаций не такое уж большое, то можно и немного подождать, раз уж на то пошло ;)

    Запускаем наше творение и по истечении нескольких минут получаем желанное - "SS7RGRN2". В радости прыгаем по комнате
  и открываем новую бутылку пива.

    Ну и самое приятное - написание кейгена. Я думаю по его алгоритму никаких вопросов остаться не должно.Поэтому только
  покажу как может выглядеть сам код (точнее - его основные куски):

  ;---------------------------------------------------------------------------------------------------------------------
  szKey   db KEYSIZE dup(0)
  defname db "dzen",0
  bigName db "Your name is very long...", 0

  szString  db "23456789ARTFGNSM", 0
  szFormat  db "%s-%s", 0
  szSrcKey  db "SS7RGRN2", 0
  szBuf   db 10 dup (0)

  tabl    dd 0, 77073096h, ...


  KeyGen proc hWin: HWND

  LOCAL szName[97]:BYTE

      pusha

      invoke  GetDlgItem, hWin, 101
      invoke  GetWindowTextLength, eax
      .if eax > 96
        lea edx, bigName
        jmp errorC
      .elseif eax == 0
        ret
      .endif

      invoke  GetDlgItemText, hWin, 101, addr szName, 96

      xchg  eax, ecx
      lea edx, szName
      xor eax, eax
      call  CRC32

      mov ecx, 8
      xor edx, edx
      lea esi, szBuf
  
  Format:
      movzx edx, al
      and dl, 0fh
      mov dl, [szStringЬ] 
      mov [esi], dl

      .if ecx == 5
        inc esi
        mov byte ptr [esi], 2dh
      .endif

      inc esi
      ror eax, 4  
      loop  Format

      invoke  wsprintf, offset szKey, offset szFormat, offset szBuf, offset szSrcKey
      lea edx, szKey

  errorC:
      invoke  SetDlgItemText, hWin, 102, edx

      popa
      xor eax, eax
      ret


  CRC32:
      push    ebx
  loc_41B96F:
      xor     ebx, ebx
      mov     bl, al
      xor     bl, [edx]
      shr     eax, 8
      xor     eax, dword ptr [ebx*4ж]
      inc     edx
      dec     ecx
      jnz     short loc_41B96F
      pop     ebx

      retn

  KeyGen endp

  ;---------------------------------------------------------------------------------------------------------------------

    Если  чего забыл  -  извиняюсь.  Кусок выдирал из  реального кейгена, поэтому все может быть. Но я думаю должно быть
  понятно.


  3. Cerberus DE 1.1
  ~~~~~~~~~~~~~~~~~~
    Здесь порядок наших  действий не отличается  от предыдущих  исследований. Поиск процедуры  проверки точно  такой же.
  Различия только в адресах и смещениях. При просмотре образа стека нужно искать такую строчку: 

  0012EB04  |00567FEB  RETURN to boostspe.00567FEB from boostspe.00576558

    Т.е. адрес, который нам нужен - это 0x00567FEB. Переходим по нему и находим нужное:

  00567F55   50               PUSH EAX
  00567F56   E8 155FF9FF      CALL boostspe.004FDE70                   ; JMP to cerberus.regVerifyKey
  00567F5B   84C0             TEST AL,AL
  00567F5D   74 6B            JE SHORT boostspe.00567FCA

    Дальнейшие  действия  абсолютно  аналогичны  вышеописанным вплоть до шага  номер 5.  Здесь  талантливые разработчики
  применили недюжие усилия и улучшили процедуру проверки аж целым одним ксором!

  0034CD14   8955 F0          MOV DWORD PTR SS:[EBP-10],EDX            ; edx = сформированный дворд
  0034CD17   3175 F0          XOR DWORD PTR SS:[EBP-10],ESI            ; !!!
  0034CD1A   8D55 F0          LEA EDX,DWORD PTR SS:[EBP-10]
  0034CD1D   B9 04000000      MOV ECX,4
  0034CD22   33C0             XOR EAX,EAX
  0034CD24   E8 FFEBFFFF      CALL cerberus.0034B928                   ; CRC32

    Полет  мысли отмечен тремя  восклицательными знаками. В esi содержится хэш имени пользователя. Кейген также меняется
  незначительно. Все что нам нужно сделать в дополнение к  проделанному уже  -  это добавить точно такой же ксор. Правда
  есть одна  мелочь. Т.к. вторая  часть  ключа  у нас уже не константа, то придется результат ксора перевести в строчку,
  используя 16-символьный массив. Вот измененная процедура кейгена:

  ;---------------------------------------------------------------------------------------------------------------------

  szKey   db KEYSIZE dup(0)
  defname db "dzen",0
  bigName db "Your name is very long...", 0

  szString  db "23456789ARTFGNSM", 0
  szBuf   db 32 dup (0)
  dwKey   dd ?
  tabl    dd 0, 77073096h, ...


  KeyGen proc hWin: HWND

  LOCAL szName[97]:BYTE

      pusha

      invoke  GetDlgItem, hWin, 101
      invoke  GetWindowTextLength, eax
      .if eax > 96
        lea edx, bigName
        jmp errorC
      .elseif eax == 0
        ret
      .endif

      invoke  GetDlgItemText, hWin, 101, addr szName, 96

      xchg  eax, ecx
      lea edx, szName
      xor eax, eax
      call  CRC32

      mov dwKey, 4ada4492h    ; дворд-константа в программе
      xor dwKey, eax      ; нужный ксор

      mov ecx, 8
      push  ecx

      xor edx, edx
      lea esi, szBuf
      xor edi, edi
      call  Format        ; переводим в строку первую часть ключа
  
      mov byte ptr [esi], '-'
      inc esi

      pop ecx
      xor edx, edx
      mov eax, dwKey
      inc edi
      call  Format        ; и вторую часть ключа

      lea edx, szBuf

  errorC:
      invoke  SetDlgItemText, hWin, 102, edx  

      popa
      xor eax, eax
      ret

  ;______

  Format:
      movzx edx, al
      and dl, 0fh
      mov dl, [szStringЬ] 
      mov [esi], dl

      .if ecx == 5
        .if edi == 0
          inc esi
          mov byte ptr [esi], 2dh
        .endif
      .endif

      inc esi
      ror eax, 4  
      loop  Format

      retn

  ;______

  CRC32:
      push    ebx
  loc_41B96F:
      xor     ebx, ebx
      mov     bl, al
      xor     bl, [edx]
      shr     eax, 8
      xor     eax, dword ptr [ebx*4ж]
      inc     edx
      dec     ecx
      jnz     short loc_41B96F
      pop     ebx

  ;locret_41B984:
      retn

  KeyGen endp

  ;---------------------------------------------------------------------------------------------------------------------


  4. Cerberus 2
  ~~~~~~~~~~~~~
    Итак, основная тема  данной статьи. Для начала  расскажу о различиях данной версии защиты и двух предыдущих. Первое,
  что бросается  в глаза  -  это присутствие  в корневом  каталоге программы еще одного файла Cerberus`а - cerberus.ini.
  Если заглянуть внутрь, то можно обнаружить различные настройки как самой программы, так и защиты в целом.Единственное,
  что нам может понадобится здесь  -  это опция под названием  "Hash" и  значением "0EEF" (в самом конце файла). Не буду
  пока что подробно останавливаться на этом, всему свое время.

    Конечно  же,  сразу возникает  желание поменять  строчки  конфига на что-то  свое. Но делать  этого не советую, т.к.
  программа при запуске проверяет данный файл на изменения и отказывается запускаться,  если таковые обнаружатся. Причем
  если вернуть все на место, то это  тоже не поможет =)  Так что прежде  чем экспериментировать,  сохраните cerberus.ini
  где-нибудь в сухом и прохладном месте.

    Изменения  номер два  -  это файл cerberus.dll. Теперь  он ничем не запакован (и это в принципе нам на руку, т.к. не
  нужно  лишний раз  запускать UPX). Также,  если посмотреть  на  экспорты, то  там мы не обнаружим  большинство функций
  предыдущей версии. Теперь их всего семь штук.

    Принцип  работы  защиты так  же  изменился в  значительной степени, хотя сама  логика осталась прежней. Разработчики
  постарались усложнить жизнь возможным исследователям (т.е. нам)и теперь добиться своего будет не так легко как раньше.
  Например,  чтобы найти  процедуры  защиты  -  мало поставить  бряк  на  MessageBoxA. Но  нас это  в общем-то не должно
  останавливать :)

    Но  начнем мы, как  ни странно, все тем же путем, т.е. отлавливая вызов MessageBox`а. Для этого откроем cuckoo.exe в
  Оле  и  после того  как  загрузятся  все  нужные библиотеки,  перейдем  непосредственно  к  cerberus.dll (давим правую
  кнопку -> View  ->  Module 'CERBERUS') и  поставим бряк  на MessageBoxA. Запустим программу по F9, введем левые данные
  (заметьте, что внешний вид изменился - теперь это  диалоговое окно; также  обратите  внимание на  измененную структуру
  ключа) и нажмем "Next". Оля приземлится здесь:

  10002263   50               PUSH EAX
  10002264   6A 00            PUSH 0
  10002266   FF15 94810010    CALL DWORD PTR DS:[<&USER32.MessageBoxA>>; USER32.MessageBoxA  ; <-- мы тут

    И здесь нас  ждет первое разочарование. Вблизи вызова MessageBox`а нет ничего такого, что напоминало бы  проверку на
  правильно введенный ключ. Так же если побродить по стеку и посмотреть на порядок  вызова процедур, то ничего толкового
  тоже  не  обнаружится. А  все дело  в том, что, как  я уже и говорил, внешний вид защиты превратился в диалог, который
  порождается в  функцией StartTryWizard. Можно в общем-то ставить бряки на нее и дальше трейсить до нужного момента, но
  как мне  показалось  -  это не лучший вариант,  т.к. придется пройти  кучу ненужного кода. Так же ничего не выйдет и с
  отловом всевозможных GetDlgItemTextA и GetWindowTextA, т.к. их нет (точнее есть, но к делу они не относятся). Текст из
  полей ввода берется посредством SendMessage.

    Я покажу  вам  тот способ, которым  пользовался сам.  Возможно и  не самый элегантный вариант, но зато действенный и
  проверенный.  В  данный момент  (если конечно вы  ничего больше  не делали) мы находимся на вызове MessageBox, который
  отображает текст о неудачно введенной регистрационной информации.Если прокрутить листинг чуть вверх, то в комментариях
  (а Оля любовно проставляет их для вас) можно увидеть введенный ключ в текстовом виде:

  10002220   F3:A4            REP MOVS BYTE PTR ES:[EDI],BYTE PTR DS:[ESI]
  10002222   BF 8CAD0010      MOV EDI,CERBERUS.1000AD8C                ; ASCII "2222-2222-2222-2222-2222"
  10002227   83C9 FF          OR ECX,FFFFFFFF
  1000222A   F2:AE            REPNE SCAS BYTE PTR ES:[EDI]

    Сделаем следующее: перейдем по адресу 0x1000AD8C и, как ни странно, увидим там наш ключ. Выделим его и поставим бряк
  на запись  в  память по  этому адресу  (жмем правой кнопкой  на  выделенном тексте -> Breakpoint -> Memory, on write).
  Отпустим программу и снова нажмем "Next". Теперь Оля приземлится тут:

  10001D87   6A 0D            PUSH 0D
  10001D89   C1E9 02          SHR ECX,2
  10001D8C   F3:A5            REP MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI]   ; <-- мы здесь
  10001D8E   8BC8             MOV ECX,EAX

    На первый  взгляд может показаться, что  ни к чему толковому это не привело, но скажу вам по секрету, что это и есть
  нужная  нам  процедура ;) Удалим поставленный бряк ('md' в  командной  строке)  и  начнем разбираться в том,  куда нас
  занесло.

    Начиная с  адреса 0x10001D4F  и вплоть до 0x10001F27 считываются текстовые поля ввода и  формируется ключ в памяти в
  формате  "XXXX-XXXX-XXXX-XXXX-XXXX".  Далее, с  0x10001F30  по  0x10001F78 проверяется  принадлежность  символов ключа
  промежутку '0..9', 'A..F', т.е. легко сделать вывод о том, что каждая  четырехзначная часть ключа  будет в последствии
  переводиться программой в word и использоваться.

    Все последующие действия завязаны друг на друге, поэтому чтобы не запутать себя и читателя, я приведу алгоритм того,
  что происходит внутри исследуемой программы:

  1) Получение хэша на основе введенного имени
  2) Получение хэша части ключа (без первого "XXXX-")
  3) xor полученных на первом и втором шаге значений
  4) Перевод первой части ключа ("XXXX-" без '-') в ворд
  5) Изменение конечной части ключа (все без того же "XXXX-") на основе хэша имени
  6) Получение хэша измененной части ключа
  7) Сравнение значения шага 7 с константой и установка флага
  8) Сравнение значений шага номер 3 и 4
  9) Проверка установленного флага на шаге номер 7

    Шаг номер один начинается по адресу 0x10001F89 и выглядит следующим образом:

  10001F89   51               PUSH ECX                                             ; длина
  10001F8A   50               PUSH EAX
  10001F8B   68 A8AD0010      PUSH CERBERUS.1000ADA8                               ; имя
  10001F90   E8 6B1D0000      CALL CERBERUS.10003D00

    Как  и в  предыдущих версиях Cerberus,  код получения хэша  един для всей процедуры проверки регистрационных данных.
  Единственное большое отличие здесь в том, что хэш есть НЕ дворд как раньше, а только лишь ворд и используемый алгоритм
  не CRC32, а CRC16. Выглядит он так:

  10003D00   56               PUSH ESI
  10003D01   8B7424 10        MOV ESI,DWORD PTR SS:[ESP+10]                        ; кол-во байт
  10003D05   33C9             XOR ECX,ECX
  10003D07   85F6             TEST ESI,ESI
  10003D09   7E 31            JLE SHORT CERBERUS.10003D3C
  10003D0B   8B4424 0C        MOV EAX,DWORD PTR SS:[ESP+C]                         ; начальное значение
  10003D0F   53               PUSH EBX
  10003D10   57               PUSH EDI
  10003D11   8B7C24 10        MOV EDI,DWORD PTR SS:[ESP+10]                        ; с чего получать хэш
  10003D15   33D2             XOR EDX,EDX
  10003D17   8BD8             MOV EBX,EAX
  10003D19   8A1439           MOV DL,BYTE PTR DS:[ECX+EDI]
  10003D1C   81E3 FF000000    AND EBX,0FF
  10003D22   33D3             XOR EDX,EBX
  10003D24   33DB             XOR EBX,EBX
  10003D26   8ADC             MOV BL,AH
  10003D28   66:8B0455 BCA10010    MOV AX,WORD PTR DS:[EDX*2+1000A1BC]             ; таблица
  10003D30   66:33C3          XOR AX,BX
  10003D33   41               INC ECX
  10003D34   3BCE             CMP ECX,ESI
  10003D36  ^7C DD            JL SHORT CERBERUS.10003D15
  10003D38   5F               POP EDI
  10003D39   5B               POP EBX
  10003D3A   5E               POP ESI
  10003D3B   C3               RETN                                                 ; результат в ax
  10003D3C   66:8B4424 0C     MOV AX,WORD PTR SS:[ESP+C]
  10003D41   5E               POP ESI
  10003D42   C3               RETN

    Размер используемого массива как и раньше состоит из 256 элементов,только вот размер каждого элемента сменился с 4-х
  байт на 2. Да  и  вообще, весь алгоритм проверки и генерации больше оперирует вордами, нежели двордами. Но это не суть
  важно.

    Шаг номер 2 и 3 подробно описывать не буду, т.к. там и так все довольно банально. Программа  передвигает указатель в
  ключе  на  нужное место  (вторая его  часть)  и  вызывает всю  ту же процедуру по адресу 0x10003D00. Ну и затем ксорит
  предыдущие вычисления,  оставляя  результат в ebx.  Перевод части  ключа в ворд  также занятие не  очень сложное. Весь
  процесс состоит  из четырех  вызовов функции,  которая преобразует символ  в число, и сохранение результата в регистре
  (в esi в данном случае).

    И вот, быстро рассмотрев частности, переходим к сути :) Самое интересное начинается здесь:

  1000200E   E8 BDFAFFFF           CALL CERBERUS.10001AD0

    Данная  процедура  реализует шаги с 5  по  7. На основе  вновь  подсчитанного хэша  имени она изменяет  вторую часть
  введенного ключа и работает дальше с ней. Преобразование - это  просто замена символа на новый. Причем стоит отметить,
  что данная функция обратна сама себе,т.е. например если ввести ключ "2222-2222-2222-2222-2222", то последняя его часть
  будет   преобразована  в  "7777-7777-BBBB-6666".  Но  если  в  качестве  ключа  использовать  полученную  строку, т.е.
  "2222-7777-7777-BBBB-6666", то в результате  получим исходное, т.е. "2222-2222-2222-2222". Такие вот фокусы :)  И если
  посмотреть дальше и найти строки,где полученный хэш ключа сравнивается с константой (шаг 7), то все становится на свои
  места. Для непонятливых разъясняю: в программу исходно зашит хэш некой строки, которую  выдумал автор. Кстати,  именно
  это значение и присутствует в конфиге cerberus.ini ("0EEF"). Обрабатывая вторую часть ключа, защита должна привести ее
  (часть) в изначальный вид, т.е в строку, хэш которой равен 0x0EEF. Да да, и  снова нам потребуется написание брутфорса
  чтобы эту самую строчку обнаружить и использовать в кейгене.Такой перебор, по сравнению с предыдущими двумя, процедура
  долгая. Но авторы и здесь позаботились о нас. Почему? Читайте дальше ;)

    Чтобы больше не возвращаться к этому, опишу оставшиеся действия программы. Код шага номер 5 я приводить не буду, его
    (правда  слегка  в измененном  виде)  вы  увидите  в брутфорсе.  Получение хэша  преобразованного  ключа  в точности
    соответствует получению хэша имени (процедура по адресу 0x10003D00). И шаг 7 - собственно само сравнение результатов
    и установка флага. Выглядит все это так:

  10001BF1   66:3B05 40C10010      CMP AX,WORD PTR DS:[1000C140]                   ; сравнение с константой
  [...]
  10001BFA   0F94C1                SETE CL                                         ; установка флага
  10001BFD   5D                    POP EBP
  10001BFE   8AC1                  MOV AL,CL                                       ; результат (флаг) в al
  [...]
  10001C02   C3                    RETN

    И сразу после возвращения из функции проверяются результаты всей проделанной работы:

  10002013   3BDE                  CMP EBX,ESI                                     ; шаг 3 и 4
  10002015   A2 84C70010           MOV BYTE PTR DS:[1000C784],AL
  1000201A   0F85 2F020000         JNZ CERBERUS.1000224F
  10002020   84C0                  TEST AL,AL                                      ; шаг 7
  10002022   0F84 27020000         JE CERBERUS.1000224F

    Привожу код брутфорса, который писался поздней ночью (а точнее ранним утром). Оптимизировать  его у меня не  было ни
  сил,  ни желания,  поэтому за стиль  прошу  не кидать в  меня тапками. И в данной ситуации это не очень критично, т.к.
  нужная нам строка находится практически сразу же.

  ;---------------------------------------------------------------------------------------------------------------------

  .386
  .model flat, stdcall
  option casemap :none

  include c:\masm32\include\windows.inc
  include c:\masm32\include\kernel32.inc
  include c:\masm32\include\user32.inc

  includelib c:\masm32\lib\kernel32.lib
  includelib c:\masm32\lib\user32.lib

  .data
    o_fmt db '%.4X-%.4X-%.4X-%.4X', 0                ; формат ключа для wsprintf
    n1  dw 0
    n2  dw 0
    n3  dw 0
    n4  dw 0
    buf db 24 dup (0)
                                                                   ; массив из 256 полиномов
    tabl  dw 0, 0C0C1h, 0C181h, 140h, 0C301h, 3C0h, 280h, 0C241h
  dw 0C601h, 6C0h, 780h, 0C741h, 500h, 0C5C1h, 0C481h, 440h
  dw 0CC01h, 0CC0h, 0D80h, 0CD41h, 0F00h, 0CFC1h, 0CE81h
  dw 0E40h, 0A00h, 0CAC1h, 0CB81h, 0B40h, 0C901h, 9C0h, 880h
  dw 0C841h, 0D801h, 18C0h, 1980h, 0D941h, 1B00h, 0DBC1h
  dw 0DA81h, 1A40h, 1E00h, 0DEC1h, 0DF81h, 1F40h, 0DD01h
  dw 1DC0h, 1C80h, 0DC41h, 1400h, 0D4C1h, 0D581h, 1540h
  dw 0D701h, 17C0h, 1680h, 0D641h, 0D201h, 12C0h, 1380h
  dw 0D341h, 1100h, 0D1C1h, 0D081h, 1040h, 0F001h, 30C0h
  dw 3180h, 0F141h, 3300h, 0F3C1h, 0F281h, 3240h, 3600h
  dw 0F6C1h, 0F781h, 3740h, 0F501h, 35C0h, 3480h, 0F441h
  dw 3C00h, 0FCC1h, 0FD81h, 3D40h, 0FF01h, 3FC0h, 3E80h
  dw 0FE41h, 0FA01h, 3AC0h, 3B80h, 0FB41h, 3900h, 0F9C1h
  dw 0F881h, 3840h, 2800h, 0E8C1h, 0E981h, 2940h, 0EB01h
  dw 2BC0h, 2A80h, 0EA41h, 0EE01h, 2EC0h, 2F80h, 0EF41h
  dw 2D00h, 0EDC1h, 0EC81h, 2C40h, 0E401h, 24C0h, 2580h
  dw 0E541h, 2700h, 0E7C1h, 0E681h, 2640h, 2200h, 0E2C1h
  dw 0E381h, 2340h, 0E101h, 21C0h, 2080h, 0E041h, 0A001h
  dw 60C0h, 6180h, 0A141h, 6300h, 0A3C1h, 0A281h, 6240h
  dw 6600h, 0A6C1h, 0A781h, 6740h, 0A501h, 65C0h, 6480h
  dw 0A441h, 6C00h, 0ACC1h, 0AD81h, 6D40h, 0AF01h, 6FC0h
  dw 6E80h, 0AE41h, 0AA01h, 6AC0h, 6B80h, 0AB41h, 6900h
  dw 0A9C1h, 0A881h, 6840h, 7800h, 0B8C1h, 0B981h, 7940h
  dw 0BB01h, 7BC0h, 7A80h, 0BA41h, 0BE01h, 7EC0h, 7F80h
  dw 0BF41h, 7D00h, 0BDC1h, 0BC81h, 7C40h, 0B401h, 74C0h
  dw 7580h, 0B541h, 7700h, 0B7C1h, 0B681h, 7640h, 7200h
  dw 0B2C1h, 0B381h, 7340h, 0B101h, 71C0h, 7080h, 0B041h
  dw 5000h, 90C1h, 9181h, 5140h, 9301h, 53C0h, 5280h, 9241h
  dw 9601h, 56C0h, 5780h, 9741h, 5500h, 95C1h, 9481h, 5440h
  dw 9C01h, 5CC0h, 5D80h, 9D41h, 5F00h, 9FC1h, 9E81h, 5E40h
  dw 5A00h, 9AC1h, 9B81h, 5B40h, 9901h, 59C0h, 5880h, 9841h
  dw 8801h, 48C0h, 4980h, 8941h, 4B00h, 8BC1h, 8A81h, 4A40h
  dw 4E00h, 8EC1h, 8F81h, 4F40h, 8D01h, 4DC0h, 4C80h, 8C41h
  dw 4400h, 84C1h, 8581h, 4540h, 8701h, 47C0h, 4680h, 8641h
  dw 8201h, 42C0h, 4380h, 8341h, 4100h, 81C1h, 8081h, 4040h

  .code
  start:
      movzx eax, n4
      push  eax
      movzx eax, n3
      push  eax
      movzx eax, n2
      push  eax
      movzx eax, n2
      push  eax
      push  offset o_fmt
      push  offset buf
      call  wsprintf
      add esp, 18h

      inc n4
      .if n4 == 0
        inc n3
        .if n3 == 0
          inc n2
          .if n2 == 0
            inc  n1
            .if n1 == 0
              jmp _exit
            .endif
          .endif
        .endif
      .endif

      push  13h
      push  0
      push  offset buf
      call  CRC16
      add esp, 0ch

      cmp ax, 0eefh
      jz  YEAH
      
      jmp start

  YEAH:
      invoke  MessageBox, NULL, offset buf, NULL, 0

  _exit:
      invoke  ExitProcess, 0


  ; Генерация хэша
  CRC16:
      push    esi
      mov     esi, [esp+10h]
      xor     ecx, ecx
      test    esi, esi
      jle loc_10003D3C
      mov     eax, [esp+0ch]
      push    ebx
      push    edi
      mov     edi, [esp+10h]

  loc_10003D15:                           ; CODE XREF: sub_10003D00+36j
      xor     edx, edx
      mov     ebx, eax
      mov     dl, byte ptr [ecxШ]
      and     ebx, 0FFh
      xor     edx, ebx
      xor     ebx, ebx
      mov     bl, ah
      mov     ax, word ptr [edx*2ж]
      xor     ax, bx
      inc     ecx
      cmp     ecx, esi
      jl      short loc_10003D15

      pop     edi
      pop     ebx
      pop     esi
      retn       

  loc_10003D3C:
      mov ax, word ptr [espИ]
      pop esi
      ret

  end start
  end

  ;---------------------------------------------------------------------------------------------------------------------

    После старта желанный messagebox  появляется через секунду (у  меня) со строкой  "0000-0000-000B-1049".  Ну вот... и
  теперь действительно все =) Данную защиту можно считать пройденной и изученной.Дальнейшие похожие вещи не должны будут
  останавливать ваше победное шествие. Так. Нужно завязывать, а то что-то меня уже понесло.

    Если вы читали  внимательно и вникали  в суть дела,  то написание кейгена не должно представлять каких-либо проблем.
  Единственное его отличие от алгоритма защиты - это первая часть ключа. Ее вы  должны сформировать сами на основе имени
  пользователя. Изменение же второй части в точности похоже на описанное,т.к., как я уже и говорил, функция обратна сама
  себе. Вот кусок, выдранный из реального кейгена:

  ;---------------------------------------------------------------------------------------------------------------------

  defname db "dzen",0
  bigName db "Your name is very long...", 0
  shortName db "Name must be at least 4 characters", 0

  _srcSN  db "0000-0000-000B-1049", 0
  srcSN   db 22 dup (0)
  szFormat  db "%.04X-%s", 0

  srcSN_1 equ srcSN
  srcSN_2 equ srcSN+5
  srcSN_3 equ srcSN+10
  srcSN_4 equ srcSN+15

  bHZ   db 0
  tabl    dw 0, ...


  KeyGen proc hWin: HWND

  LOCAL szName[97]:BYTE
  LOCAL dwNameLen:DWORD
  LOCAL dwNameHash:DWORD

      pusha

      invoke  GetDlgItem, hWin, 101
      invoke  GetWindowTextLength, eax
      .if eax > 96
        mov edx, offset bigName
        jmp errorC
      .elseif eax < 4
        mov edx, offset shortName
        jmp errorC      
      .endif

      invoke  GetDlgItemText, hWin, 101, addr szName, 96
      mov dwNameLen, eax

      invoke  lstrcpy, offset srcSN, offset _srcSN ; восстанавливаем исходные данные, если
                                                               ; процедура вызывается не в первый раз

      push  dwNameLen
      push  0
      lea eax, szName
      push  eax
      call  CRC16
      add esp, 0ch

      movzx ecx, ax
      mov dwNameHash, ecx

                                                              ; нижележащий код в основном рипнут из
                                                              ; cerberus.dll
      mov bx, ax
      mov dl, al
      xor ecx, ecx

      and     al, 0Fh
      mov     cl, ah
      mov bHZ, al
      and cl, 0fh
      shr     dl, 4
      shr     bx, 0Ch
      and     dl, 0Fh

      push  ebp

      movsx   edi, bl
      xor     esi, esi
      movsx   ebx, cl
      movsx   ebp, dl

  loc_10001B58:                           ; CODE XREF: sub_10001AD0+108j
      mov     cl, byte ptr [esi+srcSN]
      push  ecx                                 ; в винде тоже есть функция, которая переводит
                                                              ; символ в число... :) это девелоперам защиты
      call  ChrToInt                                            
      movsx   edx, al
      xor     edx, edi
      mov     al, [tabl2+edx]
      mov [esi+srcSN_1], al                   ; сохраняем промежуточные значения, чтобы потом
                                                              ; все склеить

      mov cl, [esi·_2]
      push  ecx
      call  ChrToInt
      movsx   edx, al
      xor     edx, ebx
      mov al, [tabl2Ь]
      mov [esi·_2], al

      mov cl, [esi·_3]
      push  ecx
      call  ChrToInt
      movsx   edx, al
      xor     edx, ebp
      mov al, [tabl2Ь]
      mov [esi·_3], al

      mov cl, [esi·_4]
      push  ecx
      call  ChrToInt
      movsx   edx, al
      movsx eax, bHZ
      xor     edx, eax

      inc esi
      mov al, [tabl2Ь]
      cmp esi, 4
      mov [esi·_4-1], al
      jl  loc_10001B58

      pop ebp

      push  13h
      push  0
      push  offset srcSN
      call  CRC16
      add esp, 0ch

      movzx eax, ax
      mov ebx, dwNameHash
      xor ebx, eax                            ; ксорим хэш имени с хэшем второй части
                                                              ; ключа (шаг 3)
  
      invoke  wsprintf, offset szKey, offset szFormat, ebx, offset srcSN ; склеиваем все вместе
      lea edx, szKey

  errorC:
      invoke  SetDlgItemText, hWin, 102, edx  

      popa
      xor eax, eax
      ret


  CRC16:
      push    esi
      mov     esi, [espH]
      xor     ecx, ecx
      test    esi, esi
      jle loc_10003D3C
      mov     eax, [espИ]
      push    ebx
      push    edi
      mov     edi, [espH]

  loc_10003D15:                           ; CODE XREF: sub_10003D00+36j
      xor     edx, edx
      mov     ebx, eax
      mov     dl, byte ptr [ecxШ]
      and     ebx, 0FFh
      xor     edx, ebx
      xor     ebx, ebx
      mov     bl, ah
      mov     ax, word ptr [edx*2ж]
      xor     ax, bx
      inc     ecx
      cmp     ecx, esi
      jl      short loc_10003D15

      pop     edi
      pop     ebx
      pop     esi
      retn       

  loc_10003D3C:
      mov ax, word ptr [espИ]
      pop esi
      ret

  KeyGen endp

  ;---------------------------------------------------------------------------------------------------------------------

    Такие вот дела. Я бы не сказал, что очень уж сложная защита, но лично мне было интересно в ней копаться. И я  все же
  настаиваю вам последовать совету, который давал в  начале статьи. Поковырять ее  самим. Но раз уж вы дочитали до этого
  места, то толку от этого будет 0. Хотя кто знает ;)

  happy reversing!

  ЗЫ. Все версии Cerberus`а хранят правильно введенные регистрационные данные тут: HKEY_CURRENT_USER\Software\Cerberus.

  ЗЫ2. big thx to GL#0M & dMNt.

  dzen, 03.03.05-05.03.05.