Как разобрать, изменить, а затем собрать исполняемый файл Linux?
есть в любом случае это можно сделать? Я использовал objdump, но это не производит вывод сборки, который будет принят любым ассемблером, о котором я знаю. Я хотел бы иметь возможность изменять инструкции в исполняемом файле, а затем тестировать его впоследствии.
7 ответов
Я не думаю, что есть любой способ, чтобы сделать это. Форматы машинного кода очень сложны, сложнее, чем файлы сборки. На самом деле невозможно взять скомпилированный двоичный файл (скажем, в формате ELF) и создать исходную программу сборки, которая будет компилироваться в тот же (или аналогичный) двоичный файл. Чтобы получить представление о различиях, сравните результат компиляции GCC непосредственно с ассемблером (gcc -S
) против выхода из objdump на исполняемый (objdump -D
).
есть два основных осложнения, о которых я могу думать. Во-первых, сам машинный код не является соответствием 1 к 1 с кодом сборки из-за таких вещей, как смещения указателей.
например, рассмотрим код C для Hello world:
int main()
{
printf("Hello, world!\n");
return 0;
}
это компилируется в код сборки x86:
.LC0:
.string "hello"
.text
<snip>
movl $.LC0, %eax
movl %eax, (%esp)
call printf
где .LCO-именованная константа, а printf-символ в таблице символов общей библиотеки. Сравните с выходом objdump:
80483cd: b8 b0 84 04 08 mov x80484b0,%eax
80483d2: 89 04 24 mov %eax,(%esp)
80483d5: e8 1a ff ff ff call 80482f4 <printf@plt>
во-первых, константа .LC0 теперь просто случайное смещение в памяти где-то-было бы трудно создать исходный файл сборки, который содержит эту константу в правильном месте, так как ассемблер и компоновщик могут свободно выбирать места для этих констант.
во-вторых, я не совсем уверен в этом (и это зависит от таких вещей, как независимый от позиции код), но я считаю, что ссылка на printf фактически не закодирована на адрес указателя в этом коде есть вообще, но заголовки ELF содержат таблицу поиска, которая динамически заменяет его адрес во время выполнения. Поэтому разобранный код не совсем соответствует исходному коду сборки.
В общем, исходная сборка имеет символы в то время как машинный код адреса, которые трудно отменить.
второе серьезное осложнение заключается в том, что исходный файл сборки не может содержать все информация, которая присутствовала в заголовках исходного файла ELF, например, библиотеки для динамической ссылки, и другие метаданные, размещенные там оригинальным компилятором. Это было бы трудно восстановить.
как я уже сказал, Возможно, что специальный инструмент может манипулировать всей этой информацией, но маловероятно, что можно просто создать код сборки, который можно собрать обратно в исполняемый файл.
если вы заинтересованы в изменении лишь малая раздел исполняемого файла, я рекомендую гораздо более тонкий подход, чем перекомпиляция всего приложения. Используйте objdump для получения кода сборки для интересующей вас функции(функций). Преобразуйте его в" синтаксис исходной сборки " вручную (и здесь я хотел бы, чтобы был инструмент, который фактически произвел разборку в том же синтаксисе, что и вход), и измените его, как вы хотите. Когда вы закончите, перекомпилируйте только эти функции и используйте objdump, чтобы выяснить машинный код для измененной программы. Затем, используйте шестнадцатеричный редактор, чтобы вручную вставить новый машинный код поверх соответствующей части исходной программы, заботясь о том, чтобы ваш новый код был точно таким же количеством байтов, как и старый код (или все смещения были бы неправильными). Если новый код короче, вы можете заполнить его с помощью инструкций NOP. Если это дольше, вы можете быть в беде, и, возможно, придется создавать новые функции и вызывать их вместо этого.
для изменения кода внутри двоичной сборки обычно существует 3 способа сделать это.
- Если это просто какая-то тривиальная вещь, как константа, то вы просто меняете местоположение с помощью шестнадцатеричного редактора. Предполагая, что вы сможете найти его для начала.
- Если вам нужно изменить код, используйте LD_PRELOAD для перезаписи какой-либо функции в вашей программе. Это не работает, если функция не находится в таблицах функций.
- взломать код в функцию вы хотите исправить прямой переход к функции, которую вы загружаете через LD_PRELOAD, а затем вернуться в то же место (это комбинация двух выше)
конечно, только 2-й будет работать, если сборка выполняет какую-либо проверку целостности.
Edit: если это не очевидно, то игра с бинарными сборками-это очень высокий уровень разработчика, и вам будет трудно спросить об этом здесь, если это не действительно конкретные вещи, которые вы спрашиваете.
@mgiuca правильно рассмотрел этот ответ с технической точки зрения. На самом деле, разборка исполняемой программы в простой для перекомпиляции источник сборки не является легкой задачей.
, чтобы добавить некоторые биты к обсуждению, есть несколько методов/инструментов, которые могут быть интересны для изучения, хотя они технически сложные.
- статические / динамические приборы. Этот метод предполагает анализ исполняемого формата, вставьте/удалите / замените конкретные инструкции по сборке для данной цели, исправьте все ссылки на переменные / функции в исполняемом файле и испустите новый измененный исполняемый файл. Некоторые инструменты, о которых я знаю: PIN-код, удалить, PEBIL, DynamoRIO. Считайте, что настройка таких инструментов для целей, отличных от тех, для которых они были разработаны, может быть сложной и требует понимания как исполняемых форматов, так и инструкций наборы.
- полная исполняемая декомпиляция. Этот метод пытается восстановить полный источник сборки из исполняемого файла. Возможно, вы захотите взглянуть на Онлайн Дизассемблер, который пытается выполнить задание. Вы теряете информацию о различных исходных модулях и, возможно, функциях/именах переменных.
-
Переназначаемая декомпиляции. Этот метод пытается извлечь дополнительную информацию из исполняемого файла, глядя на отпечатки пальцев компилятора (т. е., шаблоны кода, созданного известными компиляторами) и другие детерминированные вещи. Основная цель-восстановить исходный код более высокого уровня, например c source, из исполняемого файла. Иногда это позволяет восстановить информацию об именах функций / переменных. Считайте, что компиляция источников с
-g
часто обеспечивает лучший результат. Возможно, вы захотите дать Retargetable Decompiler попробовать.
большая часть этого происходит от vulnerbility оценка и анализ выполнения области исследований. Это сложные методы, и часто инструменты не могут быть использованы сразу из коробки. Тем не менее, они оказывают неоценимую помощь при попытке декомпилировать программное обеспечение.
миазм
https://github.com/cea-sec/miasm
это, по-видимому, наиболее перспективное конкретное решение. Согласно описанию проекта, Библиотека может:
- Открытие / изменение / генерация PE / ELF 32 / 64 LE / be с помощью Elfesteem
- сборка / разборка X86 / ARM / MIPS / SH4 / MSP430
так и должно в основном:
- разберите эльфа на внутреннее представление (разборка)
- изменить то, что вы хотите
- создать новый ELF (сборка)
Я не думаю, что это создает текстовое представление разборки, вам, вероятно, придется пройти через структуры данных Python.
TODO найти минимальный пример того, как сделать все это с помощью библиотеки. Хорошей отправной точкой кажется example/disasm/full.py, который анализирует данный файл ELF. Ключ верхнего уровня structurei составляет Container
, который читает файл ELF с Container.from_stream
. Как его потом собрать? Эта статья, кажется, делает это:http://www.miasm.re/blog/2016/03/24/re150_rebuild.html
этот вопрос спрашивает, Есть ли другие такие библиотеки: https://reverseengineering.stackexchange.com/questions/1843/what-are-the-available-libraries-to-statically-modify-elf-executables
вопросы:
- https://reverseengineering.stackexchange.com/questions/185/how-do-i-add-functionality-to-an-existing-binary-executable
- https://askubuntu.com/questions/617441/how-can-edit-a-executable-file-linux
I думаю, эта проблема не является автоматизируемой
Я думаю, что общая проблема не полностью автоматизирована, и общее решение в основном эквивалентно "как перепроектировать" двоичный файл.
чтобы вставить или удалить байты значимым образом, мы должны были бы гарантировать, что все возможные прыжки продолжают прыгать в одни и те же места.
в формальных терминах нам нужно извлечь график потока управления двоичного файла.
однако, с косвенным ветви, например,https://en.wikipedia.org/wiki/Indirect_branch, нелегко определить этот график, см. Также: косвенный расчет назначения прыжка
еще одна вещь, которую вам может быть интересно сделать:
- binary instrumentation-изменение существующего кода
Если интересно, проверьте: Pin, Valgrind (или проекты, делающие это: NaCl - родной клиент Google, возможно, QEmu.)
вы можете запустить исполняемый файл под контролем ptrace (другими словами, отладчика, такого как gdb) и таким образом контролировать выполнение по мере его выполнения, не изменяя фактический файл. Конечно, требует обычных навыков редактирования как найти где конкретные инструкции ты хочешь повлиять в исполняемый.
я делаю это с hexdump
и текстовый редактор. Вы должны быть действительно удобный с машинным кодом и форматом файла, хранящим его, и гибкий с тем, что считается "разбирать, изменять, а затем собирать".
Если Вам сойдет с рук только "точечные изменения" (переписывание байтов, но не добавление и удаление байтов), это будет легко (относительно говоря).
вы действительно не хочу смещать существующие инструкции, потому что тогда вам придется вручную настроить любое произведенное относительное смещение в машинном коде для прыжков / ветвей/нагрузок / магазинов относительно счетчика программы, как в hardcoded немедленно значения и вычисленные через регистры.
вы всегда должны иметь возможность уйти, не удаляя байты. Добавление байтов может потребоваться для более сложных модификаций и становится намного сложнее.
Шаг 0 (подготовка)
после на самом деле правильно разобрал файл с помощью objdump -D
или все, что вы обычно используете сначала, чтобы действительно понять его и найти места, которые вам нужно изменить, вам нужно будет принять к сведению следующие вещи, чтобы помочь вам найти правильные байты для изменения:
- "адрес" (смещение от начала файла) байт нужно менять.
- необработанное значение этих байтов, как в настоящее время они (the до
objdump
очень полезно здесь).
Шаг 1
дамп необработанное шестнадцатеричное представление двоичного файла с hexdump -Cv
.
Шаг 2
открыть hexdump
ed файл и найти байты по адресу, который вы хотите изменить.
быстрый ускоренный курс в hexdump -Cv
выход:
- самый левый столбец-это адреса байтов (относительно начала двоичного файла сам файл, как и
objdump
предоставляет). - самый правый столбец (в окружении
|
символы) - это просто "читаемое человеком" представление байтов-символ ASCII, соответствующий каждому байту, записывается там, с.
standing in для всех байтов, которые не сопоставляются с печатаемым символом ASCII. - важный материал находится между-каждый байт в виде двух шестнадцатеричных цифр, разделенных пробелами, 16 байт на строку.
Осторожно: В Отличие От objdump -D
, который дает вам адрес каждой инструкции и показывает необработанный шестнадцатеричный код инструкции, основанный на том, как он задокументирован как закодированный,hexdump -Cv
сбрасывает каждый байт точно в том порядке, в котором он появляется в файле. Это может быть немного запутанным, так как сначала на машинах, где байты инструкций находятся в противоположном порядке из-за различий endianness, которые также могут дезориентировать, когда вы ожидаете определенный байт в качестве определенного адреса.
Шаг 3
изменить байты, которые нужно изменить-Вам, очевидно, нужно выяснить кодировку команды raw machine (а не мнемонику сборки) и вручную записать в правильные байты.
Примечание:не надо менять удобочитаемый в самой правой колонке. hexdump
проигнорирует его, когда вы "сбросите" его.
Шаг 4
"Un-dump" измененный файл hexdump с помощью hexdump -R
.
Шаг 5 (здравомыслие проверка)
objdump
ваш новый unhexdump
ed файл и убедитесь, что разборка, которую вы изменили, выглядит правильно. diff
против objdump
оригинала.
серьезно, не пропустите этот шаг. Я делаю ошибку чаще всего при ручном редактировании машинного кода, и именно так я ловлю большинство из них.
пример
вот реальный пример работы, когда я недавно изменил двоичный файл ARMv8 (little endian). (Я знаю, вопрос помеченx86
, но у меня нет примера x86, и фундаментальные принципы одинаковы, просто инструкции разные.)
в моей ситуации мне нужно было отключить конкретную проверку" вы не должны этого делать": в моем примере binary, in objdump --show-raw-insn -d
вывод строки, о которой я заботился, выглядел так (одна инструкция до и после контекста):
f40: aa1503e3 mov x3, x21
f44: 97fffeeb bl af0 <error@plt>
f48: f94013f7 ldr x23, [sp, #32]
как вы можете видеть, наша программа "услужливо" выходит, прыгая в error
функция (которая завершает программу). Неприемлемый. Поэтому мы собираемся превратить эту инструкцию в запрет. Итак, мы ищем байты 0x97fffeeb
по адресу / файлу-offset 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]
затем я запустил двоичный файл и получил поведение, которое я хотел - соответствующая проверка больше не вызывала прерывания программы.
изменение машинного кода успешно.
!!! Предупреждение !!!
или мне это удалось? Вы заметили, что я пропустил в этом примере?
я уверен, что вы сделали - так как вы спрашиваете о том, как вручную изменить машинный код программы, вы, по-видимому, знаете, что вы делаете. Но для блага всех читателей, которые могут читать, чтобы учиться, я уточните:
я только изменил последние инструкция в ветви error-case! Переход в функцию, которая выходит из проблемы. Но, как видите, Регистрация 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
код функции a no-op, я создал путь кода, где мы достигаем этого кода с несогласованным / неправильным состоянием программы!
в моем случае, это на самом деле казалось не вызывает никаких проблем. Так что мне повезло. очень lucky: только после того, как я уже запустил свой модифицированный двоичный файл (который, кстати, был безопасность-критические бинарные: у него была возможность 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-строка") и при необходимости перезаписать жестко закодированный размер строки в машине код, который его использует.
вы также можете заменить все нежелательные инструкции на no-ops. Другими словами, мы можем превратить нежелательный код в то, что называется "no-op sled":
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) раз при ручном кодировании этой безусловной инструкции ветви. И это не всегда наша вина: в первый раз это было потому, что документация, которую я имел, была устаревшей/неправильной и сказала, что один бит был проигнорирован в кодировке, когда это на самом деле не было, поэтому я установил его на ноль с первой попытки.
добавить Код
вы мог бы теоретически используйте эту технику для добавить машинные инструкции тоже, но это сложнее, и мне никогда не приходилось это делать, поэтому в настоящее время у меня нет работающего примера.
С точки зрения машинного кода это просто: выберите одну инструкцию в том месте, где вы хотите добавить код, и преобразуйте ее в инструкцию прыжка в новый код, который вам нужно добавить (не забудьте добавить инструкции), которые вы таким образом заменили в новый код, если вам это не нужно для вашей добавленной логики, и вернуться к инструкции, к которой вы хотите вернуться в конце добавления). В принципе, вы" соединяете " новый код.
но вы должны найти место, чтобы сделать новый код, и это самое трудное.
если вы действительно lucky, вы можете просто добавить новый машинный код в конце файла, и он будет "просто работать": новый код будет загружен вместе с остальными в те же ожидаемые машинные инструкции, в адресное пространство, которое попадает на страницу памяти, правильно помеченную исполняемым файлом.
по моему опыту hexdump -R
игнорирует не только самый правый столбец, но и самый левый столбец, поэтому вы можете буквально просто поместить нулевые адреса для всех добавленных вручную строк, и это сработает.
Если вам повезло меньше, после добавления кода вам придется фактически настроить некоторые значения заголовка в том же файле: если загрузчик для ваша операционная система ожидает, что двоичный файл будет содержать метаданные, описывающие размер исполняемого раздела (по историческим причинам, часто называемым разделом "текст"), который вам нужно будет найти и настроить. В старые времена двоичные файлы были просто сырым машинным кодом - в настоящее время машинный код завернут в кучу метаданных (например, ELF в Linux и некоторых других).
Если вам все еще немного повезло, у вас может быть какое-то "мертвое" место в файле, которое правильно загружается как часть двоичный файл с теми же относительными смещениями, что и остальной код, который уже находится в файле (и это мертвое место может соответствовать вашему коду и правильно выровнено, если ваш процессор требует выравнивания слов для инструкций процессора). Затем вы можете перезаписать его.
Если вам действительно не повезло, вы не можете просто добавить код, и нет мертвого пространства, которое вы можете заполнить своим машинным кодом. В этот момент Вы в основном должны быть хорошо знакомы с исполняемым форматом и надеяться, что вы сможете выяснить что-то внутри этих ограничений, что по-человечески возможно снять вручную в течение разумного количества времени и с разумным шансом не испортить его.