Часть 3. Практика под виндой
Теорию можно продолжать бесконечно, тем-более что тема эта настолько обширна, что закончить её можно только ко ‘второму пришествию’. Без практики она теряет смысл, поэтому пробежавшись по макушкам основ, рассмотрим фундаментальные команды ассемблера на практике.
В качестве компилятора буду использовать FASM, т.к. считаю его синтаксис самым правильным из всех ассемблеров. В отличии от своих собратьев, в нём нет двумысленных команд, да и в использовании он до неприличия прост - компилирует без всяких батников, по одной клавише F9 в своём окне. Весит всего 1 Мб и бесплатно валяется на сайте разработчика, от куда
Ещё нам понадобится отладчик, чтобы воочию лицезреть происходящее внутри программы. Под виндой большой популярностью пользуется OllyDBG, причём советую скачать не вторую,
3.0. Приложение для тестов
Если с инструментами разобрались, напишем Win32 приложение, которое в дальнейшем будем использовать как ‘бокс’ для вставки в него различных инструкций. Запустим FASMW.EXE и скопи\пастим в окно его редактора такой код:
Теперь просто жмём F9 и должна появиться такая форточка..
Если при компиляции получили ошибку, значит у вас система 64-битная - тогда смотрите примеры из папки Example. Если окно всё-же появилось, значит исходник скомпилировался - разберём его на атомы..
Весь код состоит из четырёх частей. В заголовке строка PE-GUI указывает fasm’у собрать оконное, а не консольное приложение. Инклуд почти всегда будет только один
За секцией-данных сразу начинается секция-кода. Метка start: (с двоеточием в конце) является точкой-входа в программу, что на буржуйский манер звучит как ‘Entry-Point’ или просто ЕР. Если открыть эту программу в отладчике, то первым его Breakpoint будет как раз ЕР.
Для вывода диалогового окна используем Win-API ‘MessageBox’, которой в качестве параметров передаём адреса выводимых строк. Последний её параметр(0) задаёт кол-во буттонов в окне. Попробуйте установить его в 1, получите две кнопки ОК\Отмена.
Следом за первой API идёт вторая ‘ExitProcess’, которая прихлопнет нашу программу и выгрузит её из памяти Win. В самом хвосте кода, расположилась секция-импорта виндовых API-функций - это макрос такой ‘.end метка’. В качестве стартовой метки ЕР можно использовать любое имя (не обязательно start, главное – потом подставить в макрос .end это-же имя.
3.1. Основные команды ассемблера.
3.1.0. Организация циклов
Циклы в ассемблере можно строить по разному - тут зависит от поставленной задачи.
Допустим нужно посчитать длину строки и мы знаем, что она заканчивается нулём. Тогда обнуляем один из свободных регистров, и на каждом шаге (итерации) увеличиваем этот регистр на 1. Как-только найдём терминальный нуль, выходим из цикла и в регистре-счётчике получаем длину строки. В этом случае нужна проверка и переход назад, если условие ложно. Пример такого цикла представлен ниже:
В комментах есть все пояснения, поэтому повторяться не буду.
Загрузите эту программу в отладчик OllyDbg и потрассируйте её клавишей
По окончании цикла, в
Другой вариант цикла – это когда мы знаем длину строки, и нам нужно что-нибудь с ней сделать. В этом случае можно пойти по такому-же алгоритму, только не увеличивать счётчик, а поместив в него длину наоборот уменьшать его, пока не получим в нём нуль. Однако есть и другой вариант.
Для организации циклов, в ассемблере предусмотрена специальная команда LOOP, которая требует в качестве счётчика строго регистр
Попробуем таким макаром, например, зашифровать всю строку. Метод шифрования сейчас не важен.. пусть будет обычная операция XOR с любым 1-байтным ключом, таким как
3.1.1. Команды для работы со-строками
Часто приходится иметь дело с большими массивами данных. Это может быть и просто копирование блоков памяти с места-на-место, или например повторить одну операцию несколько раз. Для таких случаев в ассемблере предусмотрены т.н. ‘цепочечные’ команды. Плюсы их в том, что не нужно вручную двигать указатели – они сдвигаются на автомате:
Для организации циклов к ним применяется префикс REP. Как и в случае с командой LOOP, счётчиком служит регистр
В качестве демки напишем программу, которая выведет в окно Environment машины, на которую попала. Среди этих данных будет: имя пользователя, версия Win, пути установки и прочая информация. Эти данные система копирует в самый подвал адресного пространства любого процесса, который запускает на исполнение. Это адрес
Как видно из этого скрина, текстовые строки хранятся там в виде ‘Unicode’, т.е. каждый символ кодируется двумя байтами, а не одним как в ASCII. Второй байт всегда нуль. Значит наша задача поставить указатель источника
По сути, окружение можно получить и другими, более изящными методами, а это лишь демонстрация строковых команд, половина из которых осталась за бортом. Фрагмент результата представлен выше.
3.1.2. Команды CALL и RET – передача и возврат управления
Эта парочка должна всегда ходить вместе. Команда CALL кладёт на вершину стека адрес-возврата и вызывает какую-нибудь процедуру, а команда RET должна находится в конце вызванной процедуры, и сняв со стека адрес-возврата, передаёт на него управление. Рассмотрим такой код в отладчике..
Инструкция RET вообще окутана тайнами, к которым прибегают многие асматики. Дело в том, что для процессора регистр
И тут на помощь приходит инструкция RET. Достаточно через PUSH поместить указатель на любую область памяти в стек, и следом вызвать инструкцию RET, как
3.1.3. Условные переходы
Под занавес главы, вспомним таблицу условных переходов, где литер N втиснутый по-середине, просто обращает условие на противоположное. Литер E в конце, уточняет условие и означает равно (например, JBE - это "ниже или равно", а JNG - "не больше"):
Теорию можно продолжать бесконечно, тем-более что тема эта настолько обширна, что закончить её можно только ко ‘второму пришествию’. Без практики она теряет смысл, поэтому пробежавшись по макушкам основ, рассмотрим фундаментальные команды ассемблера на практике.
В качестве компилятора буду использовать FASM, т.к. считаю его синтаксис самым правильным из всех ассемблеров. В отличии от своих собратьев, в нём нет двумысленных команд, да и в использовании он до неприличия прост - компилирует без всяких батников, по одной клавише F9 в своём окне. Весит всего 1 Мб и бесплатно валяется на сайте разработчика, от куда
Ссылка скрыта от гостей
. Теперь создайте в любой директории папку FASM и просто распакуйте в неё архив – fasm не требует установки в систему.Ещё нам понадобится отладчик, чтобы воочию лицезреть происходящее внутри программы. Под виндой большой популярностью пользуется OllyDBG, причём советую скачать не вторую,
Ссылка скрыта от гостей
, т.к. она возвращает более детальную информацию. Кроме того, для исследования ядерных структур, позже нужен будет и WinDbg от Microsoft. Этот зверь может отлаживать как пользовательские программы, так системные драйвера, до чего Оля пока не доросла. Ну и по мелочи, это дизассемблеры W32Dasm и HackersDisassembler, хек-редакторы HIEW32 и HxD. Калькулятор винды в инженерном виде, можно вообще закрепить на рабочем столе. Для начала этого хватит за глаза.3.0. Приложение для тестов
Если с инструментами разобрались, напишем Win32 приложение, которое в дальнейшем будем использовать как ‘бокс’ для вставки в него различных инструкций. Запустим FASMW.EXE и скопи\пастим в окно его редактора такой код:
C-подобный:
format PE gui
include 'win32ax.inc'
;------
.data
capt db 'Прога v1.0',0
text db 'Программируем на FASM под Win32.',13,10
db 'Для выхода из программы нажмите ОК!',0
;------
.code
start: invoke MessageBox,0,text,capt,0
invoke ExitProcess,0
;------
.end start
Если при компиляции получили ошибку, значит у вас система 64-битная - тогда смотрите примеры из папки Example. Если окно всё-же появилось, значит исходник скомпилировался - разберём его на атомы..
Весь код состоит из четырёх частей. В заголовке строка PE-GUI указывает fasm’у собрать оконное, а не консольное приложение. Инклуд почти всегда будет только один
‘win32ax.inc’
– он универсальный и подходит для большинства случаев. Дальше идёт секция-данных, в которой задаём имя окну, и текст в его тушке. 13,10
означает перевод строки, сама-же строка должна быть в кавычках и заканчиваться нулём. Под виндой все строки нуль-терминальные, в отличии от ДОС, где маркером окончания строки служит символ доллара($).За секцией-данных сразу начинается секция-кода. Метка start: (с двоеточием в конце) является точкой-входа в программу, что на буржуйский манер звучит как ‘Entry-Point’ или просто ЕР. Если открыть эту программу в отладчике, то первым его Breakpoint будет как раз ЕР.
Для вывода диалогового окна используем Win-API ‘MessageBox’, которой в качестве параметров передаём адреса выводимых строк. Последний её параметр(0) задаёт кол-во буттонов в окне. Попробуйте установить его в 1, получите две кнопки ОК\Отмена.
Следом за первой API идёт вторая ‘ExitProcess’, которая прихлопнет нашу программу и выгрузит её из памяти Win. В самом хвосте кода, расположилась секция-импорта виндовых API-функций - это макрос такой ‘.end метка’. В качестве стартовой метки ЕР можно использовать любое имя (не обязательно start, главное – потом подставить в макрос .end это-же имя.
3.1. Основные команды ассемблера.
3.1.0. Организация циклов
Циклы в ассемблере можно строить по разному - тут зависит от поставленной задачи.
Допустим нужно посчитать длину строки и мы знаем, что она заканчивается нулём. Тогда обнуляем один из свободных регистров, и на каждом шаге (итерации) увеличиваем этот регистр на 1. Как-только найдём терминальный нуль, выходим из цикла и в регистре-счётчике получаем длину строки. В этом случае нужна проверка и переход назад, если условие ложно. Пример такого цикла представлен ниже:
C-подобный:
format PE gui
include 'win32ax.inc'
;------
.data
capt db 'Считаем длину строки',0
text db 'Программируем на FASM под Win32.',13,10
db 'Для выхода из программы нажмите ОК!',0
;------
.code
start: sub ecx,ecx ; ECX будет счётчик - обнуляем его.
mov esi,text ; ESI = указатель на начало строки
@cycle: cmp byte[esi],0 ; цикл! сравнить байт из ESI с нулём.
je @ok ; переход на метку, если равно (Jump Equal)
inc ecx ; иначе: счётчик +1
inc esi ; сдвигаем указатель ESI на сл.позицию в строке
jmp @cycle ; вернуться в начало цикла!
@ok: invoke MessageBox,0,text,capt,0
invoke ExitProcess,0
;------
.end start
Загрузите эту программу в отладчик OllyDbg и потрассируйте её клавишей
F8
.По окончании цикла, в
ECX
получим длину строки равной 45h
(69) символов.Другой вариант цикла – это когда мы знаем длину строки, и нам нужно что-нибудь с ней сделать. В этом случае можно пойти по такому-же алгоритму, только не увеличивать счётчик, а поместив в него длину наоборот уменьшать его, пока не получим в нём нуль. Однако есть и другой вариант.
Для организации циклов, в ассемблере предусмотрена специальная команда LOOP, которая требует в качестве счётчика строго регистр
ECX
. Как только он обнуляется, LOOP автоматически останавливает цикл. Кроме того, эта инструкция сама и уменьшает ECX
на каждой итерации, и нам не нужно двигать его руками. Вообщем как-раз то, что доктор прописал.Попробуем таким макаром, например, зашифровать всю строку. Метод шифрования сейчас не важен.. пусть будет обычная операция XOR с любым 1-байтным ключом, таким как
7Eh
. Вот пример такого алгоритма (будем считать, что длину строки(69) мы уже знаем из предыдущего кода):
C-подобный:
.code
start: mov ecx,69 ; ECX = длина строки для LOOP
mov esi,text ; ESI = адрес строки
@crypt: xor byte[esi],7Eh ; цикл! шифруем байт из ESI нашим ключом
inc esi ; сл.байт в строке..
loop @crypt ; повторить ECX-раз!
invoke MessageBox,0,text,capt,0
invoke ExitProcess,0
.end start
3.1.1. Команды для работы со-строками
Часто приходится иметь дело с большими массивами данных. Это может быть и просто копирование блоков памяти с места-на-место, или например повторить одну операцию несколько раз. Для таких случаев в ассемблере предусмотрены т.н. ‘цепочечные’ команды. Плюсы их в том, что не нужно вручную двигать указатели – они сдвигаются на автомате:
- LODSB - чтение в AL из ESI;
- STOSB - запись AL в EDI;
- MOVSB - копирование из ESI в EDI;
- CMPSB - сравнивает байт EDI, с байтом ESI (сравнивает строки);
- SCASB - сравнивает байт AL, с байтом EDI (поиск символа в строке).
ESI
, а EDI
– это всегда приёмник. Окончание(B) в названии команды означает байт, соответственно можно заменить его на: W=2-байта (word), или D=4-байта (dword). В зависимости от этого окончания, указатели авто\увеличиваются на 1, 2 или 4-байта.Для организации циклов к ним применяется префикс REP. Как и в случае с командой LOOP, счётчиком служит регистр
ECX
, который при каждом шаге уменьшается на 1. Ниже приведены модификации префикса REP для строковых команд:
Код:
REP - применяется для MOVSB (копирование строки)
REPE - применяется для CMPSB (сравнение строк)
REPNE - применяется для SCASB (поиск символа в строке)
0х10000
, хотя для самой программы выделяется база 0х400000
. Адреса ниже 0х10000 система резервирует для отлова нулевых указателей. Это 64К-байтный сегмент от нуля, куда приложениям доступ закрыт:Как видно из этого скрина, текстовые строки хранятся там в виде ‘Unicode’, т.е. каждый символ кодируется двумя байтами, а не одним как в ASCII. Второй байт всегда нуль. Значит наша задача поставить указатель источника
ESI
на адрес 0х10000
, а приёмник EDI
прицелить на буфер в своей программе. Теперь, чтобы отсеять нули, читать будем из ESI
по 2-байта, а сохранять в EDI
по одному. Тогда получим ASCII-строку, которую можно будет сбоксить в мессагу функцией MessageBox(). Каждая строка текста заканчивается Unicode нулём (выделен синим), а после всего блока данных идёт топкое болото нулей, т.е. маркером окончания блока - будет 4 нуля подряд. Это на словах громоздко, а на деле..
C-подобный:
format PE gui
include 'win32ax.inc'
;-------
.data
capt db 'Блок окружения системы',0
buff db 2048 dup(0) ; выделяем 2К буфер в секции-данных
;-------
.code
start: mov esi,10000h ; указатель на источник
mov edi,buff ; адрес приёмника
@unicode:
lodsw ; берём 2-байта из ESI в АХ
cmp ax,0 ; конец строки?
jz @test ; да - на метку! (Jump Zero)
stosb ; иначе: сохраняем в EDI только байт AL (из всего АХ)
jmp @unicode ; продолжить.. (lodsw сам увеличивает esi на 2)
;// Встретилась пара нулей! Проверка на конец блока данных --------
@test: cmp dword[esi],0 ; проверить 4-байта из ESI на нуль
je @stop ; если нарвались на болото нулей - стоп!
mov ax,0a0dh ; иначе: перевод строки 13,10
stosw ; вставить их в буфер!
jmp @unicode ; продолжить..
@stop: invoke MessageBox,0,buff,capt,0 ; выводим мессагу на экран
invoke ExitProcess,0 ; выход из приложения!
.end start
По сути, окружение можно получить и другими, более изящными методами, а это лишь демонстрация строковых команд, половина из которых осталась за бортом. Фрагмент результата представлен выше.
3.1.2. Команды CALL и RET – передача и возврат управления
Эта парочка должна всегда ходить вместе. Команда CALL кладёт на вершину стека адрес-возврата и вызывает какую-нибудь процедуру, а команда RET должна находится в конце вызванной процедуры, и сняв со стека адрес-возврата, передаёт на него управление. Рассмотрим такой код в отладчике..
C-подобный:
format PE gui
include 'win32ax.inc'
;-------
.data
capt1 db 'Окно процедуры',0
text1 db 'Это вызов процедуры из приложения!',0
capt2 db 'Выход',0
text2 db 'Программа завершена успешно!',0
;-------
.code
start: call Merylin ;// вызов процедуры!
nop
nop
invoke MessageBox,0,text2,capt2,0
invoke ExitProcess,0
;- Процедура ------
proc Merylin
invoke MessageBox,0,text1,capt1,0
ret ;// возврат управления
endp
.end start
Инструкция RET вообще окутана тайнами, к которым прибегают многие асматики. Дело в том, что для процессора регистр
EIP
играет роль собаки-поводыря - куда он указывает, туда процессор и идёт. Значит чтобы перехватить управление у программы, нужно перехватить регистр EIP
. Только он не поддаётся никаким уговорам и ‘причесать’ его не так-уж просто. В него нельзя записать значение, его нельзя поместить в стек, и вообще с ним ничего нельзя делать.И тут на помощь приходит инструкция RET. Достаточно через PUSH поместить указатель на любую область памяти в стек, и следом вызвать инструкцию RET, как
EIP
сменит свой нрав на милость и прямиком последует за нами. Это старый приём, который назвали ‘фиктивным вызовом функций’.3.1.3. Условные переходы
Под занавес главы, вспомним таблицу условных переходов, где литер N втиснутый по-середине, просто обращает условие на противоположное. Литер E в конце, уточняет условие и означает равно (например, JBE - это "ниже или равно", а JNG - "не больше"):
Код:
+----------------+-----------------+-------------------+
| Символьный | Знаковый | По-флагам |
+----------------+-----------------+-------------------+
| JB - ниже | JL - меньше | JS - знак(-) |
| JE - равно | JZ - ноль | JC - перенос |
| JA - выше | JG - больше | JP - чётный |
+----------------+-----------------+-------------------+
Последнее редактирование: