Правка двоичных файлов

Структура файла Portable Executable

To be written…

Процесс загрузки PE-файла

To be written…

Расписать про релокации…

Студентам, выполняющим работу - слушайте лекцию про релокации.

Создание правок с помощью отладчика

Отладчик x64dbg позволяет редактировать отлаживаемую программу “на горячую”, т.е. прямо в процессе отладки. Для этого достаточно выделить инструкцию, которую необходимо изменить, и нажать клавишу “пробел”. В появившемся диалоговом окне можно ввести новую инструкцию, которая заменит собой текущую.

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

mov eax, 0AABBCCDDh

на инструкцию

mov ax, 0EEFFh

в двоичном виде первая инструкция имеет вид B8 DD CC BB AA и занимает 5 байт, а вторая инструкция имеет вид 66 B8 FF EE и занимает 4 байта. Это означает, что выполнив перезапись инструкции, мы получим последовательность байт 66 B8 FF EE AA, где первые 4 байта - это инструкция mov ax ..., а последний байт AA - остаток первоначальной инструкции. Полученная последовательность соответствует коду

mov ax, 0EEFFh
stosb

Чтобы избавиться от инструкции stosb необходимо заменить её на другую однобайтовую инструкцию - nop, которая не производит никаких действий и просто переходит к следующей.

Приведённый пример позволяет сделать следующие выводы:

Теперь представим, что нам хочется избежать вызова нежелательной функции:

call some_bad_func

Если вызываемая функция имеет соглашение вызова stdcall, то затереть вызов несколькими nop будет недостаточно. Где-то выше инструкции call находятся инструкции push, передающие через стек аргументы вызываемой функции. До правки стек очищался вызываемой процедурой, а после правки он останется неочищенным:

push 1
push 2
push 3
; 6 байтов nop, которые заменили собой call
nop
nop
nop
nop
nop
nop

По этой причине, вместо вставки nop необходимо вставить код очистки стека, свободных байтов под который, к счастью, хватает:

push 1
push 2
push 3
; вместо вызова сразу чистим стек
add esp, 12
; оставшиеся 3 байта меняем на nop
nop
nop
nop

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

...
call [printf]

приведёт к поялению релокации адреса функции printf. Это означает, что даже если перезаписать все инструкции push и call, относящиеся к вызову, то при загрузке PE-файла последние инструкции nop будут снова заменены на реальный адрес printf. Этот адрес будет проинтерпретирован как код, выполнение которого скорее всего приведёт к ошибке:

; nop-ы, заменившие push-и
nop
nop
nop
; 2 nop-а, заменившие начало инструкции call
nop
nop
; 4 байта адреса релокации, которые теперь интерпретируются как инструкции
div [eax+ebx*2+3]

Для того, чтобы правильно обойти такой вызов, нужно воспользоваться инструкцией jmp и перевести выполнение программы сразу за вызов call.

Правки, внесённые во время отладочной сессии, не сохраняются между перезапусками программы и самого отладчика. Чтобы сохранить их, используется окно Patches, которое можно вызывать из меню File или комбинацией клавиш Ctrl+P. В этом окне кнопка Export позволяет создать файл-заплатку, который может быть позднее загружен в отладчик, чтобы повторить сделанные изменения заново. Кнопка Patch File сохраняет PE-файл с наложенными изменениями.

Контрольные вопросы

  1. Какие существуют секции в файле формата PE?
  2. Что такое релокации?

Задание на лабораторную работу

  1. В программе из лабораторной работы №5 прошлого семестра произвести такие изменения, чтобы функция Sleep не вызывалась и не задерживала выполнение программы.
  2. Создать файл-заплатку, содержащий произведённые изменения.
  3. Перезагрузить программу в отладчике, применить файл-заплатку, сохранить изменённый .exe файл под другим именем.
  4. Написать программу на любом языке программирования, которая подбирает логин и пароль исходной программы методом грубой силы. Ниже приведён примерный код на языке C#.

Запуск и взаимодействие с консольными программами на языке C#

Process p = new Process();
p.StartInfo.FileName = "program.exe";
// позволяет вводить строки в запущенную программу из кода этой программы
p.StartInfo.RedirectStandardInput = true;
p.Start();

// вводим строку в программу
p.StandardInput.WriteLine("bla bla bla");

// ждём её завершения
p.WaitForExit();

if (p.ExitCode == 0)
    Console.WriteLine("Программа завершилась успешно");
else
    Console.WriteLine("Программа завершилась с кодом ошибки " + p.ExitCode.ToString());