Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #102, ИЮНЬ 2007 г.

Программная оборона

Леонид «Cr@wler» Исупов

Хакер, номер #102, стр. 078

(crawlerhack@rambler.ru)

Защита PE-файлов голыми руками

Десять лет назад, когда инструменты для дизассемблирования еще не были так легки в эксплуатации и распространены, реверс-инжиниринг не был массовым явлением. Сейчас же любой среднестатистический юзер, воспользовавшись отладчиком, может распотрошить внутренности твоей программы и узнать, как работают созданные тобой защитные механизмы. Этого нельзя допустить. Значит, нужно учиться создавать методы, затрудняющие анализ.

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

Для нашей работы потребуется известная версия компилятора MASM - MASM32. Мы напишем простейший код, результатом работы которого будет выдача MessageBox’а с надписью «Hello, World!». Исследовав скомпилированный файл под отладчиком (а он может быть любым, я использовал OllyDBG), мы посмотрим, как можно создать простейший раскодировщик, используя прямые вставки двоичного кода.

Наша программа

Итак, приступим к работе. Вот код нашей программы:

.386

.model flat,stdcall ; модель памяти - flat

option casemap:none

; подключение необходимых библиотек:

include masm32includewindows.inc ;

include masm32includekeel32.inc ;

includelib masm32libkeel32.lib ;

include masm32includeuser32.inc ;

includelib masm32libuser32.lib ;

; секция данных

.data

alert_upper db "Simply program",0

alert_text db "Hello, World!",0

; секция кода

.code

start:

invoke MessageBox, NULL, addr alert_text, addr alert_upper, MB_OK

invoke ExitProcess, NULL

end start

Сохрани текст в файле c:masm32binex.asm (разумеется, если путь к компилятору другой, то вместо c:masm32 будет что-то иное) и создай объектный файл командой ml /c /coff /Cp ex.asm. Он будет в формате COFF (ключ ‘/c’). После этого доверши компиляцию командой «link /SUBSYSTEM:WINDOWS /LIBPATH:c:masm32lib /SECTION:.text, RWE ex.obj». Ключ ‘/SECTION’ позволит установить атрибут записи на секцию кода, которая именуется .text (смешная деталь: я уже скомпилировал файл и открыл его в OllyDbg для отладки, после этого решил проверить еще раз правильность параметров компиляции и долго не мог понять, почему LINK выдает ошибку 1104 :)). Компиляция успешно завершена, теперь откроем файл ex.exe под отладчиком. Что ж, листинг, выданный OllyDbg, дает полное представление о работе программы:

00401000 PUSH 0

00401002 PUSH ex.00403000 ;Title = "Simply program"

00401007 PUSH ex.0040300F ;Text = "Hello, World!"

0040100C PUSH 0; |hOwner = NULL

0040100E CALL <JMP.&user32.MessageBoxA>

00401013 PUSH 0 ;ExitCode = 0

00401015 CALL <JMP.&keel32.ExitProcess>

0040101A JMP DWORD PTR DS:[<&keel32.ExitProcess>

00401020 JMP DWORD PTR DS:[<&user32.MessageBoxA>]

Естественно, никакой антиотладки нет, файл ничем не пакован, и следовательно, он без труда обрабатывается отладчиком. Отметим точку входа в нашей программе (Entry Point, или EP): 00401000h. Откроем программу в hex-редакторе, мой любимый - WinHex. Точку входа в WinHex легко найти: нужно лишь произвести поиск байтов первых инструкций (комбинация клавиш <Ctrl-Alt-F>), которые нам любезно подскажет OllyDbg. В нашем случае это «6A006800...». Этого вполне достаточно, и WinHex показывает нам нужные байты, где пресловутая точка и расположена. Они начинаются по адресу 400h. Что мы сделаем? Мы вручную закодируем инструкции, напишем кодировщик и вставим его двоичный код в файл. Для простоты зашифруем инструкции операцией XOR 35. Тогда кодировщик будет выглядеть следующим образом:

00401000:jmp 00401028 ;Hex - коды перехода - EB26h

00401002:..... ;Закодированные инструкции...

00401028:mov ecx, 27

0040102D:push edx

0040102E:push ecx

0040102F:mov edx, [ecx+00401000]

00401035:xor edx,35

00401038:mov [ecx+00401000], edx

0040103E:pop ecx

0040103F:pop edx

00401040:loop 0040102D

00401042:jmp 00401002

Знаю, что ты сейчас скажешь: «Это неправильно! Кодировщик затирает байт перехода!». На это я отвечу: «Да, но мы уже использовали этот байт, и он нам больше не понадобится, так как переход на внедренные инструкции нужен лишь однажды - сразу после старта программы» :).

Рассмотрим код более подробно. Первая инструкция по адресу 00401028 - установка счетчика цикла декодировки (ECX - для команды loop), следующие две команды - сохранение регистров ECX и EDX в стек, для того чтобы впоследствии их восстановить и не оставить следов после работы декодировщика. mov edx, [ecx+00401000] помещает в регистр EDX закодированные команды, смещенные относительно точки входа программы на значение ECX. Причем, если учесть, что команда loop не увеличивает, а уменьшает счетчик ECX, становится понятно, что декодирование происходит по принципу «от хвоста к голове», то есть от конца закодированных данных программы по направлению к их началу. Следующая команда xor edx,35 в комментариях не нуждается - это декодировка значения, помещенного в edx. Разумеется, если ты решил выбрать более сложный способ кодировки, на месте этой инструкции должен находиться соответствующий набор команд. На следующем шаге декодированный код помещается на свое место в памяти. Следующие две инструкции восстанавливают регистры из стека (pop ecx, pop edx), далее находится команда счетчика loop 0040102D. В самом конце располагается безусловный переход на адрес памяти, начиная с которого будут находиться уже раскодированные после выполнения цикла декодировщика инструкции. Здесь, как ты, наверное, заметил, без трудностей не обойтись, так как в начало секции кода нужно вставить переход на декодировщик, то есть необходимо двигать весь код, а это приведет к необходимости впоследствии высчитывать и новое положение таблицы импорта. Но волков бояться - в лес не ходить.

А вот и код нашего декодировщика в шестнадцатеричном виде:

{B92700000052518B910010400083F235899100104000595AE2EBEBBE}

Сейчас я расскажу, как его получить.

План действий

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

1. Открыть файл под отладчиком и определить, где располагается точка входа. У нас этот адрес равен 00401000h, но, по правде говоря, точкой входа это можно назвать с натяжкой. Данный адрес - это Image Base + Entry Point. Но мы все равно будем говорить «точка входа» для простоты.

2. Записать машинные коды первых 2-3 инструкций, которые мы видим в отладчике, и не забыть посмотреть, какую длину занимают все исполняемые инструкции, вместе взятые (после их окончания обычно расположен массив нулей, созданный компилятором для выравнивания, так что мы не испортим файл, вставив свой кодировщик).

3) Открыть файл любым шестнадцатеричным редактором и найти среди двоичных кодов первые инструкции программы, которые мы записали. Далее нужно вырезать эти инструкции (длину их мы определили в предыдущем пункте) и вставить их на 2 байта ниже того места, откуда мы их взяли. В нашем примере смещение относительно начала в WinHex было равно 400h (адресация иная, нежели в OllyDBG, так как WinHex отображает MZ- и PE-заголовки, которые скрываются многими отладчиками), значит, мы вставим инструкции, начиная с адреса 402h. Перемещение это необходимо для того, чтобы записать в начало файла инструкцию перехода на декодировщик. Она занимает 2 байта (EB26h) и выглядит так: jmp 00401028h (адрес этот не случаен, он является окончанием исполняемых инструкций нашей программы). Введем по смещению 400h наш переход.

4. Над инструкциями, начиная с адреса 402h, нужно проделать преобразование. Мы выбрали простейший классический способ - побитовое логическое сложение XOR по модулю 35h. Выделяем 26h байт. Предупреждая твой вопрос, скажу: да, и последний нулевой байт этой последовательности тоже нужно выделять, так как он является частью исполняемой инструкции. Если его оставить так, то впоследствии (при декодировании) он превратится в 35h, программа перейдет по неверному адресу и все накроется тазом. Преобразование в WinHex производится выбором пункта меню «Правка -> Модифицировать». Итак, теперь все инструкции закодированы.

5. Коды инструкций декодировщика нужно ввести сразу после окончания шифрованных инструкций кода программы, то есть начиная с адреса 428h.

Как же узнать их шестнадцатеричные значения (их я привел выше)? Предлагаю самый простой путь: ввести инструкции прямо в отладчике OllyDbg, затем скопировать hex-значения, выделив набранные команды и выбрав из контекстного меню правой кнопки мыши «Binary -> Binary copy». Но тогда вводи инструкции строго с адреса 00401028h, иначе адреса перехода для инструкций jmp или loop могут быть неверно интерпретированы, ведь в отладчике они считаются по смещению относительно текущего адреса. Я категорически против того, чтобы копировать модифицированные байты в двоичный файл прямо из-под отладчика, хотя он на это и способен. Можно случайно совершить неверное действие, а шестнадцатеричный редактор - это профессиональный и удобный инструмент, специально предназначенный для таких операций. Итак, копируй и вставляй инструкции кодировщика. Кстати, когда нажмешь <Ctrl-V> для вставки, выбирай в появившемся меню пункт ASCII-HEX, так как данные в буфере обмена являются HEX-дампом. На этом этапе нужно сохранить файл под новым именем, например ex1.exe.

6. Почти готово! Осталась одна проблема - таблица импорта. Она сместилась на энное количество байт, так как мы вставляли посторонние данные в секцию кода. Можешь попробовать запустить программу, но гарантирую, что она не заработает должным образом без перемещения таблицы, так как вызов библиотечных функций не состоится. Для того чтобы все пришло в норму, высчитаем новое положение таблицы импорта для модифицированного файла. Утилитами мы пользоваться не будем, а сделаем все вручную. Откроем в WinHex оба файла: измененный и оригинальный. Теперь попробуем найти таблицу. Для этого нажмем <Ctrl-F> и введем имя функции, которая встречается почти во всех Win32-программах: ExitProcess. Проделаем то же и для второго файла. Теперь сравним: в оригинальном файле смещение строки равно 65Eh, а в модифицированном - 696h. Значит, нам нужно удалить из модифицированного файла нулевые байты в количестве, равном разности этих значений: (696h-65Eh)=53h. Причем удалить не где угодно, а перед таблицей импорта. Перемести ползунок прокрутки чуть вверх, и ты увидишь непаханое поле нулей. Удали 38h нулевых байт. Кстати, WinHex отображает в строке состояния размер выделенного блока, что очень удобно. Все готово! Запускай модифицированный файл. Разрази меня гром, это работает!

Счастливого плавания :)

Признаюсь честно, я был очень удивлен, когда узнал, что добрая половина антивирусов никак не реагирует на подобное «народное творчество». Хочется воскликнуть: «Если уж анализ инструкций никуда не годится, проверяйте хотя бы CRC!» Но даже и это, увы, зачастую не делается. Конечно, CRC все же лучше подправить.

Чем хорош этот метод? Он наглядно показывает принцип работы протекторов/пакеров (а также некоторых вирусов). Кроме того, на его основе можно разрабатывать более сложные защитные механизмы. Более же всего ценен тот опыт, который ты приобрел, ведь немного попрактиковавшись, ты сможешь кодировать файлы, не используя ничего, кроме стандартного отладчика debug.exe и собственного могучего интеллекта.

ИНФО

Эта статья, конечно, не претендует на полноту изложения темы. Для того чтобы лучше «въехать» в создание, правку и защиту двоичного кода, прочти «Дизассемблирование в уме» и «Образ мышления IDA» Криса. Кроме всего прочего, не помешает налечь и на низкоуровневое программирование, воспользовавшись учебниками, коих в Сети великое множество.

DVD

На нашем DVD ты найдешь WinHex и OllyDBG, использованные в этой статье, а также все исходники и компилированные файлы-примеры (исходный и упакованный).

Содержание
ttfb: 4.8491954803467 ms