Содержание к диссертации
Введение
Глава 1. Обзор средств защиты от удаленной эксплуатации уязвимостей 9
1.1. Системы, внедряемые во время компиляции 9
1.2. Системы, внедряемые на этапе компоновки программы 10
1.3. Рандомизация расположения адресного пространства (ASLR) 11
1.4. Иные проекты 15
1.5. Технологии защиты от эксплуатации ошибок в Windows 16
1.5.1. Защита стека 17
1.5.2. Защита кучи 18
1.5.3. Безопасность SEH 23
1.5.4. Рандомизация РЕВ 26
1.5.5. Определение относительного виртуального адреса API функции 27
1.5.6. Переписывание критичных указателей 28
1.5.7. Запуск шелкода в РЕВ 29
1.5.8. Безопасность указателей 30
1.5.9. NX память и аппаратный DEP 30
1.6. Выводы 32
Глава 2. Теоретическое обоснование возможности диверсификации ПО 36
2.1. Формальное определение нефункционального преобразования 36
2.1.1. Базовые определения 36
2.1.2. Эквивалентность программ 39
2.1.3. Нефункциональные преобразования 42
2.1.4. Свойства НФП 45
2.2. Оценки НФП 50
2.2.1. Мощность НФП 51
2.2.2. Устойчивость преобразования 56
2.2.3 Цена преобразования 58
2.2.4 Главная мера 59
2.3. Реализация нефункциональных преобразований на процессорах х86 61
2.3.1. Замена регистров (уровень 1) 61
2.3.2. Перестановка множеств инструкций местами (уровень 2) 62
2.3.3. Процедурный подход (уровень 3) 66
2.3.4. Изменение прототипа функций (уровень 4) 69
2.3.5. Выделение случайных подфункций (уровень 5) 70
2.4. Выводы 72
Глава 3. Разработка метода диверсификации программного обеспечения 74
3.1. Общая архитектура генератора кода 75
3.2. Язык FuzzAsm 76
3.3. Используемость переменных 83
3.4. Определение каркаса функции 84
3.5. Внедрение «мусорного» кода 87
3.6. Генерация ко да 90
3.7. Сборка кода 91
3.8. Выводы 92
Глава 4. Разработка алгоритмов декомпиляции и рекомпиляции исполнимого кода 94
4.1. Декомпиляция 94
4.1.1. Этапы декомпиляции 96
4.1.2. Анализ структуры РЕ 97
4.1.3. Предварительный анализ кода 101
4.1.4. Дизассемблирование 102
4.1.5. Построение связного списка блоков 106
4.2. Рекомпиляция 108
4.2.1. Пересчет относительных виртуальных и физических адресов 109
4.2.2. Пересчет таблицы элементов релокации
4.2.3. Перелинковкауказателей
4.2.4. Ассемблирование результатов в файл
4.3. Выводы 113
Глава 5. Экспериментальное исследование алгоритмов диверсифицирующих преобразований 115
5.1. Измерение характеристик программ, оценка НФП 115
5.2. Выводы 118
Заключение 120
Список использованных источников 122
Приложения 130
- Технологии защиты от эксплуатации ошибок в Windows
- Нефункциональные преобразования
- Используемость переменных
- Пересчет относительных виртуальных и физических адресов
Введение к работе
Актуальность темы
Теория компиляции начала разрабатываться и приобрела законченный вид задолго до того времени, когда необходимость защиты информации дала о себе знать. Возможно, поэтому разработчики компиляторов неохотно модифицируют языки и компиляторы для них, руководствуясь идеями экспертов защиты информации. К примеру, широко известная особенность языков С и C++, позволяющая адресовать элементы массива, находящихся за последним объявленным его элементом, требует от программиста особенного внимания при разработке ПО. Уязвимости в программах (в частности, сетевых), связанных именно с подобного рода ошибками, до сих пор являются самыми распространенными, причем пояснения для написания корректного кода существуют с 1989 года. Лишь относительно недавно появились «надстройки» над компиляторами для автоматического контроля ошибочного кода, причем только для времени исполнения. Ни один разработчик компиляторов языка C/C++ не расширил функциональность языка, добавив в нее проверки диапазонов памяти, к которым возможно обращение генерируемого кода.
Большинство существующих эксплоитов для успешной работы требуют предварительного указания некоторых характеристик, специфичных для атакуемой программной среды - к примеру, эксплоиты переполнения локальных буферов и кучи обычно привязываются к целевой системе путем указания в них фиксированного адреса определенной инструкции, передающей управления на шелкод, либо требуют точного указания размера переполняемого буфера. В обоих приведенных случаях для проведения успешной атаки атакующий должен иметь точную информацию о параметрах целевой системы. Существующие в настоящее время методы защиты от удаленных атак не предусматривают никакую модификацию защищаемого
дистрибутива на машине конечного пользователя, тем самым, позволяя атакующему использовать факт абсолютной идентичности всех дистрибутивов атакуемого ПО. При наличии информации об уязвимости, присутствующей в ПО целевой машины, успешность атаки напрямую зависит от возможности обойти установленные механизмы защиты - иначе говоря, для успешной эксплуатации необходима информация об уязвимости целевого ПО и об уязвимости используемой системы защиты. Использование методов модификации исполнимых файлов дистрибутивов без изменения их функциональности, делающих популяцию дистрибутивов на машинах пользователей разнородной (то есть диверсифицированной), позволяет скрыть информацию, необходимую для подготовки эксплоита, что позволяет обезопасить защищаемую систему от удаленных атак, эксплуатирующих неизвестные ранее уязвимости захвата управления.
Таким образом, задача разработки методов защиты от удаленной эксплуатации уязвимостей, диверсифицирующих популяцию дистрибутивов определенного ПО, требует проведения интенсивных исследований и является актуальной.
Целью работы является разработка и исследование метода диверсификации, обеспечивающего защиту сетевого программного обеспечения от атак, направленных на удаленный захват управления.
Для достижения указанной цели необходимо решить следующие задачи:
Разработать алгоритмы анализа (декомпиляции) ПО с закрытым исходным кодом для последующей модификации.
Разработать алгоритмы модификации исполнимого кода, позволяющие защищать ПО с закрытым исходным кодом от атак, использующих фиксированные адреса и фиксированные размеры локальных буферов, с целью захвата управления.
Выполнить обоснование использования диверсифицирующих преобразований с целью защиты от атак, направленных на захват управления.
Разработать алгоритмы рекомпиляции ПО с закрытым кодом, позволяющие интегрировать дополнительный функционал в исполнимый файл с сохранением оригинального функционала.
Провести экспериментальное исследование различных алгоритмов нефункциональных преобразований и установить, являются ли они диверсифицирующими в соответствии с определенными критериями качества диверсифицирующих нефункциональных преобразований.
Методы исследования основаны на использовании теории нефункциональных преобразований, теории компиляции и теории вероятностей.
Основные положения и результаты, выносимые на защиту:
Метод диверсификации ПО с закрытым исходным кодом.
Алгоритмы диверсификации исполнимого кода.
Алгоритмы анализа и модификации исполнимого кода, позволяющие осуществить встраивание пассивных алгоритмов защиты путем его рекомпиляции.
4. Результаты экспериментального исследования предлагаемого метода.
Научная новизна работы заключается в следующем:
Разработан новый метод диверсификации исполнимого кода ПО.
Разработаны теоретические основы диверсификации ПО. Даны определения запутывающим и диверсифицирующим нефункциональным преобразованиям (НФП). Доказано, что НФП обладают рядом свойств, делающих возможным создание алгоритма, осуществляющего диверсификацию кода ПО.
Разработаны алгоритмы модификации исполнимого кода ПО, позволяющие осуществить эффективную защиту от атак, направленных на удаленный захват управления.
Разработаны алгоритмы анализа (декомпиляции) исполнимого кода в промежуточный код, которые обеспечивают применение алгоритмов модификации ПО.
Разработаны алгоритмы рекомпиляции исполнимого кода из промежуточного кода, позволяющие изменять (в т.ч. добавлять) функционал в исполнимые файлы без нарушения их работоспособности.
Практическая ценность работы.
Практическая значимость результатов диссертации заключается в следующем:
Разработанный метод диверсификации позволяют осуществить разнородность популяций ПО с целью защиты от удаленных атак.
Разработанный метод диверсификации ПО с закрытым исходным кодом может быть использован для защиты проприетарного ПО в системе конечного пользователя.
Использование результатов. Алгоритмы анализа и модификации исполнимого кода, являющиеся одним из основных результатов работы, были использованы ЗАО «РНТ» (г. Москва) при выполнении научно-исследовательской работы «Плеер».
Достоверность полученных результатов подтверждается полнотой и корректностью теоретических обоснований и результатами экспериментов, проведенных с помощью разработанных в диссертации программ.
Апробация работы. По теме диссертации опубликовано 10 научных статей и тезисов докладов. Основные результаты, полученные в ходе работы над диссертацией, были представлены на:
Всероссийской научной конференции студентов и аспирантов «Информационные технологии, системный анализ и управление», Таганрог (2003 г.).
Международной научно-практической конференции «Информационная безопасность», Таганрог (2004, 2005, 2006 г.).
Международной научно-практической конференции «Black Hat», Лас-Вегас, США (2006 г.).
Технологии защиты от эксплуатации ошибок в Windows
В этом разделе мы исследуем подходы защиты от эксплуатации ошибок, недавно добавленные в Windows. Большинство из этих технологий доступно лишь в Windows ХР, Service Pack2 (XPSP2) и Windows 2003, Service Packl (W2K3). Исключение составляют опции компилятора /GS и /SAFESEH, которые могут защитить любое приложение, скомпилированное с помощью Visual Studio .NET 2003 - однако приложения Windows из более старых операционных систем не были скомпилированы таким образом, так что в этом случае будут защищены только сторонние приложения [17].
Основной технологией для защиты стека в Windows является опция компилятора /GS в Visual Studio .NET 2003. В сущности, /GS очень похоже на StackGuard с минимальной модификацией «канарейки» для защиты указателя сохраненного фрейма стека (обычно ЕВР). VS.NET 2003 дополнительно производит оптимизацию переменных в стеке, похожую на ProPolice/SSP, однако эффективность такой оптимизации расположения стека была поставлена под сомнение автором ProPolice на конференции CanSecWest в мае 2005 [18]. Одна модификация, заслуживающая особого внимания, была сделана между версиями VS.NET 7.0 и 7.1 - копирование указателей на функцию в локальные обработчики исключительных ситуаций ниже локальных переменных, что делает невозможным изменение этих указателей при классическом переполнении буфера.
Дэвид Литчфилд (David Litchfield) указывает на несколько возможных обходов защиты стека в Windows 2003 SPO [19]. Эти атаки проливают свет на некоторые особенности реализации, которые резко уменьшают эффект защищенности от использования /GS. 1. Структуры EXCEPTION_REGISTRATION, которые используются структурной обработкой исключений, хранятся на стеке. Хотя обработчик исключений для текущего фрейма хранится под всеми массивами и находится в безопасности от переполнения буфера, обработчик для вызывающей функции (и всех других) остается незащищенным. Переписывание указателя на обработчик исключений и вызов исключения (или просто его ожидание) является известной атакой. 2. Безопасная структурная обработка исключений (SafeSEH) содержит несколько аномалий, к примеру, позволяющих передавать контроль незарегистрированному обработчику исключений. 3. «Канарейка», используемая для защиты стека хранится в сегменте .data и не помечена «только для чтения», что позволяет атакующему в некоторых случаях переписать её известным значением. Недавно Microsoft представила две новые технологии для защиты кучи. Переполнения кучи в последние годы привлекают к себе много внимания -ежегодное количество таких атак утроилось в период с 2002 по 2004 год [20]. Для понимания таких ошибок необходимо знать, что указатели для освобождения участков памяти кучи хранятся в списке до момента их выделения - это так называемый «список освобождения» (freelist). Когда вызывается функция НеарА11ос(), Windows проверяет, возможно ли использование блока памяти из «списка освобождения», если нет - следует попытка использования невыделенной памяти.
В Windows существуют две технологи защиты кучи - "safe unlinking" и «канарейки для кучи».
Идея «канарейки для кучи» была впервые предложена в 2003 году независимо Хуангом (Huang) [21] и Роберстоном (Roberston) [14], а также Крюгелом, хотя детали предложенных подходов несколько различались. Microsoft является первым разработчиком, применившим такой подход в продукции. Основная идея практически идентична «канарейке» для стека -«охраняющее» значение помещается в заголовок кучи и проверяется во время операций с кучей, которые могут быть опасными (например, во время выделения памяти), для предотвращения выделения поврежденных блоков. Однако, существует некоторое ограничение в реализации - «канарейка» для кучи имеет размер всего в один байт, и благодаря этому может быть найдена атакой перебором.
"Safe unlinking" осуществляется для предотвращения возникновения известной уязвимости, позволяющей злоумышленнику произвести произвольную запись в память. Когда блок В исключается из списка, указатели Flink и Blink для А и С должны быть обновлены. Упрощая, можно сказать, что Windows пытается обновить Flink (А), копируя значение Flink (В) в место, на которое указывает Blink (В). Изменив эти указатели, атакующий, который контролирует блок В, может писать любое 32-битное значение по любому 32-битному адресу - «4 байтная перезапись». Атака проиллюстрирована на рисунке 1.2.
Нефункциональные преобразования
Пользуясь вышеприведенными определениями, дадим определение нефункционального преобразования. Нефункциональным преобразованием (НФП) г является такое изменение программы P(x) = v в программу т(Р), для которого существует программа Р , восстанавливающая исходный выходной контекст программы Р, такая, что программа т(Р) Р эквивалентна программе Р во входном контексте S и выходном S . Формально, г(Р)(у) = уг-НФП Р(\) = \ о (3P (vr) = v л r(P)\\P = P(v) = \ ) Разделение преобразованной программы на две части (г(Р) и Р ) позволяет применять обратимые преобразования как составные части НФП. Определение, основанное на эквивалентности Р и т(Р), будет менее практичным, потому что оно может описывать лишь дополнения контекстов т(Р) по сравнению с контекстами Р.
Требование эквивалентности т(Р)\\Р = Р(\) = \ заключает в себе требование сохранения отображения не-а размерностей программами r(F)(v) = vr иР(\т) = \ . Это гарантирует, что если r(/,)?(v,) = v2, то S с 5,, S aS2u r(P)P(v) = v . Мощность НФП - понятие, позволяющее определить качество НФП в выбранных критериях мер сложности. Для набора мер сложности = {,(?), і = [\,...N]}, мощностью П НФП г программы Р назовем следующее выражение: ьК N Е,(Р) НФП г программы Р является запутывающим для выбранного набора мер сложности Е, если его мощность П,;(т,Р)» 0.
Существует множество мер сложности преобразований программ, большинство из которых было выведено для частных задач. Для оценки практического эффекта запутывающего преобразования удобно использовать три меры: количество инструкций и аргументов, количество вложенных условий и количество адресаций локальных переменных.
Для определения диверсифицирующего преобразования, как преобразования, меняющего реализацию программы без оказания явного негативного влияния на скорость ее исполнения, необходимо ввести понятие временной цены НФП.
Это условие противоречит части определений об эквивалентности программ, так что согласно определению Т не является нефункциональным преобразованием (создание завершенного Т(Р) является невозможным).
Некоторые инструкции настоящих компьютеров, такие как "adc eax,ebx" могут быть одновременно как обратимой операцией (eax=eax+ebx+c), так и инструкцией, расширяющей контекст запутываемой программы (используя флаг переноса С, когда контекстом программы является {(eax,ebx)}).
Для доказательства второго свойства запутывающего преобразования будет полезна следующая лемма: После соединения запутанные программы не дадут корректный результат, эквивалентный результату, возвращенному оригинальной программой. Значение регистра ЕАХ увеличено на 10 в конце первой части запутанной программы, и вторая часть игнорирует это изменение. Это следует из определения запутывающего преобразования, которое не ставит условия возвращения оригинального контекста после преобразования, но требует, чтобы примененные преобразования были обратимы. Если мы вставим между двух программ последовательность инструкций: subeax,10 ;еах = еах-10 addebx,10 ;ebx = ebx+10 то полученная программа будет работать корректно. Анализ этого примера ведет нас к следующей теореме:
Для любой программы Р, разделенной в любом месте на две программы P=Pi Р2, которые запутаны двумя различными запутывающими преобразованиями Ti(Pi) и Т2(Р2), существует программа Рх, такая, что Т(Р) = Ti(Pi) Рх Т2(Р2) является запутывающим преобразованием Р.
Главным выводом из этой теоремы является факт, что возможно создание чистого последовательного алгоритма диверсификации (применения НФП, удовлетворяющих определению диверсифицирующих преобразований), осуществляющего диверсификацию в один проход -инструкция за инструкцией.
Используемость переменных
Алгоритм, строящий карту использования переменных в функции, основывается на критерии занятости [53]. Для достижения результата он строит дерево исполнения функции. В общем виде алгоритм выглядит так: 1. Для каждой инструкции метакода функции выясняется, какие слоты ей читаются и какие записываются. 2. Для каждой записи, найденной в теле функции: 3. Приравнять начало пути исполнения к текущей записи. 4. Сдвинуться на одну инструкцию вниз. 5. Если текущая инструкция выходит за рамки функции, выход. Иначе - присоединить ее к дереву исполнения слева. 6. Если множество слотов обрабатываемой записи (т.е. изменяющихся ей) пересекается с множеством слотов, читаемых предыдущей в дереве исполнения инструкцией, то пометить слоты пересечения как занятые в текущей инструкции и перейти на шаг 7. Если же пересечение пусто, перейти на шаг 9. 7. Если адрес рассматриваемой в дереве исполнения инструкции не совпадает с адресом текущей инструкции, то удалить из пересечения слоты, модифицируемые инструкцией из дерева исполнения. 8. Сдвинуться к инструкции, являющейся предком рассматриваемой в дереве исполнения и перейти на шаг 6. 9. Если текущая инструкция является инструкцией безусловной передачи управления по заранее не известному адресу (OP_JUMP_SLOT), то выйти. 10. Если текущая инструкция является инструкцией безусловной передачи управления по известному адресу, то проверить, принадлежит ли адрес назначения региону функции, и если да, то перейти к инструкции, располагающейся по адресу назначения. 11. Если текущая инструкция является инструкцией условной передачи управления, то присоединить ее адрес назначения справа к дереву исполнения и рекурсивно вызвать шаг 3 алгоритма. 12. Перейти к шагу 4. Результатом работы алгоритма является карта использования переменных-слотов для каждой инструкции процедуры. По окончанию работы алгоритма необходимо очистить память, занятую построенным деревом исполнения.
Каркас функции - это ключевые элементы, которые имеют важнейшее значение для формирования внешнего вида функции. К ним относятся: распределение слотов на регистровые и стековые, прототип функции, а также оформление пролога и эпилога [55-57].
Как отмечалось ранее, обычный генератор кода формирует набор регистровых переменных, руководствуясь указаниям пользователя (не всегда корректно обрабатываемое ключевое слово register) и собственными настройками оптимизации кода - по размеру, по скорости и т.п. Генератор НФП должен делать выбор между типами переменных произвольным образом, обращая внимание лишь на последующие обращения к переменным (взятие ее смещения) и возможные жесткие требования пользователя. В языке FuzzAsm вводится инструкция OP_RESERVE, обратная по смыслу ключевому слову register в С. Ее цель - информировать генератор, что указанный слот необходимо интерпретировать как стековую переменную с размером, не меньшим указанного. Это необходимо, если далее в коде встретится инструкция получения смещения относительно указанного слота, то есть он используется как локальный буфер процедуры. Если в тексте встретятся подобные инструкции, применяющиеся к слотам, не назначенным генератором в стековые переменные, то компиляция аварийно прекратится из-за ошибки «невозможно получить смещение регистровой переменной».
Обычный генератор (за редкими исключениями) распределяет локальные переменные функций по порядку (или в обратном порядке) появления их в исходном тексте, что иногда упрощает понимание назначения буферов - однотипные буферы часто располагаются программистом непосредственно друг за другом в исходном тексте [58].
Важный отрицательный момент этого постоянства оформления стековых переменных - это возможность злоумышленнику заранее знать начало буфера относительно указателя стека при входе в процедуру и его длину, чтобы корректно собрать код, переписывающий точку возврата функции для использования допущенной программистом ошибки, дающей возможность таким образом «сорвать стек». Для решения проблемы контроля длины пользовательских буферов существуют надстройки над стандартными компиляторами С, которые модифицируют прологи и эпилоги функций таким образом, что выход из функции осуществляется инструкцией относительной передачи управления, а не инструкцией возврата по адресу, взятому из стека. Все защищенные таким образом процедуры заканчиваются в общем участке кода, который проверяет, был ли изменен адрес возврата в стеке функции. Если был, то программа аварийно завершается без передачи управления по адресу из стека, так как факт его перезаписи говорит как минимум об ошибке программиста, ведущей к дальнейшей непредсказуемой работе программы.
Генератор НФП размещает локальные буферы функции в произвольном порядке в стеке, причем относительный адрес каждого следующего буфера не равен сумме адреса предыдущего и его длины, а варьируется случайным образом, так как размер каждого локального буфера увеличивается на случайную величину, колеблющуюся в промежутке от 0 до определенного количества процентов от длины пользовательского буфера.
Помимо сильной изменчивости экземпляров кода функции, связанной с почти произвольным назначением слотов в стековые переменные и в регистры, такая свобода в резервировании стековых буферов автоматически делает бесполезными атаки на срыв стека, даже если программист допустил ошибку и длина пользовательского буфера может превышать размер зарезервированной в стеке памяти. При изменчивости расстояния от начала подверженного срыву буфера до адреса возврата из функции, находящегося в стеке, становится невозможно эффективно использовать уязвимость в 100% случаев, так как атакующий должен будет угадать положение адреса возврата в стеке. Эксперименты по поиску неизвестного расстояния усложняются тем, что неизвестно, какие буферы и переменные находятся между логическим концом атакуемого буфера и адресом возврата. Каждая неудачная попытка оформить буфер, корректно подменяющий адрес возврата, весьма вероятно, приведет либо к повреждению других стековых переменных, либо к неверному указанию адреса, что чревато аварийным завершением процесса.
Теоретически, всегда достаточно сделать четыре попытки для правильного замещения адреса возврата, так как его длина - 4 байта. Попытки следует проводить следующим образом: от последнего байта атакуемого буфера (байта, находящегося по адресу «начало буфера + его логическая длина») отступить количество байт, равное номеру попытки минус 1, и заполнить память двойными словами адреса возврата, следующими друг за другом. Длина взятой памяти должна превышать максимально возможную длину всех локальных буферов функции, вместе взятых.
Но эта тактика может негативно сказаться на логике исполнения функции, так как она не должна вызывать исключений (если не был установлен фрейм SEH), а явное уничтожение содержимого буферов, находящихся после атакуемого, ведет к нестабильности работы.
Сама целесообразность использования подобных методик срывов стека ставится под вопрос - ведь для успешного использования переполнений необходимо найти инструкцию, которая передаст управление на буфер с пользовательским кодом. В случае сборки исполнимого кода генератором НФП невозможно с уверенностью сказать о смещении начала атакуемого буфера в стеке - следовательно, инструкции типа "jmp esp" в лучшем случае возвратят управление на неверное место во внедренном коде, что приведет к завершению работы процесса или к исключению.
Пересчет относительных виртуальных и физических адресов
После изменения списка (т.е. добавления или удаления ненулевого блока), очевидно, изменяются все RVA и RPA блоков, следующих за местом изменения списка. Первый этап рекомпиляции служит для восстановления корректности этих относительных адресов у всех блоков списка. Алгоритм этого шага рекомпиляции прост: 1. Организуется цикл от первого до последнего блока в списке. Обнуляются переменные текущего виртуального и физического смещений (текущая позиция - начало модуля). 2. Значения RVA и RPA текущего блока приравниваются к значениям, соответственно, виртуального и физического смещений. 3. Если текущий блок имеет флаг FL_SECTALIGN (начало секции -секционное выравнивание), то переменная виртуального смещения выравнивается на значение секционного выравнивания, а переменная физического смещения - на файловое выравнивание. Блок, имеющий флаг FL_SECTALIGN, по вышеописанному алгоритму разбиения памяти на список блоков, имеет нулевую длину, поэтому его RVA и RPA значения не имеют. 4. В другом случае переменные виртуального и физического смещения инкрементируется на длину текущего блока. 5. Цикл повторяется. Этот шаг необходим, если в восстанавливаемом модуле необходимо присутствие таблицы релокаций - например, в DLL-модуле. Алгоритм пересчета элементов релокаций модуля полностью построен на заполнении структур в формате, предусмотренном форматом РЕ, поэтому интереса не представляет [45]. Моментом, заслуживающем внимания, является обработка ситуации отсутствия места в данных блока-носителя формируемой таблицы релокаций. Эта проблема решается увеличением длины блока-носителя с последующим пересчетом RVA и RPA всего модуля по вышеприведенному алгоритму.
Этот шаг является ключевым в процессе рекомпиляции. В процессе перелинковки восстанавливаются все некорректные указатели одних блоков на другие (то есть относительные смещения команд переходов, абсолютные адреса элементов релокации и дельта-смещения). Также на этом шаге происходит раздвижение инструкций коротких переходов до длинных в случае необходимости.
Алгоритм перелинковки выполняется после пересчета таблицы элементов релокации и выглядит так: 1. Обнуляется флаг «произошло раздвижение». Организуется цикл по цепочке блоков, начиная с первого. 2. Если блок имеет флаг FL_RVA и не имеет флаг FL_PHYS, то ищется блок, имеющий старый RVA, равный аргументу текущего блока, и в значение нового аргумента текущего блока (поле dataptr) записывается поле newrva найденного блока (это поле у всех блоков корректно заполняется после шага пересчета RVA и RPA). Если текущий блок имеет флаги FL_RVA и FL_PHYS одновременно, то действия алгоритма аналогичны, за исключением последнего этапа: в поле dataptr текущего блока записывается не newrva, а значение поля newofs найденного блока. 3. Если блок имеет флаг FL_FIXUP (элемент релокации), то алгоритм выполняет шаг 2, и после его завершения прибавляет к вновь заполненному полю dataptr текущего блока базовый адрес (imagebase) рекомпилируемого модуля (по определению элемента релокации формата РЕ). 4. Если блок имеет флаг FL_DELTA (дельта-смещение), то алгоритм ищет два блока, имеющих старые RVA, равные, соответственно, первому и второму аргументу текущего блока. Если блок имеет флаг FL_PHYS, то дельта-смещение считается как разность полей newofs найденных блоков. В другом случае - как разность полей newrva. 5. Если блок имеет флаг FLFORCEFILE ALIGN (требуется файловое выравнивание), то число, стоящее в поле данных блока, выравнивается на значение файлового выравнивания. Аналогично, если блок имеет флаг FL_FORCEOBJALIGN (требуется секционное выравнивание), то число, стоящее в поле данных блока, выравнивается на значение секционного выравнивания. 6. Если блок имеет относительную ссылку (т.е. является инструкцией относительного перехода), то считается относительный (от конца инструкции перехода) адрес его пункта назначения путем вычисления разности между полем newrva блока, имеющего старый RVA, равный аргументу текущего блока, и RVA конца инструкции перехода. Если инструкция перехода длинная, то после опкода в память блока пишется корректный вычисленный адрес адреса перехода. Если же инструкция перехода короткая (1-байтовый аргумент), то проверяется, помещается ли относительный адрес перехода в тип данных «7бит + знак». Если да, то повторяется ситуация с длинным переходом. Если нет, то происходит раздвижение инструкции короткого перехода до длинного [70]. Высчитывается новая длина инструкции, ассемблируется новый опкод и алгоритм выставляет флаг «произошло раздвижение». 7. Если цикл завершился, то проверяется флаг «произошло раздвижение». В случае, если он установлен, управление передается на шаг 1 алгоритма пересчета RVA и RPA, затем, после завершения пересчета, управление передается на шаг 1 алгоритма пересчета таблицы элементов релокации и цикл повторяется. Если флаг «произошло раздвижение» сброшен, то алгоритм завершается.