Как разобрать, изменить, а затем собрать исполняемый файл 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 правильно рассмотрел этот ответ с технической точки зрения. На самом деле, разборка исполняемой программы в простой для перекомпиляции источник сборки не является легкой задачей.

, чтобы добавить некоторые биты к обсуждению, есть несколько методов/инструментов, которые могут быть интересны для изучения, хотя они технически сложные.

  1. статические / динамические приборы. Этот метод предполагает анализ исполняемого формата, вставьте/удалите / замените конкретные инструкции по сборке для данной цели, исправьте все ссылки на переменные / функции в исполняемом файле и испустите новый измененный исполняемый файл. Некоторые инструменты, о которых я знаю: PIN-код, удалить, PEBIL, DynamoRIO. Считайте, что настройка таких инструментов для целей, отличных от тех, для которых они были разработаны, может быть сложной и требует понимания как исполняемых форматов, так и инструкций наборы.
  2. полная исполняемая декомпиляция. Этот метод пытается восстановить полный источник сборки из исполняемого файла. Возможно, вы захотите взглянуть на Онлайн Дизассемблер, который пытается выполнить задание. Вы теряете информацию о различных исходных модулях и, возможно, функциях/именах переменных.
  3. Переназначаемая декомпиляции. Этот метод пытается извлечь дополнительную информацию из исполняемого файла, глядя на отпечатки пальцев компилятора (т. е., шаблоны кода, созданного известными компиляторами) и другие детерминированные вещи. Основная цель-восстановить исходный код более высокого уровня, например 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

вопросы:

I думаю, эта проблема не является автоматизируемой

Я думаю, что общая проблема не полностью автоматизирована, и общее решение в основном эквивалентно "как перепроектировать" двоичный файл.

чтобы вставить или удалить байты значимым образом, мы должны были бы гарантировать, что все возможные прыжки продолжают прыгать в одни и те же места.

в формальных терминах нам нужно извлечь график потока управления двоичного файла.

однако, с косвенным ветви, например,https://en.wikipedia.org/wiki/Indirect_branch, нелегко определить этот график, см. Также: косвенный расчет назначения прыжка


еще одна вещь, которую вам может быть интересно сделать:

  • binary instrumentation-изменение существующего кода

Если интересно, проверьте: Pin, Valgrind (или проекты, делающие это: NaCl - родной клиент Google, возможно, QEmu.)


вы можете запустить исполняемый файл под контролем ptrace (другими словами, отладчика, такого как gdb) и таким образом контролировать выполнение по мере его выполнения, не изменяя фактический файл. Конечно, требует обычных навыков редактирования как найти где конкретные инструкции ты хочешь повлиять в исполняемый.


я делаю это с hexdump и текстовый редактор. Вы должны быть действительно удобный с машинным кодом и форматом файла, хранящим его, и гибкий с тем, что считается "разбирать, изменять, а затем собирать".

Если Вам сойдет с рук только "точечные изменения" (переписывание байтов, но не добавление и удаление байтов), это будет легко (относительно говоря).

вы действительно не хочу смещать существующие инструкции, потому что тогда вам придется вручную настроить любое произведенное относительное смещение в машинном коде для прыжков / ветвей/нагрузок / магазинов относительно счетчика программы, как в hardcoded немедленно значения и вычисленные через регистры.

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

Шаг 0 (подготовка)

после на самом деле правильно разобрал файл с помощью objdump -D или все, что вы обычно используете сначала, чтобы действительно понять его и найти места, которые вам нужно изменить, вам нужно будет принять к сведению следующие вещи, чтобы помочь вам найти правильные байты для изменения:

  1. "адрес" (смещение от начала файла) байт нужно менять.
  2. необработанное значение этих байтов, как в настоящее время они (the до objdump очень полезно здесь).

Шаг 1

дамп необработанное шестнадцатеричное представление двоичного файла с hexdump -Cv.

Шаг 2

открыть hexdumped файл и найти байты по адресу, который вы хотите изменить.

быстрый ускоренный курс в hexdump -Cv выход:

  1. самый левый столбец-это адреса байтов (относительно начала двоичного файла сам файл, как и objdump предоставляет).
  2. самый правый столбец (в окружении | символы) - это просто "читаемое человеком" представление байтов-символ ASCII, соответствующий каждому байту, записывается там, с . standing in для всех байтов, которые не сопоставляются с печатаемым символом ASCII.
  3. важный материал находится между-каждый байт в виде двух шестнадцатеричных цифр, разделенных пробелами, 16 байт на строку.

Осторожно: В Отличие От objdump -D, который дает вам адрес каждой инструкции и показывает необработанный шестнадцатеричный код инструкции, основанный на том, как он задокументирован как закодированный,hexdump -Cv сбрасывает каждый байт точно в том порядке, в котором он появляется в файле. Это может быть немного запутанным, так как сначала на машинах, где байты инструкций находятся в противоположном порядке из-за различий endianness, которые также могут дезориентировать, когда вы ожидаете определенный байт в качестве определенного адреса.

Шаг 3

изменить байты, которые нужно изменить-Вам, очевидно, нужно выяснить кодировку команды raw machine (а не мнемонику сборки) и вручную записать в правильные байты.

Примечание:не надо менять удобочитаемый в самой правой колонке. hexdump проигнорирует его, когда вы "сбросите" его.

Шаг 4

"Un-dump" измененный файл hexdump с помощью hexdump -R.

Шаг 5 (здравомыслие проверка)

objdump ваш новый unhexdumped файл и убедитесь, что разборка, которую вы изменили, выглядит правильно. 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. Я изменяет строку в hexdumped файл соответственно:

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 и некоторых других).

Если вам все еще немного повезло, у вас может быть какое-то "мертвое" место в файле, которое правильно загружается как часть двоичный файл с теми же относительными смещениями, что и остальной код, который уже находится в файле (и это мертвое место может соответствовать вашему коду и правильно выровнено, если ваш процессор требует выравнивания слов для инструкций процессора). Затем вы можете перезаписать его.

Если вам действительно не повезло, вы не можете просто добавить код, и нет мертвого пространства, которое вы можете заполнить своим машинным кодом. В этот момент Вы в основном должны быть хорошо знакомы с исполняемым форматом и надеяться, что вы сможете выяснить что-то внутри этих ограничений, что по-человечески возможно снять вручную в течение разумного количества времени и с разумным шансом не испортить его.