Грибы свопята

+--------------------------------------------------------------------------------------------------------------------------[release: 07.11.04]----+
|
|
|
|
|
|
|
  _|_|    _|_|_|    _|_|    _|_|_|    _|_|    _|  _|    _|  _|_|    _|_|_|      _|  _|    _|_|_|
_|      _|  _|  _|  _|  _|  _|        _|  _|      _|_|_|_|  _|  _|  _|        _|_|_|_|_|  _|    
_|      _|_|_|_|_|  _|  _|  _|_|      _|_|    _|  _| |  _|  _|_|    _|_|_|      _|  _|    _|_|_|
_|      _|  _|  _|  _|  _|  _|        _|      _|  _|    _|  _|          _|    _|_|_|_|_|      _|
  _|_|    _|_|_|    _|_|    _|_|_|    _|      _|  _|    _|  _|      _|_|_|      _|  _|    _|_|_|
|
|
|
|
|
|
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------+
+-------------------------------------------------[0x05: Введениe в реверс инжиниринг]------------------------------------------------+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|

Исповедь начинающего крякера или
Введение в реверс инжиниринг
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 г.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+-----[content]-----------------------------------------------------------------------------------------------------------------------[mail us]-----+