As

Прикреплённые файлы к статье

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

Начало

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

Что такое DLL?

DLL – Dynamic Link Library (Динамически подключаемая библиотека). Она представляет собой исполняемый файл (PE). С точки зрения структуры заголовков исполняемого
файла DLL почти не отличается от EXE. Впрочем, в данной статье я не буду описывать заголовки PE, а ограничусь лишь упоминанием таблицы импорта. Сразу скажу, что
у DLL есть точка входа, в DLL хранят ресурсы и код, таким образом программа, загружая к себе в
адресное пространство DLL, получает доступ ко всему что в ней хранится. Сейчас нам важен код, и далее мы поговорим о нем. Для того чтобы получить доступ к коду, нужно узнать, где именно
в памяти хранится нужный нам код. Для этого в DLL существует таблица экспорта, она содержит информацию о соответствии между определёнными смещениями в DLL и
их именами или номерами. Имена хранятся в формате ANSI. В принципе, по смещению заданному именем (или номером) может находиться как код функции, так и данные,
но обычно там – код.

API

API – application programming interface (интерфейс программирования приложений). В API входят системные функции, которые вызывает программа при необходимости.
Все эти функции хранятся в системных DLL-библиотеках и указаны там в таблицах экспорта. Сразу оговорюсь, что при вызове этих функций значение регистра rsp
должно быть кратно 16, если вы вызываете функции не используя стандартные макросы это необходимо учитывать, со стандартными макросами об этом можно не
беспокоиться, если конечно вы не “дёргаете” самостоятельно rsp.

Импорт

Импорт функции из библиотеки(DLL) подразумевает загрузку этой DLL в адресное пространство процесса с последующим использованием этой функции. Импорт бывает
статическим и динамическим. Статический импорт подразумевает загрузку DLL и получение указателей на функции при старте процесса, и освобождение DLL при его
завершении. Всё это делает операционная система. Динамический импорт заключается в том, что DLL вы можете загружать и выгружать когда угодно, и получать
указатели на функции когда угодно.

Статический импорт

Статический импорт осуществляется с помощью таблицы импорта, в которой записаны имена библиотек и имена функций, импортируемых из них. Начинается таблица импорта
в коде с 2-х слов: data import, а заканчивается end data. Внутри таблицы импорта нужно написать, какие библиотеки нам нужны, и какие функции мы будем
импортировать. Делается это примерно так (слеш в конце строки означает, что эту и следующую строку надо интерпретировать как одну):

data import
library kernel32,'kernel32.dll',user32,'user32.dll'
import kernel32,\
		CreateFileA,'CreateFileA',\
		CloseHandle,'CloseHandle'

import user32,\
		MessageBoxA,'MessageBoxA'
end data

В данном примере мы сначала объявляем, какие библиотеки нам нужны и каждому имени библиотеки ставим в соответствие идентификатор (идентификатор – это набор символов,
с помощью которого можно обращаться к соответствующим объектам, например: имя переменной – идентификатор, имя метки – тоже, и т.д.): kernel32 – для kernel32.dll,
user32 – для user32.dll. Потом с помощью макроса import мы перечисляем функции, которые хотим импортировать из данной библиотеки. Первый аргумент этого макроса —
идентификатор из library, а дальше идут пары: имя функции для использования из кода, и имя функции в кавычках, как оно записано в таблице экспорта. Для примера
рассмотрим программу, которая выводит на консоль долгожданную строку “Hello world!”:

format PE64 console; формат -- 64 бита, подсистема -- console
entry start;точка входа -- start

include 'win64a.inc';заголовки с нужными макросами

section '.code' readable executable code;секция кода

proc start;точка входа
	locals;локальные переменные
	temp dd ?;переменная, в неё будет записано количество символов при вводе и выводе
	endl
	frame

	invoke GetStdHandle,STD_OUTPUT_HANDLE;получение описателя вывода
	lea rdx,[Text]
	lea r9,[temp]
	invoke WriteConsoleA,rax,rdx,14,r9,0;вывод на консоль

	invoke GetStdHandle,STD_INPUT_HANDLE;получение описателя ввода
	lea rdx,[temp]
	lea r9,[temp]
	invoke ReadConsoleA,rax,rdx,1,r9,0;читаем с консоли до нажатия Enter

	invoke ExitProcess,0;завершаем процесс
	endf
	ret;возврат из процедуры
endp

section '.data' readable writeable data;секция данных
data import;начало таблицы импорта
library kernel32,'kernel32.dll';импортируем только из kernel32.dll
import kernel32,\;функции:
	   GetStdHandle,'GetStdHandle',\
	   WriteConsoleA,'WriteConsoleA',\
	   ReadConsoleA,'ReadConsoleA',\
	   ExitProcess,'ExitProcess'
end data
Text db 'Hello world!',13,10;текст для вывода

Сразу скажу, что без строки include 'win64a.inc' нельзя использовать макросы, которые тут применяются. Здесь мы объявляем одну локальную переменную temp
для записи туда ненужных для нас данных. Дальше мы вызываем функцию GetStdHandle и первым (и единственным) параметром к ней идёт STD_OUTPUT_HANDLE.
STD_OUTPUT_HANDLE – это константа. Константы объявляются так: имя константы=значение, в данном случае: STD_OUTPUT_HANDLE = -11, пока что я не буду на этом
заострять внимание. Сама же функция GetStdHandle возвращает (в rax) хендлы ввода, вывода или ошибок. Хендл (или описатель) — это некоторое “магическое число” нужное
для доступа к некоторым системным ресурсам. В этой программе мы один раз получаем хендл вывода, и в другой раз — хендл ввода, эти хендлы можно спользовать сколько
угодно раз в программе. После получения описателя вывода мы сразу передаём его в функцию WriteConsoleA
Она выводит на консоль текст и имеет параметры:

    1. Хендл вывода
    2. Указатель на строку в формате ANSI
    3. Количество символов для вывода
    4. Указатель на переменную типа dword, в которую будет записано число выведенных символов
    5. должен быть 0

Заметьте, здесь я положил в rdx значение второго параметра, поэтому макрос invoke не добавит дополнительных обращений к rdx. Важно: если вы используете для передачи
параметров в функцию регистры rcx,rdx,r8,r9, тогда вы должны передавать rcx как первый параметр, rdx – второй, r8 – третий, r9 – четвёртый. Никогда не делайте так:

lea rcx,[SomeData]
invoke SomeFunc,FirstParam,rcx,...

Это может привести к ошибкам.
Далее получаем хендл вывода и передаём в функцию ReadConsoleA. Она читает введённую
строку из консоли, у неё аналогичные параметры. Только первый параметр — хендл ввода, а второй — указатель на область памяти, в которую будут записаны прочитанные с консоли символы. Как вы могли заметить,
я передал в качестве указателя на строку адрес той же переменной temp и передал в качестве длины входящей строки 1 символ. Я это сделал, т.к. ни количество введённых
символов, ни введённая строка нас в данном случае не интересуют. В конце вызывается функция ExitProcess, она завершает процесс c кодом завершения, передаваемым в
качестве параметра, поэтому вызов ret в конце процедуры не обязателен.

И тут нужно сказать про директиву include… Она… Как бы это… Ну, вон там вот… В общем, она вставляет в исходник содержимое файла, имя которого следует после неё.
Например строка include 'win64a.inc' вставляет в исходник содержимое файла “win64a.inc”, он входит в стандартную поставку fasm-а в папке “INCLUDE”.
Если вы его откроете, то увидите ещё кучу разных include-ов.

Вообще-то говоря, можно было бы использовать и юникодную версию функций, просто вместо A в конце имени функции нужно ставить W, и Text объявить не как db, а как du, да и
у ReadConsoleW последний параметр не обязан быть нулём.

Очевидно, что постоянно прописывать все импортируемые функции не удобно, поэтому в стандартной поставке fasm-а есть файлы в INCLUDE\API, которые облегчат нам жизнь.
Просто вместо макроса import вставляете include 'api\kernel32.inc', аналогично и со всеми библиотеками для которых есть инклуды в этой папке:

data import;начало таблицы импорта
library kernel32,'kernel32.dll'
include 'api\kernel32.inc'
end data

Помимо макроса import в этих инклудах используется макрос api. Этот макрос предназначен для функций, у которых есть 2 варианта: ANSI и Unicode. У ANSI функций
в конце названия стоит буква A, у Юникод — W. Так вот, макрос api позволяет пользоваться функцией не указывая A или W. Работает это так: если в начале файла стоит
include 'win64a.inc', то все обращения к ReadConsole (например) будут заменены на ReadConsoleA, а если стоит include 'win64w.inc', то ReadConsoleW.
Пример той же программы, только – Юникод:

format PE64 console
entry start

include 'win64w.inc'

section '.code' readable executable code

proc start
	locals
	temp dd ?
	endl
	frame

	invoke GetStdHandle,STD_OUTPUT_HANDLE
	lea rdx,[Text]
	lea r9,[temp]
	invoke WriteConsole,rax,rdx,14,r9,0

	invoke GetStdHandle,STD_INPUT_HANDLE
	lea rdx,[temp]
	lea r9,[temp]
	invoke ReadConsole,rax,rdx,1,r9,0

	invoke ExitProcess,0
	endf
	ret
endp

section '.data' readable writeable data
data import;начало таблицы импорта
library kernel32,'kernel32.dll'
include 'api\kernel32.inc'
end data
Text du 'Hello world!',13,10

У меня есть программа для генерации import-инклудников. Её вместе с моими инклудами я прикреплю к статье, правда после создания инклудника иногда возникают 2 проблемы:

    1. В разных инклудах совпадают функции, соответственно, где-то нужно их удалить или закомментировать.
    2. Имена функций совпадают со словами, используемыми fasm-ом, надо менять имя функции !!НЕ ИМЯ В КАВЫЧКАХ, А ИМЯ ИДЕНТИФИКАТОРА!!

Есть так же импорт по ординалам: это импорт не по имени функции, а по номеру, но он в документированном API не встречается… вроде бы.

Динамический импорт

Динамический импорт осуществляется в явном виде программой. Для начала нужно либо загрузить нужную библиотеку, либо получить указатель на уже загруженную. Чтобы получить указатель
на уже загруженную библиотеку надо использовать функцию GetModuleHandle (A/W) с единственным параметром — указателем на имя библиотеки,
она вернёт адрес, где расположена библиотека, либо же 0 в случае ошибки. В WinApi (не в Native API) в качестве строк используются строки с нулем на конце, т.е. последний
символ должен быть нулевым. Чтобы загрузить библиотеку надо использовать функцию LoadLibrary (A/W), у которой тоже 1 параметр:
имя библиотеки, эта функция возвращает либо указатель на загруженную библиотеку, либо 0 в случае ошибки. На каждый вызов LoadLibrary в вашем коде должен присутствовать
вызов FreeLibrary, освобождающий память, в которой хранится библиотека. Ну и наконец, чтобы найти нужную функцию, используется функция GetProcAddress с 2-мя параметрами: первый — адрес загрузки библиотеки, второй — указатель на строку в
формате ANSI с именем функции. Обратите внимание, что нет функций GetProcAddress(A/W), есть только одна, и она принимает строку в ANSI.
Пример:

format PE64 console
entry start

include 'win64a.inc'

section '.code' readable executable code

proc start
	locals
	temp dd ?
	endl
	frame

	lea rcx,[Msvcrt_name]
	invoke LoadLibrary,rcx;загружаем библиотеку msvcrt.dll
	mov [hMsvcrt],rax

	lea rdx,[printf_name]
	invoke GetProcAddress,[hMsvcrt],rdx;получаем адрес функции printf
	mov [printf],rax

	lea rcx,[K32_name]
	invoke GetModuleHandle,rcx;получаем адрес kernel32.dll

	lea rdx,[WriteConsole_name]
	invoke GetProcAddress,rax,rdx;получаем адрес функции WriteConsole
	mov [WriteConsole],rax

	;Дальше всё то же самое, как и в предыдущем примере, за исключением вызова printf

	invoke GetStdHandle,STD_OUTPUT_HANDLE
	lea rdx,[Text]
	lea r9,[temp]
	invoke WriteConsole,rax,rdx,19,r9,0

	lea rcx,[Text1]
	invoke printf,rcx;вызов printf

	invoke GetStdHandle,STD_INPUT_HANDLE
	lea rdx,[temp]
	lea r9,[temp]
	invoke ReadConsole,rax,rdx,1,r9,0

	invoke FreeLibrary,[hMsvcrt];освобождаем память

	invoke ExitProcess,0
	endf
	ret
endp

section '.data' readable writeable data
data import;начало таблицы импорта
library kernel32,'kernel32.dll'
import kernel32,\
	   GetModuleHandle,'GetModuleHandleA',\
	   LoadLibrary,'LoadLibraryA',\
	   FreeLibrary,'FreeLibrary',\
	   GetProcAddress,'GetProcAddress',\
	   GetStdHandle,'GetStdHandle',\
	   ReadConsole,'ReadConsoleA',\
	   ExitProcess,'ExitProcess'
end data
Text db 'WriteConsole used',13,10
Text1 db 'printf used',13,10,0

printf_name db 'printf',0
WriteConsole_name db 'WriteConsoleA',0
Msvcrt_name db 'msvcrt.dll',0
K32_name db 'kernel32.dll'

hMsvcrt dq ?;адрес загрузки msvcrt.dll
printf dq ?;адрес printf
WriteConsole dq ?;адрес WriteConsole

Скажу так-же, что если загрузить одну и ту же библиотеку n раз, тогда не будет n копий этой библиотеки, а будет одна, но освободится она только после n вызовов FreeLibrary.

На этом, как говорится, “полномочия этой статьи — всё”.

Комментарии

l_inc18.04.2016 02:20

Надо сказать, в этой статье серьёзных технических недочётов или упущений я не заметил. Есть, правда, педагогические, стилистические и грамматические, а из технического несколько мелочей. Но кое-какие комментарии, я думаю, не помешают, раз автор (исходя из реакции на предыдущие комментарии) не против. 🙂

DLL – Dynamic Link Library (Динамически подключаемая библиотека).

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

программа, загружая к себе в адресное пространство DLL, получает доступ ко всему что в ней хранится

Стоит заметить, что и код dll тоже получает доступ ко всему, что находится в программе. Причём, возможность воспользоваться этим доступом у dll есть сразу в момент попытки её загрузить, что часто используется во вредоносах.

Имена хранятся в формате ANSI

Вообще ANSI – это организация. Хотя исторически эта аббревиатура неудачно используется для ссылки на программу, WinAPI-функцию или даже на строку, требующую применения символьных кодовых страниц, по-хорошему правильно всё-таки сказать “в кодировке ASCII”.

section '.data' readable writeable data;секция данных

Все данные в секции используются только для чтения. Вполне можно убрать у неё writeable и традиционно переименовать в '.rdata' .

Заметьте, здесь я положил в rdx значение второго параметра, поэтому макрос invoke не добавит дополнительных обращений к rdx.

Это, конечно, неплохо, но этот макрос поддерживает спецификатор addr . Поэтому следующий код сделал бы то же самое:

invoke WriteConsoleA,rax,addr Text,14,addr temp,0

И, кстати, неплохо бы не привыкать использовать числовые константы. Вместо 14 лучше объявить автоматически вычисляемую sizeof.Text или ещё лучше countof.Text или lengthof.Text , т.к. sizeof всё же обычно относится к размеру в байтах, а не к длине в символах.

Важно: если вы используете для передачи параметров в функцию регистры rcx,rdx,r8,r9, тогда вы должны передавать rcx как первый параметр, rdx – второй, r8 – третий, r9 – четвёртый. Никогда не делайте так … Это может привести к ошибкам.

Надо сказать, передача rax в качестве аргумента — тоже дело рискованное, т.к. rax используется для вычисления дальнейших аргументов, если их нельзя положить в стек напрямую.

поэтому вызов ret в конце процедуры не обязателен.

Да вообще-то и точку входа-то оформлять как процедуру в связи с этим не стоит, т.к. не нужны ни пролог, ни эпилог, ни сохранение регистров. Правда, как без процедуры правильно оформить точку входа — без подробного разбора совсем не очевидно.

Просто вместо макроса import вставляете include 'api\kernel32.inc', аналогично и со всеми библиотеками для которых есть инклуды в этой папке

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

Пример той же программы, только – Юникод

Ну раз уж в этом примере используются универсальные имена функций, а не с суффиксом W, то хорошо бы и вместо du использовать TCHAR, чтобы при обратной замене на win64a.inc программа работала адекватно. И, возможно, стоило также упомянуть про подключение соответствующего файла кодировки (например, include 'encoding\utf8.inc' ), иначе юникод будет правильным только для младшей половины ASCII-таблицы.

надо использовать функцию GetModuleHandle (A/W) с единственным параметром — указателем на имя библиотеки, она вернёт адрес, где расположена библиотека, либо же 0 в случае ошибки

В общем случае как GetModuleHandle, так и LoadLibrary возвращают не адрес, а хэндл, о чём и говорит документация. Этот хэндл отличается от адреса загрузки библиотеки специальным значением пары-тройки младших бит, которые выставляются в 1 для нестандартно подгруженных библиотек (например, с флагом LOAD_LIBRARY_AS_IMAGE_RESOURCE ).

На каждый вызов LoadLibrary в вашем коде должен присутствовать вызов FreeLibrary

На каждый успешный вызов LoadLibrary . Возможно это и очевидно, но стоит уточнить, что если DllMain библиотеки вернёт ошибку, то библиотека будет выгружена автоматически без FreeLibrary .