| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
Исповедь начинающего крякера или
Введение в реверс инжиниринг
ver1.0
Глава первая
Дзен для самых маленьких или
Мои впечатления от crackmes’ов FaNt0m’a
0000
Тема взлома программ интересовала меня давно. Я представлял себе крякера, как мрачноватого гика, бросающегося на любую прогу с дебаггером наперевес. У крякера обязательно должны быть покрасневшие от бессонных ночей за дизассемблером глаза, он может читать листинг в хексах, потому что наизусть помнит все опкоды, а числа из одного радикса в другой гоняет быстрее любого калькулятора. Причем делает это ради развлечения, чтобы показать глупой машине все превосходство человеческого разума. Я хотел быть, как они, я хотел уметь то, что умеют они. Вот только я не знал, с чего и как следует начинать.
Я узнал о так называемых «крякми» из одного обширного crack-faq’а. Для тех, кто не в курсе: crackme – это программа, специально написанная для обучения взлому: нахождению пароля, пары имя/серийник, снятию наг-экранов, определенного вида защиты и т.д. (или все вышеперечисленное в комплексе). Тогда же я узнал, что лучшие собаководы рекомендуют начинать свое знакомство с увлекательным миром реверс инжиниринга с crackmes’ов некоего FaNt0m’a (респект!). Что же, я не стал тянуть кота за хвост в долгий ящик и решил попробовать...
Requirements:
- базовые знания принципов работы операционных систем линейки Windows
- умение программировать
- хотя бы минимальное знание языка ассемблера
- необходимый инструментарий (дизассемблер, отладчик, хекс-редактор, компилятор) и документация (MS Win32 Developer’s Reference – обычно идет в комплекте с компиляторами от Борланда, входит в состав MSDN)
- немного мозгов
0001
Таргет: crackme1.exe
Рулесы (original):
- Find the correct password
- Do NOT use Soft Ice or any other debugger!
- You must use only a disassembler/Hex-editor
- Bonus: Patch the file so it doesn't matter what the input is.
Это в довольно вольном переводе на великий и могучий означает следующее:
- Найди верный пасс
- Не юзай SoftIce или другие дебаггеры
- Ты должен юзать только дизасм или хекс-редактор
- Бонус: пропатчи файл, чтоб его не волновало, какую хрень ему вводят.
Инструменты:
Что ж, следуем правилам и откладываем дебаггер в сторону, его время придет позже. Выбираем инструмент: в самый раз будет виндовый блокнот (известный также в узких кругах как notepad.exe), также понадобится хекс-редактор (исключительно для патча) или дизассемблер (что-то типа HIEW), позволяющий править непосредственно код исполнимого файла.
(Прошу обратить внимание на то, что инструментарий у меня, мягко говоря, староват, и сильно за это не пинать. Может быть, вы обойдетесь набором программ посвежее.)
Notepad.exe
IDA 3.76
HIEW 6.11 (его я использую исключительно для штопки программ вручную, да простят меня любители данного дизассемблера за такое пренебрежительное к нему отношение)
Солюшен:
Открываем подопытную программу. Смотрим на крякозябры. Листаем. Примерно в середине файла начинается что-то осмысленное. Впрочем, как начинается, так и заканчивается. Однако вот оно: m0tNaF-EmKCARc! Проверяем. Да, так и есть – это искомый пароль. (Однако, стоит заметить, что в полевых условиях подобное никогда не встречается. Разработчики ведь далеко не дураки и никогда не выкладывают свой пароль открытым текстом.)
Теперь подумаем, каким образом программа отличает правильный пароль от неправильного. Очевидно, она сравнивает их между собой. Для сравнения строк в Win используются несколько функций, (пусть остальные сейчас останутся за кадром – этот материал вам в качестве домашнего задания) сейчас нам нужна только одна (обратимся к MS Win32 Developer’s Reference):
lstrcmp сравнивает две символьные строки, регистр учитывается, синтаксис вызова:
int lstrcmp(
LPCTSTR lpString1, // адрес первой строки
LPCTSTR lpString2 // адрес второй строки
);
Еще нам нужно знать, что если сравниваемые строки равны, то функция возвращает 0 в регистре еах (все функции WinAPI результат возвращают в регистре еах).
Далее: открываем программу дизассемблером и с умным видом смотрим в листинг. Интересно? Так, все окружающие поняли уже, что мы чрезвычайно умны, а главное – жутко заняты и отстали от нас с попытками заставить вынести мусорное ведро (неделю же назад выносил!). Читаем код (да, без знания ассемблера, хотя бы минимального, тут никуда), читаем, читаем и видим следующее место (IDA проделал кучу работы, для того, чтобы жизнь простого реверсера была чуточку проще: любезно заменил малозначащие call’ы по адресу на call’ы по метке, сделал так, что переменные из адресов превратились именно в переменные и т.д.):
...
00401287 push offset unk_40309C ; сует в стек адрес нашего пасса
0040128C push offset aM0tnafEmkcarc ; сует в стек адрес правильного пасса
00401291 call j_lstrcmpA ; сравнивает, скотина!
00401296 cmp eax, 0 ; проверяет, что она там насравнивала
00401299 jz short loc_4012B0 ; прыгает, если пасс верен
0040129B push 30h ; а если не верен, то показывает нам
0040129D push offset aCheckStatus ; окошко и глумится
004012A2 push offset aWrongPasswordK
004012A7 push 0
004012A9 call j_MessageBoxA
...
Как отучить crackme показывать окно с сообщением о неверном пароле? Довольно просто. Нужно поставить по смещению 00401287 десять нопов (nop = NO oPeration) и добавить к ним mov eax, 0. После этого в регистре еах у нас всегда будет 0, что также заставит программу всегда показывать окошко с поздравлением о верном вводе пароля, не смотря на то, что ей вводят в качестве такового.
В принципе, можно было бы и проверку значения еах тоже занопить, а условный джамп заменить на безусловный, но я ленивый, так что пусть и программа немного поработает.
0010
Таргет: сrackme2.exe
Рулесы:
- Patch the file to prevent the MesageBox and the DialogBox from showing
- You cannot delete the dialog from the resource
- Write up some sort of document (.txt or .html) which explains how and where you patched the program. As well how you found where to patch
- Bonus: Write a program to patch the exe file
Для самых маленьких: это можно примерно перевести как:
- Пропатчи файл, чтобы предотвратить появление злобных MesageBox’a и DialogBox’a
- Ты не можешь удалять диалог из ресурса
- Черкни пару строк, которые объясниют как и где ты пропатчил программу. Было б неплохо, если б ты еще и написал, как ты определил, где патчить
- Бонус: напиши патчер
Инструменты:
Нам понадобятся дизассемблер (любой, лишь бы определял символьные имена функций) и компилятор (тоже любой, чтобы написать патчер).
IDA 3.76
HIEW 6.11
Delphi 6.0
Солюшен:
Как известно, окна в Win создаются и выводятся несколькими функциями. Автор любезно подсказал, какие именно он использовал в своем crackme: MesageBox и DialogBox. Посмотрим, что нам известно об этих функциях:
MessageBox создает, показывает и обрабатывает окошко с сообщением, синтаксис вызова:
int MessageBox(
HWND hWnd, // хэндл окна-владельца
LPCTSTR lpText, // адрес текста окна-сообщения
LPCTSTR lpCaption, // адрес текста заголовка окна
UINT uType // стиль окна с сообщением
);
DialogBox создает модальный диалог из шаблона в ресурсах программы, не возвращает управление программе, пока обрабатывающая диалог функция не закроет его, синтаксис вызова:
int DialogBox(
HINSTANCE hInstance, // хэндл инстанса приложения
LPCTSTR lpTemplate, // идентификатор ресурса
HWND hWndParent, // хэндл окна-владельца
DLGPROC lpDialogFunc // указатель на оконную функцию диалога
);
Дизассемблируем программу и смотрим в код, ищем нужные вызовы. Вот они:
...
004010D0 push 30h ; сует в стек параметры для
004010D2 push offset aRemoveMe ; первого нага
004010D7 push offset aNagNagNag___Re
004010DC push 0
004010DE call j_MessageBoxA ; показывает первый наг
004010E3 push 0
004010E5 push offset loc_401210 ; сует в стек параметры для
004010EA push [ebp+var_50] ; второго нага
004010ED push offset aNagdialog
004010F2 push dword_40306C
004010F8 call j_DialogBoxParamA ; показывает второй наг
...
Чтобы заставить программу навсегда забыть о существовании этих вызовов внутри своего кода, забиваем по смещению 004010D0 сорок пять (45) нопов (nop – опкод 90h). Запускаем: наги исчезли. Далее, код патчера (Delphi):
procedure PatchIt;
const nop: Byte = $90; // nop опкод в хексах
var F: TFileStream; // юзаем файловый поток
I: Integer; // счетчик записанных байтов
begin
try
// открываем подопытного, как файловый поток
F := TFileStream.Create ('crackme2.exe', fmOpenReadWrite or fmShareExclusive);
// смещаемся на то место, которое будем лечить: 1024d – смещение от начала файла до
// секции кода, 208d – смещение в секции кода до инструкций, подлежащих патчу
F.Seek (1024 + 208, soFromBeginning);
// собственно, пишем по байтику
for I := 1 to 45 do F.WriteBuffer (nop, 1);
F.Free;
// выведем сообщение об удачном завершении процесса
Application.MessageBox ('Successfully patched! No nag screens are now and forever!',
'Congrats', mb_IconInformation);
еxcept
// сообщим, что ни фига не вышло
Application.MessageBox ('Error occurs when patching!', nil, mb_IconError);
end;
end;
0011
Таргет: сrackme3.exe
Рулесы:
- Patch the file to prevent the cdrom error messagebox
- Write up some sort of document (.txt or .html) which explains how and where you patched the program. As well how you found where to patch
- Bonus: Write a program to patch the exe file
Типа того:
- Пропатчи прогу, чтобы предотвратить появление messagebox’а с ошибкой
- Черкни пару строк, которые объясняли бы, как ты это сделал
- Бонус: напиши патчер
Инструменты:
IDA 3.76
HIEW 6.11
Солюшен:
Программы в Win определяют наличие или отсутствие подставки для кофе, известной также, как CD-ROM drive, несколькими способами. Один из них – это вызов функции WinAPI GetDriveType и проверка возвращаемого ею результата.
GetDriveType определяет является ли диск сменным, жестким (фиксированным), CD-ROM, RAM-диском или сетевым диском, синтаксис:
UINT GetDriveType(
LPCTSTR lpRootPathName // адрес строки с корнем диска
);
Как показывает все тот же MS Win32 Developer’s Reference, если функция GetDriveType возвращает 5, значит, текущий диск является CD.
Вооружаемся дизассемблером и ищем в подопытной программе вызовы GetDriveType:
...
004011FA push 0 ; указатель на корень текущего диска
004011FC call j_GetDriveTypeA ; вот он – наш зловредный вызов!
00401201 cmp eax, 5 ; в еах DRIVE_CDROM = 5
00401204 jz short loc_40121D ; переход на «правильное» сообщение
00401206 push 30h
00401208 push offset aFant0mSCrackme
0040120D push offset aErrorFindingCd
00401212 push 0
00401214 call j_MessageBoxA ; вывод «неправильного» сообщения
...
Следовательно, надо заменить инструкцию cmp eax, 5 на три нопа (именно три байта занимает данная инструкция), чтобы переход всегда осуществлялся на «правильный» messagebox. (Хотя, если честно, то нам неизвестно, что может случиться с флагом нуля во время выполнения программы до данной проверки. Поэтому, правильно было бы заменить push и вызов GetDriveType’а на два нопа и mov eax, 5. Применительно к боевым условиям именно так и следует поступить, но у нас просто задача, поэтому можно оставить, и так будет корректно работать. Но лучше сразу учиться делать все правильно.)
0100
Таргет: сrackme4.exe
Рулесы:
- Find a valid serial for your nick/name
- Patch the crackme to accept any input as valid serial
- Use any tools at your disposal, however I doubt you'll need anything more than SoftIce and W32Dasm
- Do not tamper with the resources! (I.E. No Resource Editors allowed!)
- Bonus: write a keygen to produce a valid serial given the user's name
А по-русски это будет выглядеть примерно следующим образом:
- Найди валидный серийник для своего ника
- Пропатчи crackme, чтобы она хавала любое дерьмо, как валидный серийник
- Юзайте любые тулзы, какие сочтете нужными, однако я сомневаюсь, что вам понадобится что-то еще, кроме SoftIce’a и W32Dasm’a
- Не трогать ресурсы! (т.е. не дозволяются никакие редакторы ресурсов)
- Бонус: напиши кейген, чтобы выдавал валидный серийник к данному имени
Инструменты:
IDA 3.76
HIEW 6.11
SoftIce 4.05 for WinNT (патченный для работы под WinXP)
Солюшен:
Эта задачка уже на порядок интереснее, чем предыдущие, чуть сложнее, хотя и не намного.
Как известно, все, что мы видим в Win состоит из окон (ну, почти все), все кнопки, окна редактирования и прочее – все это окна. Для того, чтобы получить текстовую информацию из окошка в Win используются две функции: GetWindowText и GetDlgItemText.
GetWindowText копирует текст из заголовка заданного окна (если заголовок имеется) в буфер. Если заданное окно является контролом, то копируется его текст, синтаксис:
int GetWindowText(
HWND hWnd, // хэндл окна или контрола с текстом
LPTSTR lpString, // адрес буфера, в который будет скопирован текст
int nMaxCount // максимальное число символов для копирования
);
GetDlgItemText получает заголовок или текст контрола в диалоге, синтаксис:
UINT GetDlgItemText(
HWND hDlg, // хэндл диалога
int nIDDlgItem, // идентификатор контрола
LPTSTR lpString, // указатель на строку-буфер
int nMaxCount // мксимальный размер строки
);
Запустим подопытную программу, запустим SoftIce. Откроем окно отладчика (Ctrl+D) и скажем ему:
bpx GetWindowTextA и bpx GetDlgItemTextA
Это заставит его поставить брейкпоинты на названные функции, и отлаживаемая программа прервется, когда сделает вызов хотя бы одной из них. Далее жмем F5, SoftIce прячется, а crackme продолжает свою работу. Забиваем свой ник в поле для имени и какую-нибудь ботву в поле для серийника, какую – совершенно не важно. Нажимаем кнопку с надписью Check, и – вот оно – всплывает SoftIce, и клянется, что де сработал бряк на GetWindowTextA. Нажимаем F11, чтобы выйти из функции туда, откуда она была вызвана, но SoftIce прячется, а программа выводит messagebox с сообщением о неверном серийнике. Не угадали, бывает.
Попробуем мыслить логически: для того, чтобы определить, правильный или нет мы ввели серийник, программа должна сначала его сгенерировать, а потом сравнить с тем, что мы ей дали. Сравнивает она, очевидно, какой-то функцией. Попробуем поставить брейкпоинт на нее. Открываем SoftIce, говорим ему:
bc *
чтобы очистить список брейкпоинтов, а потом
bpx lstrcmpa
чтобы заставить прерываться на функции lstrcmp. Нажимаем F5, а в crackme снова кликаем на кнопке Check. Всплывает SoftIce, нажимаем F11, потом еще раз и еще – ровно столько раз, сколько нужно, чтобы оказаться в коде исследуемой программы (в заголовке окна кода SoftIce’а не должно быть имени модуля и имени функции). Видим мы что-то типа этого (адреса могут быть другими):
...
0010:00401334 6884324000 push 00403284 ; указатель на первую строку
0010:00401339 6884314000 push 00403184 ; указатель на вторую строку
0010:0040133E E8A1000000 сall [KERNEL32!lstrcmpA] ; сравнивает их
0010:00401343 83F800 cmp eax, 00000000 ; смотрит результат
0010:00401346 7404 je 0040134C ; прыгает, если совпадение
...
Теперь осталось только посмотреть, что находится по 00403284 и 00403184 в памяти. Говорим SoftIce’у:
d 00403284 l 20 и d 00403184 l 20
и он выводит по 20h байт с заданных адресов. Итак, смотрим, что находится там: по одному адресу находится тот серийник, который мы скормили программе, а по второму странная последовательность символов. Записываем ее. Теперь проверяем в crackme – это и есть валидный серийник!
Осталось восстановить алгоритм генерации сериала и пропатчить программу.
Дизассемблируем crаckme и ищем вызовы messagebox’ов. Их должно быть всего два – один с сообщением об ошибке, а второй с сообщением об успешном вводе. Находим первый messagebox:
...
00401233 push dword ptr [ebp+8]
00401236 call sub_4012F9 ; а это что еще такое?
0040123B cmp eax, 0 ; проверяем еах – зачем?
0040123E jz short loc_401255 ; прыгаем
00401240 push 40h
00401242 push offset aCheckSerial
00401247 push offset aYouGotItCongra
0040124C push 0
0040124E call j_MessageBoxA ; «удачное» сообщение
...
Очевидно, что call sub_4012F9 по 00401236 осуществляет проверку сериала на валидность (имеет смысл перейти в функцию и разобраться с алгоритмом). Запоминаем место, где надо патчить. Очевидно, что функция возвращает 0, если серийники не совпали, и нас устроит любое отличное от него значение – заменим по 00401233 push и call на mov eax, 1 и добавим три нопа (nop). Тогда следующая за этим проверка всегда будет проходить удачно, и нас всегда будут благодарить за регистрацию (как альтернатива: можно забить нопами джамп на сообщение об ошибке – это тоже приведет к положительному результату).
Теперь кейген (специально использованы промежуточные переменные для того, чтобы проще было понять алгоритм генерации сериала):
function GenerateKey(Nick: String): String;
var S: Pointer; // указатель на имя
D: Pointer; // указатель на сериал, который собираем
C: Cardinal;
A, B: Byte;
begin
// +1 к длине потому, что в Win используются строки с завершающим 0
// если не зарезервировать символ под него, то результат вернется некорректный
C := Length (Nick) + 1;
// получим немного памяти
GetMem (S, C);
GetMem (D, C);
// обнулим памятку
ZeroMemory (S, C);
ZeroMemory (D, C);
// имя в байтики
for C := 0 to Length (Nick) do PByte (Cardinal (S) + C)^ := Ord (Nick [C + 1]);
C := 0;
// в цикле кодируем имя и суем все это в пасс
while PByte (Cardinal (S) + C)^ <> 0 do begin
A := PByte (Cardinal (S) + C)^ + C;
A := A xor C;
B := A mod 26;
PByte (Cardinal (D) + C)^ := B + 65;
Inc (C);
end;
// возвращаем результат
Result := PChar (D);
// обязательно освободим все, что взяли
FreeMem (S);
FreeMem (D);
end;
0101
Таргет: crackme5.exe
Рулесы:
- No resource hacking! (This means you DarkMooN`) ;)
- Find the Password
- Patch the crackme to make the program believe this is your CDROM
- Patch it once again to get rid of the Nags (dialogs and MsgBox)
- Find a serial for your name (or nick)
- Bonus: write a program to patch the cd check and nags
- Bonus: write a keygen for the serial
- Super Bonus: Reverse Engineer this crackme to display a message box with greets and your nick when the About menu is clicked instead of loading the dialog box
По-русски, кажется, так:
- Не хачить ресурсы! (тебя касается, DarkMooN)
- Найди пасс
- Пропатчи crackme, чтобы заставить программу верить, что это твой CDROM
- Пропатчи ее еще раз, чтобы избавиться от нагов
- Найди сериал для своего ника
- Бонус: напиши патчер для CD-проверки и удаления нагов
- Бонус: напиши кейген
- Мега-бонус: реверсни этот crackme, чтобы, когда выбирается пункт меню About, показывался твой messagebox с гритзами и твоим ником вместо загрузки диалога
Инструменты:
IDA 3.76
HIEW 6.11
SoftIce 4.05
Солюшен:
Это, так сказать, повторение и закрепление пройденного. Этот crackme объединяет в себе все те виды защит, которые мы сломали в предыдущих примерах.
Для начала попытаемся найти пароль. Для этого дизассемблируем программу (или воспользуемся любым текстовым редактором). Пароль лежит открытым текстом: JD39-CK4-5QV345.
Вспоминаем, что программа определяет наличие CDROM путем вызова функции GetDriveType. Ищем ее вызовы:
...
0040143A push 0 ; определяет для текущего диска
0040143C call j_GetDriveTypeA ; искомый вызов
00401441 xor ebx, ebx ; обнуляет регистр ebx
00401443 inc ebx ;
00401444 inc ebx ; ebx = 2
00401445 div bl ; делит еах на 2 (то, что в младшем слове ebx)
00401447 cmp al, 2 ; результат целочисленного деления = 2?
00401449 jnz short loc_401467 ; прыгает, если не равно
0040144B cmp ah, 1 ; остаток от деления = 1?
0040144E jz short loc_401452 ; прыгает, если равно
...
Как мы видим, тут уже не просто сравнение регистра еах с константой DRIVE_CDROM. Тут совершены кое-какие действия, чтобы нас запутать. Сначала результат, который нам вернула функция, делится на 2, и проверяется уже результат от этого деления. Если получилось число, отличное от 2, значит GetDriveType вернула число, которое меньше 4 или больше 5. Затем проверяется остаток от деления, если он равен 1, значит функция вернула нам DRIVE_CDROM = 5, и программа делает переход к сообщению об удачной проверке. Ничего сложного – это элементарная математика.
Для того, чтобы результат проверки на CDROM всегда был положительным, нужно по 00401441 заменить восемь байт на mov eax, 5 и cmp eax, 5 (опкоды: B805000000 83F805 – ровно 8 байт) и по 0040144B заменить на jmp 00401452 (E902000000).
Теперь пришло время разобраться с наг-экранами. Их в программе два: один MesageBox и один DialogBox. Как с ними поступать, мы уже знаем: ищем в дизассемблированном тексте программы вызовы этих функций и находим их по 004011С8:
...
004011C8 call sub_401424 ; там функция, которая выодит первый наг-экран
004011CD lea eax, ds:4013C3h
004011D3 push 0
004011D5 push eax
004011D6 push dword ptr [ebp+8]
004011D9 push offset aNagdialog
004011DE push dword_4031B0
004011E4 call j_DialogBoxParamA ; вывод второго наг-экрана
...
Чтобы предотвратить появление наг-экранов, можно забить нопами все эти инструкции. Но есть способ лучше! (с) Найдем место, откуда происходит переход на самый первый call, вызывающий наг-экран. Вот это место:
...
0040115D cmp ax, 9C42h
00401161 jz short loc_4011A5
00401163 cmp ax, 9C43h : идентификатор меню для показа наг-экранов
00401167 jz short loc_4011C8 ; переход на вызовы наг-экранов
00401169 cmp ax, 9C44h
0040116D jz short loc_4011EB
...
На первый взгляд, это немного странный код: много сравнений и переходов. Очевидно, что это та часть оконной функции программы, которая обрабатывает сообщения меню. Чтобы навсегда отучить программу показывать наги, нужно пропатчить ее по 00401163 – чуть-чуть подправить инструкцию сравнения – поставить cmp ax, FFFFh (опкод – 663DFFFF), что заставит программу ждать сообщения с идентификатором пункта меню, имеющим значение FFFFh. Но такого идентификатора для меню нет (программа ждет 9C43h), значит, пункт, вызывающий появление наг-экранов, никогда не будет отрабатывать, и, соответственно, мы больше никогда не увидим наг-экранов.
Теперь найдем серийник для своего ника. В этом нам поможет SoftIce. Запустим его и скажем
bpx lstrcmpa
Теперь он будет ждать любого вызова функции lstrcmp и прерываться на ней. Запускаем программу, вводим свой ник в поле для имени и что-нибудь в поле для сериала. Нажимаем кнопку Chеck, всплывает SoftIce – сработал вызов функции lstrcmp. Нажимаем F11, пока не окажемся в коде исследуемой программы. Видим примерно следующее (в вашем случае адреса будут другими):
...
001B:00401599 68B8334000 push 004033B8
001B:0040159E 68B8324000 push 004032B8
001B:004015A3 E810020000 сall [KERNEL32!lstrcmpа]
...
Смотрим, что находится в памяти по адресам 004033B8 и 004032B8:
d 004033B8 l 20
d 004032B8 l 20
Там тот сериал, который мы скормили программе и пустота. Видимо, в этом месте серийник проверяется на равенство пустой строке. Если посмотреть код выше, то можно увидеть вызовы функции lstrlen, которая определяет длину строки. Нажимаем F5, чтобы продолжить выполнение программы: может быть, есть еще вызовы lstrcmp. Снова всплывает SoftIce, переходим в код crackme, смотрим:
...
001B:0040170C 68B8334000 push 004033B8
001B:00401711 68B8324000 push 004032B8
001B:00401716 E89D000000 сall [KERNEL32!lstrcmpа]
...
Теперь определим, что передается функции: указатель на строку с сериалом, который программе дали мы, а второй – указатель на строку символов, запишем их. Проверяем – это и есть валидный серийник! Теперь осталось разобраться с генерацией сериала. Вот код (Delphi), этот кейген не позволяет создавать сериал для имени, длина которого больше 15 символов:
function GenerateKey(S: String): String;
const pass = 'JD39-CK4-5QV345'; // «ключ кодирования»
var S: Array [0..15] of Byte; // массив кодов символов для имени
D: Array [0..15] of Byte; // массив кодов символов для результата
P: Array [0..15] of Byte; // массив кодов символов для ключа
A, B, C, I, J: Integer;
begin
Result := '';
// обнуляем массивы
ZeroMemory (@S, SizeOf (S));
ZeroMemory (@D, SizeOf (D));
ZeroMemory (@P, SizeOf (P));
// получаем коды символов имени и ключа
for C := 0 to Length (Edit1.Text) do S[C] := Ord (S[C + 1]);
for C := 0 to Length (pass) do P[C] := Ord (pass[C + 1]);
C := 1;
I := 0;
J := 0;
// кодируем
while S[I] <> 0 do begin
A := S[I] xor P[I];
A := A * 69;
B := not A;
B := B and $0FFF;
B := B mod 26;
B := B + 65;
// вставляем разделитель в сериал
if ((C and $0FFF) mod 5) = 0 then begin
D[J] := 45;
Inc (J);
end;
D[J] := B;
Inc (J);
Inc (I);
Inc (C);
end;
// возвращаем результат
for C := 0 to J - 1 do Result := Result + Chr (D[C]);
end;
Осталось самое интересное – вставить в программу свой код, чтобы при вызове пункта меню Аbout она показывала наше сообщение. Это можно сделать несколькими способами. Мы не будем изобретать велосипед, и просто модифицируем уже имеющийся код. Как помнится, когда мы избавили программу от наг-экранов, то довольно большой кусок программы просто перестал выполняться – можно воспользоваться этим, а можно поправить вызов оригинального Аbout – так и поступим.
Сделаем так, чтобы при выборе пункта меню Аbout программа показывала messagebox с нашим сообщением. Поскольку, crackme использовала в своей работе вызовы функций MesageBox и DialogBox, то никаких сложностей у нас возникнуть не должно. В случае, если бы она их не использовала, то нужно было бы сначала определить адреса вызова этих функций в user32.dll и только потом делать к ним call’ы.
Вставим наши сообщения, исправив не используемые строки. Запишем по 0040303C ‘Cracked!’, 0, 0 (старое значение = ‘NAGDIALOG’, 0) и по 0040314A ‘Cracked by Zool! Tnkx to FaNt0m for his crackmes!’, 0 (там было несколько строк, но все они теперь не используются).
Кстати, давно надо было сказать, что любое неаккуратное вмешательство в код программы может привести к печальным последствиям – программа перестанет работать. Думаю, что этот вывод очевиден, но все же заостряю на этом ваше внимание.
Теперь находим место, в котором происходит вызов диалога Аbout. Это происходит по 00401187:
...
00401187 lea eax, ds:40121Eh
0040118D push 0
0040118F push eax
00401190 push dword ptr [ebp+8]
00401193 push offset aAboutdialog
00401198 push dword_4031B0
0040119E call j_CreateDialogParamA
004011A3 jmp short loc_401215
...
Эти инструкции занимают ровно 28 байт (до джампа, его трогать не надо), нам нужно уложиться в этот размер. Заменяем существующие инструкции следующими:
00401187 push 2040h
0040118C lea eax, [0040303C]
00401192 push eax
00401193 lea eax, [0040314A]
00401199 push eax
0040119A push 0
0040119C call 0040176А ; вызов MesageBox
004011A1 nop
004011A2 nop
Запускаем программу и проверяем. Если при выборе пункта меню Аbout появляется наше окошко, значит, мы все сделали правильно. Если программа не запускается или падает при попытке вывести Аbout, значит, что-то пошло не так (с) и нужно проверить адреса функции и передаваемых параметров. Поверьте, у меня тоже все не с первого раза заработало.
0110
Таргет: crackme6.exe
Рулесы:
- Create a file which holds a valid key
- Find out what a valid key is
- No patching! (don't make it say you have a valid key wihtout having one)
- No resource hacking ;)
Перевод такой:
- Создай файл, который содержит валидный ключ
- Найди, что представляет собой валидный ключ
- Никаких патчей! (не заставляй прогу говорить, что у тебя есть валидный ключ, не имея его на самом деле)
- Не хакай ресурсы
Инструменты:
IDA 3.76
Солюшен:
Для начала запустим программу и попытаемся проверить ключевой файл. Она выдает сообщение о том, что файл не найден. Прекрасно. Для того, чтобы открыть файл в Win используется функция CreateFile. Вот ее описание:
CreateFile создает или открывает существующие объекты и возвращает хэндл, который может быть использован для доступа к: файлам, пайпам, мэйлслотам, коммуникационным ресурсам, дисковым устройствам (только под WinNT), консолям и каталогам (только открытие)
HANDLE CreateFile(
LPCTSTR lpFileName, // указатель на имя файла
DWORD dwDesiredAccess, // режим доступа (чтение-запись)
DWORD dwShareMode, // режим разделения ресурса
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // указатель на атрибуты безопасности
DWORD dwCreationDistribution, // каким образом создавать
DWORD dwFlagsAndAttributes, // атрибуты файла
HANDLE hTemplateFile // хэндл файла с атрибутами для копирования
);
Следует заметить, что в случае ошибки функция возвращает не хэндл файла, а INVALID_HANDLE_VALUE = FFFFFFFFh в регистре еах. Дизассемблируем crackme и ищем вызовы CreateFile:
...
004010E1 push 0
004010E3 push 80h
004010E8 push 3 ; открывает, а не создает
004010EA push 0
004010EC push 1
004010EE push 80000000h
004010F3 push offset aKeyfile_dat ; смещение на строку с именем файла
004010F8 call j_CreateFileA ; вызов CreateFile
...
Теперь найдем имя файла: keyfile.dat. Это уже кое-что, создаем файл на диске, в каталоге запуска программы. Делаем проверку: программа ругается, что ключ не верен, значит, она читает из файла и на основе прочитанного делает свои далеко идущие выводы.
Для чтения из файла используется функция ReadFile:
ReadFile читает данные из файла, начиная с позиции, определяемой файловым указателем, синтаксис:
BOOL ReadFile(
HANDLE hFile, // хэндл файла, из которого читать
LPVOID lpBuffer, // адрес буфера, в который производится чтение
DWORD nNumberOfBytesToRead, // сколько байтов читать
LPDWORD lpNumberOfBytesRead, // сколько байтов реально прочитано
LPOVERLAPPED lpOverlapped // адрес структуры для данных
);
Функция возвращает 0, если из файла не удалось произвести чтение, и любое другое число в случае успешного завершения операции (если верить MS Win32 Developer’s Reference).
...
0040111B push 0
0040111D push offset unk_403096
00401122 push 0Ah
00401124 push offset unk_40308C ; буфер для ключа
00401129 push dword_403088 ; хэндл файла ключа
0040112F call j_ReadFile ; чтение из файла ключа
00401134 cmp eax, 0 ; проверка результата
00401137 jnz short loc_40114E ; прыгает, если удачно прочитала
...
Переходим на 0040114E и смотрим, что программа делает после того, как прочитала ключ в буфер:
...
0040114E lea edx, ds:40308Ch ; загружает в еах адрес буфера
00401154 add edx, 5 ; смещается на 5 символов
00401157 cmp byte ptr [edx], 31h ; проверяет по коду его равенство 31h
0040115A jnz short loc_40116C ; прыгает, если не равен
0040115C add edx, 3 ; смещается еще на 3 символа
0040115F cmp byte ptr [edx], 33h ; проверяет по коду равенство 33h
00401162 jnz short loc_40116C ; если не равен, прыгает
00401164 inc edx ; смещается на следующий символ
00401165 cmp byte ptr [edx], 30h ; проверяет по коду равенство 30h
00401168 jz short loc_40116C ; прыгает, если равен
...
Символы в буфере нумеруются, начиная с нуля. Итак, получается, что программа сверяет 6-й символ с «1» (код 31h), 8-й символ с «3» (33h) и 9-й символ с «0» (код 30h). Причем последний – 9-й символ не должен быть равен 30h! Теперь у нас достаточно информации, чтобы сформировать верный ключ:
в тексте: x x x x x 1 x x 3 3
в хексах: 78 78 78 78 78 31 78 78 33 33
На месте «х» могут быть любые символы. Проверяем: ключ работает.
0111
Таргет: сrackme7.exe
Рулесы:
- Eliminate the protection by patching
- Make sure the program doesn't show any message boxes when "Check" is hit
- Bonus: Write a patcher
- Bonus: Write a tutorial and send it to me and i'll put it on my webpage
Русским языком:
- Уничтожьте защиту, путем патча
- Удостоверьтесь, что программа не показывает никаких сообщений, когда нажимаешь "Check"
- Бонус: напиши патчер
- Бонус: напиши туториал
Инструменты:
RegMon 6.12
IDA 3.76
Солюшен:
Смысл задачи в том, чтобы снять trial-защиту. Программа показывает количество попыток (первоначально их 30). Значит, где-то она должна хранить их счетчик. Хранить его можно в каком-то файле или в системном реестре. Воспользуемся утилитой RegMon и отследим ключи реестра, к которым crackme обращается, когда отрабатывает пункт меню Check. Находим: HKCU\Software\FaNt0m's Crackme #7\. Открываем ключ чем-нибудь (я использовал обычный regedit.exe). В defаult (тип REG_DWORD) находится число – это и есть значение счетчика.
Можно удалить ключ или обнулить переменную, но нам нужно сделать так, чтобы программа вообще не обрабатывала счетчик.
Для работы с реестром существуют свои функции. Для того, чтобы поместить в реестр значение используется функция RegSetValue:
RegSetValue связывает переменную с заданным ключом:
LONG RegSetValue(
HKEY hKey, // хэндл ключа, в который записывать переменную
LPCTSTR lpSubKey, // адрес подключа
DWORD dwType, // тип переменной
LPCTSTR lpData, // адрес переменной
DWORD cbData // размер данных
);
Ищем вызовы RegSetValue. Находим вызов функции в процедуре, первоначально создающей ключ в реестре:
...
004010FA call j_RegOpenKeyA ; пытается открыть ключ
004010FF test eax, eax ; проверяет результат
00401101 jz short loc_40113F ; прыгает, если ключ открылся
...
0040111C call j_RegCreateKeyA ; создает ключ
00401121 push dword_403228
00401127 push offset dword_403224
0040112C push 4
0040112E push 0
00401130 push 0
00401132 push dword_403220
00401138 call j_RegSetValueExA ; записывает в реестр значение счетчика
...
а потом пытаемся подняться к тому месту, откуда был вызван этот кусок кода:
...
00401072 call sub_4010E1
00401077 jmp short loc_401095
...
Ничего не говорит? Попробуем найти место, откуда это было вызвано. Видим следующее:
...
0040105E cmp ax, 9C41h
00401062 jz short loc_40104F
00401064 cmp ax, 9C43h ; идентификатор пункта меню
00401068 jz short loc_401072 ; наш джамп
0040106A cmp ax, 9C42h
0040106E jz short loc_401079
00401070 jmp short loc_401095
...
Этот код обрабатывает оконные сообщения, касающиеся меню. Патчим по 00401064 – меняем инструкцию на следующую: cmp ax, FFFFh. Тем самым мы отключаем пункт меню, и программа больше не сможет произвести trial-проверку.
1000
Таргет: crackme8.exe
Рулесы:
- Unpack the exe and dll (should be really easy)
- Remove the SoftICE detection (by patching, unless you know another way)
- Find a correct serial for your nick
- Bonus: Make up a keygen
- Bonus: Write up a tut and I'll post it on my webpage!
Что означает примерно следующее:
- Распакуй файлы (это не трудно)
- Удали детектирование SoftICE (пропатчи, если не знаешь другого способа)
- Найди верный серийник для своего ника
- Бонус: напиши кейген
- Бонус: напиши туториал
Инструменты:
upx 1.25
IDA 3.76
HIEW 6.11
SoftIce 4.05
Солюшен:
Для начала, определим, чем упакованы файлы. Для этого можно воспользоваться чем-нибудь типа PETools, а можно просто открыть файлы в виндовом блокноте и внимательно посмотреть. Строка «This file is packed with the UPX executable packer» говорит о том, что файлы упакованы пакером upx.exe. Для того, чтобы их распаковать, нужно сказать:
upx.exe -d crackme8.exe
upx.exe -d cm8.dll
Теперь посмотрим на программу в работе. Запустим и попытаемся ввести имя и серийник. Программа выводит сообщение о том, что обнаружен SoftIce. Если нет, значит, вы его еще не запустили. (Небольшое уточнение: у меня и с запущенным сайсом программа не выводила никакого предупреждения: WinXP Pro eng build 2600 без сервиспаков, SoftIce 4.05 for WinNT (патченный для работы под WinXP – может патчик такой хитрый?)).
Дизассемблируем программу и ищем вызов messagebox’a с предупреждением:
...
0040131B push 10h
0040131D push offset aSofticeDetecti
00401322 push offset aSofticeWasDete
00401327 push 0
00401329 call j_MessageBoxA ; показывает предупреждение
...
Анализируем код функции выше:
...
0040130E call j_CreateFileA ; пытается открыть какой-то файл
00401313 cmp eax, 0FFFFFFFFh ; проверяет результат
00401316 jnz short loc_40131B ; прыгает, если создать не удалось
...
Находим в секции данных имя файла, который пытается создать эта процедура: ‘\\.\SICE’, 0. Если создать файл не удалось, то делается переход, и выводится предупреждение. Видимо, при работе SoftIce создает какие-то файлы – вот на этой особенности и построено детектирование его запуска. Для того, чтобы заставить программу поверить в то, что SoftIce не запущен, нужно по 00401316 занопить условный переход. Получится:
...
0040130E call j_CreateFileA
00401313 cmp eax, 0FFFFFFFFh
00401316 nop ; вот так
00401317 nop ; и вот так
00401318 xor eax, eax
...
Теперь найдем валидный сериал. Как и раньше, вбиваем в поле для имени свой ник, а в поле для серийника все, что захочется. Теперь ставим брейкпоинт на функцию lstrcmpa. Нажимаем кнопку Verify, всплывает SoftIce, нажимаем F11 и смотрим на код. Видим примерно следующее:
...
017B:100012F1 EBC7 jmp 100012BA
017B:100012F3 C60700 mov byte ptr [edi], 00
017B:100012F6 6824300010 push 10003024 ; проверить!
017B:100012FB FF7508 push [ebp+08] ; проверить!
017B:100012FE E829000000 call [KERNEL32!lstrcmpa] ; сравнение сериала
017B:10001303 83F800 cmp eax, 00000000
017B:10001306 7409 je 10001311
...
Проверяем, что передается в функцию сравнения строк в качестве параметров. По 10003024 и находится искомый серийник. Записываем, проверяем в программе – задача решена.
Кейген (Delphi):
function GenerateKey(Nick: String): String;
label lbl1, lbl2, lbl3, lbl4, lbl5;
var S: Array [0..63] of Byte; // коды символов имени
D: Array [0..63] of Byte; // коды символов серийника
I: Integer;
begin
// очистим рабочие массивы
ZeroMemory (@S, SizeOf (S));
ZeroMemory (@D, SizeOf (D));
// получим коды символов имени
for I := 0 to Length (Nick) do S[I] := Ord (Nick[I + 1]);
I := Length (Nick) + 1; // длина имени + 1 = количество проходов цикла кодирования
asm
lea eax, S // загружаем адрес массива кодов имени
mov esi, eax
lea eax, D // загружаем адрес массива кодов сериала
mov edi, eax
mov ecx, I // инициализируем счетчик цикла
lbl1:
mov ebx, 42h
add ebx, ecx
movzx eax, byte ptr [esi]
inc esi
xor eax, ebx
rol eax, 5
shr eax, 2
mov bl, 10h
div bl
lbl2:
add ah, 30h
cmp ah, 40h
jle lbl3
add ah, 7
lbl3:
mov [edi], ah
inc edi
cmp al, 10h
jge lbl4
shl ax, 8
mov al, 11h
jmp lbl2
lbl4:
dec ecx
cmp ecx, 0
jz lbl5
jmp lbl1
lbl5:
end;
Result := '';
// формируем строку-результат
For I := 0 to Length (Nick) + 1 do Result := Result + Chr (D[I]);
end;
1001
Реверс инжиниринг – вещь непростая, поэтому очень важно первое впечатление, которое новичок получает при знакомстве с ним. Очень важно не отпугнуть его сложностью, а наоборот привлечь новыми знаниями и возможностями. Crackmes’ы FaNt0m’a как раз и должны привлечь своей простотой, дать мощный стимул к дальнейшему обучению.
Эти crackmes’ы довольно легкие, однако, очень интересные, и свою цель они выполняют прекрасно: наглядно демонстрируют основные виды программных защит и способы их обхода, позволяют получить и развить необходимые для взлома программ навыки.
Время, потраченное на решение этих маленьких задачек, определенно проведено с пользой.
22 октября 2004 г.
|
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |