Метка: patch
Что такое патч кода: как один байт может изменить программу
Если говорить просто, патч кода — это небольшое целевое изменение программы, которое исправляет ошибку или меняет её поведение.
В обычной разработке patch часто представляет собой набор изменений исходного кода: какие строки были добавлены, удалены или исправлены. Такие патчи используются в Git, при code review и исправлении ошибок.
Но в системном программировании и reverse engineering под патчем кода часто понимают другое — изменение уже скомпилированной программы на уровне машинных инструкций.
Иногда для этого достаточно изменить всего один байт.
Как один байт может изменить работу программы
Представим следующий x86-код:
test eax, eax
jne short loc_continue
Команда jne выполняет переход только при определённом результате проверки.
Но что произойдёт, если заменить условный переход jne на безусловный jmp?
test eax, eax
jmp short loc_continue
В машинном коде короткий jne и короткий jmp имеют разные opcodes. В подходящем случае изменение одного байта превращает условный переход в безусловный.
Исходный код программы не изменился.
Программа не была заново скомпилирована.
Но логика выполнения уже другая.
Это и есть один из простейших примеров binary patch — бинарного патча кода.
Зачем вообще патчить готовый код
На первый взгляд кажется странным изменять машинные инструкции вместо исходников. Но на практике такие ситуации возникают регулярно.
Нет исходного кода
Допустим, ошибка находится в сторонней DLL или другом компоненте, исходники которого недоступны.
Можно ждать исправления от разработчика.
А можно найти проблемное место в дизассемблере и изменить несколько инструкций.
Именно такой подход иногда используется при исправлении старого или больше не поддерживаемого программного обеспечения.
Нужно изменить поведение функции
Патч позволяет:
- убрать ненужную проверку;
- изменить условие перехода;
- заменить вызов функции;
- перенаправить выполнение в собственный обработчик;
- добавить дополнительную логику.
Вместо изменения всей программы модифицируется конкретная точка выполнения.
Нужен перехват функции
Один из классических вариантов выглядит примерно так:
jmp my_handler
Начало функции заменяется переходом в собственный код.
Теперь управление получает my_handler.
Он может записать параметры в лог, изменить данные, выполнить дополнительную проверку или полностью заменить исходное поведение.
Именно на таких техниках построено множество систем перехвата и инструментирования кода.
Нужно исправить работающий процесс
Отдельное направление — hot patching, когда изменяется код уже загруженного процесса.
В некоторых системах это позволяет применить исправление без перезапуска процесса или замены всего исполняемого файла.
Основные техники патчинга кода
Inline patch
Самый прямой вариант.
Машинные инструкции изменяются непосредственно в теле функции.
Например:
jne short error
можно заменить подходящей по размеру инструкцией или последовательностью инструкций.
Inline patch удобен для небольших изменений, но требует точного понимания машинного кода.
NOP patch
Инструкция NOP не выполняет полезной операции и позволяет фактически убрать часть кода.
Например, несколько инструкций можно заменить NOP-последовательностью, сохранив занимаемое ими место.
Так часто отключают проверки или ненужные вызовы.
Однако нельзя просто записать произвольное количество NOP, не разобрав исходные инструкции. Границы инструкций должны быть определены правильно.
Jump patch
В код записывается переход:
jmp my_handler
Выполнение перенаправляется в другую область памяти.
Собственный обработчик может полностью заменить исходную функцию или выполнить дополнительную работу.
Trampoline
Trampoline используется, когда после перехвата нужно сохранить возможность вызвать оригинальный код.
Общая схема выглядит так:
- Сохраняются инструкции, которые будут перезаписаны.
- В начало функции устанавливается переход на обработчик.
- Сохранённые инструкции помещаются в отдельную область памяти.
- После них выполняется переход обратно в оригинальную функцию.
Полученная область кода называется trampoline.
Подобную схему, например, использует Microsoft Detours.
Patch и hook — это не одно и то же
Эти понятия часто смешивают.
Patch — изменение существующего кода или данных программы.
Hook — перехват вызова или события.
Патчинг машинных инструкций может использоваться для установки hook. Например, начало функции можно заменить инструкцией jmp.
Но существуют и другие способы перехвата.
В Windows вызовы импортированных функций связаны с Import Address Table — IAT. Изменение адреса функции в такой таблице позволяет перенаправить вызов без перезаписи первых ASM-инструкций самой функции.
Поэтому правильнее сказать так:
patch может использоваться для реализации hook, но patch и hook не являются синонимами.
Почему патчинг машинного кода опасен
Изменить один байт легко.
Понять, какой именно байт можно изменить, значительно сложнее.
Инструкции x86 имеют разную длину
Нельзя считать, что одна строка дизассемблера всегда занимает одинаковое количество байт.
Если перезаписать только часть инструкции, процессор начнёт декодировать повреждённую последовательность байтов.
Результатом обычно становится неправильное выполнение кода или падение процесса.
Нужно учитывать относительные адреса
Инструкции перехода и вызова могут использовать относительные смещения.
Простое копирование такой инструкции в trampoline не всегда сохраняет её первоначальный смысл.
Адреса приходится пересчитывать или использовать другую последовательность инструкций.
Кодовая память обычно защищена от записи
Страницы памяти с исполняемым кодом обычно нельзя просто изменить обычной операцией записи.
В Windows защита страниц может временно изменяться с помощью VirtualProtect.
После модификации исполняемого кода необходимо также учитывать согласованность кэша инструкций.
Есть проблема многопоточности
Представим, что один поток перезаписывает начало функции, а другой именно в этот момент начинает её выполнять.
Второй поток может увидеть код в промежуточном состоянии.
Поэтому runtime patching требует продуманной синхронизации. Недостаточно просто вызвать memcpy по адресу функции.
Версия бинарного файла имеет значение
Патч, созданный для одной версии программы, может оказаться неприменим к другой.
Компилятор изменил оптимизацию — инструкции переместились.
Добавилась функция — изменились адреса.
Вышла новая DLL — старый набор байтов больше не совпадает.
Поэтому надёжный patch должен проверять, что изменяется именно ожидаемый бинарный код.
Когда патч кода действительно полезен
Binary patching оправдан, когда:
- нет доступа к исходникам;
- нужно исправить сторонний компонент;
- требуется исследовать или изменить поведение программы;
- выполняется reverse engineering;
- разрабатывается система инструментирования или перехвата;
- необходимо точечное изменение машинного кода.
Но патч не является заменой нормальной архитектуре.
Если программа находится под вашим контролем и проблему можно исправить в исходниках, в большинстве случаев именно это и следует сделать.
Так что же такое патч кода
Патч кода — это целевое изменение программы, исправляющее ошибку или меняющее её поведение.
На уровне исходников patch может быть обычным набором изменений файлов.
На уровне готового бинарного кода всё становится интереснее: изменяются уже машинные инструкции, переходы и адреса.
Иногда сложный перехват требует trampoline и анализа десятков инструкций.
А иногда поведение целой функции действительно меняется заменой одного байта.
Именно поэтому binary patching остаётся важным инструментом системного программирования и reverse engineering.