Злачное место в РЕ-файлах под названием «Импорт» создало много проблем инженерам Microsoft. Базовые ошибки на этапе проектирования формата привели к тому, что в своё распоряжение мы получили массу недокументированных возможностей, которые Microsoft не может исправить, пока полностью не изменит подход. В предыдущих частях был рассмотрен NT-заголовок и структура программных секций, а в этой уделим внимание уязвимым местам импорта, что именно и где нужно искать реверс-инженерам.
1. Взгляд на импорт по микроскопом
Рассвет портируемых РЕ (Portable Executable) пришёлся на Win95, которая была заточена под cpu i386 с тактовой частотой ~40 МHz. Поэтому разработчики формата должны были предусмотреть механизм для сокращения времени загрузки файла в память, а львиную долю в этом процессе занимал как раз импорт API из системных dll. Тяжкий груз докатился и до наших дней, в результате чего имеем целых три пути для решения одной задачи:
1. Статический импорт «Bound». Здесь имена библиотек и линейные адреса импортируемых API жёстко прошиваются в тело программы ещё на этапе компиляции. При такой схеме загрузчику не нужно тратить время на двоичный поиск функций внутри dll, и соответственно порог вхождения кода в готовность повышается. Но если версия библиотек в системе изменится, тогда могут съехать и прописанные в программе адреса, что приведёт к полному краху приложения.
2. Динамический импорт. Эта универсальная схема решает проблемы предыдущей, и является для загрузчика дефолтной по-умолчанию. Здесь в таблице-импорта хранятся только имена библиотек и функций, а линейные их адреса вычисляются загрузчиком уже после загрузки образа в память. Алгоритм хоть и занимает просто огромное кол-во времени, зато абсолютно не зависит от версий dll в системе. Собственно это и делает РЕ портируемым.
3. Отложенный импорт "Delay". Если функция вызывается пару раз где-нибудь в конце кода, можно отложить её загрузку в память до востребования. Для реализации данной схемы, компилятор вставляет в наш код свою дежурную процедуру, в теле которой имеются обычные LoadLibrary() + GetProcAddress(). Таким образом, отложенный импорт сокращает время только на первоначальную загрузку образа с диска в память, а потом обратно съедает его-же во время исполнения кода. В общем профит от этого дела сомнительный.
Наличие широкого ассортимента конечно-же хорошо, но чем проще устройство, тем стабильнее оно работает. Например, ничто не предвещает бурю, когда в программе указан лишь один из трёх механизмов – загрузчик просто прыгает на нужный участок кода, и выполняет его в штатном режиме. Теперь задумаемся, что произойдёт, когда мы укажем ему на сразу все возможные варианты импорта? Тогда лоадеру придётся выбрать из трёх самый предпочтительный, и как показали тесты, им является именно «Bound». Вот здесь и начинается самое интересное..
Скажем пришёл загрузчик в статический импорт, а там мусор c перенаправлением в «Delay», процедура load() которого почему-то находится вообще в незапланированной области памяти (типа секция-данных). Ясное дело, что после такого рода пинг-понга загрузчик всё-таки должен найти «правильный» импорт, иначе какой в этом смысл. Но что примечательно, система-то справится, а вот смогут-ли преодолеть полосу препятствий инструменты анализа, которые используют свои загрузчики образов? Правда подловить на этом Иду врядли получится.. лично я ещё не встречал исполняемых файлов, которые Ида не смогла-бы переварить. А вот с отладчиками дела обстоят куда плачевней. Кривой или не типичный импорт для них полная катастрофа, со всеми вытекающими.
На рис.ниже представлена общая картина поддержки импорта системой. Как видим это иерархия, которая берёт своё начало в каталоге «DATA_DIRECTORY». Для импорта в нём выделяются аж четыре 8-байтных записей – в первый дворд компилятор сохраняет RVA на подчинённую структуру, а во второй – размер данных в ней.
В этом производственном цеху все работают исключительно на таблицу адресов «IMPORT_ADDRESS_TABLE» или просто IAT, куда после загрузки образа в память, лоадер должен будет записать вирт.адреса всех импортируемых приложением API. При этом без разницы, какая из схем Bound, Std или Delay указана в РЕ-файле, всё-равно в центре внимания только IAT. Цифры в кругах определяют последовательность поиска импорта загрузчиком. Обратите внимание на поле «TimeDataStamp» во всех дескрипторах – оно играет немаловажную роль, о чём пойдёт речь далее.
2. Основная таблица «Image Import Table»
Отсчёт записей в каталоге начинается с нуля, а сам каталог является частью опционального заголовка. В файлах РЕ32 смещение каталога равно
78h
, а в РЕ64 88h
. Поскольку файл импортирует как минимум ExitProcess(), запись под номером #01 присутствует в каталоге всегда, указывая на таблицу импорта. Внутри таблицы размещается массив 14h-байтных дескрипторов, каждый дескриптор описывает импорт из одной dll. Если приложение использует функции из 4-х библиотек, то в таблице получим 5 дескрипторов – последний будет забит нулями, определяя конец таблицы.Первое поле «OriginalFirstThunk» в дескрипторе указывает на начало «IMPORT_LOOKUP_TABLE», в которой лежит массив 32-битных значений «ImageThunkData», по одному уже на каждую функцию API (для файлов pe64 Thunk 64-битные). Эти значения трактуются двояко и зависят от состояния старшего бита в них. Если он взведён, то остальные биты содержат ординал (номер) функции в экспорте dll, иначе это RVA-адрес записи в таблице Name, где лежит указатель на строку с именем импортируемой функции.
Импорт по ординалу скорее исключение чем правило, а потому «санк» в таблице Lookup указывает обычно на строку с именем. Имя всегда предваряется 16-битной подсказкой «Hint». Подсказка – это просто индекс функции в экспорте dll (была введена для быстрого поиска имени). Когда Hint=0, загрузчику приходится сравнивать буквально все строки в экспорте dll, на что расходуется драгоценное время. Отметим, что «Hint» и «Ordinal» это разные вещи, хотя одного поля ягоды. Хинт это порядковый номер функции в отсортированном по алфавиту списке экспорта, а ординал – в порядке добавления всё новых и новых функций в хвост библиотеки. Более того, 16-битный хинт начинается с нуля, а 32/64-битный ординал всегда с единицы. Все значения на рис.ниже это RVA-адреса, к которым нужно прибавлять базу образа в памяти.
В последнее поле дескриптора «FirstThunk», компилятор прописывает указатель на основную «таблицу адресов» импорта IAT. В файле на диске, значения массива AddressThunkValue в точности совпадают со-значениями массива NameThunkValue, хотя это две разные таблицы (см.указатели 30D8h и 3100h). Но после загрузки библиотек в память, загрузчик находит в их экспорте адреса нужных нам API, и перезаписывает этими (готовыми к употреблению) адресами, все значения в IAT.
Таким образом, поле OriginalFirstThunk указывает на список импортируемых функций, а поле FirstThunk на список соответствующих им линейных адресов.
Иногда в каталоге DATA_DIRECTORY запись об IAT может отсутствовать – это означает, что она размещена рядом с самой таблицей импорта. Поскольку FirstThunk первой dll по любому указывает на начало IAT, на правильность загрузки образа это никак не влияет. Если-же компилятор надумает перенести IAT в другую область памяти, он обязательно отметит сей факт в записи каталога.
Всё вышеизложенное можно наглядно продемонстрировать написав небольшой парсер импорта в памяти, прихватив с собой и таблицу-секций. Позже, в практической части мы будем перемещать целые структуры в различные секции, а потому их таблица не будет лишней.
C-подобный:
format pe64 console
entry start
include 'win64ax.inc'
;//----------
section '.data' data readable writeable
baseAddr dq 0
peHeader dq 0
impTable dq 0
lookup dd 0
iat dd 0
peLog db 10,' Тип файла...........: 0x020b PE32+ x64'
db 10,' База образа в памяти: 0x%016x'
db 10,' Всего секций........: %d',10
db 10," Каталог 'IMPORT'....: 0x%08x %4d байт"
db 10," Каталог 'BOUND'.....: 0x%08x %4d байт"
db 10," Каталог 'IAT'.......: 0x%08x %4d байт"
db 10," Каталог 'DELAY'.....: 0x%08x %4d байт",10,10
db 10,' ************* ДАМП ТАБЛИЦЫ СЕКЦИЙ 0x00400188 *****************',10
db 10,' Секция В памяти: размер На диске: размер Атрибуты'
db 10,' ~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~ ~~~~~~~~~~',0
impLog db 10,10
db 10,' ************* ДАМП ДЕСКРИПТОРОВ ИМПОРТА 0x%08x **********************************',10
db 10,' Library name TimeStamp IAT FirstThunk Lookup OriginThunk Hint Function name'
db 10,' ~~~~~~~~~~~~ ~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~ ~~~~ ~~~~~~~~~~~~~',0
buff db 0
;//----------
section '.code' code readable executable
start: sub rsp,8
frame
invoke SetConsoleTitle,'**** PE import parser ****'
invoke CharToOem,peLog, peLog
invoke CharToOem,impLog,impLog
invoke CharToOem,szHello,szHello
cinvoke printf, ' %s' ,szHello
invoke GetModuleHandle,0 ;//
mov [baseAddr],rax ;// база образа в памяти
endf
mov esi,eax ;//
mov eax,[esi+0x3c] ;//
add eax,esi ;//
mov [peHeader],rax ;// файловый заголовок
mov ebx,[eax+144] ;//
add rbx,[baseAddr] ;//
mov [impTable],rbx ;// адрес таблицы импорта
mov esi,eax ;//
movzx ebx,word[esi+06] ;// всего секций
mov r10d,dword[esi+144] ;// каталог IMPORT
add r10,[baseAddr]
mov r11d,dword[esi+224] ;// каталог BOUND
or r11,r11
je @f
add r11,[baseAddr]
@@: mov r12d,dword[esi+232] ;// каталог IAT
or r12,r12
je @f
add r12,[baseAddr]
@@: mov r13d,dword[esi+240] ;// каталог DELAY
or r13,r13
je @f
add r13,[baseAddr]
@@: cinvoke printf,peLog,[baseAddr],ebx,\
r10,dword[esi+148],r11,dword[esi+228],\
r12,dword[esi+236],r13,dword[esi+244]
;//**************************************************
;//************ Обход заголовка секций **************
;//**************************************************
mov rsi,[peHeader]
movzx ecx,word[esi+06] ;// всего секций (длина цикла)
add rsi,264 ;// указатель на "SectionHeader"
@@: push rsi rcx ;// парсим дескрипторы в цикле..
mov eax, [esi+12] ;//
add rax, [baseAddr] ;// вирт.адрес секции в памяти
mov ebx, [esi+08] ;// ...и её размер
mov r10d,[esi+20] ;// смещение секции на диске
mov r11d,[esi+16] ;// ...и её размер
mov r12d,[esi+36] ;// атрибуты защиты -RWE-
cinvoke printf,<10,' %-12s 0x%08X %4Xh 0x%08X %4Xh 0x%08X',0>,\
rsi,rax,rbx,r10,r11,r12
pop rcx rsi
add rsi,28h ;// смещаемся к сл.дескриптору
dec ecx ;// все секции обошли?
jz @f ;// да - на выход
jmp @b ;// иначе: крутим цикл..
@@:
;//******************************************************
;//************ Обход дескрипторов импорта **************
;//******************************************************
cinvoke printf,impLog,[impTable]
@@: mov rsi,[impTable] ;//
mov eax,[esi+12] ;// имя DLL
add rax,[baseAddr] ;//
mov ebx,[esi+04] ;// TimeDataStamp
mov r10d,[esi+00] ;// lookup
add r10, [baseAddr] ;//
mov [lookup],r10d ;// <------ для остальных
mov r10d,[r10d] ;//
add r10, [baseAddr] ;//
push r10
mov r11d,[esi+16] ;// iat
add r11, [baseAddr] ;//
mov [iat],r11d ;// <------ для остальных
mov r11d,[r11d] ;//
pop rbp ;//
mov edi,ebp ;//
add edi,2 ;// имя API-функции
movzx ebp,word[ebp] ;// hint
cinvoke printf,<10,' %12s 0x%08x 0x%016x 0x%016x %04d %s',0>,\
rax,rbx,r11,r10,rbp,rdi
;//----------------
@nextLookup:
mov eax,[lookup]
add eax,8
cmp dword[eax],0
je @nextDescriptor
mov eax,[eax] ;//
add rax,[baseAddr] ;// lookup
push rax
mov ebx,[iat]
add ebx,8
mov ebx,[ebx]
add rbx,[baseAddr] ;// iat
pop r10 ;//
mov r11,r10
movzx r10,word[r10] ;// hint
add r11,2
cinvoke printf,<10,' ',\
'0x%016x 0x%016x %04d %s',0>,rbx,rax,r10,r11
add [lookup],8
add [iat],8
jmp @nextLookup
;//----------------
@nextDescriptor:
cinvoke printf,<10,'',0>
add [impTable],14h ;// сл.дескриптор
mov rax,[impTable]
cmp dword[eax],0 ;// это последний?
je @exit
jmp @b ;// крутим цикл..
;//**************************************************
invoke GetCommandLine ;// добавим нескольку fn. для кол-ва,
invoke GetDC ;// хотя их результаты в игнор
invoke AddAtom,'Hello!'
@exit: cinvoke _getch
cinvoke exit,0
;//----------
section '.idata' import data readable writeable
library msvcrt, 'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
;//----------
section '.codeby' data readable writeable
szHello db 10,' Привет codeby.net!'
db 10,' Изучаем таблицу импорта РЕ-файла'
db 10,' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',10,0
Как видно из этого лога, загрузчик и вправду заполнил таблицу IAT вирт.адресами функций в памяти, хотя на диске все значения совпадали с таблицей имён Lookup. Зато область хинтов полностью пуста, что не есть хорошо. Правильные компиляторы всегда указывают валидные подсказки в этих полях, но видимо автор fasm’a Т.Грыштар не учёл этот момент, создав «неправильный» компиль. При поиске имён в экспорте библиотек, загрузчик первым делом сверяет имена по их хинтам, и если облом, то вынужден будет со-скоростью улитки искать подстроку в огромной строке экспорта dll.
Чтобы узнать общее число экспортируемых функций любой из библиотек, можно открыть её в «PE Anatomist», перейти в раздел экспорта и просто отсортировать список по хинтам. На своей Win-7/x64 я получил: User32.dll = 845 функций, Kernel32 = 1392, и Ntdll = 1994 функи. Теперь представьте, сколько времени потребуется загрузчику на поиск имени среди такого кол-ва текстовых строк. Причём искать сразу несколько имён никак не получится, и для сл.функции в той-же dll нужно начинать всё сначала. На системе Win95 с процессором в 40..60 MHz это создавало серьёзную проблему, для чего и была предусмотрена вторая схема импорта «Bound».
3. Детали статического импорта
Обычно статическим связыванием PE-файла с библиотеками dll занимается сам компилятор, но можно организовать это дело просто дёрнув за спец.функцию
Ссылка скрыта от гостей
из либы ImageHlp.dll. Однако здесь мы исследуем внутренности импорта, а потому будем править бинарный файл на диске в ручную. Во-первых это интересно, во-вторых можно не соблюдать глупых правил, ну и в третьих просто добавит нам скилла.Суть Bound’a в том, что в IAT на диске сразу прописываются вирт.адреса импортируемых функций с расчётом, что все dll загружаются строго по указанным в их ImageBase адресам. Так мы полностью освободим загрузчик от его прямых обязанностей, возложив всю черновую работу на себя. Но тут возникает вопрос: «А что, если связанный таким образом файл попадёт на другую машину, версия библиотек которой отличается от нашей?» На такой случай у загрузчика всегда имеется план(Б) – он просто вернётся к стандартой схеме динамического импорта, посчитав Bound невалидным.
В процессе эволюции появилось два типа статического связывания библиотек – на смену устаревшей модели пришла новая.
Для совместимости старых приложений с WinXP+, системный лоадер поддерживает оба варианта.
3.1. Устаревший режим.
Загрузчик считает РЕ-файл статически связанным, если в поле «TimeDataStamp» таблицы-импорта обнаружит значение отличное от нуля. То-есть в файле без связывания, поле с отметкой времени всегда должно быть равно нулю! Когда компилятор осуществляет биндинг файла, он читает поле «TimeStamp» из dll, и прямиком отправляет его в «TimeStamp» импорта РЕ-файла. Теперь загрузчик сравнивает 2 штампа и если гуд, то вопрос решён, иначе ошибка с передачей управления в схему динамического импорта.
Проблема в том, что большинство системных dll выдают на экспорт API, которых фактически нет в их теле, а значит мы не можем заполучить их вирт.адреса в памяти. При вызове таких функций, загрузчик перенаправляет их в другую dll, где и расположен код функции. Этот механизм известен как «форвардинг».
Для выхода из ситуации, в дескрипторе импорта было предусмотрено поле «ForwardingChain», которое хранит индекс первой перенаправленной функции в IAT. Эти индексы могут собираться в цепочки «Chain». Флагом последнего индекса является значение -1. В частности это означает, что в отсутствии перенаправлений, поле ForwardingChain содержит значение -1 =
0xffffffff
.
C-подобный:
struct IMAGE_IMPORT_DESCRIPTOR ;// <--- Размер 14h. указатель лежит в "DATA_DIRECTORY" #01
OriginFirstThunk dd 0 ;// Lookup RVA
TimeDataStamp dd 0 ;// дата/время создания DLL (только для статич.связывания)
ForwardingChain dd 0 ;// индекс цепочки перенаправлений (только для старой схемы статич.связывания)
ModuleName dd 0 ;// RVA на имя библиотеки DLL
FirstThunk dd 0 ;// IAT RVA
ends
3.2. Новый режим по схеме «Bound»
Усовершенствованная схема оформляется немного иначе, и в основном расширяет возможности всё того-же форвардинга. Здесь в игру вступает таблица связывания, указатель на которую компилятор прописывает в запись каталога «Bound» под номером #11. Интересно, что в отличие от остальных, таблицу Bound компиль обычно размещает не внутри какой-либо секции, а хвосте NT-заголовка. Я проверил несколько системных dll, и везде одинаково. Зачем мокрые туда его прячут, остаётся вопросом.
Как это принято, внутри таблицы размещается уже массив 8-байтных дескрипторов, по одному на каждую библиотеку dll.
Виртуальные адреса импортируемых функций сохраняются туда, где они и должны быть – в таблицу IAT.
C-подобный:
struct BOUND_IMPORT_DESCRIPTOR ;//<---- Размер 8-байт. Указатель лежит в каталоге «Bound»
TimeDateStamp dd 0 ;// должен быть валидный, иначе -1
OffsetModuleName dw 0 ;// RAW-cмещение от начала этого дескриптора, до строки с именем DLL
ForwarderRefsCount dw 0 ;// кол-во структур FORWARDER_REF ниже, после этой
ends
struct BOUND_FORWARDER_REF ;//<---- Если есть перенаправление к другой DLL
TimeDateStamp dd 0 ;// аналогично выше
OffsetModuleName dw 0 ;//
Padding dw 0 ;// (слово выравнивания на 8-байт)
ends
Чтобы указать загрузчику на новую схему Bound, компилятор выставляет на макс поля «TimeStamp» и «ForwarderChain» в дескрипторе импорта, поскольку эта информация теперь хранится в самой таблице Bound. Таким образом получаем следующую картину..
TimeStamp в дескрипторе импорта:
1. Если равен 0, значит перед нами дефолтная схема с динамическим импортом.
2. Если равен -1, значит это новая схема статич.связывания через дескрипторы таблицы Bound.
3. Все остальные значения определяют действительное время, и это старая схема статич.связывания без таблицы Bound.
Обратите внимание на содержимое таблицы связывания Bound. Здесь видно, что либы Ntdll и Gdi32 не имеют перенаправляемых функций, а вот в поле «Forward» дескриптора следующей Kernel32 видим счётчик(1), и ниже сразу примкнувшую к ней структуру «Forwarder_Ref» с именем Ntdll.dll. Таким образом, новая схема позволяет загрузчику более гибко проводить биндинг, поскольку здесь мы имеем всю информацию для анализа форвард-цепочек практически любой длины. Всё-что для этого нужно, просто увеличивать счётчик, добавляя в хвост всё новые и новые структуры Ref.
Отложенный импорт «Delay» отложим до лучших времён, т.к. для нас в нём нет ничего интересного, а теории получилось и без того много. Но без понимания происходящего на низком уровне, дальнейшее обсуждение было-бы попросту бесполезно.
4. РЕ32+ без импорта
Ответим на простой вопрос: «Можно-ли создать приложение Windows вообще без секции-импорта?»
Учитывая, что ос проецирует Kernel32 и Ntdll.dll во все пользовательские процессы, ответ – можно. Кстати любопытно, что вплоть до Win2k система не раздавала всем процессам копии своих dll (т.е. маппила их как сейчас), а тупо расшаривала свою собственную либу (наивные). Как результат, любое приложение могло внедрить шелл в системную dll, и этот шелл автоматом распространялся на все юм процессы. Сейчас это даже звучит смешно, но именно ошибки двигают прогресс вперёд.
Значит чтобы избавиться от секции импорта, нужно всего-то:
1. Найти базу Kernel32.dll в собственной памяти,
2. Контрабандным путём пробраться к её экспорту,
3. Вытащить от туда адреса всех необходимых API.
То-есть проделать вручную всё, что делает загрузчик.
Более того, 64-битные регистры CPU позволяют хранить в них строки размером до 8-байт, а 30 доп.регистров мультимедиа
xmm/ymm
в 2 и даже в 4-раза длиннее, в результате чего можно вырезать из программы и секцию-данных. Вы только посмотрите на это (не используемое большинством из нас) свободное пространство.Для поиска базы Kernel32 можно использовать три известных всем метода: это поле «Ldr» в структуре РЕВ процесса, указатель на обработчик исключений SEH в стеке (не работает на х64, т.к. на смену пришёл VEH), а так-же самый простой из способов – это адрес возврата в kernel на точке-входа в программу. Если запустить отладчик, этот адрес будет лежать в открытом виде прямо на макушке стека, и нам остаётся просто снять его от туда. Правда это ещё не база, а лишь адрес fn. ExitUserThread(), что даёт возможность без ошибок выходить из приложения обычной инструкцией
ret
.Но главное есть адрес, который ведёт в нёдра kernel32, и теперь шагая по страницам вверх можно уткнуться в базу, определив её по сигнатуре «MZ». Код ниже демонстрирует это, и в экспорте dll ищет функцию _hwrite() для вывода сообщения, причём 4 первых символа имени функции хранятся для поиска в регистре
ECX
. Можно установить значение ImageBase на мин.доступный юзеру адрес 0x10000
(см.директиву «at» fasm’a).
C-подобный:
format pe64 console at 0x0010000
;//----------
section ' ' executable ;// безымянная секция с флагом только(Ех)
start: sub rsp,8 ;// требует fastcall
;// Поиск базы kernel32.dll
mov rsi,[rsp+8] ;// снять со стека адрес-возврата в kernel
and rsi, not 0xfff ;// выравнивание адреса на 4К страницу
@@: cmp word[rsi],'MZ' ;// заголовок? (совпадает с базой)
je @found ;// ок!
sub rsi,0x1000 ;// шаг по страницам вверх
jmp @b ;// цикл..
;// Есть база! Теперь ищем таблицу экспорта в kernel32
@found: mov eax,dword[rsi+0x3c]
add rax,rsi ;// PE-header
add eax,136 ;// экспорт в каталоге
mov eax,[eax] ;// берём указатель
add rax,rsi ;// делаем из него вирт.адрес!
;// Поиск в экспорте нужных API ( RSI = база kernel32, RAX = таблица экспорта )
mov ebx,[eax+28] ;//
mov ecx,[eax+32] ;//
add rbx,rsi ;// линк на табл.адресов
add rcx,rsi ;// линк на табл.имён
@@: push rbx rcx ;//
mov ebx,[ebx] ;// RVA-адрес API
mov ecx,[ecx] ;//
add rcx,rsi ;// линк на имя API
cmp dword[ecx],'_hwr' ;// это _hwrite ???
je @f ;// ок!
pop rcx rbx ;//
add rbx,4 ;// шаг в таблицах
add rcx,4 ;//
jmp @b ;// цикл..
;// RBX = адрес функции
@@: add rsp,16 ;// восстановить стек от пушей
add rbx,rsi ;// RVA to VA
mov ecx,-11 ;// STD_OUTPUT_HANDLE
mov edx,0x1004E ;// адрес строки в заглушке DOS
mov r8,43 ;// её длина
call rbx ;// вызов API !!!
add rsp,8 ;// восстановить стек до kernel32.ExitUserThread()
;// jmp $
ret ;// Bye!
Заключение
К сожалению лимит на кол-во символов в статье не позволил здесь пощупать ручками недокументированные возможности импорта, ведь составители спеки РЕCOFF просто не описали всех его тонкостей. Только эксперименты на практике позволяют выявить ошибки, и уверяю вас, что они оккупировали РЕ-файл от чердака до самого подвала. Одни поля Hint, Ordinal и Forwarding чего стоят, не говоря уже о статическом импорте Bound. Подковавшись теорией, всю следующую часть мы посвятим практике, а пока уходим на перекур. В скрепке два файла для тестов, до скорого.