Статья ASM. РЕ файл – ломаем стереотипы. Часть-1 Заголовки.

Marylin

Mod.Assembler
Red Team
05.06.2019
324
1 441
BIT
649
Logo.png

Первая спецификация формата РЕ вышла в свет вместе с Win3.1, и вот уже как 40 лет особых изменений в нём не наблюдается. Даже переход на архитектуру х64 особо не повлиял на общую картину – инженеры ограничились лишь расширением некоторых полей до 8-байтных значений. Политика Microsoft здесь прозрачна, поскольку мин.правки формата могут привести к катастрофических последствиям для уже написанного софта. Да и зачем что-то трогать, если за это время всё притёрлось и работает исправно?

Но не будем торопиться в выводами. Пусть формат РЕ-файла и остался прежним, зато начиная с Висты на стороне самой системы всё в корень изменилось. Пополнение штата оси такими технологиями как рандом адресного пространства ASLR и прочих не могло пройти мимо системного загрузчика образов LDR, ведь только он напрямую и взаимодействует с РЕ-файлом загружая его в память. Таким образом, прежний лоадер WinXP технично cпрыгнул с поезда, предоставив полный карт-бланш более молодому своему преемнику.

Данный цикл из 4-х статей посвящён разбору деталей усовершенствований, но ситуацию усугубляет отсутствие внятных доков по загрузчику. Вариант с реверсом ядра сразу отпадает в силу его неэффективности, ведь без руководств разобраться в тысячах строк кода Ntoskrnl.exe и Ntdll.dll во-первых не так просто, а во-вторых не даёт гарантии, что мы на правильном пути. Поэтому лучше сделать ставку на практический анализ с помощью отладчика WinDbg и всей его братии. Для тестов была выбрана «схема отрицания», т.е. если в спеке на РЕ-файл утверждается (давно известная всем) истина, то я буду принимать её за ложь. Например Microsoft ограничивает общее кол-во секций в файле числом 96. А что, если это было давно и не правда, ведь в заголовке под лимит выделяется поле размером word = 65.536? Ну и далее в том-же духе..


1. Общие сведения о формате РЕ-файла

Пересказывать спеку в сотый раз не вижу смысла, поэтому коротко об основных моментах.
Значит имеем РЕ-файл на диске, образ которого системный лоадер загружает в память. Сам файл содержит информацию (типа код, данные, импорт, ресурсы), которую описывают соответствующие заголовки «Header». Ключевых заголовков всего три – это глобально «файловый», уточняющий мелкие детали «опциональный», и наконец заголовок «секций». В первых/двух имеются поля, которые задают им размеры, причём опциональный считается вложенным в файловый заголовок. Надеюсь схема ниже немного украсит пейзаж – она демонстрирует, в каком виде представлен исполняемый файл в PECOFF-спецификации:

PE-Header.png

Здесь я перечислил лишь те поля, которые будут для нас в приоритете, а числа снаружи являются их DEC-смещениями от начала файлового заголовка. Примечательно то, что уловив общую суть расположения блоков в файле, мы можем как в тетрисе перемещать их в произвольном порядке. На работоспособность файла это никак не повлияет, ведь загрузчику образов нужны только указатели, а куда смотрят эти указатели – ему далеко по-барабану. Если что-то пойдёт не так, то файл просто не пройдёт фейс-контроль загрузчика, и он тупо откажется принимать его на борт с сообщением типа «Файл не является приложением Win32».

Однако на такого рода «курсах кройки и шитья» мы должны соблюдать строго определённые правила, которых придерживается системный LDR. В частности, нельзя двигать с места только файловый заголовок не прихватив с собой опциональный, т.к. его смещение нигде не указывается, а значит загрузчик рассчитывает найти последний сразу после файлового. Это касается и заголовка секций, оффсет которого так-же отсутсвует в файле, и по факту является константой после каталога секций «Data Directory».

Отдельного внимания заслуживает и файловый оверлей – нигде не прописанный бомж. Он не входит в состав РЕ-файла, и при загрузки образа в память игнорируется загрузчиком. Безобидная на первый взгляд тушка оверлея опасна тем, что может содержать вредоносный код. Но если загрузчик всё-равно оставляет оверлейный хвост на диске, то в чём подвох? Просто в РЕ-файле обычно располагается вполне легальный код, который при загрузки образа не привлекая к себе внимания сможет пройти мимо антивирусов. Получив лимит доверия от авера, позже код спокойно сможет уже подтянуть в своё пространство и оверлей.

В рамках следующих экспериментов потребуется файл для тестов, а поскольку речь идёт о заголовках, пусть это будет парсер хидеров именно образа в памяти, т.к. на диске мы сможем просмотреть их и во-внешних утилитах. Код 64-битный, а смещения нужных полей представлены на скрине выше.

C-подобный:
format   pe64 console
entry    start
include 'win64ax.inc'
;//----------
section '.data' data readable writeable
baseAddr  dq  0
peHeader  dq  0

peLog     db  10,' База образа в памяти:  0x%08x'
          db  10,' Адрес PE-заголовка..:  0x%08x -> "%.4s"'
          db  10,'    ~ размер в памяти:  0x%04x = %4d byte'
          db  10,'    ~ размер на диске:  0x%04x = %4d byte',10
          db  10,' Точка входа.........:  0x%08x'
          db  10,' Всего секций........:  %d'
          db  10,' Выравнивание секций'
          db  10,'           ~ в памяти:  0x%04x = %4d byte'
          db  10,'           ~ на диске:  0x%04x = %4d byte',10,10

          db  10,' ~~~~~~~~~~~~~~~ ДАМП ТАБЛИЦЫ СЕКЦИЙ 0x%08x ~~~~~~~~~~~~~',10

          db  10,' Name      Virt: Addr  Size    Raw:  Addr  Size    Flags'
          db  10,' ~~~~~~~~  ~~~~~~~~~~~~~~~~    ~~~~~~~~~~~~~~~~    ~~~~~~~~~~',0

;//----------
section '.text' code readable executable
start:  sub     rsp,8

        invoke  SetConsoleTitle,'**** PE-Header ****'
        invoke  CharToOem,peLog, peLog
        invoke  CharToOem,szText,szText
       cinvoke  printf,' %s',szText

        invoke  GetModuleHandle,0    ;//
        mov     [baseAddr],rax       ;//  база образа в памяти
        mov     rsi,rax              ;//
        mov     eax,dword[rsi+0x3c]  ;//  e_lfanew
        add     rax,rsi              ;//
        mov     [peHeader],rax       ;//  файловый заголовок

        mov     ebx,[eax+84]         ;//  его размер в памяти
        movzx   esi,word[eax+20]     ;//  размер опционального заголовка
        add     esi,24               ;//  + 0x18 = размер File + Opt заголовков на диске
        push    rsi rsi              ;//  запомнить!

        mov     r10d,[eax+40]        ;//  точка-входа
        add     r10, [baseAddr]      ;//
        movzx   r11d,word[eax+06]    ;//  всего секций
        mov     r12d,[eax+56]        ;//  выравнивание в памяти
        mov     r13d,[eax+60]        ;//  выравнивание в файле
        pop     r14                  ;//
        add     r14, [peHeader]      ;//  адрес заголовка секций в памяти

       cinvoke  printf,peLog,[baseAddr],rax,rax,rbx,rbx,rsi,rsi,\
                                        r10,r11,r12,r12,r13,r13,r14

;//**************************************************
;//************ Обход заголовка секций **************
;//**************************************************
        mov     rsi,[peHeader]
        movzx   ecx,word[esi+06]     ;//  всего секций (длина цикла)
        pop     rsi                  ;//  размер File + Opt заголовков на диске
        add     rsi,[peHeader]       ;//  вычисляем указатель на "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,' %-8.8s  0x%08X %5d    0x%08X %5d    0x%08X',0>,\
                                     rsi,rax,rbx,r10,r11,r12
        pop     rcx rsi
        add     rsi,28h     ;//  смещаемся к сл.дескриптору
        dec     ecx         ;//  все секции обошли?
        jz      @exit       ;//  да - на выход
        jmp     @b          ;//  иначе: крутим цикл..
;//**************************************************

@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
szText    db  10,' Привет codeby.net!'
          db  10,' Изучаем манипуляции с РЕ-заголовком'
          db  10,' ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~',10,0

;//----------
section '.fix' data writeable
freeBuff  dq  200

testImage.png

Обратите внимание на размер РЕ-хидера 264 байт. Чтобы найти заголовок секций, загрузчик образов прибавляет к этому значению адрес РЕ-заголовка, и получает таким образом 0x00400080 + 0x0108 = 0x00400188. Остаётся запросить общее число секций в файле(5), и можно дампить в память процесса сведения о каждой из них.


2. Манипуляции с заголовком «File Header»

Получив небольшую дозу общей информации о РЕ-файлах, перейдём к практике.
Из инструментов понадобится редактор «HxD» для перемещения блоков в файле, крутая утилита сбора подноготной , а так-же редактор исполняемых файлов . Ну и отладчик, как-правило под Win это x64dbg.

Сразу определимся с полями и формулами поиска нужных смещений. Расчёты указаны относительно файлового заголовка, указатель на который лежит по смещению 0x3C от начала самого файла «e_lfanew». Файловый заголовок по его сигнатуре называют ещё РЕ-заголовком. Гремучая смесь из файлового и опционального заголовков вместе взятых, в спецификации майков числится в розысках под кличкой .
  1. FileHeaderSize – константа 0x18 байт.
  2. OptionalHeaderSize – хранится в слове по смещению(0x14) в файловом заголовке.
  3. SectionHeaderAddr = FileHeaderAddr + FileHeaderSize + OptionalHeaderSize.
Получив указатель на заголовок «SectionHeader», мы попадаем в массив структур «SectionDescriptor». Каждая такая структура имеет фиксированный размер 0x28 байт и описывает отдельно взятую секцию (адрес, размер, флаги). Общее число секций (и соответственно структур SectDesc) хранится в поле «NumberOfSection» файлового заголовка.

На схемах ниже представлены возможные варианты модификации формата исполняемых файлов, которые малварь может использовать для скрытия своей тушки. Всё это для того, чтобы запутать инструменты анализа РЕ-файлов как на диске, так и в памяти. Обнаружить такие техники антипода добра можно только научившись создавать их, а потому имеет смысл ознакомиться с ними поближе.

PE-Header2.png


2.1. Расширение заголовка

Задача – максимально растянуть NT-заголовок так, чтобы для загрузчика образов всё содержимое файла представилось одним заголовком. Поскольку для поиска опционального лоадер использует константу 0x18, то файловый увеличивать в размерах нельзя. Тогда остаётся раздуть только сам опциональный. Организовать это дело можно или правкой значения в поле «NumberOfDataDirectory» по смещению(132), или-же напрямую прописать размер в поле «OptionalHeaderSize», что выглядит более привлекательней.

Однако в процессе экспериментов выяснилось, что на размер NT-заголовка загрузчик накладывает лимит в 4 КБ = 0x1000, в противном случае получаем ошибку «Приложению не удалось корректно запуститься (0xC0000018)» и оно прямиком отправляется к праотцам. Таким образом секция «fix» моей программы не подходит на роль кандидата, а вот предыдущая «codeby» в самый раз.

e00.png

Значит делаем копию скомпилированной прожки, и переименуем её в «Extended». На скрине нашего парсера мы уже получили адрес таблицы-секций 0x188, так-что просто переходим к нему в редакторе HxD, и выделив все дескрипторы захватим их в буфер по Ctrl+C. Размер таблицы в данном случае получился 0xC8 – запомним его.

Copy.png

Теперь прыгаем на адрес 0x0E00, находим там подходящее болото нулей, выделяем 0xC8 байт для удаления по Del, и на их место по Ctrl+V сбрасываем содержимое буфера. Поскольку секции на диске обязательно должны быть выровнены на границу 512-байт, то обязательно сначала нужно вырезать блок соответствующего размера, и только потом вставлять новый.

Paste.png

Так мы прописали таблицу-секций по новому месту жительства, и остаётся оформить доки.
В дефолте, размер опционального заголовка уже лежит в файловом по смещению(0х14), и для РЕ32+ он как-правило равен 0xF0 (хотя не помешает проверить). Но поскольку NT-хидер теперь разбух, нужно вычислить новый его размер, для чего ставим курсор по адресу прописки 0x0E70, и выделяем всё до начала оригинальной таблицы. В результате HxD сам вычислит длину получившегося блока, а нам остаётся добавить к нему прежний размер. Поле «OptionalHeaderSize» размером в слово, а потому меняем байты местами D8.0D и записываем их по смещению(0x14).

SizeOfCalc.png

Если всё сделали правильно, то после запуска приложения получим такую картину где видно, что системный лоадер проглотил наживку. Судя по всему он нашёл таблицу секций, получил адрес импорта в ней, загрузил в память процесса все указанные там либы DLL, а вот саму таблицу отказался принимать на борт, о чём свидетельствует лог ниже. И это при том, что адрес таблицы 0x00400E70 вычислен верно, да и размер всего NT-заголовка увеличился с дефолтных 264-байт аж до 3568.

Некоторые инструменты анализа РЕ-файлов берут заголовки именно из памяти, а не с диска.. особенно когда предлагают опцию «Присоединиться к активному процессу». Не трудно догадаться, что будет, когда они нарвутся на такой вот файл. Правда за овер 40 лет большинство прогеров уже съели на этом собаку, а потому полностью отказались от парсинга хидеров в памяти, в пользу чтения с диска. Более того, в системе имеется и библиотека ImgHelp.dll, функции API которой так-же в качестве источника использует заголовки в памяти. В общем профит от этого всё-таки можно поиметь.

ExtResult.png


2.2. Перемещение заголовка в произвольную позицию

Здесь всё аналогично, только есть несколько нюансов.
Теперь нужно не расширять опциональный заголовок с жёстко привязанной к его подвалу «SectionHeaders», а сдвигать весь NT-заголовок через указатель «e_lfanew». Перемещаемый блок должен включать в себя сразу все заголовки скопом, а это File/Optional/Section. По логике вещей, 32-битное поле «e_lfanew» позволяет катапультировать блок вплоть до макс.адреса 4 ГБ, что часто и практиковала малварь. Но видимо инженерам Microsoft надоело с этим бороться, и начиная с Висты они прописали в загрузчик код проверки лимита, который выставлен на такое-же значение всего 4 КБ. Другими словами, в поле «e_lfanew» мы не можем прописать адрес выше 0х1000 (размер одной вирт.страницы), хотя на WinXP такого ограничения не было.

Чтобы вычислить лимит загрузчика опытным путём (а как иначе, ведь это не разглашается), в своём приложении я последовательно выстроил три секции размером по 0x500 байт каждая. В результате, перемещение блока заголовков в первые две секции срабатывало без проблем, а на последнюю «fix3» с базовым адресом 0x1000 загрузчик начинал уже ругаться матом.

e_lfanew.png

Данные на диске должны быть кратны 512-байтам 200h, поэтому мои 500h превратились в 600h (лишнее забивается нулями), хотя в памяти загрузчик резервирует реально-требуемое кол-во байт, с выравниванием их на границу 4К.

Теперь открываем файл в редакторе HxD, и начиная с РЕ-заголовка копируем всё до «SectionHeaders» включительно. При этом редактор покажет нам размер блока заголовков. Далее нужно перейти в самый хвост доступного адреса 0x1000, и вставить выше него скопированные данные. На финишной прямой зарегеcтрируем изменения в поле «e_lfanew» заголовка DOS, а так-же в поле(54h) «SizeOfHeaders» опционального хидера. Вся эта котовасия в графической форме представлена ниже.

Shift.png

Здесь нужно отметить, что в поле с размером можно прописывать кратное(8) значение. Я попробовал указать размер без учёта последней таблицы-секций, в результате чего загрузчик оставил её на диске, и не стал подгружать в память. Соответственно это отразилось и на логе программы, которая планировала её там найти. Затаив надежду я таким-же образом попытался вырезать из памяти и опциональный заголовок, на что загрузчик грубо выругался не разделив моего оптимизма. Править значение полей можно прямо в HEX-редакторе, но удобней использовать для этого «CFF-Explorer». Сравните значения полей до и после правки, и увидите разницу.

ShiftHeader.png


Заключение

Материала по исследованию РЕ-файлов накопилось много, и как упоминалось ранее, было решено собрать его в цикл из 4-х частей. В следующей(2) рассмотрим реакцию загрузчика на нестандартное офомление самих секций и в частности импорта. Часть(3) будет посвящена пакерам от обычных UPX до более своенравных протекторов, а прихлопнет цикл заключительная часть(4), где подвергнем трепанации файлы с расширением PDB (Programm Data-Base), в которых хранится информация о символах РЕ-файла для отладчиков. В скрепку положил 2 инклуда для компилятора fasm с описанием структур РЕ32/64, а так-же 3 представленных выше исполняемых файлов для дебага. Всем удачи, пока!
 

Вложения

  • PE-Header.zip
    15,8 КБ · Просмотры: 100
Последнее редактирование:

ex0dus

Red Team
21.05.2022
5
8
BIT
21
Хммм.....Такого не было еще...Не знаю кто такой Тимур, но выглядит очень даже не плохо! Молодца! Возможно подключусь к данной тематике.
 
  • Нравится
Реакции: Marylin и larchik

Marylin

Mod.Assembler
Red Team
05.06.2019
324
1 441
BIT
649
Такого не было еще...
это было доступно с момента рождения РЕ,
просто сейчас большинство багов уже пофиксили лимитом 1К, да и DEP с ASLR не дают как следует развернуться.

Прикольной была в то время фишка со-сбросом в ре/хидере поля "EntryPoint" в нуль, в результате чего управление получал MZ-заголовок, от куда уже прыгали на ОЕР. Но сейчас загрузчик выставляет всей странице с заголовком атрибут "только-чтение" DEP, и соответственно код сразу падает с исключением AV 0xC0000005.

Однако все баги они просто физически не могут залатать, особенно в огромном импорте.
В сл.части как-раз об этом и пойдёт речь.
 

Pavel Shuhray

Green Team
14.12.2016
19
5
BIT
58
При компиляции выдаёт ошибку на строке
include 'api\msvcrt.inc'
Где его взять?
И вопрос новичка : вот эти картинки вроде "Базовая информация из заголовков PE-файла" - как они получены, какой программой?
 
Последнее редактирование:

Pavel Shuhray

Green Team
14.12.2016
19
5
BIT
58
При компиляции выдаёт ошибку на строке
include 'api\msvcrt.inc'
Где его взять?
И вопрос новичка : вот эти картинки вроде "Базовая информация из заголовков PE-файла" - как они получены, какой программой?
А, я понял, это само тестовое приложение и выдаёт (посмотрел, что в нём написано). Но у меня пока не компилируется, выдаёт ошибку на строке
include 'win64ax.inc'
 

Pavel Shuhray

Green Team
14.12.2016
19
5
BIT
58
Этот файл лежит в папке "include" компилятора fasm. Пропишите полный путь до него.
Прописать прямо в тексте программы? (попробовал в переменных среды - не помогает)
P.S. Если же прописать в программе, выдаёт ошибку на первой же строчке msvcrt.inc (может, он неправильный)
 
Последнее редактирование:

Marylin

Mod.Assembler
Red Team
05.06.2019
324
1 441
BIT
649
выдаёт ошибку на первой же строчке msvcrt.inc
ну так укажите и до него полный путь.
ясно-же, что компиль не может найти этот файл.

А вообще,
1. Откройте файл fasm.ini и допишите в него такие строки (вместо F:\ свой диск):

[Environment]
Include=F:\FASM\INCLUDE

2. Если не поможет, то в самом исходнике нужно указывать полный путь до всех инклудов.
 

Pavel Shuhray

Green Team
14.12.2016
19
5
BIT
58
ну так укажите и до него полный путь.
ясно-же, что компиль не может найти этот файл.

А вообще,
1. Откройте файл fasm.ini и допишите в него такие строки (вместо F:\ свой диск):

[Environment]
Include=F:\FASM\INCLUDE

2. Если не поможет, то в самом исходнике нужно указывать полный путь до всех инклудов.
Спасибо, я понял с путями, но на первой же строчке msvcrt.inc ошибка "illegal instruction", а инструкция выглядит так
externdef __imp_$I10_OUTPUT:pPROC
(почему получился смайлик, не знаю, там двоеточие)
 

Marylin

Mod.Assembler
Red Team
05.06.2019
324
1 441
BIT
649
попробуйте msvcrt.inc заменить этим файлом
 

Вложения

  • MSVCRT.ZIP
    8,7 КБ · Просмотры: 39
Мы в соцсетях:

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