Как разобрать, изменить и затем собрать исполняемый файл Linux? - PullRequest
47 голосов
/ 30 ноября 2010

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

Ответы [ 8 ]

29 голосов
/ 30 ноября 2010

Я не думаю, что есть надежный способ сделать это. Форматы машинного кода очень сложны, более сложны, чем файлы сборки. На самом деле невозможно взять скомпилированный двоичный файл (скажем, в формате 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    $0x80484b0,%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. Если это больше, у вас могут быть проблемы, и вам, возможно, придется создавать новые функции и вызывать их вместо этого.

7 голосов
/ 10 февраля 2016

@ mgiuca правильно ответил на этот ответ с технической точки зрения. На самом деле, разборка исполняемой программы в простой для перекомпиляции исходный код сборки не является легкой задачей.

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

  1. Статические / Динамические приборы . Этот метод влечет за собой анализ исполняемого формата, вставку / удаление / замену определенных инструкций по сборке для определенной цели, исправление всех ссылок на переменные / функции в исполняемом файле и создание нового измененного исполняемого файла. Вот некоторые известные мне инструменты: PIN , Hijacker , PEBIL , DynamoRIO . Учтите, что настройка таких инструментов для целей, отличных от предназначенных для них, может быть сложной задачей и требует понимания как исполняемых форматов, так и наборов инструкций.
  2. Полная исполняемая декомпиляция . Этот метод пытается восстановить полный источник сборки из исполняемого файла. Возможно, вы захотите взглянуть на Online Disassembler , который пытается выполнить эту работу. В любом случае вы теряете информацию о различных исходных модулях и, возможно, именах функций / переменных.
  3. Ретаргетинг декомпиляции . Этот метод пытается извлечь больше информации из исполняемого файла, взглянув на отпечатки компилятора (т.е. шаблоны кода, сгенерированные известными компиляторами) и другие детерминированные вещи. Основная цель состоит в том, чтобы восстановить исходный код более высокого уровня, например, исходный код C, из исполняемого файла. Иногда это позволяет восстановить информацию об именах функций / переменных. Учтите, что компиляция источников с -g часто дает лучшие результаты. Возможно, вы захотите попробовать Retargetable Decompiler .

В основном это связано с оценкой уязвимостей и выполнением анализа исследований. Это сложные методы, и часто инструменты не могут быть использованы сразу же из коробки. Тем не менее, они оказывают неоценимую помощь при попытке перепроектировать какое-то программное обеспечение.

7 голосов
/ 30 ноября 2010

Для изменения кода внутри двоичной сборки, как правило, есть 3 способа сделать это.

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

Конечно, будет работать только 2-й, если сборка выполняет какую-либо проверку самосохранения.

Редактировать:Если это не очевидно, то поиграть с бинарными сборками - это ОЧЕНЬ высокоуровневый материал для разработчиков, и вам будет трудно спросить об этом здесь, если вы не спросите действительно конкретные вещи.

2 голосов
/ 24 ноября 2018

Мой "дизассемблер ci на ассемблере" является единственной системой, которая, как мне известно, основана на том принципе, что, какой бы ни была дизассемблирование, она должна повторно собираться в байт для одного и того же двоичного байта.1004 *

Существует два примера исполняемых файлов elf с их разборкой и повторной сборкой.Первоначально он был разработан, чтобы иметь возможность изменять систему загрузки, состоящую из кода, интерпретируемого кода, данных и графических символов, с такими тонкостями, как переход из реального в защищенный режим.(Это удалось.) Примеры демонстрируют также извлечение текста из исполняемых файлов, который впоследствии используется для меток.Пакет debian предназначен для Intel Pentium, но доступны дополнительные модули для Dec Alpha, 6809, 8086 и т. Д.

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

В отношении двоичного двоичного объекта вообще не делается никаких предположений, но, конечно, разборка Intel мало полезна для двоичного файла Dec Alpha.

2 голосов
/ 01 октября 2018

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

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

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

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

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

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

  1. «Адрес» (смещение от начала файла) байтов, которые необходимо изменить.
  2. Необработанное значение тех байтов, какими они являются в настоящее время (параметр --show-raw-insn для objdump действительнополезно здесь).

Шаг 1

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

Шаг 2

Открытьhexdump ed и найдите байты по адресу, который вы хотите изменить.

Быстрый ускоренный курс при выводе hexdump -Cv:

  1. Самый левый столбецадреса байтов (относительно начала биСам файл, как и в objdump).
  2. Крайний правый столбец (окруженный | символами) представляет собой просто «читаемое человеком» представление байтов - записывается символ ASCII, соответствующий каждому байтутам с ., стоящим для всех байтов, которые не отображаются на печатный символ ASCII.
  3. Важный материал находится между ними - каждый байт в виде двух шестнадцатеричных цифр, разделенных пробелами, 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 и некоторых других).

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

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

1 голос

миазмы

https://github.com/cea-sec/miasm

Это наиболее перспективное конкретное решение. Согласно описанию проекта, библиотека может:

  • Открытие / изменение / генерация PE / ELF 32/64 LE / BE с использованием Elfesteem
  • Сборка / разборка X86 / ARM / MIPS / SH4 / MSP430

Так и должно быть в основном:

  • парсинг ELF во внутреннее представление (разборка)
  • изменить то, что вы хотите
  • создать новый ELF (сборка)

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

TODO нашел минимальный пример того, как сделать все это с помощью библиотеки. Хорошей отправной точкой является example / disasm / full.py , который анализирует данный файл ELF. Ключевой структурой верхнего уровня является 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://en.wikipedia.org/wiki/Indirect_branch, определить этот график непросто, см. Также: Расчет назначения косвенного перехода

0 голосов
/ 03 января 2011

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

0 голосов
/ 03 января 2011

Еще одна вещь, которая может вас заинтересовать:

  • бинарный инструментарий - изменение существующего кода

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

...