Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 12 ноября!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

KeyGen для Crazy Minesweeper 2.0

Обсудить статью на форуме

Очень удобно, когда все крэкерские инструменты, книги и статьи в одном месте. Используйте сборник от EXELAB - вот тут.

Автор: Godness <godness@omen.ru>

Данный материал опубликован ТОЛЬКО для образовательных целей. Ответственность за незаконное использование данной информации несете ТОЛЬКО Вы сами. Если Вам понравился данный программный продукт – купите его !!!

Объект исследования: программа Crazy Minesweeper v2.0 (Rus)
Инструменты:
сосулька (под '98)
fasm для keygen-а
мозги?… хм-м… – да на кой ваще они нада…
Цель: угадай с трех раз…

Как-то этой зимой мой друг Oldman (крутой квэйкер… с рельсы валит, шо зверь – просто жуть(!), но я его делаю… – иногда :) попросил меня посмотреть эту прогу. Ну я тогда только начинал заниматься реверсингом, поэтому результатов особых не получил и оставил эту прогу до лучших времен, но обещания не забыл… И вот, где-то к лету на одном из форумов, мной часто посещаемых, стали обсуждать эту тему… Ничего конструктивного достигнуто не было, однако я вспомнил, что обещал. Автор статьи на тот момент поломал всего лишь три реальных проги, потому опытным взломщиком не является (… и не особо стремится им быть), а посему если кому-то покажется что статья получилась слишком кривая, то прошу ногами не пинать :). Сначала я решил, что просто “погоняю понты” на форуме и выложу материал там, однако написать раз в жизни статью мне показалось гораздо интереснее…

Итак, что мы имеем поиметь… Crazy Minesweeper – прикольная переделка стандартного сапера, поставляемого вместе с виндой. Скачать можно на www.astatix.com/rus. Зарегистрированным пользователям становятся доступными два новых уровня – “Эксперт” и “Другой”, где можно установить гигантское минное поле на весь экран 8)… и че-то еще… Достаю из ящика кучу какой-то бумаги и с трудом нахожу в них те два листика, которые я исписал зимой, ковыряя прогу. Ну и че там? А, вспомнил – несчастные попытки распаковать прогу… Прога упакована каким-то petite, так называются все секции. Только поставишь бряк bpmd тут-же при загрузке кричит, что CRC сумма не совпала. Что за антиотладочный прием я так и не понял… Ну, и черт с ней… Забил я на распаковку и решил пойти в лоб, обратно по пройденному. Захожу в регистрацию, набираю

имя: godness
e-mail: coolness
код: 0123456789

Кстати упаковщик определяет присутствие сосульки, поэтому предварительно запустите IceDump. Жму OK. Опа-па… хочет перезапустится для завершения регистрации – хм… ерунда! Наверно прога уже проверила правильность ключа, а щас просто морочит голову. Перезапустив ee, обратно ввожу те же данные. Ставлю стандартные бряки GetWindowTextA, GetDlgItemTextA и конечно hmemcpy. Жму OK. Как и ожидал срабатывает последний бряк. Находимся в 16-разрядном коде, жмем F12 (я жал раз 7) пока не окажемся в 32-разрядном – это длинный эпилог. Жмем F10 пока не выйдем на самую вершину. По пути главное не пропустить команду вроде lea eax,[ebp-04] или mov edx,[ebp-08], короче в один из регистров будет занесен адрес скопированной строки или указатель на нее. (Примечание: на 16-разрядную hmemcpy можно смело забить большой болт! Внимательно потрассировав прогу не трудно убедится, что hmemcpy вызывается из CallWindowProcA, т.е. что бы выловить следующие фрагменты:

some_string := edit1.text или form1.caption := some_string надо этот бряк и поставить
bpx CallWindowProcA if (*(esp+c)==d)||(*(esp+c)==c) do "d *(esp+14)",
где *(esp+14) это адресс буфера, который получит текст с контрола или установит его… 100%-но так делают все компоненты делфи и вероятно борланд с++. Это я вообще к тому, что на форумах появились вопросы типа “А, что же с hmemcpy под 2000-ой?”… Ну, типа вот и ответ :) Первым делом прога считывает введенное мыло, это вызов по адресу 017F:00436F88 call 0041ACCC, потом введенный код 017F:00436FB7 call 0041ACCC. В общем, как оказалось, прога действительно заносит преобразованные данные в реестр и ничего на данном этапе не проверяет. Но чтобы это выкупит я потратил целую неделю. Вот чего происходит:
Листинг_1:
 017F:00436F82	MOV	EAX,[ESI+000001EC]
 017F:00436F88	CALL	0041ACCC	;считывает введенное мыло
 017F:00436F8D	MOV	EDX,[EBP-08]
 017F:00436F90	MOV	EAX,[00443014]
 017F:00436F95	CALL	0040371C
 017F:00436F9A	MOV	EAX,[00443014]
 017F:00436F9F	CALL	00436C24
 017F:00436FA4	PUSH	00000600
 017F:00436FA9	CALL	KERNEL32!Sleep	;понятно почему так долго думает
 017F:00436FAE	LEA	EDX,[EBP-04]
 017F:00436FB1	MOV	EAX,[ESI+000001F4]
 017F:00436FB7	СALL	0041ACCC	;считывает регистрационный код
 017F:00436FBC	LEA	EAX,[EBP-04]
 017F:00436FBF	CALL	00436C24	;проверяет код на наличие в начале и конце пробелов
 017F:00436FC4	MOV	DL,01
 017F:00436FC6	MOV	EAX,[00434B8C]
 017F:00436FCB	CALL	00434CC0
 017F:00436FD0	MOV	EBX,EAX
 017F:00436FD2	MOV	CL,01
 017F:00436FD4	MOV	EDX,[00442E7C]
 017F:00436FDA	MOV	EAX,EBX
 017F:00436FDC	CALL	00434DB8
 017F:00436FE1	TEST	AL,AL
 017F:00436FE3	JZ	0043702F
 017F:00436FE5	LEA	EDX,[EBP-08]
 017F:00436FE8	MOV	EAX,[ESI+0000020C]
 017F:00436FEE	CALL	0041ACCC	;считывает имя пользователя
 017F:00436FF3	MOV	ECX,[EBP-08]
 017F:00436FF6	MOV	EDX,004370E4
 017F:00436FFB	MOV	EAX,EBX
 017F:00436FFD	CALL	00434F54
 017F:00437002	MOV	ECX,[00443014]
 017F:00437008	MOV	ECX,[ECX]
 017F:0043700A	LEA	EAX,[EBP-10]
 017F:0043700D	MOV	EDX,[EBP-04]
 017F:00437010	CALL 	00403990	;соединяет в одну строку_1 регистрационный код и введенное мыло,
 017F:00437015	MOV	EAX,[EBP-10]	;первым стоит регистрационный код
 017F:00437018	LEA	EDX,[EBP-0C]
 017F:0043701B	CALL	00436E0C	;элементы полученной строки_1 преобразует по несложному алгоритму
 017F:00437020	MOV	ECX,[EBP-0C]	;и перемещает в строку_2, правда так намучано это перемещение (!),
 017F:00437023	MOV	EDX,004370F4	;блин… тут и запнулся в первый раз, думал что где упускаю момент
 017F:00437028	MOV	EAX,EBX		;проверки…
 017F:0043702A	CALL	00434F54	;заносит строку_2 в реестр, где и можно ее созерцать в ключе
 017F:0043702F	MOV	EAX,EBX		;HKEY_CURRENT_USER\SOFTWARE\Astatix\Crazy Minesweeper Rus
 …					;перед входом в процедуру в ECX содержится адрес строки_2
 017F:00437076	CALL	00429698	;просит перезапустится
 
Не будучи уверенным, что не пропустил момент проверки, пошел дальше – смотреть, что прога делает после перезапуска. Строка_2 хранится в ключе с названием “Code”, ставим бряк на обращение именно к ней. Когда выльемся по бряку в стеке по смещению esp+8 будет находится адрес строки с названием считываемого ключа, а по смещению esp+14 адрес буфера, куда ключ будет считан, строка “Сode” это 436F6465 в шестнадцатиричном виде, поэтому
bpx RegQueryValueExA if **(esp+8)==65646F43 do "d *(esp+14)"
Бряк выливается два раза, сначала с нулевым указателем на приемный буфер (непонятно зачем), а потом как полагается. Выходим по F12 и ставим бряк на считанную строку_2 bpmd (bpr у меня не по-детски глючит). Первое обращение происходит в процедуре которая просто считает длину строки_2, ничего интересного. Жмем два раза F12 и оказываемся на “потолке” (т.е. выходить еще выше по F12 больше не потребуется) процедуры преобразования и первичной проверки регистрационного кода:
Листинг_2:
 017F:0043A7D6	MOV	EAX,[EBP-08]
 017F:0043A7D9	CALL	00434F80	;читает из реестра строку_2
 017F:0043A7DE	MOV	EAX,[EBP-20]
 017F:0043A7E1	LEA	EDX,[EBP-1C]
 017F:0043A7E4	CALL	00436EB8	;по аналогичному алгоритму преобразует строку_2 обратно к строке_1
 017F:0043A7E9	MOV	EDX,[EBP-1C]
 017F:0043A7EC	MOV	EAX,[00443014]
 017F:0043A7F1	CALL	0040371C
 017F:0043A7F6	LEA	EAX,[EBP-0C]
 017F:0043A7F9	PUSH	EAX
 017F:0043A7FA	MOV	EAX,[00443014]
 017F:0043A7FF	MOV	EAX,[EAX]
 017F:0043A801	MOV	ECX,0000000F
 017F:0043A806	MOV	EDX,00000001
 017F:0043A80B	CALL	00403B48	;выделяет из строки_1 первые 15 символов
 017F:0043A810	MOV	EAX,[00443014]
 017F:0043A815	MOV	ECX,0000000F
 017F:0043A81A	MOV	EDX,00000001
 017F:0043A81F	CALL	00403B88	;выделяет из строки_1 остальные символы с 16-го
 017F:0043A824	MOV	EAX,[EBP-08]
 017F:0043A827	CALL	00402C64
 017F:0043A82C	LEA	EAX,[EBP-10]
 017F:0043A82F	PUSH	EAX
 017F:0043A830	MOV	ECX,0000000A
 017F:0043A835	MOV	EDX,00000002
 017F:0043A83A	MOV	EAX,[EBP-0C]
 017F:0043A83D	CALL 	00403B48	;еще раз выделяет из строки_1 символы со 1-го по 10
 017F:0043A842	LEA	EAX,[EBP-1C]
 017F:0043A845	CALL	00436D0C	;создает настоящий код с 1-го по 10 символы!
 017F:0043A84A	MOV	EAX,[EBP-1C]
 017F:0043A84D	MOV	EDX,[EBP-10]
 017F:0043A850	CALL 	00403A54	;непосредственное сравнение выделенных символов с 1-го по 10 с настоящими
 017F:0043A855	JNZ	0043A860
 017F:0043A857	CMP	DWORD PTR [004454D8],00
 
После непродолжительной медитации выясняем, что зашифрованная строка обратно преобразуется в исходную – регистрационный код + мыло и делается три копии последней (если честно, то этот кусок кода я возил раз 40 точно :). Первая копия – символы с 0-го по 14, вторая – остальные с 15-го, третья – с 1-го по 10-й символы. Отпускаем F5 и выливаемся внутри процедуры call 00403A54, где происходит непосредственное сравнение введенного регистрационного кода и части настоящего (т.е. символов с 1-го по 10-й).
Листинг_3:
 017F:00403A7D	MOV	ECX,[ESI]	;в esi адрес истинного кода, 10 символов с 1 по 10
 017F:00403A7F	MOV	EBX,[EDI]	;в edi адрес введенного кода, последняя копия, символы с 1-го по 10-ый
 017F:00403A81	CMP	ECX,EBX
 017F:00403A83	JNZ	00403ADD	;и так далее…
 
“вот он – момент истины!” – подумал я и с радостью скопировал часть настоящего кода в две копии введенного. То, что сравниваются не все символы, а только 10 (с 1 по 10), внимания особого не обратил. Подумаешь – такой алгоритм… отпускаю прогу – ура !!!, в окошке “о программе” красуется мой логин, доступны все опции. Однако после непродолжительной игры… Ууууууу… [нецензурное выражение] ~|;( – вырывается спонтанно. По всему игровому полю смачно моргают надписи “YOU MUST REGISTER!”… Типа, не шали. Плюс на лицо явные баги – не открываются где надо поля, невозможная по правилам игры комбинация цифр, а иногда и глюк конкретный выскакивает типа access violation и т.д.
Для начала я решил "не гнать картину”, а разобраться с процедурой генерации части регистрационного ключа – мало ли, может тут и кроется причина глюков (как оказалось, нет). Протрассировав листинг_2 по F10 пару раз методом исключения (ведь мы уже знаем адрес где лежит настоящий код) выясняем какая процедура нам нужна. Это вызов по адресу 017F:0043A845 call 00436D0C. В ней
Листинг_4:
 017F:00436D29	MOV	AL,01
 017F:00436D2B	CALL 	00436C80	;возвращает в ax какое-то кеш-число
 017F:00436D30	MOV	EBX,EAX
 017F:00436D32	XOR	EAX,EAX
 017F:00436D34	CALL	00436C80	;вычисляет второе кеш-число
 ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++;
 017F:00436D39	MOV	ESI,EAX		;в bx – первое кеш-число, в si-второе кеш-число
 017F:00436D3B	XOR	SI,7887
 017F:00436D40	MOVZX	EAX,BX		;как видно далее, кеш-числа немного преобразовываются
 017F:00436D43	MOV	ECX,0000000A
 017F:00436D48	CDQ
 017F:00436D49	IDIV	ECX
 017F:00436D4B	MOV	ECX,EDX
 017F:00436D4D	INC	ECX
 017F:00436D4E	MOVZX	EAX,SI
 017F:00436D51	MOV	EDI,0000000A
 017F:00436D56	CDQ
 017F:00436D57	IDIV	EDI
 017F:00436D59	MOV	EAX,EDX
 017F:00436D5B	INC	EAX
 017F:00436D5C	MOV	EDX,ECX
 017F:00436D5E	IMUL	DX,AX
 017F:00436D62	ADD	BX,DX
 017F:00436D65	ADD	SI,DX
 ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++;
 017F:00436D68	LEA	EDX,[EBP-08]
 017F:00436D6B	MOVZX	EAX,BX
 017F:00436D6E	CALL	00406A08	;на входе данной процедуры в ax лежит первое кеш-число
 017F:00436D73	JMP	00436D85	;поковыряв эту процедуру по F8 выясняем, что здесь создаются
 017F:00436D75	LEA	EAX,[EBP-08]	;символы проверочного кода с 6-го по 10-ый
 017F:00436D78	MOV	ECX,[EBP-08]
 017F:00436D7B	MOV	EDX,00436E08
 017F:00436D80	CALL	00403990
 017F:00436D85	MOV	EAX,[EBP-08]
 017F:00436D88	CALL	00403944
 017F:00436D8D	CMP	EAX,05		;если некоторые символы не с 6-го по 10-ый не получены
 017F:00436D90	JL	00436D75	;то данный цикл просто заполняет их нулями, т.е. 30h
 017F:00436D92	LEA	EDX,[EBP-0C]
 017F:00436D95	MOVZX	EAX,SI
 017F:00436D98	CALL 	00406A08	;на входе данной процедуры в ax лежит второе кеш-число
 017F:00436D9D	MOV	EDX,[EBP-0C]	;создаются символы проверочного кода с 1-го по 5-тый
 017F:00436DA0	LEA	EAX,[EBP-08]
 017F:00436DA3	MOV	ECX,[EBP-08]
 017F:00436DA6	CALL 	00403990	;оба найденных слова проверочного кода объединяются в одно
 017F:00436DAB	JMP	00436DBD
 
Два вызова процедуры call 00436C80 создают какие-то кеш-числа, которые в регистре ax передаются процедуре call 00406A08, а та в зависимости от того какое кеш-число ей передается, создает либо с 6-го по 10-ый символы, либо с 1-го по 5-ый символы. Очень внимательно трассируем последнюю по F8 и первое же обращение к переданному числу и является процедурой генерации символов:
Листинг_5:
 017F:0040726A	MOV	ECX,0000000A	;здесь в ax лежит кеш-число
 017F:0040726F	LEA	ESI,[EBP-3C]	;в esi лежит адрес, куда скидываются полученные символы
 017F:00407272	XOR	EDX,EDX
 017F:00407274	DIV	ECX
 017F:00407276	ADD	DL,30
 017F:00407279	CMP	DL,3A
 017F:0040727C	JB	00407281
 017F:0040727E	ADD	DL,07
 017F:00407281	DEC	ESI
 017F:00407282	MOV	[ESI],DL	;полученный в dl символ отправляется по назначению
 017F:00407284	OR	EAX,EAX
 017F:00407286	JNZ	00407272
 
Простой алгоритм – отлично! (… баба з возу – кобыле легче). Осталось посмотреть откуда процедура call 00436C80 берет эти самые кеш-числа:
Листинг_6:
 017F:00436C89	MOV	WORD PTR [EBP-02],0000
 017F:00436C8F	MOV	EAX,[00443014]
 017F:00436C94	MOV	EAX,[EAX]	;е-мое !, да тут в eax адрес второй копии, которая содержит все
 017F:00436C96	CALL	00403944	;оставшиеся символы начиная с 15-го, т.е. кусок введенного мыла – еss.
 017F:00436C9B	MOV	ESI,EAX		;что и указывает на то, что размер регистрационного кода должен быть
 017F:00436C9D	TEST	SI,SI		;равен 15 символам. Для чистоты эксперимента заходим обратно в
 017F:00436CA0	JBE	00436CFF	;регистрацию и вводим 15 символов ключа: 0123456789ABCDE, а
 017F:00436CA2	MOV	DX,0001		;в качестве мыла – coolness. Выливаемся обратно здесь и как ожидали в
 017F:00436CA6	MOVZX	EAX,DX		;регистре eax адрес строки coolness.
 017F:00436CA9	MOV	ECX,EAX		;вызов CALL  00403944 всего лишь проверяет имеется ли в наличии
 017F:00436CAB	AND	ECX,80000001	;строка или нет и соответственно возвращает либо ноль либо размер
 017F:00436CB1	JNS	00436CB8
 017F:00436CB3	DEC	ECX
 017F:00436CB4	OR	ECX,-02
 017F:00436CB7	INC	ECX
 017F:00436CB8	MOV	EDI,EBX
 017F:00436CBA	AND	EDI,000000FF
 017F:00436CC0	CMP	ECX,EDI
 017F:00436CC2	JNZ	00436CF9	;прыгает если число четное при первом вызове и если нечетное при втором
 017F:00436CC4	MOV	ECX,[00443014]
 017F:00436CCA	MOV	ECX,[ECX]
 ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++;
 017F:00436CCC	MOVZX	EAX,BYTE PTR [EAX+ECX-01]
 017F:00436CD1	SHL	EAX,08		;все просто – при первом вызове эта процедура выбирает из мыла
 017F:00436CD4	XOR	[EBP-02],AX	;символы с нечетных позиций и по данному алгоритму создает первое
 017F:00436CD8	MOV	AX,[EBP-02]	;кеш-число, при втором вызове выбирает символы с четных позиций и
 017F:00436CDC	MOV	ECX,00000008	;создает второе кеш-число
 017F:00436CE1	TEST	AX,8000
 017F:00436CE5	JZ 	00436CF0
 017F:00436CE7	SHL	AX,1
 017F:00436CEA	XOR 	AX,1021
 017F:00436CEE	JMP	00436CF3
 017F:00436CF0	SHL	AX,1
 017F:00436CF3	LOOP	00436CE1
 017F:00436CF5	MOV	[EBP-02],AX
 ;++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++;
 017F:00436CF9	INC	EDX
 017F:00436CFA	DEC	SI
 017F:00436CFD	JNZ	00436CA6
 017F:00436CFF	MOV	AX,[EBP-02]
 
Окончательный алгоритм получения символов с 1-го по 10-ый регистрационного кода выглядит следующим образом: по алгоритму на листинге_6 (выделено комментарием ;+++;), получаем из мыла два кеш-числа – для нечетных позиций и для четных. По алгоритму на листинге_4 (выделено, сразу после второго вызова call 00436С80) полученные кеш-числа немного преобразуются и далее по алгоритму на листинге_5 из первого кеш-числа (для нечетных позиций) получаем символы с 6-го по 10-ый, а из второго кеш-числа (для четных позиций) получаем символы с 1-го по 5-ый. Если некоторые символы найдены не будут, то они заменяются символами 30h, т.е. ‘0’. Дабы на этом этапе поставить крест – быстро наклепал на Delphi прошку, которая по этому алгоритму вычисляет часть регистрационного кода (окончательный вариант процедуры генерации ключа приведен внизу). Проверяю раз десять – все правильно, работает. Но неизвестны еще 0-ой, 11, 12, 13 и 14 символы.
…Поехали дальше. Ставим бряки на соответствующих копиях ключа и выясняется что после первой проверки та копия, которая содержит символы с 0-го по 14-ый включительно копируется по адресу 0044749С с предварительным вычитание от каждого символа числа 30h, а все остальные копии забиваются нулями или чем-то еще (уже легче):
Листинг_7:
 017F:0043AFFE	MOV	EDX,[EBP-0C]		;адрес копии, содержащей символы с 0-го по 14-ый
 017F:0043B001	MOV	DL,[EBX+EDX-01]
 017F:0043B005	SUB	DL,30			;вычитается из значения каждого символа 30h
 017F:0043B008	MOV	ECX,[EBP-18]		;адрес 0x0044749С
 017F:0043B00B	MOV	[ECX],DL
 017F:0043B00D	INC	EBX
 017F:0043B00E	INC	DWORD PTR [EBP-18]
 017F:0043B011	DEC	EAX
 017F:0043B012	JNZ	0043AFFE
 
Ставим бряк bpmd 0044749C, а для уверенности и на последние значения bpmd 004474A8. Бряк срабатывает только тогда, когда начинаешь играть. И что мы видим – ага, некоторые из недостающих символов используются, а именно 11 и 12.
Листинг_8:
 017F:0043BD6B	MOV	EAX,[00442F64]		;в eax адрес 0044749С
 017F:0043BD70	MOVZX	EAX,BYTE PTR [EAX+01]	;берется первый символ, умножается на 10,
 017F:0043BD74	ADD	EAX,EAX			;складывается со вторым и инкрементируется
 017F:0043BD76	LEA	EAX,[EAX*4+EAX]		;после чего складывается с инкрементированным
 017F:0043BD79	MOV	EDX,[00442F64]		;7-м символом и делится на 64h
 017F:0043BD7F	MOVZX	EDX,BYTE PTR [EDX+02]
 017F:0043BD83	ADD	EAX,EDX
 017F:0043BD85	INC	EAX
 017F:0043BD86	MOV	EDX,[00442F64]
 017F:0043BD8C	MOVZX	EDX,BYTE PTR [EDX+07]
 017F:0043BD90	INC	EDX
 017F:0043BD91	IMUL	EDX
 017F:0043BD93	MOV	ECX,00000064
 017F:0043BD98	CDQ
 017F:0043BD99	IDIV	ECX
 017F:0043BD9B	MOV	EAX,EDX
 017F:0043BD9D	MOV	EDX,[00442F64]
 017F:0043BDA3	MOVZX	EDX,BYTE PTR [EDX+0B]	;11-тый символ умножается на 10, складывается с
 017F:0043BDA7	ADD	EDX,EDX			;12-тым символом и отнимается от предыдущего
 017F:0043BDA9	LEA	EDX,[EDX*4+EDX]		;результата, после чего вся эта байда делится на 10
 017F:0043BDAC	MOV	ECX,[00442F64]		;и полученый остаток от деления куда-то отправляется…
 017F:0043BDB2	MOVZX	ECX,BYTE PTR [ECX+0C]
 017F:0043BDB6	ADD	EDX,ECX
 017F:0043BDB8	SUB	EAX,EDX
 017F:0043BDBA	MOV	ECX,0000000A
 017F:0043BDBF	CDQ
 017F:0043BDC0	IDIV	ECX
 017F:0043BDC2	MOV	EAX,[00442FB8]		;а точнее полученный остаток
 017F:0043BDC7	MOV	[EAX],EDX		;копируется по адресу 0x00442EE0
 
Видно, что по следующему алгоритму ((((X1*0Ah + X2 + 1)*(X7 + 1)) mod 64h) – (XB*0Ah + XC)) mod 0Ah вычисляется какой-то остаток. Кроме этого остатка больше ничего дальше не используется. Поэтому ставим соответствующий бряк на адрес 00442EE0, по которому последний копируется, и выясняем что… данный остаток используется как дополнительное смещение, по которому находится контрольная сумма из какой-то битовой таблицы, потом эта контрольная сумма используется как смещение для вычисления какого-то значения из какой-то таблицы адресов (!!!) и понеслась (ударение на “е”)…, при этом битовая таблица разная от игры к игре… ууу-ёёё !?!?!?… (держусь за голову) Вот здесь-то и самая главная идея (!) защиты, получается что вычисленное значение используется в процессе самой игры. Наверно, я открыл для себя велосипед, но для меня это было в новинку! Что делать? Втыкать в то, как создается эта битовая таблица, зачем она нужна, практически тоже самое как втыкать в алгоритм игры без исходников – дело безнадежное… куча вложенностей, перевложенностей, рекурсивных вызовов, переменных и т.д. и т.п. Возможно даже, что краеугольный камень защиты вовсе не здесь, а где-то дальше, но – тут-то и самое слабое место в защите и самая ГЛАВНАЯ ОШИБКА(!) разработчика, потому что определить, какой остаток нужно получить для корректной работы программы, можно “не отxодя от кассы” (я сразу не вьехал, а читатель может догадаться и сам)… Кстати по ходу выясняем, что бряк bpmd 0044749C, при установке мины правой кнопкой мыши, выливается по адресу:
Листинг_9:
 017F:0043B6CC	MOV	EAX,[00442F64]
 017F:0043B6D1	MOVZX	EAX,BYTE PTR [EAX+01]	;1-й символ умножается на 10 и прибавляется ко 2-му
 017F:0043B6D5	ADD	EAX,EAX
 017F:0043B6D7	LEA	EAX,[EAX*4+EAX]
 017F:0043B6DA	MOV	EDX,[00442F64]
 017F:0043B6E0	MOVZX	EDX,BYTE PTR [EDX+02]
 017F:0043B6E4	ADD	EAX,EDX
 017F:0043B6E6	МOV	EDX,[00442F64]
 017F:0043B6EC	MOVZX	EDX,BYTE PTR [EDX+0A]	;10-й символ умножается на 10 и прибавляется к 11-му
 017F:0043B6F0	ADD	EDX,EDX			;после чего производится операция xor с предыдущим
 017F:0043B6F2	LEA	EDX,[EDX*4+EDX]		;результатом и все это дело делится на 3…
 017F:0043B6F5	MOV	ECX,[00442F64]
 017F:0043B6FB	MOVZX	ECX,BYTE PTR [ECX+0B]
 017F:0043B6FF	ADD	EDX,ECX
 017F:0043B701	XOR	EAX,EDX
 017F:0043B703	MOV	ECX,00000003
 017F:0043B708	CDQ
 017F:0043B709	IDIV	ECX
 017F:0043B70B	MOV	ECX,0000000A		;а потом делится на 10…
 017F:0043B710	CDQ
 017F:0043B711	IDIV	ECX
 017F:0043B713	MOV	EAX,[00442F64]
 017F:0043B718	MOVZX	EAX,BYTE PTR [EAX]	;в результате чего остаток от последнего деления должен
 017F:0043B71B	CMP	EDX,EAX			;быть равен 0-му символу регистрационного кода
 017F:0043B71D	JZ 	0043B761
 
Отлично, теперь имеем формулу по которой можно найти нулевой символ регистрационного кода:
X0 = (((X1*0Ah+X2) xor (XA*0Ah+XB)) div 3) mod 0Ah). Но возвратимся к нашим бАрррАнам – листингу_8. Сначала я долго пытался проследить, что прога делает с полученным остатком, потом (еще дольше) пытался выловить процедуру, которая выводит на экран битмап “you must register!” – совсем запутался в кольцах рекурсивных вызовов. И тут меня расколбасило (или разколбасило)! – блин, так ведь нам надо всего лишь остаток от деления на 0Ah, а это ведь всего лишь 10 значений (всего ничего !!!) от 0 до 9, их можно вручную перебрать и один из них должен работать правильно! Ну, такие экзотические остатки как 0xFFFFFFFB, которые иногда получались, врядле являются правильными. Это первое, что можно сделать. А если, предположим, последнее деление происходило бы на 0x0FFFF (вместо 0Ah), соответственно и имели бы огромный диапазон остатков, то второе, что можно сделать, – имея все предыдущие алгоритмы, подобрать таким образом строку мыла, чтобы результат данной процедуры ((((X1*0Ah + X2 + 1)*(X7 + 1)) mod 64h) – (XB*0Ah + XC)) как можно ближе приближался по значению к 0Ah, таким образом мы получим наименьший диапазон остатков для перебора! Чем я и занялся. Дописав кое-что в моей проге, в течение получаса “методом математического тыка (втыка)” я подобрал такую строку мыла, которая дает всего лишь 3(!) остатка … хе-хе :) (конечно, можно и меньше). У меня получилось: kap@omen.ru. Здесь я предположил, что 11-й символ всегда равен нулю (т.е. 30h), вполне возможное решение, ведь нам надо чтобы совпадали результаты найденных формул, а сам 11-й символ потом найдется сам по себе от обратного…
 значение 11-го символа XB	значение 12-го символа XC	Значение остатка
 	0				1				2
 	0				2				1
 	0				3				0
 
При остальных значениях 11-го и 12-го символов остаток получается либо отрицательный либо нулевой. Дрожащими руками я стал проверять полученные значения на корректность (предварительно вычислив нулевой символ по предыдущей формуле).

остаток 2 – ругается… хм-м, ладно
остаток 1 – Иии… ?!? (проходит минута), черт!!! – опять ругается;
остаток 0 – … проходит минута, две… душа трепещит – ДА!!! ха-ха-ха :)))… игра заканчивается и мне предлагают записать свой рекорд! Все отлично.

Значит: ((((X1*0Ah + X2 + 1)*(X7 + 1)) mod 64h) – (XB*0Ah + XC)) mod 0Ah = 0
Кстати, по ходу дела я наткнулся на интересный такой момент
Листинг_10:
 017F:0043CE62	MOV	EAX,[00442F64]
 017F:0043CE67	MOVZX	EAX,BYTE PTR [EAX+05]	;5-й символ умножается на 3E8h, складывается с
 017F:0043CE6B	IMUL	EAX,EAX,000003E8	;6-м символом, который предварительно умножается на 64h,
 017F:0043CE71	MOV	EDX,[00442F64]		;плюс 8-й символ, умноженный на 0Ah, плюс 9-й символ
 017F:0043CE77	MOVZX	EDX,BYTE PTR [EDX+06]
 017F:0043CE7B	IMUL	EDX,EDX,64
 017F:0043CE7E	ADD	EAX,EDX
 017F:0043CE80	MOV	EDX,[00442F64]
 017F:0043CE86	MOVZX	EDX,BYTE PTR [EDX+08]
 017F:0043CE8A	ADD	EDX,EDX
 017F:0043CE8C	LEA	EDX,[EDX*4+EDX]
 017F:0043CE8F	ADD	EAX,EDX
 017F:0043CE91	MOV	EDX,[00442F64]
 017F:0043CE97	MOVZX	EDX,BYTE PTR [EDX+09]
 017F:0043CE9B	ADD	EAX,EDX
 017F:0043CE9D	МOV	ECX,0000000B		;все это дело делится на 0Bh, а потом на 64h
 017F:0043CEA2	CDQ
 017F:0043CEA3	IDIV	ECX
 017F:0043CEA5	MOV	ECX,00000064
 017F:0043CEAA	CDQ
 017F:0043CEAB	IDIV	ECX
 017F:0043CEAD	MOV	EAX,[00442F64]
 017F:0043CEB2	MOVZX	EAX,BYTE PTR [EAX+0D]	;а равно должно это быть 13-му символу
 017F:0043CEB6	ADD	EAX,EAX			;умноженному на 0Ah, плюс 14-й символ
 017F:0043CEB8	LEA	EAX,[EAX*4+EAX]
 017F:0043CEBB	MOV	ECX,[00442F64]
 017F:0043CEC1	MOVZX	ECX,BYTE PTR [ECX+0E]
 017F:0043CEC5	ADD	EAX,ECX
 017F:0043CEC7	CMP	EDX,EAX
 017F:0043CEC9	JZ	0043CF01
 
А, что тут у нас такое? – да это же позабытые 13 и 14 символы! Т.е.
((X5*3E8h + X6*64h + X8*0Ah + X9) div 0Bh) mod 64h = XD*0Ah + XE

Теперь у нас есть все формулы для вычисления правильного ключа. Далее приведена процедура генерации ключа. Только надо не забыть, что от значений регистрационного ключа при копировании еще отнималось 30h, тогда все понятно. Передаете первым параметром адрес строки, содержащей мыло, вторым параметром – адрес буфера размером 15 байт, который получит регистрационный ключ.
Исходный текст процедуры генерации ключа.
 ;+++++++++++++++++++++++++++++++++++++++++++++++++++
 ;		   CrazyMinKeyGen
 ;+++++++++++++++++++++++++++++++++++++++++++++++++++
  proc CrazyMinKeyGen, string_in, key_out
  var_1	dd  ?
  var_2	dd  ?
 	enter
 	push	edi ebx esi
 	cld
 	mov	eax, 30h
 	mov	ecx, 0Fh
 	mov	edi, [key_out]
 	repz	stosb
 	xor	eax, eax
 	mov	[var_1], eax
 	mov	[var_2], eax
 	mov	esi, [string_in]
 	mov	edi, 0xFFFFFFFF
  M1:
 	xor	eax, eax
 	lodsb
 	or	eax, eax
 	jz	M2
 	inc	edi
 	test	edi, 1
 	jz	ODD
 
 ;++++ вычисляем кеш-число для четных позиций +++++++
 	shl	eax, 8
 	xor	word [var_1], ax
 	mov	ax, word [var_1]
 	mov	ecx, 8
  AAA_1:
 	test	ax, 8000h
 	jz 	AAA_2
 	shl	ax, 1
 	xor	ax, 1021h
 	jmp	AAA_3
  AAA_2:
 	Shl	eax, 1
  AAA_3:
 	loop	AAA_1
 	mov	word [var_1], ax
 	jmp	M1
 
 ;++++ вычисляем кеш-число для нечетных позиций +++++
  ODD:
 	shl	eax, 8
 	xor	word [var_2], ax
 	mov	ax, word [var_2]
 	mov	ecx, 8
  BBB_1:
 	test	ax, 8000h
 	jz	BBB_2
 	shl	ax, 1
 	xor	ax, 1021h
 	jmp	BBB_3
  BBB_2:
 	shl	eax, 1
  BBB_3:
 	loop	BBB_1
 	mov 	word [var_2], ax
 	jmp	M1
  M2:
 
 ;++++ полученные кеш-числа преобразовываются ++++++
 	xor	word [var_1], 7887h
 	mov	eax, [var_1]
 	mov	ecx, 0Ah
 	cdq
 	idiv	ecx
 	mov	ebx, edx
 	inc	ebx
 	mov	eax, [var_2]
 	mov	ecx, 0Ah
 	cdq
 	idiv	ecx
 	mov	ecx, edx
 	inc	ecx
 	imul	bx, cx
 	add 	word [var_1], bx
 	add	word [var_2], bx
 
 ;++++ получаем символы с 1-го по 5-ый ключа +++++++
 	mov	esi, [key_out]
 	add	esi, 6
 	mov	eax, [var_1]
  CCC_1:
 	xor	edx, edx
 	mov	ecx, 0Ah
 	div	ecx
 	add	dl, 30h
 	cmp	dl, 3Ah
 	jb	CCC_2
 	add	dl, 7
  CCC_2:
 	dec	esi
 	mov	byte [esi], dl
 	or	eax, eax
 	jnz	CCC_1
 
 ;++++ получаем символы с 6-го по 10-ый ключа ++++++
 	mov	esi, [key_out]
 	add	esi, 0Bh
 	mov	eax, [var_2]
  DDD_1:
 	xor 	edx, edx
 	mov	ecx, 0Ah
 	div	ecx
 	add	dl, 30h
 	cmp	dl, 3Ah
 	jb	DDD_2
 	add	dl, 7
  DDD_2:
 	dec	esi
 	mov	byte [esi], dl
 	or	eax, eax
 	jnz	DDD_1
 
 ;++++ получаем символы с 11-ый и 12-ый ключа ++++++
 	mov	esi, [key_out]
 	movzx	eax, byte [esi + 1]
 	sub	eax, 30h
 	add	eax, eax
 	lea	eax, [eax*4 + eax]
 	movzx	edx, byte [esi + 2]
 	sub	edx, 30h
 	add	eax, edx
 	inc	eax
 	movzx	edx, byte [esi + 7]
 	sub	edx, 30h
 	inc	edx
 	imul	edx
 	cdq
 	mov	ecx, 64h
 	idiv	ecx
 	mov	eax, edx
 	mov	ecx, 0Ah
 	idiv	cl
 	add	al, 30h
 	add	ah, 30h
 	mov	byte [esi + 0Bh], al
 	mov	byte [esi + 0Ch], ah
 
 ;++++ получаем 0-ой символ ключа ++++++++++++++++++
 	movzx	eax, byte [esi + 1]
 	sub	eax, 30h
 	add	eax, eax
 	lea	eax, [eax*4 + eax]
 	movzx	edx, byte [esi + 2]
 	sub	edx, 30h
 	add	eax, edx
 	movzx	edx, byte [esi + 0Ah]
 	sub	edx, 30h
 	add	edx, edx
 	lea	edx, [edx*4 + edx]
 	movzx	ecx, byte [esi + 0Bh]
 	sub	ecx, 30h
 	add	edx, ecx
 	xor	eax, edx
 	mov	ecx, 3
 	cdq
 	idiv	ecx
 	mov	ecx, 0Ah
 	cdq
 	idiv	ecx
 	add	dl, 30h
 	mov	byte [esi], dl
 
 ;++++ получаем последние символы – 13-ый и 14-ый ++++
 	movzx	eax, byte [esi + 5]
 	sub	eax, 30h
 	imul	eax, eax, 3E8h
 	movzx	edx, byte [esi + 6]
 	sub	edx, 30h
 	imul	edx, edx, 64h
 	add	eax, edx
 	movzx	edx, byte [esi + 8]
 	sub	edx, 30h
 	add	edx, edx
 	lea	edx, [edx*4 + edx]
 	add	eax, edx
 	movzx	edx, byte [esi + 9]
 	sub	edx, 30h
 	add	eax, edx
 	mov	ecx, 0Bh
 	cdq
 	idiv	ecx
 	mov	ecx, 64h
 	cdq
 	idiv	ecx
 	mov	eax, edx
 	mov	ecx, 0Ah
 	idiv	cl
 	add	al, 30h
 	add	ah, 30h
 	mov	byte [esi + 0Dh], al
 	mov	byte [esi + 0Eh], ah
 	pop	esi ebx edi
 	return
 
Приколько – если прогнать данную процедуру с адресом пустой строки е-мэйла, то получим регистрационный код 130861000063190, т.е.

имя: Kalupan
e-mail:
код: 130861000063190

… и мыла не надо :)

Веселое послебредие:

С момента взлома проги до момента выхода статьи прошло несколько месяцев, в течении которых я сыграл в эту игрушку на уровне “Эксперт” (со стандартными настройками) более 7000 партий (!!!)… и ни одной (!!!) не выиграл… Постоянно после разминирования практически всех ячеек, когда остается совсем, ну совсем немного открыть, возникала ситуация где требовалось просто угадать нужную ячейку с миной… и каждый раз я не угадывал(!). При этом на других вновь доступных уровнях все ok. Что это?!?! Невезение?… или может я плохой игрок?… А, может это еще один уровень защиты!!!… Так сказать – зАпАдло на последок. И с тем злополучным остатком все таки что-то не то… Мм-дя… Разбираться очень харит… Аааааа!… – ну и черт с ним :).

Статью написал типа я, Godness <godness@omen.ru>. Спасибо за интерес к последней...

PS\2: Quake rulezzz!!!


Обсуждение статьи: KeyGen для Crazy Minesweeper 2.0 >>>


Материалы находятся на сайте https://exelab.ru



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS