
Скриншот программы WinCHM
WinCHM - небольшой по размерам, но сильный по возможностям редактор для создания файлов справок в формате CHM, HLP а также PDF- и DOC-файлов. Про все возможности этой замечательной программы во можете почитать на офсайте, и там же вдруг обнаружится ссылка на страницу покупки. Туда ходить не надо, обойдемся своими силами.
Первым делом качаем
дистрибутив, устанавливаем, запускаем. Сразу получаем триальное окно:

Триальное окно
Окей, введем какой-нибудь левак в качестве регистрационного имени и номера. Получаем сообщение "
Illegal registration code!". Начальные данные для анализа у нас есть, переходим к стадии кодокопания. Файл ничем не упакован, поэтому сразу загоним его в дизассемблер, а пока тот работает, поищем нехорошую строчку.

Нехорошая строка найдена
Если приглядеться повнимательнее, то там же рядышком есть сообщение об успешной регистрации, а также какие-то интересные строки типа названий ключей реестра для хранения регистрационных данных, строки с вариантами лицензий программы и т.п. В дизассемблере смотрим откуда это все добро запрашивается:
...
CODE:0051017E mov eax, ebx
CODE:00510180 call sub_403574
CODE:00510185 call sub_50FB80
; Проверить, если DWORD по адресу 54B184 имеет не нулевое значение,
; то программа считается зарегистрированной
CODE:0051018A mov eax, ds:off_54B184
CODE:0051018F cmp dword ptr [eax], 0
CODE:00510192 jz short loc_5101EB
; Сообщение об удачной регистрации
CODE:00510194 mov ecx, ds:off_54B184
CODE:0051019A mov ecx, [ecx]
CODE:0051019C lea eax, [ebp+var_4]
CODE:0051019F mov edx, offset aThankYouForPur
; "Thank you for purchasing our product!\r\n"...
CODE:005101A4 call sub_404670
CODE:005101A9 mov eax, ds:off_54B4E8
CODE:005101AE cmp dword ptr [eax], 1
CODE:005101B1 jnz short loc_5101CF
CODE:005101B3 push [ebp+var_4]
CODE:005101B6 push offset dword_5102B8
CODE:005101BB push offset aVersionWinchmP
; "Version: WinCHM Pro"
CODE:005101C0 lea eax, [ebp+var_4]
CODE:005101C3 mov edx, 3
CODE:005101C8 call sub_4046E4
CODE:005101CD jmp short loc_5101F8
CODE:005101CF ; ------------------------------------------------------
CODE:005101CF loc_5101CF:
CODE:005101CF push [ebp+var_4]
CODE:005101D2 push offset dword_5102B8
CODE:005101D7 push offset aVersionWinchmS
; "Version: WinCHM (Standard)"
CODE:005101DC lea eax, [ebp+var_4]
CODE:005101DF mov edx, 3
CODE:005101E4 call sub_4046E4
CODE:005101E9 jmp short loc_5101F8
CODE:005101EB ; ------------------------------------------------------
; Сообщение о некорректной регистрации
CODE:005101EB loc_5101EB:
CODE:005101EB lea eax, [ebp+var_4]
CODE:005101EE mov edx, offset aIllegalRegistr
; Указатель на строку "Illegal registration code!"
CODE:005101F3 call sub_4043E0
...
Тут вроде все понятно. Если DWORD по адресу 54B184 имеет нулевое значение, то программа работает в триальном режиме. Теперь надо найти в коде то место, где эта ячейка памяти инициализируется правильным ненулевым значением. Дизассемблер показывает, что ссылок на эту ячейку памяти очень много, и практически все они так или иначе связаны с проверкой ее значения. Все, кроме одного блока:
...
CODE:0050FC75 mov eax, [ebp+var_4]
CODE:0050FC78 call sub_403574
; Загрузить в регистр EDX ссылку на регистрационный код из реестра
CODE:0050FC7D mov edx, ds:off_54B33C
CODE:0050FC83 mov edx, [edx]
CODE:0050FC85 mov eax, ds:off_54B5E8
; Загрузить в регистр EAX ссылку на регистрационное имя из реестра
CODE:0050FC8A mov eax, [eax]
; Передать результаты в какую-то функцию проверки
CODE:0050FC8C call sub_50F3B4
; Результат проверки вернулся в регистре EAX
CODE:0050FC91 mov ebx, eax
CODE:0050FC93 mov eax, ebx
; Если EAX был = 0, то выполнится следующий условный переход
CODE:0050FC95 sub eax, 1
CODE:0050FC98 jb short loc_50FCA5
; Если EAX было = 1, то будет переход на Single-user License
CODE:0050FC9A jz short loc_50FCB1
; Если EAX было = 270Eh, то переход на Unlimited-user License
CODE:0050FC9C sub eax, 270Eh
CODE:0050FCA1 jz short loc_50FCC2
; Иначе переход на NN-user License
CODE:0050FCA3 jmp short loc_50FCD3
CODE:0050FCA5 ; -------------------------------------------------
CODE:0050FCA5 loc_50FCA5:
CODE:0050FCA5 mov eax, ds:off_54B184
CODE:0050FCAA call sub_404348
CODE:0050FCAF jmp short loc_50FCEF
CODE:0050FCB1 ; -------------------------------------------------
CODE:0050FCB1 loc_50FCB1:
CODE:0050FCB1 mov eax, ds:off_54B184
CODE:0050FCB6 mov edx, offset aSingleUserLice
; "Single-user License"
CODE:0050FCBB call sub_40439C
CODE:0050FCC0 jmp short loc_50FCEF
CODE:0050FCC2 ; -------------------------------------------------
CODE:0050FCC2 loc_50FCC2:
CODE:0050FCC2 mov eax, ds:off_54B184
CODE:0050FCC7 mov edx, offset aUnlimitedUserL
; "Unlimited-user License"
CODE:0050FCCC call sub_40439C
CODE:0050FCD1 jmp short loc_50FCEF
CODE:0050FCD3 ; -------------------------------------------------
CODE:0050FCD3 loc_50FCD3:
CODE:0050FCD3 lea edx, [ebp+var_18]
CODE:0050FCD6 mov eax, ebx
CODE:0050FCD8 call sub_4094E8
CODE:0050FCDD mov edx, [ebp+var_18]
CODE:0050FCE0 mov eax, ds:off_54B184
CODE:0050FCE5 mov ecx, offset aUserLicense
; "-user License"
CODE:0050FCEA call sub_404670
CODE:0050FCEF loc_50FCEF:
CODE:0050FCEF mov eax, ds:off_54B184
CODE:0050FCF4 cmp dword ptr [eax], 0
CODE:0050FCF7 jnz short loc_50FD0A
CODE:0050FCF9 mov eax, ds:off_54B3B4
CODE:0050FCFE mov edx, offset aUnregistered
; " [Unregistered]"
CODE:0050FD03 call sub_40439C
CODE:0050FD08 jmp short loc_50FD14
...
Интересного много, но логика работы не совсем понятна. Попробуем под отладчиком поставить точку остановка на адрес 0050FC75 и пройти код в пошаговом режиме. Я прокомментировал приведенный выше код на основании данных из отладчика, обратите внимание на условные переходы по адресу 0050FC93 и дальше. Ситуация потихоньку проясняется. Получается, что регистрационные данные передаются в процедуру по адресу 50F3B4, если она вернула ненулевое значение, то программа считается зарегистрированной. Возвращаемся к дизассемблеру:
CODE:0050F3B4 push ebp
CODE:0050F3B5 mov ebp, esp
CODE:0050F3B7 mov ecx, 4
CODE:0050F3BC loc_50F3BC:
CODE:0050F3BC push 0
CODE:0050F3BE push 0
CODE:0050F3C0 dec ecx
CODE:0050F3C1 jnz short loc_50F3BC
CODE:0050F3C3 push ecx
CODE:0050F3C4 push ebx
CODE:0050F3C5 mov [ebp+var_8], edx
CODE:0050F3C8 mov [ebp+var_4], eax
CODE:0050F3CB mov eax, [ebp+var_4]
CODE:0050F3CE call sub_404814
CODE:0050F3D3 mov eax, [ebp+var_8]
CODE:0050F3D6 call sub_404814
CODE:0050F3DB xor eax, eax
CODE:0050F3DD push ebp
CODE:0050F3DE push offset loc_50F9B9
CODE:0050F3E3 push dword ptr fs:[eax]
CODE:0050F3E6 mov fs:[eax], esp
CODE:0050F3E9 lea eax, [ebp+var_14]
CODE:0050F3EC mov edx, [ebp+var_8]
CODE:0050F3EF call sub_4043E0
CODE:0050F3F4 mov eax, [ebp+var_14]
CODE:0050F3F7 call sub_404624
CODE:0050F3FC mov ebx, eax
CODE:0050F3FE mov eax, ds:off_54B4E8
CODE:0050F403 mov dword ptr [eax], 0FFFFFFFFh
; В регистр EAX занести ссылку на серийный номер
CODE:0050F409 mov eax, [ebp+var_14]
; Вызвать очередную функцию проверки
CODE:0050F40C call sub_50EBD0
; Результат функции возвращается в регистрах EDX и EAX
CODE:0050F411 mov [ebp+var_10], eax
CODE:0050F414 mov [ebp+var_C], edx
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F417 cmp [ebp+var_C], 0
CODE:0050F41B jnz short loc_50F430
; Если EAX=2B77h, то результат всей функции будет 1
CODE:0050F41D cmp [ebp+var_10], 2B77h
CODE:0050F424 jnz short loc_50F430
CODE:0050F426 mov ebx, 1
CODE:0050F42B jmp loc_50F991
CODE:0050F430 ; ----------------------------------------------
CODE:0050F430 loc_50F430:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F430 cmp [ebp+var_C], 0
CODE:0050F434 jnz short loc_50F449
; Если EAX=2B81h, то результат всей функции будет 2
CODE:0050F436 cmp [ebp+var_10], 2B81h
CODE:0050F43D jnz short loc_50F449
CODE:0050F43F mov ebx, 2
CODE:0050F444 jmp loc_50F991
CODE:0050F449 ; ----------------------------------------------
CODE:0050F449 loc_50F449:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F449 cmp [ebp+var_C], 0
CODE:0050F44D jnz short loc_50F462
; Если EAX=2EF2h, то результат всей функции будет 3
CODE:0050F44F cmp [ebp+var_10], 2EF2h
CODE:0050F456 jnz short loc_50F462
CODE:0050F458 mov ebx, 3
CODE:0050F45D jmp loc_50F991
CODE:0050F462 ; ----------------------------------------------
CODE:0050F462 loc_50F462:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F462 cmp [ebp+var_C], 0
CODE:0050F466 jnz short loc_50F47B
; Если EAX=2FA7h, то результат всей функции будет 5
CODE:0050F468 cmp [ebp+var_10], 2FA7h
CODE:0050F46F jnz short loc_50F47B
CODE:0050F471 mov ebx, 5
CODE:0050F476 jmp loc_50F991
CODE:0050F47B ; ----------------------------------------------
CODE:0050F47B loc_50F47B:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F47B cmp [ebp+var_C], 0
CODE:0050F47F jnz short loc_50F494
; Если EAX=2D80h, то результат всей функции будет 10
CODE:0050F481 cmp [ebp+var_10], 2D80h
CODE:0050F488 jnz short loc_50F494
CODE:0050F48A mov ebx, 0Ah
CODE:0050F48F jmp loc_50F991
CODE:0050F494 ; ----------------------------------------------
CODE:0050F494 loc_50F494:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F494 cmp [ebp+var_C], 0
CODE:0050F498 jnz short loc_50F4AD
; Если EAX=2CE2h, то результат всей функции будет 20
CODE:0050F49A cmp [ebp+var_10], 2CE2h
CODE:0050F4A1 jnz short loc_50F4AD
CODE:0050F4A3 mov ebx, 14h
CODE:0050F4A8 jmp loc_50F991
CODE:0050F4AD ; ----------------------------------------------
CODE:0050F4AD loc_50F4AD:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F4AD cmp [ebp+var_C], 0
CODE:0050F4B1 jnz short loc_50F4C6
; Если EAX=388Ah, то результат всей функции будет 50
CODE:0050F4B3 cmp [ebp+var_10], 388Ah
CODE:0050F4BA jnz short loc_50F4C6
CODE:0050F4BC mov ebx, 32h
CODE:0050F4C1 jmp loc_50F991
CODE:0050F4C6 ; ----------------------------------------------
CODE:0050F4C6 loc_50F4C6:
; Если EDX не равен 0, то перейти к следующей проверке
CODE:0050F4C6 cmp [ebp+var_C], 0
CODE:0050F4CA jnz short loc_50F4DF
; Если EAX=25C1h, то результат всей функции будет 270Fh
CODE:0050F4CC cmp [ebp+var_10], 25C1h
CODE:0050F4D3 jnz short loc_50F4DF
CODE:0050F4D5 mov ebx, 270Fh
CODE:0050F4DA jmp loc_50F991
...
Что мы имеем? На основе серийного номера проводятся расчеты, и, в зависимости от этого результата, возвращается количество лицензий. Как видно из кода, их может быть 1, 2, 3, 5, 10, 20, 50 и Unlimited. Осталось выяснить какие же это расчеты. Идем обратно в отладчик, при этом ставим точку останова на вызов функции 50EBD0. Расчет заветного числа выполняется следующим образом:
...
; Счетчик символов
CODE:0050EC1C mov ecx, 1
CODE:0050EC21 loc_50EC21:
; Указатель на серийный номер
CODE:0050EC21 mov eax, [ebp+var_14]
; Записать в EAX символ из серийного номера на позиции ECX
CODE:0050EC24 movzx eax, byte ptr [eax+ecx-1]
; Умножить его код на значение ECX
CODE:0050EC29 imul ecx
CODE:0050EC2B cdq
; Прибавить к счетчику
CODE:0050EC2C add eax, [ebp+var_20]
CODE:0050EC2F adc edx, [ebp+var_1C]
CODE:0050EC32 mov [ebp+var_20], eax
CODE:0050EC35 mov [ebp+var_1C], edx
; Взять следующий символ
CODE:0050EC38 inc ecx
CODE:0050EC39 dec ebx
; Достигнут конец серийника?
CODE:0050EC3A jnz short loc_50EC21
...
Ну вот и настал момент истины, мы добрались до алгоритма проверки серийного номера. Регистрационное имя любое, лишь бы не пустое, длина серийника тоже произвольная. Основная проверка заключается в следующем: код символа умножается на его позицию, причем счетчик начинается с 1. Результаты всех умножений суммируются. Затем проверяется итоговая сумма, она должна быть одним из следующих значений: 2B77h - 1 лицензия, 2B81h - 2 лицензии, 2EF2h - 3 лицензии, 2FA7h - 5 лицензий, 2D80h - 10 лицензий, 2CE2h - 20 лицензий, 388Ah - 50 лицензий, 25C1h - неограниченное количество лицензий. Например, валидным серийником будет строка MTXSMZTGYKSNFWQ. Судя по варезным релизам, константы в некоторых версиях меняются, но суть алгоритма остается неизменной. Кейген под любой тип лицензий пишется на основе простейшего брутфорса, теперь вы можете сделать это самостоятельно. Очередная защита повержена, спасибо автору за программу и за интересную головоломку.