Первая спецификация формата РЕ вышла в свет вместе с Win3.1, и вот уже как 40 лет особых изменений в нём не наблюдается. Даже переход на архитектуру х64 особо не повлиял на общую картину – инженеры ограничились лишь расширением некоторых полей до 8-байтных значений. Политика Microsoft здесь прозрачна, поскольку мин.правки формата могут привести к катастрофических последствиям для уже написанного софта. Да и зачем что-то трогать, если за это время всё притёрлось и работает исправно?
Но не будем торопиться в выводами. Пусть формат РЕ-файла и остался прежним, зато начиная с Висты на стороне самой системы всё в корень изменилось. Пополнение штата оси такими технологиями как рандом адресного пространства ASLR и прочих не могло пройти мимо системного загрузчика образов LDR, ведь только он напрямую и взаимодействует с РЕ-файлом загружая его в память. Таким образом, прежний лоадер WinXP технично cпрыгнул с поезда, предоставив полный карт-бланш более молодому своему преемнику.
Данный цикл из 4-х статей посвящён разбору деталей усовершенствований, но ситуацию усугубляет отсутствие внятных доков по загрузчику. Вариант с реверсом ядра сразу отпадает в силу его неэффективности, ведь без руководств разобраться в тысячах строк кода Ntoskrnl.exe и Ntdll.dll во-первых не так просто, а во-вторых не даёт гарантии, что мы на правильном пути. Поэтому лучше сделать ставку на практический анализ с помощью отладчика WinDbg и всей его братии. Для тестов была выбрана «схема отрицания», т.е. если в спеке на РЕ-файл утверждается (давно известная всем) истина, то я буду принимать её за ложь. Например Microsoft ограничивает общее кол-во секций в файле числом 96. А что, если это было давно и не правда, ведь в заголовке под лимит выделяется поле размером word = 65.536? Ну и далее в том-же духе..
1. Общие сведения о формате РЕ-файла
Пересказывать спеку в сотый раз не вижу смысла, поэтому коротко об основных моментах.
Значит имеем РЕ-файл на диске, образ которого системный лоадер загружает в память. Сам файл содержит информацию (типа код, данные, импорт, ресурсы), которую описывают соответствующие заголовки «Header». Ключевых заголовков всего три – это глобально «файловый», уточняющий мелкие детали «опциональный», и наконец заголовок «секций». В первых/двух имеются поля, которые задают им размеры, причём опциональный считается вложенным в файловый заголовок. Надеюсь схема ниже немного украсит пейзаж – она демонстрирует, в каком виде представлен исполняемый файл в PECOFF-спецификации:
Здесь я перечислил лишь те поля, которые будут для нас в приоритете, а числа снаружи являются их 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
Обратите внимание на размер РЕ-хидера 264 байт. Чтобы найти заголовок секций, загрузчик образов прибавляет к этому значению адрес РЕ-заголовка, и получает таким образом
0x00400080 + 0x0108 = 0x00400188
. Остаётся запросить общее число секций в файле(5), и можно дампить в память процесса сведения о каждой из них.2. Манипуляции с заголовком «File Header»
Получив небольшую дозу общей информации о РЕ-файлах, перейдём к практике.
Из инструментов понадобится редактор «HxD» для перемещения блоков в файле, крутая утилита сбора подноготной
Ссылка скрыта от гостей
, а так-же редактор исполняемых файлов
Ссылка скрыта от гостей
. Ну и отладчик, как-правило под Win это x64dbg.Сразу определимся с полями и формулами поиска нужных смещений. Расчёты указаны относительно файлового заголовка, указатель на который лежит по смещению
0x3C
от начала самого файла «e_lfanew». Файловый заголовок по его сигнатуре называют ещё РЕ-заголовком. Гремучая смесь из файлового и опционального заголовков вместе взятых, в спецификации майков числится в розысках под кличкой
Ссылка скрыта от гостей
.- FileHeaderSize – константа 0x18 байт.
- OptionalHeaderSize – хранится в слове по смещению(0x14) в файловом заголовке.
SectionHeaderAddr = FileHeaderAddr + FileHeaderSize + OptionalHeaderSize
.
На схемах ниже представлены возможные варианты модификации формата исполняемых файлов, которые малварь может использовать для скрытия своей тушки. Всё это для того, чтобы запутать инструменты анализа РЕ-файлов как на диске, так и в памяти. Обнаружить такие техники антипода добра можно только научившись создавать их, а потому имеет смысл ознакомиться с ними поближе.
2.1. Расширение заголовка
Задача – максимально растянуть NT-заголовок так, чтобы для загрузчика образов всё содержимое файла представилось одним заголовком. Поскольку для поиска опционального лоадер использует константу 0x18, то файловый увеличивать в размерах нельзя. Тогда остаётся раздуть только сам опциональный. Организовать это дело можно или правкой значения в поле «NumberOfDataDirectory» по смещению(132), или-же напрямую прописать размер в поле «OptionalHeaderSize», что выглядит более привлекательней.
Однако в процессе экспериментов выяснилось, что на размер NT-заголовка загрузчик накладывает лимит в 4 КБ = 0x1000, в противном случае получаем ошибку «Приложению не удалось корректно запуститься (0xC0000018)» и оно прямиком отправляется к праотцам. Таким образом секция «fix» моей программы не подходит на роль кандидата, а вот предыдущая «codeby» в самый раз.
Значит делаем копию скомпилированной прожки, и переименуем её в «Extended». На скрине нашего парсера мы уже получили адрес таблицы-секций
0x188
, так-что просто переходим к нему в редакторе HxD, и выделив все дескрипторы захватим их в буфер по Ctrl+C. Размер таблицы в данном случае получился 0xC8
– запомним его.Теперь прыгаем на адрес
0x0E00
, находим там подходящее болото нулей, выделяем 0xC8
байт для удаления по Del, и на их место по Ctrl+V сбрасываем содержимое буфера. Поскольку секции на диске обязательно должны быть выровнены на границу 512-байт, то обязательно сначала нужно вырезать блок соответствующего размера, и только потом вставлять новый.Так мы прописали таблицу-секций по новому месту жительства, и остаётся оформить доки.
В дефолте, размер опционального заголовка уже лежит в файловом по смещению(0х14), и для РЕ32+ он как-правило равен
0xF0
(хотя не помешает проверить). Но поскольку NT-хидер теперь разбух, нужно вычислить новый его размер, для чего ставим курсор по адресу прописки 0x0E70
, и выделяем всё до начала оригинальной таблицы. В результате HxD сам вычислит длину получившегося блока, а нам остаётся добавить к нему прежний размер. Поле «OptionalHeaderSize» размером в слово, а потому меняем байты местами D8.0D
и записываем их по смещению(0x14).Если всё сделали правильно, то после запуска приложения получим такую картину где видно, что системный лоадер проглотил наживку. Судя по всему он нашёл таблицу секций, получил адрес импорта в ней, загрузил в память процесса все указанные там либы DLL, а вот саму таблицу отказался принимать на борт, о чём свидетельствует лог ниже. И это при том, что адрес таблицы
0x00400E70
вычислен верно, да и размер всего NT-заголовка увеличился с дефолтных 264-байт аж до 3568.Некоторые инструменты анализа РЕ-файлов берут заголовки именно из памяти, а не с диска.. особенно когда предлагают опцию «Присоединиться к активному процессу». Не трудно догадаться, что будет, когда они нарвутся на такой вот файл. Правда за овер 40 лет большинство прогеров уже съели на этом собаку, а потому полностью отказались от парсинга хидеров в памяти, в пользу чтения с диска. Более того, в системе имеется и библиотека ImgHelp.dll, функции API которой так-же в качестве источника использует заголовки в памяти. В общем профит от этого всё-таки можно поиметь.
2.2. Перемещение заголовка в произвольную позицию
Здесь всё аналогично, только есть несколько нюансов.
Теперь нужно не расширять опциональный заголовок с жёстко привязанной к его подвалу «SectionHeaders», а сдвигать весь NT-заголовок через указатель «e_lfanew». Перемещаемый блок должен включать в себя сразу все заголовки скопом, а это File/Optional/Section. По логике вещей, 32-битное поле «e_lfanew» позволяет катапультировать блок вплоть до макс.адреса 4 ГБ, что часто и практиковала малварь. Но видимо инженерам Microsoft надоело с этим бороться, и начиная с Висты они прописали в загрузчик код проверки лимита, который выставлен на такое-же значение всего 4 КБ. Другими словами, в поле «e_lfanew» мы не можем прописать адрес выше
0х1000
(размер одной вирт.страницы), хотя на WinXP такого ограничения не было.Чтобы вычислить лимит загрузчика опытным путём (а как иначе, ведь это не разглашается), в своём приложении я последовательно выстроил три секции размером по
0x500
байт каждая. В результате, перемещение блока заголовков в первые две секции срабатывало без проблем, а на последнюю «fix3» с базовым адресом 0x1000
загрузчик начинал уже ругаться матом.Данные на диске должны быть кратны 512-байтам 200h, поэтому мои 500h превратились в 600h (лишнее забивается нулями), хотя в памяти загрузчик резервирует реально-требуемое кол-во байт, с выравниванием их на границу 4К.
Теперь открываем файл в редакторе HxD, и начиная с РЕ-заголовка копируем всё до «SectionHeaders» включительно. При этом редактор покажет нам размер блока заголовков. Далее нужно перейти в самый хвост доступного адреса
0x1000
, и вставить выше него скопированные данные. На финишной прямой зарегеcтрируем изменения в поле «e_lfanew» заголовка DOS, а так-же в поле(54h) «SizeOfHeaders» опционального хидера. Вся эта котовасия в графической форме представлена ниже.Здесь нужно отметить, что в поле с размером можно прописывать кратное(8) значение. Я попробовал указать размер без учёта последней таблицы-секций, в результате чего загрузчик оставил её на диске, и не стал подгружать в память. Соответственно это отразилось и на логе программы, которая планировала её там найти. Затаив надежду я таким-же образом попытался вырезать из памяти и опциональный заголовок, на что загрузчик грубо выругался не разделив моего оптимизма. Править значение полей можно прямо в HEX-редакторе, но удобней использовать для этого «CFF-Explorer». Сравните значения полей до и после правки, и увидите разницу.
Заключение
Материала по исследованию РЕ-файлов накопилось много, и как упоминалось ранее, было решено собрать его в цикл из 4-х частей. В следующей(2) рассмотрим реакцию загрузчика на нестандартное офомление самих секций и в частности импорта. Часть(3) будет посвящена пакерам от обычных UPX до более своенравных протекторов, а прихлопнет цикл заключительная часть(4), где подвергнем трепанации файлы с расширением PDB (Programm Data-Base), в которых хранится информация о символах РЕ-файла для отладчиков. В скрепку положил 2 инклуда для компилятора fasm с описанием структур РЕ32/64, а так-же 3 представленных выше исполняемых файлов для дебага. Всем удачи, пока!
Вложения
Последнее редактирование: