Пишем упаковщик по шагам. Шаг седьмой. Релокации.

Предыдущий шаг здесь. Там, кстати, имелась ошибка в коде, я ее поправил. Она проявлялась, когда у файла было больше одного TLS-коллбэка.

Появилась новая версия библиотеки для работы с PE-файлами (0.1.7). Перекачайте и пересоберите ее.

Перейдем к следующей немаловажной части многих PE-файлов - релокациям. Они используются, когда невозможно загрузить образ по указанному в заголовке базовому адресу. Преимущественно такое поведение характерно для DLL-файлов (они в принципе без релокаций не могут нормально работать). Представьте, что exe-файл грузится по адресу 0x400000. Этот exe-файл грузит DLL, которая также грузится по этому адресу. Адреса совпадают, и загрузчик будет искать релокации у DLL-файла, потому что он грузится вторым после exe. И если релокаций не будет, то загрузка не пройдет.

Сами релокации - это просто набор таблиц с указателеми на DWORD'ы, которе загрузчик должен пересчитать, если образ загружается по адресу, отличному от базового. Типов релокаций много, но реально в x86 (PE) используются только два: IMAGE_REL_BASED_HIGHLOW = 3 и IMAGE_REL_BASED_ABSOLUTE = 0, причем второй ничего не делает, а нужен только для выравнивания таблиц релокаций.

Сразу скажу, что загрузчик exe-файлы грузит практически всегда по базовому адресу, не применяя релокации. DLL наш упаковщик паковать пока не умеет, поэтому для теста упаковки релокаций мы должны создать exe-файл с некорректным базовым адресом, и тогда загрузчик будет вынужден этот файл в памяти переместить. Я тут не буду приводить исходный код проекта для теста, вы найдете его в солюшене в конце статьи. Базовый адрес загрузки (Linker - Advanced - Base Address) я выбрал 0x7F000000.

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

Чтобы дать загрузчику знать о том, что у нашего файла есть релокации, делать ничего и не надо - у нас еще от оригинального файла остались нужные флаги, выставленные в заголовках PE-файла. Однако, нам нужно знать, по какому адресу файл загрузился.

Начнем с кода распаковщика (проект unpacker). Чтобы знать, по какому адресу файл должен был загрузиться, и по какому он реально загрузился, мы можем сделать так:

Мы добавили переменную, смысл которой полностью аналогичен добавленной в каком-то из предыдущих шагов переменной original_image_base. Отличие будет в том, что к переменной original_image_base мы применим релокации, узнав таким образом, по какому реальному адресу загрузился образ. При этом все последующие действия в распаковщике, которые мы производим с использованием этой переменной, править будет не нужно. А вот содержимое переменной original_image_base_no_fixup мы не будем модифицировать, запомнив тем самым, по какому адресу образ должен был загрузиться. Эту переменную, как и две другие, будет записывать упаковщик для распаковщика.

Модифицируем в распаковщике файл parameters.h, обновив смещения к этим трем переменным:

Теперь, как всегда, модифицируем структуру packed_file_info упаковщика (проект simple_pe_packer), добавив в нее два поля:

Далее, аналогично тому, как мы делали с импортами и ресурсами:

После строки:

допишем следующую:

которая в свежедобавленную в распаковщик переменную запишет значение базового адреса загрузки образа. На этом этапе после упаковки любого файла переменные original_image_base и original_image_base_no_fixup будут содержать одинаковые значения. Нужно натравить загрузчик на содержимое original_image_base, чтобы он при условии перемещения образа в памяти пофиксил ее. Добавим таблицу релокаций для этого. Код будем писать после следующих строк:

Итак,

Тут все просто - мы просто создали таблицу релокаций из единственного элемента и добавили ее в PE-файл.

Кроме того, необходимо заменить строки:

на:

дабы последние байты данных, используемых для инициализации локальной памяти потока, не налезли на релокации, которые мы размещаем прямо за ними.

Осталось убрать ранее добавленную строку:

чтобы директория релокаций не убиралась из файла (вызов image.rebuild_relocations заполняет ее таким образом, чтобы она указывала на новую директорию релокаций).

Все, что осталось сделать - обработать релокации оригинального файла в распаковщике (проект unpacker):

Этот код я разместил в распаковщике прямо перед кодом, который производит обработку TLS. Мы действуем как загрузчик. Убедившись в том, что файл был перемещен и что он имеет таблицу релокаций, осуществляем перебор всех таблиц релокаций (или перемещаемых элементов, другими словами) и всех релокаций в пределах таблицы. Просчитываем значения по каждому адресу, на которые указывают перемещаемые элементы. Если, например, DWORD по адресу, который должен быть пересчитан, содержал значение 0x800000, базовый адрес загрузки PE-файла 0x400000, а реально он загрузился по адресу 0x500000, то мы высчитываем новое значение по формуле [0x800000 - 0x400000 + 0x500000] = 0x900000.

Забавно кстати, чуть раньше я писал о том, что в naked-функциях MSVC++ не позволяет одновременно объявлять и инициализировать переменные. Оказалось, что это так только в общей области видимости функции. Если мы сделаем новую вложенную область видимости, то все работает. То есть, код

не соберется, а

отлично сработает.

На этом работа с релокациями завершена, и любой файл, имеющий релокации и даже некорректный базовый адрес загрузки, как в солюшене, запустится. Но есть еще кое-что: если файл помимо релокаций имеет TLS, то нас ждет неудача. В директории TLS (структуре IMAGE_TLS_DIRECTORY32) адреса используются абсолютные, а не относительные, поэтому нам необходимо их перемещать, если загрузчик разместил образ по адресу, отличному от базового адреса загрузки, указанного в заголовке PE-файла. Кроме того, адреса TLS-коллбэков, если они есть, также абсолютные, и их тоже нужно править.

Перед началом работы над релокациями TLS я задался вопросом, как это все протестировать. Собирать руками бинарники, которые бы имели релокации и TLS, не было никакого желания. Поэтому я модифицировал пример для тестирования релокаций (reloc_test), о котором я говорил выше, и слинковал его с помощью бесплатного линкера UniLink. Это, пожалуй, единственный линкер, который умеет собирать TLS с коллбэками. Исходный код примера теперь такой:

Поясню, что делает этот пример. При запуске будут вызваны два TLS-коллбэка - tls_callback и tls_callback2. Будут отображены два мессаджбокса с текстами "Process Callback!" и "Process Callback 2!". После этого в консоль будет выведено следующее:

Relocation test 123, 456
x

Наконец, через 2 секунды создастся новый поток, и TLS-коллбэки будут вызваны снова, но выдадут мессаджбоксы уже с текстами "Thread Callback!" и "Thread Callback 2!", и через 2 секунды программа завершится. Тут мы протестируем по полной программе обработку нашим упаковщиком и TLS, и релокаций. Чтобы собрать эту программу, для начала скомпилируем этот исходник (правой кнопкой мышки на файле main.cpp - Compile). Получим файл main.obj, который и скормим линкеру UniLink, набрав в консоли такую строку:

Эта команда говорит линкеру ulink.exe о том, что из файла main.obj нужно сделать файл main.exe, установив ему базовый адрес загрузки 0x7F000000 (чтобы наверняка применились релокации) и добавив сами релокации (опция -B-). После выполнения команды у нас будет файл с недопустимым базовым адресом загрузки, TLS с коллбэками и релокациями. Идеально для тестирования!

Переходим к проекту упаковщика (simple_pe_packer). Вынесем переменную first_callback_offset в более широкую область видимости, заменив строки

на

и дописав строки

перед

Далее, после строк

Дописываем код релокаций TLS:

Мы добавили перемещаемые элементы для всех ненулевых полей структуры IMAGE_TLS_DIRECTORY32, содержащих абсолютные адреса. Если у нас есть TLS-коллбэки, то мы добавляем релокацию и для нашего абсолютного адреса пустого TLS-коллбэка. Самое интересное - в распаковщике ничего править не нужно, потому что он обработает релокации оригинального файла, пересчитав тем самым оригинальные адреса TLS-коллбэков, и лишь после этого будет их вызывать. Единственное, что я сделал - это в очередной раз увеличил объем выделяемой распаковщиком на стеке памяти, так как ее уже начало не хватать. (Я заменил команду sub esp, 256 на sub esp, 4096, чтобы уже наверняка).

Протестировав упаковщик на созданном нами ядреном примере main.exe убеждаемся, что все прекрасно работает.

К этому моменту я уже проверил текущую версию упаковщика на главных exe-файлах следующих приложений: IrfanView, HM NIS Edit, Firefox, Notepad++, NSIS, Opera (ее нужно переименовывать в opera.exe после упаковки), Winamp, WinDjView, ResEd, Quake3, CatMario, Media Player Classic, Windows Media Player. После упаковки они работают!

Замечу напоследок, что в комментариях к исходникам UPX есть пометка о том, что если релокации и TLS находятся в одной секции, то загрузчик не будет фиксить адреса в TLS. Я, как видно, сделал именно так, и, как ни странно, все работает на Windows XP и 7 (на других не проверял).

Полный солюшен для этого шага: Own PE Packer Step 7

Пишем упаковщик по шагам. Шаг седьмой. Релокации.: 6 комментариев

  1. Круто,спасибо. Сам не собирал, жду завершения цикла, чтобы проделать с нуля.
    Подскажи, а как со степенью сжатия?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *