В предыдущей статье мы рассматривали использованию отладчика GDB. Продолжим тему отладки и рассмотрим дизассемблирование в Linux.
Еще по теме: Удаленная отладка вредоносных программ
Дизассемблирование в Linux
Штатным дизассемблером в Linux является утилита objdump. Скомпилируем пример HelloWorld:
1 2 3 4 5 6 |
#include <iostream> int main() { std::cout << "Hello, world!" << std::endl; return 0; } |
Используем для этого команду
1 |
g++ helloworld.cpp -o helloworld |
И сразу дизассемблируем исполняемый файл следующей командой, перенаправив вывод в файл, потому что он получится длинным:
1 |
objdump -M intel -d helloworld > code.txt |
В параметре -M указывается архитектура, для которой обрабатывается файл. Значениями могут выступать конкретные архитектуры (x86-64, i386, i8086) или, как в данном случае, синтаксис ассемблера — intel,att. Второе значение определяет синтаксис AT&T. Параметр -d указывает на то, что надо дизассемблировать весь файл.
Получим такой дизассемблерный листинг (приведено с сокращениями):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
helloworld: file format elf64-x86-64 … Disassembly of section .text: … 00000000000010c0 <_start>: 10c0: f3 0f 1e fa endbr64 10c4: 31 ed xor ebp,ebp 10c6: 49 89 d1 mov r9,rdx 10c9: 5e pop rsi 10ca: 48 89 e2 mov rdx,rsp 10cd: 48 83 e4 f0 and rsp,0xfffffffffffffff0 10d1: 50 push rax 10d2: 54 push rsp 10d3: 45 31 c0 xor r8d,r8d 10d6: 31 c9 xor ecx,ecx 10d8: 48 8d 3d ca 00 00 00 lea rdi,[rip+0xca] # 11a9 <main> 10df: ff 15 f3 2e 00 00 call QWORD PTR [rip+0x2ef3] # 3fd8 <__libc_start_main@GLIBC_2.34> - вызов main 10e5: f4 hlt 10e6: 66 2e 0f 1f 84 00 00 cs nop WORD PTR [rax+rax*1+0x0] 10ed: 00 00 00 … 00000000000011a9 <main>: 11a9: f3 0f 1e fa endbr64 11ad: 55 push rbp 11ae: 48 89 e5 mov rbp,rsp 11b1: 48 8d 05 4c 0e 00 00 lea rax,[rip+0xe4c] # 2004 <_IO_stdin_used+0x4> 11b8: 48 89 c6 mov rsi,rax 11bb: 48 8d 05 7e 2e 00 00 lea rax,[rip+0x2e7e] # 4040 <_ZSt4cout@GLIBCXX_3.4> 11c2: 48 89 c7 mov rdi,rax 11c5: e8 c6 fe ff ff call 1090 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt> # Вывод строки «Hello, world!» на консоль… 11ca: 48 8b 15 ff 2d 00 00 mov rdx,QWORD PTR [rip+0x2dff] #3fd0 <_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GLIBCXX_3.4> 11d1: 48 89 d6 mov rsi,rdx 11d4: 48 89 c7 mov rdi,rax 11d7: e8 c4 fe ff ff call 10a0 <_ZNSolsEPFRSoS_E@plt> # … вслед за ней вывод символа конца строки 11dc: b8 00 00 00 00 mov eax,0x0 11e1: 5d pop rbp 11e2: c3 ret |
Исполняемый файл для Linux — ELF-файл — содержит отличные от PE-файла секции. Но секция с именем .text играет важную роль — содержит исполняемый код. Обрати внимание: в выведенном objdump дизассемблерном коде роль символа начала комментария играет решетка — #. Функция _start подготавливает среду выполнения перед вызовом main. А в последней происходит подготовка и вывод строки на экран. Между тем objdump смог определить имя единственной функции — main.
Типы дизассемблеров
Что представляет собой objdump? Вроде он неплохо справился со своей задачей. Но задача эта была самая элементарная! Мы ее привели лишь для того, чтобы оценить способность дизассемблера превращать нолики и единицы в ассемблерные инструкции. Тем не менее, если бы у нас была программа с условными переходами, циклами и вызовами функций, результат бы не был настолько идеальным!
А все потому, что objdump — линейный дизассемблер. Он просто перебирает все сегменты кода в двоичном файле, декодируя и преобразуя их в команды. Подобным образом ведет себя большинство простых дизассемблеров. Проблемы могут возникнуть в тот момент, когда вместо кода дизассемблер встретит данные. И, находясь в полном неведении, преобразует их в ассемблерные мнемоники. Хуже того, когда блок данных закончится, дизассемблер останется в рассинхронизованном состоянии относительно текущего кода. Хорошо хоть, что скоро он все равно войдет в колею благодаря специфике кода на платформе x86.
Иначе ведут себя рекурсивные дизассемблеры. Они учитывают поток управления, другими словами, во время анализа бинарника они прогоняют программу на собственном виртуальном процессоре, дизассемблируя код, попадающийся на пути. Этот подход показывает в точности такой код, который выполняется физическим процессором. Безусловно, этот метод позволяет избежать декодирования данных, потому что процессор в здравом уме их не выполняет!
К рекурсивным дизассемблерам относится много раз выручавшая нас IDA Pro. Когда она встречает данные, она передает управление человеку, потому что восстановление первоначального вида данных остается нерешенной технической задачей. Речь идет о сложных типах данных: о массивах, структурах и классах. Одинокую переменную (или несколько переменных) IDA раскусит без труда и без помощи человека.
Между тем рекурсивные дизассемблеры тоже могут страдать детскими болезнями. Например, не каждый поток управления легко проследить. В силу своей статической природы дизассемблерам бывает сложно обнаружить адреса косвенных переходов или вызовов подпрограмм. Тогда в бой вступают разные эвристические механизмы под конкретные компиляторы. Но это тема отдельного разговора.
В последние годы в Linux особое место занимают дизассемблеры Radare2 и Ghidra. Оба представляют собой бесплатные продукты с открытым исходным кодом. Первый появился на свет в 2006 году, тогда еще в качестве дискового редактора. Сейчас это многофункциональный инструмент хакера. Ghidra — ориентированный на спецов дизассемблер, разработанный Агентством национальным безопасности США и выпущенный на просторы интернета в 2019 году как ответ несокрушимой IDA Pro. Мы подробнее поговорим об этих инструментах в следующий раз.
ПОЛЕЗНЫЕ ССЫЛКИ: