Я делаю это с hexdump
и текстовым редактором.Вы должны быть действительно знакомыми с машинным кодом и форматом файла, в котором он хранится, и гибкими с тем, что считается "разобрать, изменить, а затем собрать".
Если вы можете уйтипросто внося «точечные изменения» (перезаписывая байты, но не добавляя и не удаляя байты), это будет легко (условно говоря).
Вы действительно не хотите вытеснятьсуществующих инструкций, потому что тогда вам придется вручную корректировать любое произведенное относительное смещение в машинном коде для переходов / переходов / загрузок / запоминаний относительно счетчика программы, как в жестко заданных немедленных значениях , так и единиц, вычисленных с помощью регистров .
Вы всегда должны иметь возможность избегать удаления байтов.Добавление байтов может быть необходимо для более сложных модификаций и становится намного сложнее.
Шаг 0 (подготовка)
После того, как вы на самом деле правильно разобрали файл с помощью objdump -D
или что вы обычно используете в первую очередь, чтобы действительно понять это и найти места, которые нужно изменить, вам нужно принять к сведению следующее, чтобы помочь вам найти правильные байты для изменения:
- «Адрес» (смещение от начала файла) байтов, которые необходимо изменить.
- Необработанное значение тех байтов, какими они являются в настоящее время (параметр
--show-raw-insn
для objdump
действительнополезно здесь).
Шаг 1
Сброс необработанного шестнадцатеричного представления двоичного файла с помощью hexdump -Cv
.
Шаг 2
Открытьhexdump
ed и найдите байты по адресу, который вы хотите изменить.
Быстрый ускоренный курс при выводе hexdump -Cv
:
- Самый левый столбецадреса байтов (относительно начала биСам файл, как и в
objdump
). - Крайний правый столбец (окруженный
|
символами) представляет собой просто «читаемое человеком» представление байтов - записывается символ ASCII, соответствующий каждому байтутам с .
, стоящим для всех байтов, которые не отображаются на печатный символ ASCII. - Важный материал находится между ними - каждый байт в виде двух шестнадцатеричных цифр, разделенных пробелами, 16 байт на строку.
Осторожно: в отличие от objdump -D
, который дает вам адрес каждой инструкции и показывает необработанный шестнадцатеричный код инструкции, основанный на том, как она задокументирована как кодируемая, hexdump -Cv
выводит каждый байт точно впорядок он появляется в файле.Это может немного сбивать с толку, как в первую очередь на машинах, где байты инструкций находятся в противоположном порядке из-за различий в порядке байтов, что также может дезориентировать, когда вы ожидаете, что конкретный байт является конкретным адресом.
Шаг 3
Измените байты, которые нужно изменить - вам, очевидно, нужно выяснить кодировку необработанных машинных инструкций (не мнемонику сборки) и вручную записать правильные байты.
Примечание: вы не нужно изменять удобочитаемое представление в крайнем правом столбце.hexdump
будет игнорировать его, когда вы его «снимите».
Шаг 4
«Снимите» модифицированный файл hexdump, используя hexdump -R
.
Шаг5 (проверка работоспособности)
objdump
ваш новый un hexdump
ed файл и убедитесь, что измененная разборка выглядит правильно.diff
это против objdump
оригинала.
Серьезно, не пропустите этот шаг.Я ошибаюсь чаще, чем нет, когда вручную редактирую машинный код, и именно так я ловлю большинство из них.
Пример
Вот реальный отработанный пример, когда я модифицировал ARMv8(little endian) бинарный в последнее время.(Я знаю, вопрос помечен x86
, но у меня нет под рукой примера x86, и основные принципы те же, только инструкции разные.)
В моей ситуации мне нужно было отключить специальную проверку «вы не должны делать этого»: в моем примере двоичного файла в objdump --show-raw-insn -d
выведите строку, о которой я заботился, похожую на эту (одна инструкция до и после дано для контекста):
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
Как вы можете видеть, наша программа «услужливо» завершается, переходя в функцию error
(которая завершает программу). Неприемлемый. Итак, мы собираемся превратить эту инструкцию в неоперативный. Итак, мы ищем байты 0x97fffeeb
по адресу / смещение файла 0xf44
.
Вот строка hexdump -Cv
, содержащая это смещение.
00000f40 e3 03 15 aa eb fe ff 97 f7 13 40 f9 e8 02 40 39 |..........@...@9|
Обратите внимание на то, как соответствующие байты фактически перевернуты (кодирование с прямым порядком байтов в архитектуре применяется к машинным инструкциям, как и ко всему прочему) и как это немного неинтуитивно относится к тому, какой байт находится на каком смещении байта:
00000f40 -- -- -- -- eb fe ff 97 -- -- -- -- -- -- -- -- |..........@...@9|
^
This is offset f44, holding the least significant byte
So the *instruction as a whole* is at the expected offset,
just the bytes are flipped around. Of course, whether the
order matches or not will vary with the architecture.
Во всяком случае, я знаю, глядя на другие разборки, что 0xd503201f
разбирается на nop
, так что это кажется хорошим кандидатом для моей инструкции no-op. Я изменяю строку в hexdump
ed файле соответственно:
00000f40 e3 03 15 aa 1f 20 03 d5 f7 13 40 f9 e8 02 40 39 |..........@...@9|
Преобразован обратно в двоичный файл с hexdump -R
, разобрал новый двоичный файл с objdump --show-raw-insn -d
и проверил, что изменение было правильным:
f40: aa1503e3 mov x3, x21
f44: d503201f nop
f48: f94013f7 ldr x23, [sp, #32]
Затем я запустил двоичный файл и получил желаемое поведение - соответствующая проверка больше не вызывала прерывание программы.
Успешное изменение машинного кода.
!!! Предупреждение !!!
Или я был успешным? Вы заметили, что я пропустил в этом примере?
Я уверен, что вы это сделали - поскольку вы спрашиваете, как вручную изменить машинный код программы, вы, вероятно, знаете, что делаете. Но для пользы читателей, которые могут читать, чтобы учиться, я уточню:
Я только изменил инструкцию last в ветке ошибок! Прыжок в функцию, которая выходит из проблемы. Но, как вы можете видеть, регистр x3
был изменен на mov
чуть выше! Фактически, в общей сложности четыре (4) регистра были изменены как часть преамбулы для вызова error
, и один регистр был. Вот полный машинный код для этой ветви, начиная с условного перехода через блок if
и заканчивая переходом к нему, если условное if
не выполнено:
f2c: 350000e8 cbnz w8, f48
f30: b0000002 adrp x2, 1000
f34: 91128442 add x2, x2, #0x4a1
f38: 320003e0 orr w0, wzr, #0x1
f3c: 2a1f03e1 mov w1, wzr
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
Весь код после ветви был сгенерирован компилятором в предположении, что состояние программы было , как это было до условного перехода ! Но просто сделав последний переход к коду функции error
без операции, я создал путь к коду, где мы достигаем этот код с несовместимым / неправильным состоянием программы !
В моем случае это на самом деле казалось, что не вызывает никаких проблем. Так что мне повезло. Очень повезло: только после того, как я уже запустил мой модифицированный двоичный файл (который, кстати, был критическим для безопасности двоичным файлом : он имел возможность setuid
, setgid
и изменять Контекст SELinux !) Я понял, что забыл на самом деле отследить пути кода того, повлияли ли эти изменения регистра пути кода, которые появились позже!
Это могло бы привести к катастрофическим последствиям - любой из этих регистров мог бы использоваться в более позднем коде, предполагая, что он содержит предыдущее значение, которое теперь было перезаписано! И я тот человек, которого люди знают за тщательно и тщательно продуманные коды, а также как педант и приверженец того, как всегда быть преданным делу компьютерной безопасности.
Что, если бы я вызывал функцию, в которой аргументы передавались из регистров в стек (как это часто случается, например, в x86)? Что если на самом деле в наборе команд было несколько условных инструкций, которые предшествовали условному переходу (как обычно, например, в старых версиях ARM)? Я был бы в еще более безрассудном непоследовательном состоянии после того, как сделал это простейшее изменение!
Итак, это мое предостерегающее напоминание: Ручное переключение с двоичными файлами буквально удаление каждые безопасность между вами и тем, что разрешают машина и операционная система. Буквально все успехи, которые мы сделали в наших инструментах для автоматического выявления ошибок наших программ, ушел .
Так как нам исправить это более правильно? Продолжайте читать.
Удаление кода
До эффективно / логически «удалить» более одной инструкции, вы можете заменить первую команду, которую хотите «удалить», безусловным переходом к первой инструкции в конце из "удаленных" инструкций. Для этого двоичного файла ARMv8 это выглядело так:
f2c: 14000007 b f48
f30: b0000002 adrp x2, 1000
f34: 91128442 add x2, x2, #0x4a1
f38: 320003e0 orr w0, wzr, #0x1
f3c: 2a1f03e1 mov w1, wzr
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
По сути, вы «убиваете» код (превращаете его в «мертвый код»). Sidenote: Вы можете сделать что-то похожее с литеральными строками, встроенными в двоичный файл: если вы хотите заменить его на строку меньшего размера, вы почти всегда можете избежать перезаписи строки (включая завершающий нулевой байт, если это "C-"). string ") и при необходимости перезаписывает жестко закодированный размер строки в машинном коде, который его использует.
Вы также можете заменить все ненужные инструкции на no-ops. Другими словами, мы можем превратить ненужный код в то, что называется «бездействующими салазками»:
f2c: d503201f nop
f30: d503201f nop
f34: d503201f nop
f38: d503201f nop
f3c: d503201f nop
f40: d503201f nop
f44: d503201f nop
f48: f94013f7 ldr x23, [sp, #32]
Я бы ожидал, что это просто напрасно тратит циклы ЦП относительно перепрыгивания через них , но это проще и, следовательно, безопаснее от ошибок , потому что вы Вы должны вручную выяснить, как закодировать инструкцию перехода, включая определение смещения / адреса для использования в нем - вам не нужно думать столько же для салазок без операции.
Чтобы было ясно, ошибка проста: я испортил два (2) раза, когда вручную кодировал эту безусловную инструкцию перехода. И это не всегда наша ошибка: в первый раз это произошло из-за того, что документация, которая у меня была, устарела / неверна и в которой был указан один бит, который игнорировался в кодировке, хотя на самом деле это не так, поэтому я установил его на ноль с первой попытки. *
Добавление кода
Вы могли бы теоретически использовать эту технику для добавления машинных инструкций, но она более сложная, и мне никогда не приходилось это делать, поэтому у меня нет работы пример в это время.
С точки зрения машинного кода это довольно просто: выберите одну инструкцию в том месте, куда вы хотите добавить код, и преобразуйте ее в инструкцию перехода в новый код, который вам нужно добавить (не забудьте добавить инструкцию (ы). ) таким образом, вы заменили новый код, если вам не нужно это для добавленной логики, и чтобы вернуться к инструкции, к которой вы хотите вернуться в конце добавления). По сути, вы «склеиваете» новый код.
Но вам нужно найти место, чтобы на самом деле поместить этот новый код, и это самая сложная часть.
Если вам действительно повезло, вы можете просто добавить новый машинный код в конец файла, и он будет "просто работать": новый код будет загружен вместе с остальными в те же ожидаемые машинные инструкции, в пространство адресного пространства, которое попадает на страницу памяти, помеченную как исполняемый файл.
По моему опыту hexdump -R
игнорирует не только самый правый столбец, но и самый левый столбец - так что вы можете буквально просто поставить нулевые адреса для всех добавленных вручную строк, и это сработает.
Если вам не повезло, после добавления кода вам нужно будет на самом деле настроить некоторые значения заголовка в том же файле: если загрузчик вашей операционной системы ожидает, что двоичный файл будет содержать метаданные, описывающие размер исполняемого раздела ( по историческим причинам, часто называемым «текстовым» разделом), вам придется найти и настроить его. В старые времена двоичные файлы были просто необработанным машинным кодом - в настоящее время машинный код обернут в кучу метаданных (например, ELF в Linux и некоторых других).
Если вам все еще немного повезло, у вас может быть «мертвая» точка в файле, которая должным образом загружается как часть двоичного файла с теми же относительными смещениями, что и остальная часть кода, который уже находится в файле(и эта мертвая точка может соответствовать вашему коду и правильно выровнена, если ваш процессор требует выравнивания слов для инструкций процессора).Затем вы можете перезаписать его.
Если вам действительно не повезло, вы не можете просто добавить код и нет мертвого пространства, которое вы можете заполнить своим машинным кодом.На этом этапе вы должны быть хорошо знакомы с исполняемым форматом и надеяться, что вы сможете выяснить что-то в рамках этих ограничений, которые по-человечески выполнимо выполнить вручную в течение разумного количества времени и с разумной вероятностью не испортить его.