Исполняемым файлом в системах класса Win является "Рortable-Еxecutable", или просто РЕ. Инженеры Microsoft потрудились на славу, чтобы предоставить нам спецификацию на него, однако некоторые "тузы в рукавах" они всё-таки припрятали. В данной статье рассматривается недокументированная часть РЕ-файла, ознакомившись с которой можно будет собирать информацию о процессе создания файлов компилятором. Антивирусные компании уже давно практикуют эту фишку своих сканерах, а для широкого круга общественности её так и не предали огласке.
Под катом:
1. Общие сведения
Формат РЕ появился с выходом в свет WinNT-3.1. Защищённый режим и виртуальная память требовали определённой стратегии распределения ресурсов – эту задачу как-раз и возложили на сам исполняемый РЕ-файл. В поисках нирваны, инженерам пришлось координально изменить подход к загрузке образов в ОЗУ. Если раньше обстановка в системе была статична без каких-либо эксцессов, то теперь, получив в своё распоряжение неограниченный объём памяти, всякое приложение на радостях так и старалось всю её поиметь.
Дабы ограничить аппетиты, было решено внедрить в исполняемый файл небольшой заголовок, и связать его с загрузчиком образов. Теперь файл сам сообщает, сколько ему требуется памяти для производственных нужд, а система её предоставляет. Нужно отметить, что это было единственно-верным решением в сложившейся ситуации, иначе великие умы уже давно придумали-бы что-нибудь свежее. Но видимо слабо.. РЕ родился в 1993-году и дошёл до нас в своём первозданном виде! Если и применялись какие-то моды, то исключительно на стороне системного загрузчика, а не самого файла. Здесь нужно снять шляпу перед разработчиком формата Марком Збиковски, с инициалов "MZ" которого начинаются все исполняемые файлы. Новоиспечённый формат точили и шлифовали ещё пару лет, так-что первая его спецификация датируется 1995-годом, как дополнение к системе Win-95.
Однако позже, любопытными программистами в РЕ был обнаружен нигде не регламентируемый блок информации. Причём в некоторых файлах он присутствовал, а в некоторых нет, т.е. появлялся как призрак. Microsoft крепко держала оборону на этот счёт, и все вопросы тупо игнорировала. В конечном итоге получилось так, что подобным молчанием она выстрелила себе в ногу, поскольку "тёмные силы" не заставили себя ждать и раскурив весь план стали активно его применять в своих целях. Что примечательно, этот блок и по сей день остаётся в тени – системный протекторат посчитал оставить всё как-есть, и не включать его в спеку.
Как выяснилось спустя много лет, данный блок был включён в РЕ-файл по требованию штатных сотрудников Microsoft, которые занимались в закрытых кабинетах реверс-инженерингом уже скомпилированного, бинарного софта. Они часто сталкивались с дилемой, какой именно версией компилятора C++ был создан исследуемый файл, и сколько было исходных модулей с кодом (в больших проектах может быть более сотни отдельных файлов, которые связываются в цепочку посредством include).
Задачу возложили на двух инженеров – это Дэн Сполдинг и Ричард Шупак, которые работали в то время над компилятором Visual C++ и базой кода системных библиотек. Не заморачиваясь, инженеры тупо включили в линкер LINK.EXE опцию ведения лога о проделанной работе, с сохранением его в специально отведённый для этих целей заголовок РЕ-файла. Формат лога состоит всего из трёх полей: Product-ID в лице версии компилятора (VC, VB, MASM, Phx), номер его сборки Build, а так-же счётчика Counter скомпилированных исходных файлов, которые входили в состав проекта. Пример такого блока представлен ниже:
В документации на PE/COFF от Microsoft нет ни единого упоминания об этом блоке, а сразу после заглушки DOS указывается РЕ-Header. В качестве маркера окончания блока, Шупак вставил дворд своего имени "Rich", а с инициалов второго автора Дэна блок начинается, просто здесь они накрыты ксором и не видны. После расшифровки получим сигнатуру "DanS". Позже мы напишем приложение, которое покажет всю информацию из этого заголовка. В конечном итоге конкретно для данного случая (софт PEiD), результат будет таким где видно, что по всей вероятности проект собирался в 15-ой студии, которая комплектуется линкером версии(9). Особый интерес представляют и значения в столбце "Count" – это счётчик исходных файлов:
На данный момент, в базе "Product-ID" имеется 0x10E=270 опознанных объектов, которые я собрал в свой инклуд (см.скрепку в подвале статьи). Поскольку Microsoft их не документировала (хотя сама активно юзает), энтузиастам всё-же удалось раскопать эти коды в файле "msobj140-msvcrt.lib" студий начиная с 2015, и при помощи скрипта на питоне вытащить их от туда в приглядном виде. Более продвинутые мемберы обзавелись ими ещё раньше, вскрыв как консервную банку линкер LINK.EXE, с последующим поиском алиасов имён экспериментально. Так появился софт, который с точностью определяет среду разработки, типа тот-же PeiD, DiE и прочие.
С Rich-заголовком связан один нюанс. Поскольку мягкие так и не обнародовали его, то он присутствует только в исполняемых файлах, которые собирались в продуктах самой Microsoft, с использованием линкера LINK.EXE, а это как-правило: Visual Studio, Visual Basic и MASM. Если-же бинарь компилировали в Delphi, Pascal, Go и прочих, то хидер Rich искать в нём бесполезно – его там попросту нет. В палату кастрированных попадает и ассемблер FASM, т.к. у него свой линкер и компилятор.
Но судя по статистике, доля продуктов Microsoft на рынке двоичных файлов составляет порядка 80% от общего числа, так-что Rich в природе частое явление. Под эгидой РЕ ходят такие расширения как: *.exe (dll, sys, drv, mui, scr, efi, ocx, bpl, dpl, cpl, acm, ax), а значит вся/эта братия будет представляться нашим потенциальным клиентом.
2. Структура РЕ-файла
Перед тем как рассмотреть формат заголовка Rich, в общих чертах вспомним структуры официального РЕ-файла. Для этого напишем обычный "HelloWorld" и вскормим его любому HEX-редактору.
Бинари всех исполняемых файлов Win должны начинаться с проприетарного заголовка DOS с сигнатурой "MZ" (привет Марк Збиковски). Создающие файл линкеры выделяют под него 0x80 = 128-байт, из которых системный загрузчик образов проверяет только два поля – это непосредственно сигнатура по смещению(00), и т.н. поле
Все остальные поля хидера DOS (включая стаб) давно атрофированы и никому не нужны, так-что мы можем смело забивать их нулями, или в целях уменьшения размера вообще сместить весь файл (начиная с РЕ-хидера) вверх, по смещению(08) от его начала, соблюдая выравнивание на 8-байтную границу. Только поле
Наиболее информативным для загрузчика образов является РЕ-заголовок, за которым сразу-же следует опциональный. Здесь уже указываются требования файла к системе: размер стека и памяти Heap, предпочтительный базовый адрес загрузки образа в память (в некоторых случаях ASLR его всё-равно меняет), точка-входа в программу для регистра EIP, выравнивание программных секций на диске и в памяти, их кол-во, адреса, и прочее. Как и в случае с заголовком DOS, большинство полей в Optional-Header так-же игнорируются современными загрузчиками, поскольку достались нам в наследство от Win95/98.
В контексте данной статьи, для нас интерес представляет поле "LinkerVersion" в опциональном заголовке. Оно имеет размер 2-байта, в первом из которых указывает версия-мажор линкера, а во-втором минор. К примеру я использую компилятор FASM ver.1.73, тогда в первом байте получаю(1), а во-втором(73). Отметим, что данное поле является документированным, поэтому свой "возраст" сбрасывают в него буквально все линкеры, а не только подопечные Microsoft – запомним его на будущее.
В спойлере ниже спрятана структура PE_HEADER, в которой неиспользуемые поля я оставил без комментов, хотя по именам всегда можно догадаться об их назначении. Для 64-битных версий исполняемых файлов структура идентичная, за исключением всех полей с адресами – их нужно расширить до 8-байт qword:
В опциональном заголовке прописались ещё два интересных мембера. Первый под кличкой "SectionAlignment" сообщает системному диспетчеру-памяти размер выравнивания секций. По умолчанию все линкеры определяют его как 4Кб страницы, и лучше оставить это поле без изменений. А вот второй "FileAlignment" определяет уже размер секций образа файла на диске. Экспериментальным путём удалось определить, что диапазон этого поля варьируется от 16-байт до 64К, а в дефолте все пихают туда 512. Уменьшив это выравнивание в 2 или 4-раза, можно сжать дисковый файл, чтобы он занимал на харде меньше места. В практической части мы ещё вернемся к нему.
3. Заголовок "Rich"
Процесс создания бинарного файла из исходников включает в себя три основных этапа – трансляция, компиляция и компоновка, которую на английский манер называют ещё линковка (linking, связывание). Заглянем под капот каждого из них:
1. Трансляция. На данном этапе, препроцессор компилятора вычисляет адреса всех меток и переходов в исходнике. Часто бывает так, что адреса и переменные напрямую зависят друг от друга. То-есть пока не вычислим первый адрес, не получим второй. Поэтому трансляция повторяется циклически как-минимум два раза. Такие трансляторы называют двух/трёх-проходной. Результат вычислений сохраняется в специальной таблице. Когда определились с адресами, переходим к сл.этапу..
2. Компиляция. Здесь уже текст исходника переводится в машинный код, и на выходе получаем объектный модуль, который по-сути уже готов к исполнению на центральном процессоре. Однако большие проекты собираются из нескольких отдельных исходников, так-что процесс компиляции производится для каждого из них. Соответственно на выходе получаем уже несколько самостоятельных объектных файлов.
3. Линковка (компоновка). Это последний этап создания исполняемого файла. Процедура занимается тем, что собирает в единое целое все объектные файлы проекта, связывая их друг-с-другом, и помещая в один флакон EXE/DLL/SYS. Этим как-раз и озадачен линкер среды разработки. Результатом данной операции является готовый к исполнению файл.
Суть внедрения в РЕ-файл заголовка "Rich" заключается в том, чтобы линкер запоминал в нём каждый свой шаг. Такой алгоритм способствует не только последующему реверсу софта, но и даёт возможность контролировать целостность файлов. Например если к нам попадёт подозрительный системный файл, можно будет вычислить, каким именно инструментарием он был создан. Кстати в своё время, хакерская группировка Lazarus
Таким образом, Rich-заголовок динамически расширяется в зависимости от того, сколько логов сбросил в него линкер. Каждый лог имеет размер 8-байт и хранит три составляющие – это уже упомянутые: Product-ID(2), Build(2) и Counter(4). В скобках здесь я указал длину поля в байтах.
Маркером начала заголовка служит 4-байтная строка "DanS", а маркером окончания "Rich". Теперь весь полученный блок шифруется обычным
Значит чтобы расшифровать Rich-заголовок, мы находим маркер-окончания "Rich", сразу после которого будет лежать ключ для операции
Как видим, после сигнатуры "DanS" ровным строем до конца строки, идут три дворда нулей – это выравнивание и присутствует оно всегда. Поскольку
Непосредственно 8-байтные логи линкера начинаются со-второй строки дампа (см.нижнее окно). В данном случае, первый дворд лога(0) имеет значение 0x00964FBD, где 0x0096 будет олицетворять поле "Product-ID", а 0x4FBD это "Build=20413". Следующий дворд 0x00000003 заканчивает лог(0), и знаменует счётчик операций для данного продукта. Аналогичным образом вытаскиваем и остальные 8-байтные логи, общее число которых в данном случае 10.
Ладно.., логи мы научились добывать. Но как привести их в более дружелюбный читабельный вид?
Оказалось, что гугл знаком с этой проблемой и на запрос любезно предоставил мне файл ниже, где питон постарался и сформировал из файла библиотеки "msobj140-msvcrt.lib" лист такого содержания. Остаётся лишь оформить эти строки в виде таблицы, и можно выводить на консоль привязанные к кодам объекты. Так-же мне попалась
4. Практика – парсим хидеры
Под занавес немного попрактикуемся, чтобы заточить навыки.
В своём "Rich-parse" отечественного разлива, я соберу основную информацию из РЕ и опционального заголовка, после чего проверю бинарь на наличие в нём хидера Rich. Если таковой имеется, то взяв ключ расшифрую блок ксором, и выведу на консоль всю полученную информацию. Для этого буду преследовать следующий алгоритм:
1. Функция GetOpenFileName() из библиотеки comdlg32.dll предоставляет стандартное окно системы, для выбора файлов. Она имеет всего один аргумент – указатель на структуру "OPENFILENAME", в которой указывается маска с расширением требуемых файлов (в данном случае exe/sys/dll), а так-же линк на буфер, куда функция сбросит полный путь до выбранного файла.
2. Пакет из трёх функций _lopen/read/close() соответственно открыв читает указанный файл. Мне достаточно только первого килобайта, который сохраняю в свой буфер.
3. Далее идёт тест на присутствие Rich-Header. Получив указатель на РЕ-заголовок по смещению(3Ch) от начала файла, строковой инструкцией
4. Если обнаруживаю Rich, то первым делом нужно расшифровать весь заголовок, после чего снимаю с него хэш MD5. Он нужен для идентификации файлов, о чём пойдёт речь чуть позже. Хэш получаю при помощи функций из библиотеки Bcrypt.dll, что-да-как уже обсуждалось в статье ASM-CNG.
5. Теперь имеем расшифрованный блок и можно приступать к декодирования данных Product-ID, Build и Counter. Как уже упоминалось, в скрепке лежит инклуд с привязанными к кодам строками. Поскольку коды назначаются последовательно от нуля и до 0x10E (всего 207 штук), то обычным умножением его на 4 можно получить смещение к соответствующей строке. То-есть создаём таблицу, в которой будут лежать последовательные указатели на строки. Далее, когда получим код "Product-ID" из заголовка Rich, он будет соответствовать порядковому номеру указателя в таблице. Юзаем..
Немного пояснений..
Вначале идёт инфа из опционального заголовка где видно, что файл для платформы х32 с кодом 14Сh, имеет 4 программные секции (можно было перечислить их имена), и слинковал его компоновщик версии(10), а вот какой именно не указывается, мол догадайся сам. Далее следует база и точка-входа, после которых выравнивание секций в памяти (4096-байт = одна страница вирт.памяти), и на диске = 512 байт. Размер страницы лучше оставить в дефолте, всё-равно это значение не влияет на размер файла, а лишь на его проекцию в ОЗУ. А вот второе значение 512 имеет смысл "подёргать за вымя". Чем меньше его порог, тем меньше получим пустых нулей в образе файла на диске.
Второй блок информирует о наличии хидера Rich, показывает значение ключа шифрования, и вычисленный хэш MD5 (всегда 16-байт). Этот хэш имеет важное значение, поскольку в некоторых случаях освобождает нас от парсинга всего блока. То-есть просто снимаем отпечаток и всё. Зачем это нужно мы обсудим позже.
Ну и в последнем блоке представлена информация о процессе создания исполняемого файла. Обратите внимание, что помимо студии(16), в файле натоптала и студия(15). Такой расклад мы можем наблюдать, когда программа использует чужие, уже готовые модули. Так-же видим импорт и компоновку символов с ресурсами в файлы *.obj. На заключительном этапе LINK.EXE версии(10) собирает всю эту кухню в единый файл отладчика KD.EXE (Kernel Debug).
Ради интереса я открывал много файлов и обнаружил интересные вещи. К примеру участник нашего форума @Mikl___ на сайте wasm.in выкладывал
А вот ещё один пример, скомпилированный на этот раз в среде VisualBasic.
Ясно, что автор этого кряка не трогал дефолтные настройки выравнивания секций. Тогда почему они равны и имеют значения 4096-байт? Это на порядок увеличивает размер исполняемого файла, и обычная форточка весит аж под 20К. Странно..
5. Область применения знаний
В заключении попробуем ответить на вопрос: -"Какой вообще профит несёт в себе Rich, и что мы можем с него поиметь"? Ответ вполне очевиден. Если мы не занимаемся исследованием бинарей, защитой и реверсом софта, то естественно проку от Rich, как от козла молока. Чтобы по достоинству оценить всю мощь этого механизма, нужно мыслить иррационально, как это делают например сотрудники аверских контор. Тут и всплывают хэши заголовков Rich. Вот несколько наводок, а остальное сами..
1. Хэши позволяют без особых затрат классифицировать малварь. Правда для этого нужна база штамов, которую можно запросить у тех-же аверов, или со-временем собрать самому. Причём если файл один раз засветился (и после этого автор модифицировал его содержимое, чтобы обновить контрольную сумму), с вероятностью 99% он будет обнаружен вновь, парсингом заголовка Rich. Операции на уровне линкера – это самый нижний уровень, уйти от которого можно лишь координально сменив тактику, и все исходные файлы. Соответственно это будет уже совсем другая дичь, поймав которую за хвост мы вновь снимаем с неё хэш MD5, и помещаем его в свою базу.
2. Примечательным является то, что такие протекторы как Enigma, Themida, VMProtect и прочие, не трогают заголовок Rich. Это означает, что когда к нам попадёт накрытый протектором файл, потенциально мы можем обнаружить исходный, неупакованный пайлоад виря, путём поиска идентичных хэшей Rich из базы. Большинство этих средств защиты полностью меняют природу файла, щедро присыпая его слоем щебёнки. В обычных условиях, нам пришлось-бы весь этот пирог разгребать, а так достаточно потянуть за Rich, и не нужно пробираться в нёдра файла.
3. Недооценивать противника глупо. Нужно иметь в виду, что авторы вредоносов всегда идут на шаг впереди. Так, история знает случаи, когда точившие малварь ребята сажали аверские конторы на пятую точку, просто подменяя Rich-заголовок, выдернув его у легитимного файла, например calc или paint.exe. Совсем удалять Rich палевно, а подменить его часть или целиком уже няшно. Авер недоумевает – вроде файла в базе хэшей Rich нет, но ведёт он себя странно. Тогда приходится грузить сэмпл в вирт.машину и пытать его там. Более того, если мы знаем Rich какого-нибудь зверька, то подставив его в свой код, можно возложить всю вину на его автора, а самому остаться в тени. Именно так поступили Lazarus на олимпиаде в Пекине 2018-года. Да мало-ли чё можно придумать.. Тут главное иметь фантазию.
6. Заключение
Исследование бинарных файлов – невероятно увлекательное занятие, тем-более что в современных EXE есть-что искать. На первый взгляд вроде кажется, что ещё можно было добавить к теме РЕ-файлов, которая за столько лет изъезжена уже вдоль и поперёк. Однако вот наскребал-таки инфы на целую статью и то, принудительно удерживая себя. Как обычно, многое осталось за кадром, но смысл весь в том, чтобы просто показать направление и обрисовать возможности. Дальше, заинтересованный читатель должен продвигаться сам. Здесь вот Новый год подкрадывается, и если не встретимся, хочу пожелать всем удачи и попутного ветра. В скрепке лежит инклуд и исполняемый файл для тестов. До встречи, пока!
Под катом:
1. Общие сведения;
2. Структура РЕ-файла;
3. Заголовок Rich;
4. Практика – парсим хидеры;
5. Область применения знаний;
6. Заключение.
1. Общие сведения
Формат РЕ появился с выходом в свет WinNT-3.1. Защищённый режим и виртуальная память требовали определённой стратегии распределения ресурсов – эту задачу как-раз и возложили на сам исполняемый РЕ-файл. В поисках нирваны, инженерам пришлось координально изменить подход к загрузке образов в ОЗУ. Если раньше обстановка в системе была статична без каких-либо эксцессов, то теперь, получив в своё распоряжение неограниченный объём памяти, всякое приложение на радостях так и старалось всю её поиметь.
Дабы ограничить аппетиты, было решено внедрить в исполняемый файл небольшой заголовок, и связать его с загрузчиком образов. Теперь файл сам сообщает, сколько ему требуется памяти для производственных нужд, а система её предоставляет. Нужно отметить, что это было единственно-верным решением в сложившейся ситуации, иначе великие умы уже давно придумали-бы что-нибудь свежее. Но видимо слабо.. РЕ родился в 1993-году и дошёл до нас в своём первозданном виде! Если и применялись какие-то моды, то исключительно на стороне системного загрузчика, а не самого файла. Здесь нужно снять шляпу перед разработчиком формата Марком Збиковски, с инициалов "MZ" которого начинаются все исполняемые файлы. Новоиспечённый формат точили и шлифовали ещё пару лет, так-что первая его спецификация датируется 1995-годом, как дополнение к системе Win-95.
Однако позже, любопытными программистами в РЕ был обнаружен нигде не регламентируемый блок информации. Причём в некоторых файлах он присутствовал, а в некоторых нет, т.е. появлялся как призрак. Microsoft крепко держала оборону на этот счёт, и все вопросы тупо игнорировала. В конечном итоге получилось так, что подобным молчанием она выстрелила себе в ногу, поскольку "тёмные силы" не заставили себя ждать и раскурив весь план стали активно его применять в своих целях. Что примечательно, этот блок и по сей день остаётся в тени – системный протекторат посчитал оставить всё как-есть, и не включать его в спеку.
Как выяснилось спустя много лет, данный блок был включён в РЕ-файл по требованию штатных сотрудников Microsoft, которые занимались в закрытых кабинетах реверс-инженерингом уже скомпилированного, бинарного софта. Они часто сталкивались с дилемой, какой именно версией компилятора C++ был создан исследуемый файл, и сколько было исходных модулей с кодом (в больших проектах может быть более сотни отдельных файлов, которые связываются в цепочку посредством include).
Задачу возложили на двух инженеров – это Дэн Сполдинг и Ричард Шупак, которые работали в то время над компилятором Visual C++ и базой кода системных библиотек. Не заморачиваясь, инженеры тупо включили в линкер LINK.EXE опцию ведения лога о проделанной работе, с сохранением его в специально отведённый для этих целей заголовок РЕ-файла. Формат лога состоит всего из трёх полей: Product-ID в лице версии компилятора (VC, VB, MASM, Phx), номер его сборки Build, а так-же счётчика Counter скомпилированных исходных файлов, которые входили в состав проекта. Пример такого блока представлен ниже:
В документации на PE/COFF от Microsoft нет ни единого упоминания об этом блоке, а сразу после заглушки DOS указывается РЕ-Header. В качестве маркера окончания блока, Шупак вставил дворд своего имени "Rich", а с инициалов второго автора Дэна блок начинается, просто здесь они накрыты ксором и не видны. После расшифровки получим сигнатуру "DanS". Позже мы напишем приложение, которое покажет всю информацию из этого заголовка. В конечном итоге конкретно для данного случая (софт PEiD), результат будет таким где видно, что по всей вероятности проект собирался в 15-ой студии, которая комплектуется линкером версии(9). Особый интерес представляют и значения в столбце "Count" – это счётчик исходных файлов:
На данный момент, в базе "Product-ID" имеется 0x10E=270 опознанных объектов, которые я собрал в свой инклуд (см.скрепку в подвале статьи). Поскольку Microsoft их не документировала (хотя сама активно юзает), энтузиастам всё-же удалось раскопать эти коды в файле "msobj140-msvcrt.lib" студий начиная с 2015, и при помощи скрипта на питоне вытащить их от туда в приглядном виде. Более продвинутые мемберы обзавелись ими ещё раньше, вскрыв как консервную банку линкер LINK.EXE, с последующим поиском алиасов имён экспериментально. Так появился софт, который с точностью определяет среду разработки, типа тот-же PeiD, DiE и прочие.
С Rich-заголовком связан один нюанс. Поскольку мягкие так и не обнародовали его, то он присутствует только в исполняемых файлах, которые собирались в продуктах самой Microsoft, с использованием линкера LINK.EXE, а это как-правило: Visual Studio, Visual Basic и MASM. Если-же бинарь компилировали в Delphi, Pascal, Go и прочих, то хидер Rich искать в нём бесполезно – его там попросту нет. В палату кастрированных попадает и ассемблер FASM, т.к. у него свой линкер и компилятор.
Но судя по статистике, доля продуктов Microsoft на рынке двоичных файлов составляет порядка 80% от общего числа, так-что Rich в природе частое явление. Под эгидой РЕ ходят такие расширения как: *.exe (dll, sys, drv, mui, scr, efi, ocx, bpl, dpl, cpl, acm, ax), а значит вся/эта братия будет представляться нашим потенциальным клиентом.
2. Структура РЕ-файла
Перед тем как рассмотреть формат заголовка Rich, в общих чертах вспомним структуры официального РЕ-файла. Для этого напишем обычный "HelloWorld" и вскормим его любому HEX-редактору.
Бинари всех исполняемых файлов Win должны начинаться с проприетарного заголовка DOS с сигнатурой "MZ" (привет Марк Збиковски). Создающие файл линкеры выделяют под него 0x80 = 128-байт, из которых системный загрузчик образов проверяет только два поля – это непосредственно сигнатура по смещению(00), и т.н. поле
e_lfanew
по смещению(3Сh) от начала, где прописывается указатель на РЕ-заголовок (красная стрелка). В данном случае там лежит значение 0x00000080, хотя это не константа и в других файлах офсет может быть иным.Все остальные поля хидера DOS (включая стаб) давно атрофированы и никому не нужны, так-что мы можем смело забивать их нулями, или в целях уменьшения размера вообще сместить весь файл (начиная с РЕ-хидера) вверх, по смещению(08) от его начала, соблюдая выравнивание на 8-байтную границу. Только поле
e_lfanew
нужно будет оставить, скорректировав его соответственно к смещению(08). Если попросить гугл, то в сети можно найти доку
Ссылка скрыта от гостей
от парней из BlackHat, которые извращаются с РЕ по-чёрному. Они приводят пруфы, что РЕ-заголовок можно переносить в любое место вверх/вниз, но только в пределах 4Gb, поскольку под e_lfanew
выделяется 32-битный дворд.Наиболее информативным для загрузчика образов является РЕ-заголовок, за которым сразу-же следует опциональный. Здесь уже указываются требования файла к системе: размер стека и памяти Heap, предпочтительный базовый адрес загрузки образа в память (в некоторых случаях ASLR его всё-равно меняет), точка-входа в программу для регистра EIP, выравнивание программных секций на диске и в памяти, их кол-во, адреса, и прочее. Как и в случае с заголовком DOS, большинство полей в Optional-Header так-же игнорируются современными загрузчиками, поскольку достались нам в наследство от Win95/98.
В контексте данной статьи, для нас интерес представляет поле "LinkerVersion" в опциональном заголовке. Оно имеет размер 2-байта, в первом из которых указывает версия-мажор линкера, а во-втором минор. К примеру я использую компилятор FASM ver.1.73, тогда в первом байте получаю(1), а во-втором(73). Отметим, что данное поле является документированным, поэтому свой "возраст" сбрасывают в него буквально все линкеры, а не только подопечные Microsoft – запомним его на будущее.
В спойлере ниже спрятана структура PE_HEADER, в которой неиспользуемые поля я оставил без комментов, хотя по именам всегда можно догадаться об их назначении. Для 64-битных версий исполняемых файлов структура идентичная, за исключением всех полей с адресами – их нужно расширить до 8-байт qword:
C-подобный:
;// DWORD-указатель на РЕ-заголовок,
;// лежит по смещению 3Ch от начала файла MZ-Header
struct PE_HEADER
Signature dd 0 ;// 50450000 = 'PE..'
Machine dw 0 ;// 0x014C = Intel386, 0x8664 = AMD64
NumberOfSection dw 0 ;// кол-во секций в файле
TimeStamp dd 0
SymbolPointer dd 0
SymbolSize dd 0
OptHeaderSize dw 0 ;// 00E0 = размер "OPTIONAL HEADER"
Flags dw 0
;//--- OPTIONAL HEADER -------------------
Magic dw 0 ;// Offset 18h = 010B
LinkerVersion dw 0 ;//<-------------------- Наш клиент!
SizeOfCode dd 0
SizeOfInitData dd 0
SizeOfUnInitData dd 0
EntryPointRVA dd 0 ;// точка-входа
CodeBaseRVA dd 0 ;//
DataBaseRVA dd 0 ;//
ImageBase dd 0 ;// база в памяти
SectionAlign dd 0 ;// выравнивание секций в памяти
FileAlign dd 0 ;// выравнивание секций на диске
OsVersion dd 0
ImageVersion dd 0
SubSysVersion dd 0 ;// 4
Reserved dd 0
ImageSize dd 0 ;//
HeaderSize dd 0 ;//
FileChecksum dd 0
SubSystem dw 0 ;//
DLLflags dw 0 ;//
StackReserve dd 0
StackCommit dd 0 ;// требуемый размер стека
HeapReserve dd 0 ;// требуемая память хип
HeapCommit dd 0
LoaderFlag dd 0
NumberDataDir dd 0 ;// кол-во элементов в каталоге
;//--- DATA DIRECTORY --------------------
ExportRVA dd 0,0 ;// первый дворд = адрес в памяти, второй = размер данных
ImportRVA dd 0,0 ;//
ResourceRVA dd 0,0 ;//
ExceptionRVA dd 0,0
SecurityRVA dd 0,0
FixUpRVA dd 0,0
DebugRVA dd 0,0
ArchitectureRVA dd 0,0
GlobalRVA dd 0,0
TlsRVA dd 0,0
LoadConfigRVA dd 0,0
BoundImportRVA dd 0,0
IatRVA dd 0,0
DelayImportRVA dd 0,0
COMdescriptor dd 0,0
Reserved1 dd 0,0
ends
В опциональном заголовке прописались ещё два интересных мембера. Первый под кличкой "SectionAlignment" сообщает системному диспетчеру-памяти размер выравнивания секций. По умолчанию все линкеры определяют его как 4Кб страницы, и лучше оставить это поле без изменений. А вот второй "FileAlignment" определяет уже размер секций образа файла на диске. Экспериментальным путём удалось определить, что диапазон этого поля варьируется от 16-байт до 64К, а в дефолте все пихают туда 512. Уменьшив это выравнивание в 2 или 4-раза, можно сжать дисковый файл, чтобы он занимал на харде меньше места. В практической части мы ещё вернемся к нему.
3. Заголовок "Rich"
Процесс создания бинарного файла из исходников включает в себя три основных этапа – трансляция, компиляция и компоновка, которую на английский манер называют ещё линковка (linking, связывание). Заглянем под капот каждого из них:
1. Трансляция. На данном этапе, препроцессор компилятора вычисляет адреса всех меток и переходов в исходнике. Часто бывает так, что адреса и переменные напрямую зависят друг от друга. То-есть пока не вычислим первый адрес, не получим второй. Поэтому трансляция повторяется циклически как-минимум два раза. Такие трансляторы называют двух/трёх-проходной. Результат вычислений сохраняется в специальной таблице. Когда определились с адресами, переходим к сл.этапу..
2. Компиляция. Здесь уже текст исходника переводится в машинный код, и на выходе получаем объектный модуль, который по-сути уже готов к исполнению на центральном процессоре. Однако большие проекты собираются из нескольких отдельных исходников, так-что процесс компиляции производится для каждого из них. Соответственно на выходе получаем уже несколько самостоятельных объектных файлов.
3. Линковка (компоновка). Это последний этап создания исполняемого файла. Процедура занимается тем, что собирает в единое целое все объектные файлы проекта, связывая их друг-с-другом, и помещая в один флакон EXE/DLL/SYS. Этим как-раз и озадачен линкер среды разработки. Результатом данной операции является готовый к исполнению файл.
Суть внедрения в РЕ-файл заголовка "Rich" заключается в том, чтобы линкер запоминал в нём каждый свой шаг. Такой алгоритм способствует не только последующему реверсу софта, но и даёт возможность контролировать целостность файлов. Например если к нам попадёт подозрительный системный файл, можно будет вычислить, каким именно инструментарием он был создан. Кстати в своё время, хакерская группировка Lazarus
Ссылка скрыта от гостей
именно на этом, когда подсунула созданный в кулуарах файл, выдавая его за системный. Правда своего они всё-же добились, но оставили за собой след.Таким образом, Rich-заголовок динамически расширяется в зависимости от того, сколько логов сбросил в него линкер. Каждый лог имеет размер 8-байт и хранит три составляющие – это уже упомянутые: Product-ID(2), Build(2) и Counter(4). В скобках здесь я указал длину поля в байтах.
Маркером начала заголовка служит 4-байтная строка "DanS", а маркером окончания "Rich". Теперь весь полученный блок шифруется обычным
XOR
, а ключом для ксора является контрольная сумма блока + содержимое MZ-заголовка (при этом поле e_lfanew в MZ сбрасывается в нуль). Другими словами, чтобы получить ключ-шифрования, линкер складывает все дворды начиная от самого начала файла по смещению(0), и вплоть до маркера окончания "Rich". Полученная сумма сохраняется сразу-же за сигнатурой "Rich". Как результат получаем такую картину:Значит чтобы расшифровать Rich-заголовок, мы находим маркер-окончания "Rich", сразу после которого будет лежать ключ для операции
XOR
. В данном случае ключ имеет значение 0x4BB35FE4. Теперь ставим обратный шаг инструкцией ассемблера STD
(что подразумевает Set Direction, вкл.обратное направление) и начиная с маркера "Rich" приступаем к декрипту. После расшифровки каждого дворда, нужно проверять полученные значения на маркер-начала "DanS". То-есть расшифровывать нужно от конца к началу блока. Результат этого процесса представлен в нижнем окне скрина, который снят с отладчика.Как видим, после сигнатуры "DanS" ровным строем до конца строки, идут три дворда нулей – это выравнивание и присутствует оно всегда. Поскольку
0 xor Key
на выходе даёт Key
, то ключ для расшифровки можно брать не с конца, а с любого из двордов 2,3,4 от начала блока. Если посмотреть на верхнее окно и вправду, там лежит этот-же ключ.Непосредственно 8-байтные логи линкера начинаются со-второй строки дампа (см.нижнее окно). В данном случае, первый дворд лога(0) имеет значение 0x00964FBD, где 0x0096 будет олицетворять поле "Product-ID", а 0x4FBD это "Build=20413". Следующий дворд 0x00000003 заканчивает лог(0), и знаменует счётчик операций для данного продукта. Аналогичным образом вытаскиваем и остальные 8-байтные логи, общее число которых в данном случае 10.
Ладно.., логи мы научились добывать. Но как привести их в более дружелюбный читабельный вид?
Оказалось, что гугл знаком с этой проблемой и на запрос любезно предоставил мне файл ниже, где питон постарался и сформировал из файла библиотеки "msobj140-msvcrt.lib" лист такого содержания. Остаётся лишь оформить эти строки в виде таблицы, и можно выводить на консоль привязанные к кодам объекты. Так-же мне попалась
Ссылка скрыта от гостей
, с подробным описанием соответствий. В общем есть из чего выбирать:
Python:
#!/usr/bin/env python3
int_names = {
0x0000: 'prodidUnknown',
0x0001: 'prodidImport0',
0x0002: 'prodidLinker510',
0x0003: 'prodidCvtomf510',
0x0004: 'prodidLinker600',
0x0005: 'prodidCvtomf600',
0x0006: 'prodidCvtres500',
0x0007: 'prodidUtc11_Basic',
0x0008: 'prodidUtc11_C',
0x0009: 'prodidUtc12_Basic',
0x000a: 'prodidUtc12_C',
0x000b: 'prodidUtc12_CPP',
0x000c: 'prodidAliasObj60',
0x000d: 'prodidVisualBasic60',
0x000e: 'prodidMasm613',
0x000f: 'prodidMasm710',
0x0010: 'prodidLinker511',
0x0011: 'prodidCvtomf511',
0x0012: 'prodidMasm614',
0x0013: 'prodidLinker512',
0x0014: 'prodidCvtomf512',
0x0015: 'prodidUtc12_C_Std',
0x0016: 'prodidUtc12_CPP_Std',
0x0017: 'prodidUtc12_C_Book',
0x0018: 'prodidUtc12_CPP_Book',
0x0019: 'prodidImplib700',
0x001a: 'prodidCvtomf700',
0x001b: 'prodidUtc13_Basic',
0x001c: 'prodidUtc13_C',
0x001d: 'prodidUtc13_CPP',
0x001e: 'prodidLinker610',
0x001f: 'prodidCvtomf610',
0x0020: 'prodidLinker601',
0x0021: 'prodidCvtomf601',
0x0022: 'prodidUtc12_1_Basic',
0x0023: 'prodidUtc12_1_C',
0x0024: 'prodidUtc12_1_CPP',
0x0025: 'prodidLinker620',
0x0026: 'prodidCvtomf620',
0x0027: 'prodidAliasObj70',
0x0028: 'prodidLinker621',
0x0029: 'prodidCvtomf621',
0x002a: 'prodidMasm615',
0x002b: 'prodidUtc13_LTCG_C',
0x002c: 'prodidUtc13_LTCG_CPP',
0x002d: 'prodidMasm620',
0x002e: 'prodidILAsm100',
0x002f: 'prodidUtc12_2_Basic',
0x0030: 'prodidUtc12_2_C',
0x0031: 'prodidUtc12_2_CPP',
0x0032: 'prodidUtc12_2_C_Std',
0x0033: 'prodidUtc12_2_CPP_Std',
0x0034: 'prodidUtc12_2_C_Book',
0x0035: 'prodidUtc12_2_CPP_Book',
0x0036: 'prodidImplib622',
0x0037: 'prodidCvtomf622',
0x0038: 'prodidCvtres501',
0x0039: 'prodidUtc13_C_Std',
0x003a: 'prodidUtc13_CPP_Std',
0x003b: 'prodidCvtpgd1300',
0x003c: 'prodidLinker622',
0x003d: 'prodidLinker700',
0x003e: 'prodidExport622',
0x003f: 'prodidExport700',
0x0040: 'prodidMasm700',
0x0041: 'prodidUtc13_POGO_I_C',
0x0042: 'prodidUtc13_POGO_I_CPP',
0x0043: 'prodidUtc13_POGO_O_C',
0x0044: 'prodidUtc13_POGO_O_CPP',
0x0045: 'prodidCvtres700',
0x0046: 'prodidCvtres710p',
0x0047: 'prodidLinker710p',
0x0048: 'prodidCvtomf710p',
0x0049: 'prodidExport710p',
0x004a: 'prodidImplib710p',
0x004b: 'prodidMasm710p',
0x004c: 'prodidUtc1310p_C',
0x004d: 'prodidUtc1310p_CPP',
0x004e: 'prodidUtc1310p_C_Std',
0x004f: 'prodidUtc1310p_CPP_Std',
0x0050: 'prodidUtc1310p_LTCG_C',
0x0051: 'prodidUtc1310p_LTCG_CPP',
0x0052: 'prodidUtc1310p_POGO_I_C',
0x0053: 'prodidUtc1310p_POGO_I_CPP',
0x0054: 'prodidUtc1310p_POGO_O_C',
0x0055: 'prodidUtc1310p_POGO_O_CPP',
0x0056: 'prodidLinker624',
0x0057: 'prodidCvtomf624',
0x0058: 'prodidExport624',
0x0059: 'prodidImplib624',
0x005a: 'prodidLinker710',
0x005b: 'prodidCvtomf710',
0x005c: 'prodidExport710',
0x005d: 'prodidImplib710',
0x005e: 'prodidCvtres710',
0x005f: 'prodidUtc1310_C',
0x0060: 'prodidUtc1310_CPP',
0x0061: 'prodidUtc1310_C_Std',
0x0062: 'prodidUtc1310_CPP_Std',
0x0063: 'prodidUtc1310_LTCG_C',
0x0064: 'prodidUtc1310_LTCG_CPP',
0x0065: 'prodidUtc1310_POGO_I_C',
0x0066: 'prodidUtc1310_POGO_I_CPP',
0x0067: 'prodidUtc1310_POGO_O_C',
0x0068: 'prodidUtc1310_POGO_O_CPP',
0x0069: 'prodidAliasObj710',
0x006a: 'prodidAliasObj710p',
0x006b: 'prodidCvtpgd1310',
0x006c: 'prodidCvtpgd1310p',
0x006d: 'prodidUtc1400_C',
0x006e: 'prodidUtc1400_CPP',
0x006f: 'prodidUtc1400_C_Std',
0x0070: 'prodidUtc1400_CPP_Std',
0x0071: 'prodidUtc1400_LTCG_C',
0x0072: 'prodidUtc1400_LTCG_CPP',
0x0073: 'prodidUtc1400_POGO_I_C',
0x0074: 'prodidUtc1400_POGO_I_CPP',
0x0075: 'prodidUtc1400_POGO_O_C',
0x0076: 'prodidUtc1400_POGO_O_CPP',
0x0077: 'prodidCvtpgd1400',
0x0078: 'prodidLinker800',
0x0079: 'prodidCvtomf800',
0x007a: 'prodidExport800',
0x007b: 'prodidImplib800',
0x007c: 'prodidCvtres800',
0x007d: 'prodidMasm800',
0x007e: 'prodidAliasObj800',
0x007f: 'prodidPhoenixPrerelease',
0x0080: 'prodidUtc1400_CVTCIL_C',
0x0081: 'prodidUtc1400_CVTCIL_CPP',
0x0082: 'prodidUtc1400_LTCG_MSIL',
0x0083: 'prodidUtc1500_C',
0x0084: 'prodidUtc1500_CPP',
0x0085: 'prodidUtc1500_C_Std',
0x0086: 'prodidUtc1500_CPP_Std',
0x0087: 'prodidUtc1500_CVTCIL_C',
0x0088: 'prodidUtc1500_CVTCIL_CPP',
0x0089: 'prodidUtc1500_LTCG_C',
0x008a: 'prodidUtc1500_LTCG_CPP',
0x008b: 'prodidUtc1500_LTCG_MSIL',
0x008c: 'prodidUtc1500_POGO_I_C',
0x008d: 'prodidUtc1500_POGO_I_CPP',
0x008e: 'prodidUtc1500_POGO_O_C',
0x008f: 'prodidUtc1500_POGO_O_CPP',
0x0090: 'prodidCvtpgd1500',
0x0091: 'prodidLinker900',
0x0092: 'prodidExport900',
0x0093: 'prodidImplib900',
0x0094: 'prodidCvtres900',
0x0095: 'prodidMasm900',
0x0096: 'prodidAliasObj900',
0x0097: 'prodidResource',
0x0098: 'prodidAliasObj1000',
0x0099: 'prodidCvtpgd1600',
0x009a: 'prodidCvtres1000',
0x009b: 'prodidExport1000',
0x009c: 'prodidImplib1000',
0x009d: 'prodidLinker1000',
0x009e: 'prodidMasm1000',
0x009f: 'prodidPhx1600_C',
0x00a0: 'prodidPhx1600_CPP',
0x00a1: 'prodidPhx1600_CVTCIL_C',
0x00a2: 'prodidPhx1600_CVTCIL_CPP',
0x00a3: 'prodidPhx1600_LTCG_C',
0x00a4: 'prodidPhx1600_LTCG_CPP',
0x00a5: 'prodidPhx1600_LTCG_MSIL',
0x00a6: 'prodidPhx1600_POGO_I_C',
0x00a7: 'prodidPhx1600_POGO_I_CPP',
0x00a8: 'prodidPhx1600_POGO_O_C',
0x00a9: 'prodidPhx1600_POGO_O_CPP',
0x00aa: 'prodidUtc1600_C',
0x00ab: 'prodidUtc1600_CPP',
0x00ac: 'prodidUtc1600_CVTCIL_C',
0x00ad: 'prodidUtc1600_CVTCIL_CPP',
0x00ae: 'prodidUtc1600_LTCG_C',
0x00af: 'prodidUtc1600_LTCG_CPP',
0x00b0: 'prodidUtc1600_LTCG_MSIL',
0x00b1: 'prodidUtc1600_POGO_I_C',
0x00b2: 'prodidUtc1600_POGO_I_CPP',
0x00b3: 'prodidUtc1600_POGO_O_C',
0x00b4: 'prodidUtc1600_POGO_O_CPP',
0x00b5: 'prodidAliasObj1010',
0x00b6: 'prodidCvtpgd1610',
0x00b7: 'prodidCvtres1010',
0x00b8: 'prodidExport1010',
0x00b9: 'prodidImplib1010',
0x00ba: 'prodidLinker1010',
0x00bb: 'prodidMasm1010',
0x00bc: 'prodidUtc1610_C',
0x00bd: 'prodidUtc1610_CPP',
0x00be: 'prodidUtc1610_CVTCIL_C',
0x00bf: 'prodidUtc1610_CVTCIL_CPP',
0x00c0: 'prodidUtc1610_LTCG_C',
0x00c1: 'prodidUtc1610_LTCG_CPP',
0x00c2: 'prodidUtc1610_LTCG_MSIL',
0x00c3: 'prodidUtc1610_POGO_I_C',
0x00c4: 'prodidUtc1610_POGO_I_CPP',
0x00c5: 'prodidUtc1610_POGO_O_C',
0x00c6: 'prodidUtc1610_POGO_O_CPP',
0x00c7: 'prodidAliasObj1100',
0x00c8: 'prodidCvtpgd1700',
0x00c9: 'prodidCvtres1100',
0x00ca: 'prodidExport1100',
0x00cb: 'prodidImplib1100',
0x00cc: 'prodidLinker1100',
0x00cd: 'prodidMasm1100',
0x00ce: 'prodidUtc1700_C',
0x00cf: 'prodidUtc1700_CPP',
0x00d0: 'prodidUtc1700_CVTCIL_C',
0x00d1: 'prodidUtc1700_CVTCIL_CPP',
0x00d2: 'prodidUtc1700_LTCG_C',
0x00d3: 'prodidUtc1700_LTCG_CPP',
0x00d4: 'prodidUtc1700_LTCG_MSIL',
0x00d5: 'prodidUtc1700_POGO_I_C',
0x00d6: 'prodidUtc1700_POGO_I_CPP',
0x00d7: 'prodidUtc1700_POGO_O_C',
0x00d8: 'prodidUtc1700_POGO_O_CPP',
0x00d9: 'prodidAliasObj1200',
0x00da: 'prodidCvtpgd1800',
0x00db: 'prodidCvtres1200',
0x00dc: 'prodidExport1200',
0x00dd: 'prodidImplib1200',
0x00de: 'prodidLinker1200',
0x00df: 'prodidMasm1200',
0x00e0: 'prodidUtc1800_C',
0x00e1: 'prodidUtc1800_CPP',
0x00e2: 'prodidUtc1800_CVTCIL_C',
0x00d3: 'prodidUtc1800_CVTCIL_CPP',
0x00e4: 'prodidUtc1800_LTCG_C',
0x00e5: 'prodidUtc1800_LTCG_CPP',
0x00e6: 'prodidUtc1800_LTCG_MSIL',
0x00e7: 'prodidUtc1800_POGO_I_C',
0x00e8: 'prodidUtc1800_POGO_I_CPP',
0x00e9: 'prodidUtc1800_POGO_O_C',
0x00ea: 'prodidUtc1800_POGO_O_CPP',
0x00eb: 'prodidAliasObj1210',
0x00ec: 'prodidCvtpgd1810',
0x00ed: 'prodidCvtres1210',
0x00ee: 'prodidExport1210',
0x00ef: 'prodidImplib1210',
0x00f0: 'prodidLinker1210',
0x00f1: 'prodidMasm1210',
0x00f2: 'prodidUtc1810_C',
0x00f3: 'prodidUtc1810_CPP',
0x00f4: 'prodidUtc1810_CVTCIL_C',
0x00f5: 'prodidUtc1810_CVTCIL_CPP',
0x00f6: 'prodidUtc1810_LTCG_C',
0x00f7: 'prodidUtc1810_LTCG_CPP',
0x00f8: 'prodidUtc1810_LTCG_MSIL',
0x00f9: 'prodidUtc1810_POGO_I_C',
0x00fa: 'prodidUtc1810_POGO_I_CPP',
0x00fb: 'prodidUtc1810_POGO_O_C',
0x00fc: 'prodidUtc1810_POGO_O_CPP',
0x00fd: 'prodidAliasObj1400',
0x00fe: 'prodidCvtpgd1900',
0x00ff: 'prodidCvtres1400',
0x0100: 'prodidExport1400',
0x0101: 'prodidImplib1400',
0x0102: 'prodidLinker1400',
0x0103: 'prodidMasm1400',
0x0104: 'prodidUtc1900_C',
0x0105: 'prodidUtc1900_CPP',
0x0106: 'prodidUtc1900_CVTCIL_C',
0x0107: 'prodidUtc1900_CVTCIL_CPP',
0x0108: 'prodidUtc1900_LTCG_C',
0x0109: 'prodidUtc1900_LTCG_CPP',
0x010a: 'prodidUtc1900_LTCG_MSIL',
0x010b: 'prodidUtc1900_POGO_I_C',
0x010c: 'prodidUtc1900_POGO_I_CPP',
0x010d: 'prodidUtc1900_POGO_O_C',
0x010e: 'prodidUtc1900_POGO_O_CPP',
}
## These are somewhat guessed, don't rely on them 100%
def vs_version(i):
if i > len(int_names) or i < 0:
return '<unknown>', 'XX.XX'
elif i in range(0x00fd, 0x010e + 1):
return 'Visual Studio 2015', '14.00'
elif i in range(0x00eb, 0x00fd):
return 'Visual Studio 2013', '12.10'
elif i in range(0x00d9, 0x00eb):
return 'Visual Studio 2013', '12.00'
elif i in range(0x00c7, 0x00d9):
return 'Visual Studio 2012', '11.00'
elif i in range(0x00b5, 0x00c7):
return 'Visual Studio 2010', '10.10'
elif i in range(0x0098, 0x00b5): ## TODO: Investigate on Phoenix
return 'Visual Studio 2010', '10.00'
elif i in range(0x0083, 0x0098): ## TODO: Investigate on Phoenix
return 'Visual Studio 2008', '09.00'
elif i in range(0x006d, 0x0083):
return 'Visual Studio 2005', '08.00'
elif i in range(0x005a, 0x006d):
return 'Visual Studio 2003', '07.10'
elif i == 1:
return 'Visual Studio', '00.00'
else:
return '<unknown>', '00.00'
4. Практика – парсим хидеры
Под занавес немного попрактикуемся, чтобы заточить навыки.
В своём "Rich-parse" отечественного разлива, я соберу основную информацию из РЕ и опционального заголовка, после чего проверю бинарь на наличие в нём хидера Rich. Если таковой имеется, то взяв ключ расшифрую блок ксором, и выведу на консоль всю полученную информацию. Для этого буду преследовать следующий алгоритм:
1. Функция GetOpenFileName() из библиотеки comdlg32.dll предоставляет стандартное окно системы, для выбора файлов. Она имеет всего один аргумент – указатель на структуру "OPENFILENAME", в которой указывается маска с расширением требуемых файлов (в данном случае exe/sys/dll), а так-же линк на буфер, куда функция сбросит полный путь до выбранного файла.
2. Пакет из трёх функций _lopen/read/close() соответственно открыв читает указанный файл. Мне достаточно только первого килобайта, который сохраняю в свой буфер.
3. Далее идёт тест на присутствие Rich-Header. Получив указатель на РЕ-заголовок по смещению(3Ch) от начала файла, строковой инструкцией
LODSD
с обратным шагом, осуществляю поиск сигнатуры "Rich", продвигаясь назад от РЕ-заголовка. Между РЕ и Rich, в некоторых файлах встречаются нули, поэтому нужно учитывать их, протопав как-минимум шагов 10.4. Если обнаруживаю Rich, то первым делом нужно расшифровать весь заголовок, после чего снимаю с него хэш MD5. Он нужен для идентификации файлов, о чём пойдёт речь чуть позже. Хэш получаю при помощи функций из библиотеки Bcrypt.dll, что-да-как уже обсуждалось в статье ASM-CNG.
5. Теперь имеем расшифрованный блок и можно приступать к декодирования данных Product-ID, Build и Counter. Как уже упоминалось, в скрепке лежит инклуд с привязанными к кодам строками. Поскольку коды назначаются последовательно от нуля и до 0x10E (всего 207 штук), то обычным умножением его на 4 можно получить смещение к соответствующей строке. То-есть создаём таблицу, в которой будут лежать последовательные указатели на строки. Далее, когда получим код "Product-ID" из заголовка Rich, он будет соответствовать порядковому номеру указателя в таблице. Юзаем..
C-подобный:
format pe console
include 'win32ax.inc'
include 'equates\pe.inc'
include 'equates\bcrypt.inc'
entry start
;//----------
.data
filter db ' exe, dll, sys, ocx',0 ;// что видно в окне GetOpenFileName()
db '*.exe;*.dll;*.sys;*.ocx',0,0 ;// маска с типами файлов
x32 db 'Intel 386 or later',0
x64 db 'AMD64 (K8+)',0
align 16
ofn OPENFILENAME
;//<----- данные MD5
md5AlgHndl dd 0
md5Hndl dd 0
md5ObjLen dd 0
pcbResult dd 0
hashBuff rb 16
hashObject rb 256
;//-----------------
hFile dd 0
startRich dd 0
endRich dd 0
key dd 0
align 16
buff rb 2048
;//----------
.code
start: invoke SetConsoleTitle,<'*** PE-Rich information ***',0>
;//---- Запрашиваем у юзера исполняемый файл
cinvoke printf,<10,' Select executable file...',0>
mov [ofn.lStructSize],sizeof.OPENFILENAME
mov [ofn.lpstrFilter],filter
mov [ofn.lpstrFile],buff ;// сюда получу имя файла
mov [ofn.nMaxFile],512
mov [ofn.Flags], OFN_EXPLORER
invoke GetOpenFileName, ofn ;// 1 = OK
or eax,eax
jnz @f
cinvoke printf,<10,' ERROR! File not found.',0>
jmp @exit
;//---- Показываем его имя, и читаем первые 1024-байта из него
@@: invoke CharToOem,buff,buff
cinvoke printf,<10,' File: %s',10,0>,buff
invoke OemToChar,buff,buff
invoke _lopen,buff,0
push eax
invoke _lread,eax,buff,1024
pop eax
invoke _lclose,eax
;//---- Показываем основную инфу из РЕ и опционального заголовка
mov eax,dword[buff+3ch]
cinvoke printf,<10,' Magic..............: %.2s',\
10,' PE-Header offset...: 0x%X',0>,buff,eax
mov esi,buff
add esi,[esi+3ch]
movzx eax,[esi+PE_HEADER.Machine]
movzx ebx,[esi+PE_HEADER.NumberOfSection]
movzx ecx,[esi+PE_HEADER.LinkerVersion]
movzx edx,ch
and ecx,0xff
mov ebp,x32
cmp eax,0x14c
je @f
mov ebp,x64
@@: cinvoke printf,<10,' PE for machine.....: 0x%04X | %s',\
10,' Section count......: %d',\
10,' Linker version.....: %d.%02d',\
10,' Image base.........: 0x%08X',\
10,' EntryPointRVA......: 0x%08X',\
10,' Section alignment..: %d',\
10,' File alignment.....: %d',\
10,' Image size.........: %d',10,0>,eax,ebp,ebx,ecx,edx,\
[esi+PE_HEADER.ImageBase],[esi+PE_HEADER.EntryPointRVA],\
[esi+PE_HEADER.SectionAlign],[esi+PE_HEADER.FileAlign],\
[esi+PE_HEADER.ImageSize]
;//---- Проверить наличие хидера Rich
;//---- между РЕ и Rich могут быть нули, их нужно учитывать
cinvoke printf,<10,' *** Rich Header....: ',0>
mov esi,buff
add esi,[esi+3ch] ;// ESI = линк на РЕ-заголовок
mov ecx,10 ;// длина цикла
@@: sub esi,4 ;// двигаемся назад от РЕ
cmp dword[esi],'Rich' ;// ищем сигнатуру..
je @ok ;// если нашли!
dec ecx
jnz @b
cinvoke printf,<' Not found.',0>
jmp @exit
@ok: push esi
mov eax,[esi+4] ;// EAX = ключ шифрования
mov [key],eax ;// запомнить его.
add esi,4 ;//
mov [endRich],esi ;// указатель на хвост блока
cinvoke printf,< ' Found!',\
10,' *** Rich XOR-key...: %08X',0>,eax
;//---- Расшифруем Rich
pop esi
sub esi,4 ;// линк на первый дворд с конца
mov edi,esi ;// ESI для чтения, EDI для перезаписи STOSD
xor ecx,ecx ;// ECX=0, будет счётчиком логов
std ;// ставим обратный шаг
@@: inc ecx ;// считаем дворды..
lodsd ;// первый пошёл!
xor eax,[key] ;// расшифровать ключом
stosd ;// положить на место
cmp eax,'DanS' ;// это маркер начала?
jne @b ;// нет, продолжить..
;//---- Расшифровали блок! Запомним данные
cld ;// восстановить прямой шаг, Clear
add esi,4 ;// коррекция ESI после LODSD
mov [startRich],esi ;// запомнить линк на голову
sub ecx,4 ;// счётчик - первая строка нам не нужна (там нули)
shr ecx,1 ;// счётчик/2, т.к. парсить будем по 8, а не 4-байта
add esi,16 ;// пропустить первую строку
push esi ecx ;// про запас!
;//---- Вычисляем хэш MD5 Rich-заголовка
invoke BCryptOpenAlgorithmProvider,md5AlgHndl,BCRYPT_MD5_ALGORITHM,0,0
invoke BCryptGetProperty,[md5AlgHndl],BCRYPT_OBJECT_LENGTH,md5ObjLen,4,pcbResult,0
invoke BCryptCreateHash,[md5AlgHndl],md5Hndl,hashObject,[md5ObjLen],0,0,0
mov eax,[startRich] ;// EAX = начало блока
mov ecx,[endRich] ;//
sub ecx,eax ;// ECX = длина в байтах
invoke BCryptHashData,[md5Hndl],eax,ecx,0
invoke BCryptFinishHash,[md5Hndl],hashBuff,16,0
invoke BCryptDestroyHash,[md5Hndl]
cinvoke printf,<10,' *** Rich MD5-hash..: ',0>
mov esi,hashBuff
mov ecx,16
@@: xor eax,eax ;// покажем полученный хэш!
lodsb
push ecx
cinvoke printf,<'%02x',0>,eax
pop ecx
loop @b
;//---- Парсим расшифрованный Rich
cinvoke printf,<10,\
10,' Product-ID Build Count',\
10,' ----------------------------- ----- -----',0>
pop ecx esi ;// ESI = линк на первый лог, ECX = всего штук
@Rich: lodsd ;// EAX = "Product-ID + Build"
xchg eax,ebx ;// запомнить в EBX
lodsd ;// EAX = счётчик операций "Counter"
movzx edx,bx ;// EDX = Build
shr ebx,16 ;// EBX = Prod-ID
mov ebp,ebx ;// EBP = EBX для поиска строки в таблице
shl ebx,2 ;// получаем смещение по номеру
mov edi,PidTable ;// EDI = таблица строк "Product-ID"
add edi,ebx ;// получаем линк
mov ebx,[edi] ;// EBX = указатель на строку
push ecx esi
cinvoke printf,<10,' 0x%03X = %-22s %-6d %d',0>,ebp,ebx,edx,eax
pop esi ecx
loop @Rich ;// промотать ECX-раз..
@exit: cinvoke _getch ;// клавиша
cinvoke exit,0 ;// выход!
;//----------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',comdlg32,'comdlg32.dll',\
kernel32,'kernel32.dll',user32,'user32.dll',bcrypt,'bcrypt.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
include 'api\comdlg32.inc'
include 'api\bcrypt.inc'
Немного пояснений..
Вначале идёт инфа из опционального заголовка где видно, что файл для платформы х32 с кодом 14Сh, имеет 4 программные секции (можно было перечислить их имена), и слинковал его компоновщик версии(10), а вот какой именно не указывается, мол догадайся сам. Далее следует база и точка-входа, после которых выравнивание секций в памяти (4096-байт = одна страница вирт.памяти), и на диске = 512 байт. Размер страницы лучше оставить в дефолте, всё-равно это значение не влияет на размер файла, а лишь на его проекцию в ОЗУ. А вот второе значение 512 имеет смысл "подёргать за вымя". Чем меньше его порог, тем меньше получим пустых нулей в образе файла на диске.
Второй блок информирует о наличии хидера Rich, показывает значение ключа шифрования, и вычисленный хэш MD5 (всегда 16-байт). Этот хэш имеет важное значение, поскольку в некоторых случаях освобождает нас от парсинга всего блока. То-есть просто снимаем отпечаток и всё. Зачем это нужно мы обсудим позже.
Ну и в последнем блоке представлена информация о процессе создания исполняемого файла. Обратите внимание, что помимо студии(16), в файле натоптала и студия(15). Такой расклад мы можем наблюдать, когда программа использует чужие, уже готовые модули. Так-же видим импорт и компоновку символов с ресурсами в файлы *.obj. На заключительном этапе LINK.EXE версии(10) собирает всю эту кухню в единый файл отладчика KD.EXE (Kernel Debug).
Ради интереса я открывал много файлов и обнаружил интересные вещи. К примеру участник нашего форума @Mikl___ на сайте wasm.in выкладывал
Ссылка скрыта от гостей
работы с графикой OpenGL на ассемблере MASM. Скрин одного из его файлов представлен ниже где видно, что товарищ наш использует хитрый приём для уменьшения размера файла. Компилируя батником, он видимо определяет ключи для переназначения выравнивания секций, поскольку оба значения Alignment равны всего 16-байт, хотя в дефолте 4096/512. Код для процессора х64, компиль с линкером версии(14):А вот ещё один пример, скомпилированный на этот раз в среде VisualBasic.
Ясно, что автор этого кряка не трогал дефолтные настройки выравнивания секций. Тогда почему они равны и имеют значения 4096-байт? Это на порядок увеличивает размер исполняемого файла, и обычная форточка весит аж под 20К. Странно..
5. Область применения знаний
В заключении попробуем ответить на вопрос: -"Какой вообще профит несёт в себе Rich, и что мы можем с него поиметь"? Ответ вполне очевиден. Если мы не занимаемся исследованием бинарей, защитой и реверсом софта, то естественно проку от Rich, как от козла молока. Чтобы по достоинству оценить всю мощь этого механизма, нужно мыслить иррационально, как это делают например сотрудники аверских контор. Тут и всплывают хэши заголовков Rich. Вот несколько наводок, а остальное сами..
1. Хэши позволяют без особых затрат классифицировать малварь. Правда для этого нужна база штамов, которую можно запросить у тех-же аверов, или со-временем собрать самому. Причём если файл один раз засветился (и после этого автор модифицировал его содержимое, чтобы обновить контрольную сумму), с вероятностью 99% он будет обнаружен вновь, парсингом заголовка Rich. Операции на уровне линкера – это самый нижний уровень, уйти от которого можно лишь координально сменив тактику, и все исходные файлы. Соответственно это будет уже совсем другая дичь, поймав которую за хвост мы вновь снимаем с неё хэш MD5, и помещаем его в свою базу.
2. Примечательным является то, что такие протекторы как Enigma, Themida, VMProtect и прочие, не трогают заголовок Rich. Это означает, что когда к нам попадёт накрытый протектором файл, потенциально мы можем обнаружить исходный, неупакованный пайлоад виря, путём поиска идентичных хэшей Rich из базы. Большинство этих средств защиты полностью меняют природу файла, щедро присыпая его слоем щебёнки. В обычных условиях, нам пришлось-бы весь этот пирог разгребать, а так достаточно потянуть за Rich, и не нужно пробираться в нёдра файла.
3. Недооценивать противника глупо. Нужно иметь в виду, что авторы вредоносов всегда идут на шаг впереди. Так, история знает случаи, когда точившие малварь ребята сажали аверские конторы на пятую точку, просто подменяя Rich-заголовок, выдернув его у легитимного файла, например calc или paint.exe. Совсем удалять Rich палевно, а подменить его часть или целиком уже няшно. Авер недоумевает – вроде файла в базе хэшей Rich нет, но ведёт он себя странно. Тогда приходится грузить сэмпл в вирт.машину и пытать его там. Более того, если мы знаем Rich какого-нибудь зверька, то подставив его в свой код, можно возложить всю вину на его автора, а самому остаться в тени. Именно так поступили Lazarus на олимпиаде в Пекине 2018-года. Да мало-ли чё можно придумать.. Тут главное иметь фантазию.
6. Заключение
Исследование бинарных файлов – невероятно увлекательное занятие, тем-более что в современных EXE есть-что искать. На первый взгляд вроде кажется, что ещё можно было добавить к теме РЕ-файлов, которая за столько лет изъезжена уже вдоль и поперёк. Однако вот наскребал-таки инфы на целую статью и то, принудительно удерживая себя. Как обычно, многое осталось за кадром, но смысл весь в том, чтобы просто показать направление и обрисовать возможности. Дальше, заинтересованный читатель должен продвигаться сам. Здесь вот Новый год подкрадывается, и если не встретимся, хочу пожелать всем удачи и попутного ветра. В скрепке лежит инклуд и исполняемый файл для тестов. До встречи, пока!
Вложения
Последнее редактирование модератором: