Современные решения

для защиты Windows приложений

и восстановления исходного кода
Автор: Сергей Чубченко. Дата публикации: 29.08.2005

Дизассемблер своими руками


Как часто Вы слышали про дизассемблеры? Думаю не стоит объяснять что это такое и для чего это нужно. Вы наверняка не раз отлаживали свой проект в Olly Debugger’е или искали ошибку в ассемблерном коде штатными средствами. Во всех подобных продуктах есть дизассемблер, который довольно быстро разбирает скомпилированный машинный код из EXE файла на ассемблерный код который можно изучать и изменять. В этой статье я опишу как можно самостоятельно написать простенький дизассемблер под свои нужды.

Введение

Для начала думаю нелишне напомнить, для чего все же может пригодиться самодельный дизассемблер. Особенно это актуально, если Ваша работа связана с анализом вирусов в антивирусной лаборатории. В этом случае вы постоянно сталкиваетесь с EXE упаковщиками. Такими программами, которые способны сжимать бинарный код в 2 или более раза, при этом оставив его работоспособным. Из бесплатных представителей таких программ Вы наверняка знаете тот же UPX. А задумывались ли Вы как пишут распаковщики для таких продуктов? Вряд ли. А зря! Ядро распаковщиков, особенно статических (которые распаковывают программу без запуска) основано именно на дизассемблере, который в совокупности с анализатором кода позволяет понять код распаковываемой программы и распаковать ее используя нужный алгоритм упаковщика. Если Вы когда-нибудь решите написать такой распаковщик, то без самодельного дизассемблера не обойтись.

Дизассемблирование

Вы наверняка задаетесь вопросом: "А как это вообще работает и почему бы не написать дизассемблер с нуля?". Как бы это не было тривиально - ответ на него можно получить только осознав, насколько именно Вам сложно написать дизассемблер используя только Intel’овские мануалы на архитектуру процессора. Я лишь кратко рассмотрю принципы кодирования ассемблерных команд, чтобы принцип дизассемблирования был более прозрачным. А дальше решать Вам!

Каждый байт секции кода программы участвует в формировании той или иной машинной инструкции. Чтобы правильно определить начало следующей команды нужно правильно (как бы это сделал процессор) дизассемблировать предыдущую. Для этого необходимо четко представлять себе формат команд ассемблера. Команды процессора Intel кодируются следующим образом:

Структура команд ассемблера

При этом единственный обязательный параметр это "код операции", остальные используются в зависимости от сложности и навороченности той или иной команды. Например "преффикс" используется довольно редко, зато префиксы могут поистине творить чудеса над командами, к примеру префикс 66h меняет размерности регистров и адресов при этом в 16 битной программе этот префикс позволяет юзать 32 битные регистры, а в 32 битной - 16 битные. Поля modR/M позволяют определить формат данных, с которыми оперирует программа, будь то регистры, адреса и прочее. Поле SIB расширяет возможности адресации 32 битного режима. Процессор узнает о присутствии этого поля по битам 100b в поле R/M. Далее идут непосредственно смешения и операнды, описанные в структуре modR/M+SIB.
Расшифровкой именно этих команд и занимается дизассемблер. Чтобы его написать самому с нуля потребуется море сил и времени. Хотя есть два пути. Наиболее простой из них - составить таблицу опкодов и используя нее дизассемблировать команды (именно этот принцип используется в большинстве дизассемблеров длин и в дельфевом декомпилере DeDe). Второй путь - самый сложный. Он подразумевает полное отсутствие таблиц и использование для дизассемблирования только тех данных что описывают мануалы от авторов процессора.
Какой путь выберете Вы - решать только Вам. Я предлагаю на начальном этапе использовать уже готовые решения. Об одном из таких решений читайте ниже.

Выбираем компонент

Как ни странно, если очень сильно постараться, можно найти целых два бесплатных дизассемблера, которые можно внедрить в свою программу на Delphi. Первый можно взять из свободно распространяющегося исходника декомпилятора Delphi - DeDe. При желании этот исходник Вы можете взять на wasm.ru и самостоятельно его изучить. Мы же рассмотрим второй дизассемблер, поставляющийся в виде компонента для Delphi и бесплатный для некоммерческого использования. Называется данный компонент madDisAsm и входит в состав большой библиотеки компонентов называющейся madCollection. Взять эту коллекцию можно отсюда: http://madshi.bei.t-online.de/madCollection.exe. Ссылка актуальна на момент выхода статьи. Теперь когда Вы скачал все что нужно, давайте разберемся, что мы имеем. А имеем мы 8 мощных компонентов, среди которых даже есть madBasic (бешеный барсик :)). Как Вы понимаете, из данного мощного пакета нам потребуется только madDisAsm. Его и рассмотрим.

madDisAsm

Данный компонент практически недокументирован. Документированы только прототипы функций и структур. Примеров же использования нет ни одного, отсюда следует что разбираться придется самим. Чтобы использовать данный компонент в нашем проекте подключим его в разделе Uses модуля так:

uses madDisAsm;

Теперь давайте рассмотрим главные функции данного компонента, которых две. Первая позволяет дизассемблировать код по команде. При этом все надстройки над кодом Вы можете реализовывать самостоятельно в ходе декомпилирования. Прототип функции имеет вид:

function ParseCode (code: pointer; var disAsm: string) : TCodeInfo; overload;
code - pointer на бинарный код программы, который мы хотим дизассемблировать;
disAsm - переменная в которую будет занесена первая дизассемблированная строчка.
TCodeInfo - структура дизассемблированной строчки. одним из элементов этой структуры является ссылка на следующую строчку, которую мы можем использовать для цикличного вызова функции. Вот полный прототип данной структуры:

TCodeInfo = record IsValid : boolean; // определяет валидность pointer’а на код Opcode : word; // Опкод, один ($00xx) или два ($0fxx) байта ModRm : byte; // ModRm байт (о нем я уже писал), если присутствует, иначе 0 Call : boolean; // эта инструкция call? Jmp : boolean; // эта инструкция jmp? RelTarget : boolean; // адрес относительный (или абсолютный) ? Target : pointer; // абсолютный адрес PTarget : pointer; // pointer на информацию в коде PPTarget : TPPointer; // pointer на pointer с информацией TargetSize : integer; // размер информации в байтах (1/2/4) Enlargeable : boolean; // может ли размер опкода быть расширенным? This : pointer; // адрес начала инструкции Next : pointer; // адрес следующей инструкции end;

Также для нас будет интересная еще одна функция. Ее особенностью является то, что она способна дизассемблировать всю функцию целиком, автоматически находя конец функции по команде retn. Вот ее прототип:

function ParseFunction (func: pointer; var disAsm: string) : TFunctionInfo; overload;

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

TFunctionInfo = record IsValid : boolean; EntryPoint : pointer; CodeBegin : pointer; CodeLen : integer; LastErrorAddr : pointer; LastErrorNo : cardinal; LastErrorStr : string; CodeAreas : array of record AreaBegin : pointer; AreaEnd : pointer; CaseBlock : boolean; OnExceptBlock : boolean; CalledFrom : pointer; Registers : array [0..7] of pointer; end; FarCalls : array of record Call : boolean; // это CALL или JMP? CodeAddr1 : pointer; // начало инструкции call CodeAddr2 : pointer; // начало следующей инструкции Target : pointer; RelTarget : boolean; PTarget : pointer; PPTarget : TPPointer; end; UnknownTargets : array of record Call : boolean; CodeAddr1 : pointer; CodeAddr2 : pointer; end; Interceptable : boolean; Copy : record IsValid : boolean; BufferLen : integer; LastErrorAddr : pointer; LastErrorNo : cardinal; LastErrorStr : string; end; end;

Есть и еще одна третья функция, которая вообще недокументированна:

function ParseFunctionEx (func: pointer; var disAsm: string, exceptAddr: Pointer; maxLines: Integer; autoDelimiters: Boolean);

Насколько я понял эта функция не возвращает структуры, зато дизассемблирует весь код нужной нам функции и кладет его в переменную disAsm. exceptAddr насколько я понял - это адрес конца дизассемблируемой функции (указывать необязательно), maxLines - число дизассемблируемых строк (если 0, то все), autoDelimiters - точно не могу сказать, но ориентировочно это - завершать ли функцию первым ret’ом или нет.

Кодим

Теперь, когда мы разобрались с работой компонента давайте писать дизассемблер! Открываем Delphi и создадим новый проект, затем поместим на форму пару Edit’ов, Memo и два CommandButton’а. В результате этих несложных манипуляций мы получим что-то похожее на интерфейс программы.

Интерфейс программы

Теперь самое время поподробнее рассказать для чего будут использоваться два текстовых поля. В первое мы будем вводить имя открываемого для дизассемблирования файла, а во второе - адрес начала дизассемблируемого кода. Так как дизассемблер понимает только pointer’ы на код - напишем функцию которая будет открывать EXE, считывать с указанного смещения код в переменную и возвращать на нее pointer. Собственно функция будет иметь вид:

function TfrmMain.GetCode(strFileName: string; strOffset: string): pointer; var hFile: integer; read_bytes: cardinal; EP_code: array[1..64000] of byte; begin //открываем файл hFile:=CreateFileA(pchar(strFileName), GENERIC_READ, FILE_SHARE_READ + FILE_SHARE_WRITE, NIL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); //если файл открыт успешно if hFile<>-1 then begin //устанавливаем файловый указатель на начало дизассемблируемого кода SetFilePointer(hFile,StrToInt(strOffset),NIL,FILE_BEGIN); //считываем 64000 байт кода ReadFile(hFile,EP_Code,64000,read_bytes,NIL); //закрываем файл CloseHandle(hFile); //возвращаем pointer на считанный код result:=@EP_Code; end else begin //если не смогли открыть файл - выходим exit; end; end;

В данной функции я использовал исключительно Win32 API. Это позволяет добиться максимальной скорости кода и простоты переносимости на другие языки программирования. Теперь напишем функцию, которая будет покомандно дизассемблировать код, pointer на который будет в нее передаваться:

function TfrmMain.Disasm(strAsm: pointer): string; var strDisAsm, strdasm: string; retval: TCodeInfo; begin //получим в strDisAsm первую строчку кода, а в retval - структуру, в которой имеется pointer на следующую ассемблерную команду retval:=madDisAsm.ParseCode(strAsm,strDisAsm); //в переменной strdasm мы будем хранить весь дизассемблированный листинг strdasm:=strDisAsm; //перебераем циклом команды до тех пор пока не встретим ret while strpos(pchar(strDisAsm),’ret’)= nil do begin //дизассемблируем очередную команду retval:=madDisAsm.ParseCode(retval.Next,strDisAsm); //добавляем ее в конец дизассемблированного листинга strdasm:=strdasm + #13#10 + strDisAsm; end; //возвращаем дизассемблированный код result:=strdasm; end;

Код перестанет дизассемблироваться, когда программа наткнется на первый ret. Если программа не найдет ret, то она продолжит декомпилировать память до тех пор пока не вызовет ошибку доступа. Не забудьте сделать проверку этого. Теперь, когда основные функции готовы - нам осталось написать только обработчики для кнопок на форме. Код кнопки Dasasm будет выглядеть так:

procedure TfrmMain.cmdDisasmClick(Sender: TObject); begin txtDisasm.Text:=Disasm(GetCode(txtFileName.Text, txtOffset.Text)); end;

Код кнопка Dasasm function будет выглядеть так:

procedure TfrmMain.cmdDisAsmFunctionClick(Sender: TObject); var strDisAsm: string; begin madDisasm.ParseFunctionEx(GetCode(txtFileName.Text, txtOffset.Text),strDisAsm,nil,0,true); txtDisasm.Text:=strDisAsm; end;

Теперь, когда дизассемблер готов, напишем тестовый проект для его проверки.

Пишем тестовый проект
Раз мы написали дизассемблер, то самое логичное писать тестовый проект на ассемблере. Самым удобным на мой взгляд редактором и компилятором ассемблера является Fasm, который плюс к своему удобству и простоте очень часто обновляется и версия к версии становится все стабильнее и лучше. Взять его можно на wasm.ru. Уже скачали? Тогда запускаем и вводим следующий код:

include ’win32ax.inc’ .data ;создаем переменные с данными Serial db ’Some program’,0 _MsgCaption db ’Disasm this’,0 .code start: ;вывод сообщения на экран, что может быть проще invoke MessageBox,0,Serial,_MsgCaption,MB_OK ;выход из программы invoke ExitProcess,0 ;установим конец процедуры, чтобы наш дизассемблер не завис retn .end start

Компилируем. Получаем EXE файл размером 2 килобайта. Да, дельфям до ассемблера далеко.

Ассемблер Fasm (Flat assembler)

Тестируем

Запускайте скорее только что написанный дизассемблер. Вводите в одно текстовое поле путь к тестовому проекту, а во второе адрес точки входа: 1024 (400h). Для простеньких ассемблерных программ адрес точки входа часто равен смещению секции кода, которое часто равно именно 400h. В любом случае эти данные всегда можно взять открыва программу в любом бесплатном PE Editor'е. Жмем теперь любую из кнопок декомпилера и видими в Memo код, напоминающий только что написанный нами, но в более строгом виде:

0011fb5c push 0 0011fb5e push $40100d 0011fb63 push $401000 0011fb68 push 0 0011fb6a call dword ptr [$40307a] 0011fb70 push 0 0011fb72 call dword ptr [$40305c] 0011fb78 ret

Как видите, все прекрасно работает!

Дизассемблер в работе

Заключение

Ну вот в общем и все что хотелось рассказать. Надеюсь, Вы без труда найдете применение написанному нами дизассемблеру.

Комментарии

отсутствуют

Добавление комментария


Ваше имя (на форуме):

Ваш пароль (на форуме):

Комментарии могут добавлять только пользователи,
зарегистрированные на форуме данного сайта. Если Вы не
зарегистрированы, то сначала зарегистрируйтесь тут

Комментарий: