Как дизассемблировать в linux

Обновлено: 07.07.2024

Мне сказали использовать дизассемблер. Есть ли в gcc что-нибудь встроенное? Как это сделать проще всего?

Я не думаю, что у gcc есть флаг для этого, поскольку это в первую очередь компилятор, но есть у другого инструмента разработки GNU. objdump принимает флаг -d / --disassemble :

Разборка выглядит так:

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

ht editor может дизассемблировать двоичные файлы во многих форматах. Он похож на Hiew, но с открытым исходным кодом.

Чтобы разобрать, откройте двоичный файл, затем нажмите F6 и затем выберите elf / image.

Допустим, у вас есть:

Чтобы получить код сборки с помощью gcc, вы можете:

c++filt показывает символы

grep -vE '\s+\.' удаляет ненужную информацию

Теперь, если вы хотите визуализировать помеченную часть, просто используйте:

На моем компьютере я получаю:

Более удобный подход заключается в использовании: Compiler Explorer

Интересная альтернатива objdump - gdb. Вам не нужно запускать двоичный файл или иметь отладочную информацию.

С полной отладочной информацией это даже лучше.

Objdump имеет аналогичную опцию (-S)

Этот ответ относится к x86. Портативные инструменты, которые могут дизассемблировать AArch64, MIPS или любой другой машинный код, включая objdump и llvm-objdump .

Дизассемблер Agner Fog, objconv , довольно хорош. Он будет добавлять комментарии к выходным данным дизассемблирования для проблем с производительностью (например, ужасного срыва LCP из инструкций с 16-битными непосредственными константами).

(Он не распознает - как сокращение для stdout и по умолчанию выводит в файл с именем, аналогичным имени входного файла, с прикрепленным .asm .)

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

Он также указывает NOP более четко, чем другие дизассемблеры (давая понять, когда есть заполнение, а не дизассемблировать его как просто еще одну инструкцию).

Это открытый исходный код, и его легко компилировать для Linux. Его можно разобрать на синтаксис NASM, YASM, MASM или GNU (AT&T).

Обратите внимание, что этот вывод готов для обратной сборки в объектный файл, поэтому вы можете настроить код на уровне исходного кода asm, а не с помощью шестнадцатеричного редактора машинного кода. (Таким образом, вы не ограничены сохранением вещей одного и того же размера.) Без изменений результат должен быть почти идентичным. Однако это может быть не так, поскольку разборка таких вещей, как

В исходном коде нет ничего, что могло бы гарантировать его сборку с более длинной кодировкой, которая оставляет место для перемещений, чтобы переписать его с 32-битным смещением.

Если вы не хотите устанавливать его objconv, очень удобно использовать GNU binutils objdump -Mintel -d , и он уже будет установлен, если у вас есть обычная установка Linux gcc.

Есть также ndisasm, у которого есть некоторые особенности, но он может быть более полезным, если вы используете nasm. Я согласен с Майклом Мрозеком в том, что objdump, вероятно, лучше всего.

I was told to use a disassembler. Does gcc have anything built in? What is the easiest way to do this?


265k 40 40 gold badges 471 471 silver badges 674 674 bronze badges 5,234 12 12 gold badges 45 45 silver badges 70 70 bronze badges

9 Answers 9

I don't think gcc has a flag for it, since it's primarily a compiler, but another of the GNU development tools does. objdump takes a -d / --disassemble flag:

The disassembly looks like this:

156k 25 25 gold badges 160 160 silver badges 168 168 bronze badges Add -S to display source code intermixed with disassembly. (As pointed in another answer.)

An interesting alternative to objdump is gdb. You don't have to run the binary or have debuginfo.

With full debugging info it's even better.

objdump has a similar option (-S)

7,927 6 6 gold badges 28 28 silver badges 44 44 bronze badges


This answer is specific to x86. Portable tools that can disassemble AArch64, MIPS, or whatever machine code include objdump and llvm-objdump .

Agner Fog's disassembler, objconv , is quite nice. It will add comments to the disassembly output for performance problems (like the dreaded LCP stall from instructions with 16bit immediate constants, for example).

(It doesn't recognize - as shorthand for stdout, and defaults to outputting to a file of similar name to the input file, with .asm tacked on.)

It also adds branch targets to the code. Other disassemblers usually disassemble jump instructions with just a numeric destination, and don't put any marker at a branch target to help you find the top of loops and so on.

It also indicates NOPs more clearly than other disassemblers (making it clear when there's padding, rather than disassembling it as just another instruction.)

It's open source, and easy to compile for Linux. It can disassemble into NASM, YASM, MASM, or GNU (AT&T) syntax.

Note that this output is ready to be assembled back into an object file, so you can tweak the code at the asm source level, rather than with a hex-editor on the machine code. (So you aren't limited to keeping things the same size.) With no changes, the result should be near-identical. It might not be, though, since disassembly of stuff like

doesn't have anything in the source to make sure it assembles to the longer encoding that leaves room for relocations to rewrite it with a 32bit offset.

If you don't want to install it objconv, GNU binutils objdump -Mintel -d is very usable, and will already be installed if you have a normal Linux gcc setup.

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

sudo ln -s /home/username/fasm/fasm /usr/local/bin

ald и shed устанавливаются не сложнее:

В итоге у нас будет 3 полезных инструмента для программирования на ассемблере.

Системные вызовы

Как и большинство других операционных систем, Linux предоставляет т.н. API — набор полезных для программиста функций. В большинстве случаев вызов системной функции производится с помощью прерывания 80h. Следует отметить, что Linux используется fastcall-конвенция передачи параметров. Согласно ей параметры передаются через регистры (в windows, например, используется stdcall, где параметры передаются через стек). Номер вызываемой функции кладется в eax, а параметры в регистры:

Номер параметра / Регистр

1 / ebx
2 / ecx
3 / edx
4 / esi
5 / edi
6 / ebp

Как видите все не так сложно. Узнать номер системной функции, ее описание и параметры можно, хотя бы здесь. Возьмем, к примеру sys_exit . Как можно увидеть на той странице у нее есть один параметр — код возврата и она имеет порядковый номер 1. Таким образом мы можем вызвать ее следующим кодом:

mov eax, 1 ; 1 - номер системной функции
sub ebx, ebx ; Обнуляем регистр (можно было записать mov ebx, 0)
int 80h ; Вызываем прерывание 80h

Надеюсь, что все понятно.

Hello, World!

Ну что же. Писать мы ничего не будем, т.к. за нас все написано :) В папке fasm/examples/elfexe есть файл hello.asm, в котором находится следующий код:

; fasm demonstration of writing simple ELF executable

format ELF executable 3
entry start

segment readable executable

mov eax,4
mov ebx,1
mov ecx,msg
mov edx,msg_size
int 0x80

mov eax,1
xor ebx,ebx
int 0x80

segment readable writeable

msg db 'Hello world!',0xA
msg_size = $-msg

Как видите здесь вызываются 2 системных функции — sys_write (с порядковым номером 4) и sys_exit . sys_write принимает 3 параметра — дескриптор потока вывода (1 — stdout), указатель на строку и размер строки. Сам номер функции, как уже говорилось, мы должны положить в eax. Функцию sys_exit мы уже разобрали. Скомпилировать это чудо можно так: fasm hello.asm (но не обязательно, т.к. там же, где лежит исходник, есть и бинарник).

Посмотрим, что внутри

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

image

Мы видим всю нашу программу, данные, elf-заголовок. Неплохо? Теперь мы посмотрим на нашу программу в отладчике. Наберем в консоли:

Нас должна поприветствовать строка с предложением ввести команду. Список команд вы можете узнать, набрав help или получить помощь по отдельной команде, набрав help command . Дизассемблировать нашу программу можно командой disassemble (или ее алиас — " d "). Вы увидете дизассемблированный листинг вашей программы. Слева — адрес, справа — сама команда, а посередине — опкод команды.

Получить дамп можно командой dump (странно, но ее нет в выводе команды help ).

image

Теперь попробуем поработать с командой next . Выполните ее и в ответ вам покажут значения регистров, установленные флаги, а так же адрес, опкод и дизассемблированную команду, которая должна выполниться следующей. Попробуйте выполнять команды и следите за изменением флагов и регистров. После вызова первого прерывания у вас на экране должна появиться надпись «Hello world!».

Целью данной статьи было показать основы программирования на ассемблере в linux, а не программирования на ассемблере в общем. Надеюсь, что вы подчерпнули для себя что-то полезное от сюда.

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

Звучит несложно, правильно?

У читателя предполагается наличие опыта компиляции программ и работы в Линуксе. Небольшое умение читать ассемблерный код тоже пригодится.

Итак, вот наш простейший хелловорлд:


Скомпилируем его и посчитаем количество символов:


Фигасе! Откуда берутся эти 11 килобайт? objdump -t hello показывает 79 записей в таблице идентификаторов, за большинство из которых ответственна стандартная библиотека.

Так что мы не будем ее использовать. И printf мы тоже не будем использовать, чтобы избавиться от инклюда:


Перекомпилируем и пересчитаем количество символов:


Почти ничего не изменилось? Ха!

Проблема в том, что gcc все ещё использует startup files (?) во время линкования. Доказательства? Скомпилируем с ключом -nostdlib , после чего (в соответствии с документацией) gcc «не будет использовать при линковании системные библиотеки и startup files. Использоваться будут только явно переданные линкеру файлы».


Всего лишь предупреждение, все равно попробуем:


Выглядит неплохо! Мы уменьшили размер до значительно более вменяемого (аж на целый порядок!)…


…и заплатили за это сегфолтом. Блин.

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

Что же делает символ _start , который похоже нужен для запуска программы? Где он обычно определяется при использовании libc?

По умолчанию с точки зрения линкера именно _start , а не main , является настоящей точкой входа в программу. Обычно _start определяется в перемещаемом ELF crt1.o . Убедимся в этом, слинковав хелловорлд c crt1.o и заметив, что _start теперь обнаруживается (но взамен появились другие проблемы из-за того, что не определены другие startup symbols libc):


Проверка сообщила, что на этом компьютере _start живет в исходнике libc: sysdeps/x86_64/elf/start.S . Этот восхитительно комментированный файл экспортирует символ _start , инициализирует стек, некоторые регистры и вызывает __libc_start_main . Если посмотреть в самый низ csu/libc-start.c , можно увидеть вызов _main нашей программы:

Так вот зачем нужен _start . Для удобства подытожим происходящее между _start и вызовом main : инициализировать кучу вещей для libc и вызвать main . А раз libc нам не нужен, экспортируем собственный символ _start , который только и умеет, что вызывать main , и слинкуем с ним:


Скомпилируем и выполним хелловорлд с ассемблерной заглушкой _start :


Ура, с компиляцией проблем больше нет. Но сегфолт никуда не делся. Почему? Скомпилируем с отладочной информацией и заглянем в gdb. Установим брейкпоинт на main и пошагово исполним программу до сегфолта:


Что? main исполняется два раза? …Пришло время взяться за ассемблер:


Хех! Подробный разбор ассемблера оставим на потом, отметив вкратце следующее: после возврата из callq в main мы исполняем несколько nop и возвращаемся прямо в main . Поскольку повторный вход в main был осуществлен без установки указателя инструкции возврата на стеке (как части стандартной подготовки к вызову функции), второй вызов retq пытается достать из стека фиктивный указатель инструкции возврата и программа вылетает. Нужен способ завершения.

Буквально. После возврата из callq в %eax делается push 1 , код системного вызова sys_exit , и т.к. нужно сообщить о правильном завершении кладем в %ebx 0 , единственный аргумент SYS_exit . Теперь входим в ядро с прерыванием int $0x80 .


Ура! Программа компилируется, запускается, при прогоне через gdb даже нормально завершается.

Привет из свободного от libc мира!

Оставайтесь со мной, во второй части разберем ассемблерный код подробно, посмотрим что случится, если сделать программу более сложной, и еще немного разберемся в линковании, соглашениях о вызовах и структуре двоичного ELF файл в х86 архитектуре.

Главной особенностью дизассемблирования под Linux является полное отсутствие достойных дизассемблеров (кроме IDA PRO, конечно) и другого инструментария. Поэтому даже простая защита ставит хакеров в тупик. На самом деле это не тупик, а хитро замаскированный лаз, ведущий в хату чувака Кролика. Ну, а там, где есть Кролик, обязательно найдется и мыщъх (по-испански raton), показывающий, как эту штуку взломать.

Чувак-кролик

Рисунок 1. Чувак-кролик.

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

Нашим основным инструментом будет IDA PRO, однако мы также покажем, как взломать программу с помощью обычного hex-редактора типа HIEW'а, но это будет потом, а пока же откроем пиво и, вставив диск с "Крематорием" в дисковод, наберем в командной строке "$./tiny-crackme".

Рисунок 2. Скачиваем tiny-crackme.

Исследование tiny-crackme извне и изнутри

Сразу же после запуска крякмиса на экране появляется короткая заставка и строка "enter password", ожидающая пароля. Вводим что-нибудь наугад (например, "KPNC") и, естественно, получаем "Wrong password, sorry. ".

tiny-crackme

Рисунок 3. tiny-crackme, запрашивающий пароль.

Дальше гадать бессмысленно, надо ломать. Загружаем файл в свой любимый gdb ("$gdb tiny-crackme"), но. не тут-то было! Отладчик, грязно ругается, отказываясь признавать tiny-crackme исполняемым файлом (см. рис. 4). Что за чертовщина! Ведь мы же его только что запускали и он вполне нормально исполнялся. Ладно, берем objdump и расчехляем дизассемблер ("$objdump -D tiny-crackme"), но. он тоже не может распознать формат файла и с позором убегает.

tiny-crackme

Рисунок 4. tiny-crackme не отлаживается gdb и не дизассемблируется objdum'ом.

Почему так происходит? Да потому, что ELF-заголовок искажен, а штатные средства Linux'а таких шуток не понимают, вот и отказываются работать с ним. Пусть после этого кто-нибудь скажет, что UNIX - это хакерская ось! Ранние версии IDA вели себя точно так же, но в последнее время ELF-загрузчик был доработан и теперь мы можем дизассемблировать даже извращенные файлы. IDA жутко ругается: "The ELF header entry size is invalid (поле размера ELF-заголовка неверно), the SHT entry size is invalid (поле размера заголовка таблицы секций неверно); SHT table size or offset is invalid (размер заголовка таблицы секций или ее смещение неверно), file contains meaningless/illegal section declarations, using program sections (файл содержит бессмысленные/неверные объявления секций, поэтому будут использоваться программные секции, они же сегменты)", но все-таки открывает его и даже начинает дизассемблировать, что очень хорошо!

Экран дизассемблера должен выглядеть приблизительно так:

Листинг 1. "Сырой" дизассемблированный код tiny-crackme.

Точка входа (start), расположенная по адресу 200008h, выглядит нетипично и сразу же притягивает к себе внимание. Нормальные ELF-файлы начинаются с адреса 08048000h или около того (см. статью "Секреты покорения эльфов"), а этот. разлегся, понимаешь, в области стека и лежит себе. Ну и пускай лежит! Он же никому не мешает! Такой прием вполне законен и все нормально работает. Отладчиков это, похоже, ничуть не смущает, да и дизассемблеров тоже. Это совсем не антиотладочный прием, а просто хитрый выкрутас хакера типа "выпендреж". Ладно, идем дальше.

Расшифровка кода в дизассемблере всегда представляла большую проблему. Дизассемблер не может дизассемблировать упакованный/зашифрованный код и его надо как-то расшифровать. А как это сделать? Одни хакеры предпочитают снимать с работающей программы дамп, другие - создают специальный скрипт, расшифровывающий файл прямо в дизассемблере. Первый путь проще, второй - надежнее. Если программа использует различные антиотладочные приемы, она сможет подсунуть нам испорченный дамп, если вообще позволит дотронуться до него. Лучше расшифруем программу вручную, заодно познакомившись со скриптами IDA Pro, но для этого нам потребуется проанализировать алгоритм работы процедуры расшифровки. Это легко!

Команда "mov eax, offset loc_20004B" в строке 002002F1h загружает в регистр EAX указатель на начало зашифрованного блока, а команда "mov ecx, 2A5h" задает количество обрабатываемых байт, которое тут же делится на четыре (сдвиг на две позиции вправо эквивалентен делению на четыре, т.к. 2 * 2 = 4), поскольку расшифровка идет двойными словами. Цикл расшифровки предельно стандартен и тривиален: lodsd/xor eax, 3F5479F1h/stosd/loop (грузим в EAX очередное двойное слово/делаем ему XOR/сохраняем результат/мотаем цикл). Как это может работать?! Любой программист знает, что в Linux'е сегмент кода доступен только на исполнение (чтение) и любая попытка записи приводит к аварийному завершению приложения. На самом деле это отнюдь не ограничение системы, а. всего лишь атрибуты кодового сегмента, назначаемые линкером по умолчанию. В данном случае, выставлены все три атрибута - чтение/запись/исполнение, о чем информирует нас IDA в первой строке (Segment permissions: Read/Write/Execute). Имейте это ввиду при создании собственных защитных механизмов!

Чтобы расшифровать программу, необходимо поксорить блок от 20004Bh до (020004Bh+2A5h) константой 3F5479F1h. Нажимаем <Shift-F2> и в появившемся диалоговом окне вводим следующий скрипт:

Листинг 2. IDA-скрипт, автоматически снимающий шифровку.

Последняя проверка

Рисунок 5. Последняя проверка скрипта перед передачей на выполнение.

Если скрипт написан без ошибок, то нажатие <Ctrl-Enter> приведет к его выполнению и расшифрует весь код. Кстати говоря, создатель крякмиса допустил некритическую ошибку и расшифровал на два байта больше положенного, в результате чего угробил начало расшифровщика, к тому моменту уже отработавшее как первая ступень ракеты и никак не препятствующее нормальному выполнению программы:

Листинг 3. Процедура расшифровки, пожирающая сама себя.

Теперь, когда весь код расшифрован, мы можем продолжить его анализ. Возвращаемся к месту вызова процедуры расшифровщика call sub_2002F0, расположенной по адресу 00200046h. Мы видим полную фигню:

Листинг 4. Внешний вид дизассемблера после распаковки.

Код выглядит полной бессмыслицей. Какие тут еще sahf, and и add? Но это еще что! Присмотревшись повнимательнее (Options-> Text representation -> Number of opcode bytes - >4), мы обнаруживаем, что инструкции MOV EAX,2019Eh соответствует. однобайтовый код B8h (во всяком случае, IDA Pro уверяет нас так), чего никак не может быть! В действительности, это всего лишь багофича ИДЫ, не обновившей дизассемблерный листинг после расшифровки. Подгоняем курсор к строке 20004Bh и нажимаем <U>, чтобы перевести его в неопределенную (undefined) форму. То же самое необходимо проделать и с массивом байт, начинающимся со строки 00200053h (см. листинг 1). Но и это еще не все! Ведь после расшифровки этот массив стал частью нашей процедуры, а IDA ошибочно оборвала функцию на адресе 200050h, влепив сюда "endp" (end of procedure). Чтобы восстановить статус-кво, необходимо подогнать курсор к концу массива и нажать <E> (Edit->Functions->Set Function End). После этого можно вернуться в начало строки 20004Bh и нажать "C", чтобы превратить неопределенные байты в CODE.

Вот как будет выглядеть экран дизассемблера:

Листинг 5. Дизассемблированный листинг после расшифровки скриптом.

Листинг 6. Окрестности кода, в который нас привела цепочка перекрестных ссылок.

Остановим свой выбор на последнем, хотя он, в отличии от HIEW'а не может редактировать ELF'ы с искаженным заголовком в режиме image (то есть все виртуальные адреса мы должны вычислять самостоятельно), но зато нам не придется платить.

Загружаем файл в редактор ("$./hte tiny-crackme"), нажимаем <F6> (mode) или давим пробел, в появившемся диалоговом окне выбираем "elf/program header" (просмотр программного заголовка, описывающего сегменты) и видим один-единственный сегмент "entry 0 (load)". Нажимаем <Enter>, чтобы просмотреть его атрибуты и видим, что он начинается с виртуального адреса 200000h, расположенного в файле по смещению 0h. Следовательно, виртуальный адрес 2000СFh (по которому расположен наш злополучный условный переход) соответствует смещению 0СFh

Просмотр атрибутов единственного сегмента

Рисунок 6. Просмотр атрибутов единственного сегмента.

Возвращаемся к строке 002000CFh (той самой, в которой мы исправили условный переход), и прокручиваем экран дизассемблера вверх до тех пор, пока не встретим следующую перекрестную ссылку start+AFj, ведущую к строке 2000ACh. Посмотрим, что у нас там?

Листинг 7. Мина с детонатором.

HTE

Рисунок 7. "Сквозная" правка зашифрованного кода в HTE без его расшифровки.

Возвращаемся к нашему первому условному переходу 2000CFh (см. листинг 6) и пытаемся проанализировать, что именно он проверяет. Мы видим, что с вершины стека стягивается двойное слово и проверяется на равенство нулю. А кто его туда положил?! Переходим по перекрестной ссылке наверх и видим, что в строке 20009Ch на вершину стека забрасывается содержимое регистра EBX.

Листинг 8. Что за бикфордов шнур?!

А чему равен сам EBX? Ответ дает очередная перекрестная ссылка, ведущая нас к следующему коду:

Листинг 9. То был бикфордов шнур, а это - динамит.

На самом деле, создатель кряксима применил довольно хитрый трюк. Условный переход 2000CFh не контролирует ни правильность пароля, ни наличие отладчика. Он вставлен просто как приманка. Мина-ловушка. Кто пытается его хакнуть, тот взрывается.

Таким образом, чтобы взломать программу, необходимо изменить всего один условный переход по адресу 2000B7h. Условный переход 2000CFh трогать не нужно! Поскольку мы уже тронули его, нам надлежит вернуть все на место, заменив ханутое 85h на 84h. Сохраняем изменения по <F2>, выходим из hex-редактора и. Неужели на этот раз сработает?!

Взлом завершен

Рисунок 8. Взлом завершен.

Да! Это работает. Невероятно! У нас получилось! Программа воспринимает любые вводимые пароли как правильные, выводя победоносную надпись "Success!! Congratulations" на экран! Открываем свежее пиво и отрываем у мыщъха хвост. Теперь мы будем работать только клавиатурой!

Все это долго описывать, но быстро ломать. Чтобы захачить программу, мыщъх'у потребовалось чуть больше десяти минут, да и те ушли в основном на тормоза виртуальной Linux-машины под не самым быстрым P-III 733. В живой природе все происходит еще быстрее.

Заключение

Кстати, вот несколько подходящих паролей: b00m, v2Do, f64k. По идее, после взлома программа должна воспринимать их как неправильные (мы же ведь инвертировали условный переход), но. "вопреки усилиям врачей", она к ним вполне благосклонна. Вот так головоломка! Но на самом деле все проще простого. Создатель крякмиса совместил в одном переходе контроль целостности кода с проверкой валидности пароля. Поскольку, после хака целостность кода была нарушена, инвертированный условный переход срабатывает всегда, независимо от того, какой пароль был введен. Вообще говоря, tiny-crackme содержит довольно много секретов. Поковыряйте его на досуге. Получите массу удовольствия.

Читайте также: