Часть 3. Практика под виндой
Теорию можно продолжать бесконечно, тем-более что тема эта настолько обширна, что закончить её можно только ко ‘второму пришествию’. Без практики она теряет смысл, поэтому пробежавшись по макушкам основ, рассмотрим фундаментальные команды ассемблера на практике.
В качестве компилятора буду использовать FASM, т.к. считаю его синтаксис самым правильным из всех ассемблеров. В отличии от своих собратьев, в нём нет двумысленных команд, да и в использовании он до неприличия прост - компилирует без всяких батников, по одной клавише F9 в своём окне. Весит всего 1 Мб и бесплатно валяется на сайте разработчика, от куда его и стянем.. Теперь создайте в любой директории папку FASM и просто распакуйте в неё архив – fasm не требует установки в систему.
Ещё нам понадобится отладчик, чтобы воочию лицезреть происходящее внутри программы. Под виндой большой популярностью пользуется OllyDBG, причём советую скачать не вторую, а первую его версию, т.к. она возвращает более детальную информацию. Кроме того, для исследования ядерных структур, позже нужен будет и WinDbg от Microsoft. Этот зверь может отлаживать как пользовательские программы, так системные драйвера, до чего Оля пока не доросла. Ну и по мелочи, это дизассемблеры W32Dasm и HackersDisassembler, хек-редакторы HIEW32 и HxD. Калькулятор винды в инженерном виде, можно вообще закрепить на рабочем столе. Для начала этого хватит за глаза.
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
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 - чётный |
+----------------+-----------------+-------------------+
Последнее редактирование: