• Курсы Академии Кодебай, стартующие в мае - июне, от команды The Codeby

    1. Цифровая криминалистика и реагирование на инциденты
    2. ОС Linux (DFIR) Старт: 16 мая
    3. Анализ фишинговых атак Старт: 16 мая Устройства для тестирования на проникновение Старт: 16 мая

    Скидки до 10%

    Полный список ближайших курсов ...

Статья ASM. РЕ файл – ломаем стереотипы. Часть-3 Импорт.

Marylin

Mod.Assembler
Red Team
05.06.2019
305
1 370
BIT
306
Logo.png


Злачное место в РЕ-файлах под названием «Импорт» создало много проблем инженерам 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» во всех дескрипторах – оно играет немаловажную роль, о чём пойдёт речь далее.

ImpScheme.png


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-адреса, к которым нужно прибавлять базу образа в памяти.

ImpTable.png

В последнее поле дескриптора «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

PE-Import.png

Как видно из этого лога, загрузчик и вправду заполнил таблицу 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

Static.png


3.2. Новый режим по схеме «Bound»

Усовершенствованная схема оформляется немного иначе, и в основном расширяет возможности всё того-же форвардинга. Здесь в игру вступает таблица связывания, указатель на которую компилятор прописывает в запись каталога «Bound» под номером #11. Интересно, что в отличие от остальных, таблицу Bound компиль обычно размещает не внутри какой-либо секции, а хвосте NT-заголовка. Я проверил несколько системных dll, и везде одинаково. Зачем мокрые туда его прячут, остаётся вопросом.

BoundDir.png

Как это принято, внутри таблицы размещается уже массив 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-scheme.png

Обратите внимание на содержимое таблицы связывания 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-раза длиннее, в результате чего можно вырезать из программы и секцию-данных. Вы только посмотрите на это (не используемое большинством из нас) свободное пространство.

XmmYmm.png

Для поиска базы Kernel32 можно использовать три известных всем метода: это поле «Ldr» в структуре РЕВ процесса, указатель на обработчик исключений SEH в стеке (не работает на х64, т.к. на смену пришёл VEH), а так-же самый простой из способов – это адрес возврата в kernel на точке-входа в программу. Если запустить отладчик, этот адрес будет лежать в открытом виде прямо на макушке стека, и нам остаётся просто снять его от туда. Правда это ещё не база, а лишь адрес fn. ExitUserThread(), что даёт возможность без ошибок выходить из приложения обычной инструкцией ret.

Stack.png

Но главное есть адрес, который ведёт в нёдра 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!

NoImp.png

Заключение

К сожалению лимит на кол-во символов в статье не позволил здесь пощупать ручками недокументированные возможности импорта, ведь составители спеки РЕCOFF просто не описали всех его тонкостей. Только эксперименты на практике позволяют выявить ошибки, и уверяю вас, что они оккупировали РЕ-файл от чердака до самого подвала. Одни поля Hint, Ordinal и Forwarding чего стоят, не говоря уже о статическом импорте Bound. Подковавшись теорией, всю следующую часть мы посвятим практике, а пока уходим на перекур. В скрепке два файла для тестов, до скорого.
 

Вложения

  • PE-Import.zip
    1,9 КБ · Просмотры: 13
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!