Обратный инжиниринг исполняемого файла Linux — привет, мир

Узнайте, как реконструировать исполняемый файл Linux — привет, мир, в этой статье Реджинальда Вонга, ведущего исследователя защиты от вредоносных программ в Vipre Security, компании J2 Global, в которой рассказывается о различных технологиях безопасности, ориентированных на атаки и вредоносное ПО.

Многие наши инструменты отлично работают в Linux. В этой статье мы обсудим, как реверсировать файл ELF, изучив инструменты реверсирования.

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

sudo apt install gcc

Компилятор программ C, gcc, обычно предустановлен в Linux. Откройте любой текстовый редактор и введите строки следующего кода, сохранив его как hello.c:

#include <stdio.h>
void main(void)
{
    printf ("hello world!\n");
}

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

1.png

Файл hello — это наш исполняемый файл Linux, который отображает сообщение в консоли. Теперь о реверсировании этой программы.

dlrow olleh

В качестве примера передовой практики процесс реверсирования программы должен начинаться с надлежащей идентификации. Начнем с файла:

2.png

Это 32-битный файл формата ELF. Файлы ELF являются собственными исполняемыми файлами на платформах Linux. Следующая остановка, давайте быстро взглянем на текстовые строки с помощью команды strings:

3.png

Эта команда выдаст что-то вроде следующего вывода:

/lib/ld-linux.so.2
libc.so.6
_ IO_stdin_used
puts
__ libc_start_main
__ gmon_start__
GLIBC_2.0
PTRh
UWVS
t$,U
[^_ ]
hello world!
;* 2$"(
GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.10) 5.4.0 20160609
crtstuff.c
__ JCR_LIST__
deregister_tm_clones
__ do_global_dtors_aux
completed.7209
__ do_global_dtors_aux_fini_array_entry
frame_dummy
__ frame_dummy_init_array_entry
hello.c
__ FRAME_END__
__ JCR_END__
__ init_array_end
_ DYNAMIC
__ init_array_start
__ GNU_EH_FRAME_HDR
_GLOBAL_OFFSET_TABLE_
__ libc_csu_fini
_ ITM_deregisterTMCloneTable
__ x86.get_pc_thunk.bx
_ edata
__ data_start
puts@@GLIBC_2.0
__ gmon_start__
__ dso_handle
_ IO_stdin_used
__ libc_start_main@@GLIBC_2.0
__ libc_csu_init
_ fp_hw
__ bss_start
main
_ Jv_RegisterClasses
__ TMC_END__
_ ITM_registerTMCloneTable
.symtab
.strtab
.shstrtab
.interp
.note.ABI-tag
.note.gnu.build-id
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rel.dyn
.rel.plt
.init
.plt.got
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.jcr
.dynamic
.got.plt
.data
.bss
.comment

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

/lib/ld-linux.so.2
libc.so.6

Последняя часть списка содержит названия разделов файла. Мы знаем только несколько битов текста, которые мы поместили в наш код C. Остальные размещаются там самим компилятором как часть его кода, который подготавливает и завершает корректное выполнение нашего кода.

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

objdump -d hello > disassembly.asm

Выходной файл disassembly.asm должен содержать следующий код:

4.png

Если вы заметили, синтаксис дизассемблирования отличается от формата языка ассемблера Intel, который мы изучили. Здесь мы видим синтаксис дизассемблирования AT&T. Чтобы получить синтаксис Intel, нам нужно использовать параметр -M intel следующим образом:

objdump -M intel -d hello > disassembly.asm

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

5.png

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

Disassembly of section .init:
080482a8 <_ init>:

Disassembly of section .plt:
080482d0 <puts@plt-0x10>:
080482e0 <puts@plt>:
080482f0 <__libc_start_main@plt>:

Disassembly of section .plt.got:
08048300 <.plt.got>:

Disassembly of section .text:
08048310 <_ start>:
08048340 <__ x86.get_pc_thunk.bx>:
08048350 <deregister_tm_clones>:
08048380 <register_tm_clones>:
080483c0 <__ do_global_dtors_aux>:
080483e0 <frame_dummy>:
0804840b <main>:
08048440 <__libc_csu_init>:
080484a0 <__libc_csu_fini>:

Disassembly of section .fini:
080484a4 <_ fini>:

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

6.png

Я выделил вызов API для путов. API puts также является версией printf. GCC был достаточно умен, чтобы выбрать put вместо printf по той причине, что строка не интерпретировалась как строка форматирования в стиле C. Строка форматирования или средство форматирования содержит управляющие символы, которые обозначаются знаком %, например %d для целого числа и %s для строки. По сути, puts используется для неформатированных строк, а printf — для форматированных.

Что мы собрали на данный момент?

Предполагая, что у нас нет никакого представления об исходном коде, вот информация, которую мы собрали на данный момент:
• Файл представляет собой 32-разрядный исполняемый файл ELF.
• Он был скомпилирован с использованием GCC.
• Он имеет 15 исполняемых функций, включая функцию main().
• В коде используются распространенные библиотеки Linux: libc.so и ld-linux.so.
• В зависимости от кода дизассемблирования ожидается, что программа просто покажет сообщение.
• Ожидается, что программа отобразит сообщение с помощью puts.

Динамический анализ

Теперь давайте проведем динамический анализ. Помните, что динамический анализ следует проводить в среде песочницы. Есть несколько инструментов, обычно предустановленных в Linux, которые можно использовать для отображения более подробной информации. Мы представляем ltrace, strace и gdb для этого реверсивного действия.

Вот как используется ltrace:

7.png

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

Адрес 0x804840b также является адресом основной функции, указанной в результатах разборки.

strace — еще один инструмент, который мы можем использовать, но он регистрирует системные вызовы. Вот результат выполнения strace нашей программы hello world:

8.png

strace регистрировала каждый произошедший системный вызов, начиная с того момента, когда он выполнялся системой. execve — это первый зарегистрированный системный вызов. Вызов execve запускает программу, на которую указывает имя файла в ее аргументе функции. open и read — это системные вызовы, которые здесь используются для чтения файлов. mmap2, mprotect и brk отвечают за действия с памятью, такие как выделение, разрешения и установка границ сегмента.

Глубоко внутри кода puts он в конечном итоге выполняет системный вызов записи. write, как правило, записывает данные в объект, на который они были указаны. Обычно он используется для записи в файл. В этом случае первый параметр записи имеет значение 1. Значение 1 обозначает STDOUT, который является дескриптором вывода консоли. Второй параметр — это сообщение, поэтому он записывает сообщение в STDOUT.

Идем дальше с отладкой

Во-первых, нам нужно установить gdb, выполнив следующую команду:

sudo apt install gdb

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

9.png

Затем используйте gdb для отладки программы hello следующим образом:

gdb ./hello

gdb можно управлять с помощью команд. Команды полностью перечислены в онлайн-документации, но простой ввод справки может помочь нам с основами.

Вы также можете использовать gdb, чтобы показать дизассемблирование указанных функций, используя команду disass. Например, давайте посмотрим, что произойдет, если мы используем основную команду disass:

10.png

Потом снова нам дали разборку в AT&T sytnax. Чтобы настроить gdb на использование синтаксиса Intel, используйте следующую команду:

set disassembly-flavor intel

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

11.png

Чтобы поместить точку останова в функцию main, введите команду b * main.

После установки точки останова мы можем запустить программу с помощью команды run. Мы должны оказаться по адресу основной функции:

12.png

Чтобы получить текущие значения регистров, введите info registers. Поскольку мы находимся в 32-битной среде, используются расширенные регистры (то есть EAX, ECX, EDX, EBX и EIP). В 64-разрядной среде регистры будут отображаться с префиксом R (то есть RAX, RCX, RDX, RBX и RIP).

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

Продолжайте вводить si и disass main, пока не дойдете до строки, содержащей call 0x80482e0 puts@plt. В итоге вы должны получить следующие результаты регистров disass и info:

13.png

Знак =>, найденный слева, указывает, где находится указатель инструкции. Регистры должны выглядеть примерно так:

14.png

Перед вызовом функции puts мы можем проверить, какие значения были помещены в стек. Мы можем просмотреть это с помощью x/8x $esp:

15.png

Далее нам нужно сделать шаг по (ni) строке инструкции вызова. Это должно отобразить следующее сообщение:

16.png

Но если вы использовали si, указатель инструкции будет в коде оболочки puts. Мы все еще можем вернуться к тому месту, где остановились, используя команду until, сокращенно u. Просто используйте команду until в одной инструкции. Вам нужно будет указать адрес, где он остановится. Это как временная точка останова. Не забудьте поставить звездочку перед адресом:

17.png

Оставшиеся 6 строк кода восстанавливают значения ebp и esp сразу после входа в основную функцию, а затем возвращаются с помощью ret. Помните, что инструкция вызова будет хранить адрес возврата в верхней части стека до фактического перехода к адресу функции. Инструкция ret будет считывать возвращаемое значение, на которое указывает espregister.

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

Вот таблица, показывающая изменения значений регистров esp, ebp и ecx после выполнения инструкции по данному адресу.

18.ПНГ

Вы можете либо продолжить изучение кода очистки после ret, либо просто завершить программу, используя continue или его аббревиатуру c, как показано ниже:

19.png

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

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *