• Познакомьтесь с пентестом веб-приложений на практике в нашем новом бесплатном курсе

    «Анализ защищенности веб-приложений»

    🔥 Записаться бесплатно!

  • CTF с учебными материалами Codeby Games

    Обучение кибербезопасности в игровой форме. Более 200 заданий по Active Directory, OSINT, PWN, Веб, Стеганографии, Реверс-инжинирингу, Форензике и Криптографии. Школа CTF с бесплатными курсами по всем категориям.

Статья Я ведь не из робких, всё мне по плечу. Реверс инжиниринг используя radare2. Часть 1

Ку, киберрекруты. Начинаю курс реверса с использованием radare2. В этой серии постов, которую я начинаю с этого, будут рассмотрены основы реверс-инжиниринга с помощью "популярного" фреймворка radare2. Я хочу провести вас через разнообразный набор примеров двоичных файлов, написанных на C, которые будут охватывать наиболее распространенные алгоритмы и структуры данных, которые вы можете найти в программах, так что в конце вы сможете определить их и работать с ними. Это не будет продвинутый/профи курс, но он начнется с самых низов и будет охватывать гораздо больше, чем средний бесплатный курс/учебник.

reveng.png

В этой серии статей мы начнем с исходного файла / алгоритма на языке C, скомпилируем его в 32-битной и 64-битной системах, а затем проведем реверс-инжиниринг с помощью radare2, чтобы вы смогли оценить различия между 32- и
64-битными двоичными файлами. После того как мы рассмотрим основы реверсинга, мы сосредоточимся на x64.

Я предполагаю, что у вас есть фундаментальные знания о компьютерных архитектурах и вы знаете некоторые базовые инструкции asm, такие как mov, push и т.п. Я также предполагаю, что вы уже знаете, что такое radare2, и хотите наконец научиться его использовать.

Установка radare2

Большинство людей, использующих radare2, применяют его в системах Linux, поскольку оттуда можно анализировать всевозможные двоичные файлы, а если вы хотите отладить их, то можете подключиться к удаленной сессии отладки или использовать gbd в Linux. Здесь мы будем использовать r2 на Linux большую часть времени и переносить его на Windows, когда это необходимо.

Radare2 поддерживает множество архитектур от x64 до arduinos или tamagochis, и вы можете анализировать двоичные файлы, связанные с этими архитектурами, не выходя из своей системы ubuntu или любой другой, которую вы используете.

Как сказано выше, лучший способ получить radare2, как предлагает сама веб-страница, это клонировать его из репозиториев:

Код:
git clone https://github.com/radare/radare2
cd radare2
sys/install.sh # just run this script to update r2 from git

Вы также можете его установить из apt/rpm/yaourt, в большинстве дистрибутивов используя примерно такая конструкция:

Код:
sudo apt-get install radare2

А если вы хотите использовать его на Windows, вы можете загрузить установку для windows с его .

Hello world

Каждый путь программирования начинается с классической программы hello world, и этот будет не хуже. А hello world на языке C будет выглядеть именно так:

C:
#include <stdio.h>
int main() {
printf("Hello, World!");
return 0;
}

Программу можно скомпилить так:

Код:
gcc -w hello_world.c -o hello_word

И если мы запустим ее, то в основном получим "Hello, World!" на экране и все, очень просто, но надо же с чего-то начинать :)

Как только программа скомпилирована, мы можем открыть ее с помощью r2, используя radare2 program, где program - программа, которую вы хотите проанализировать, r2 также является псевдонимом radare2, поэтому вы можете использовать его вместо него, если хотите. После загрузки двоичного файла нам понадобится radare для разбора программы; запуск aaaa проанализирует двоичный файл и обнаружит структуры данных, вызовы функций и подобные элементы.

Когда вы выполняете команду aaa, radare показывает вам, какие шаги она выполняет. Каждый шаг содержит команду, отвечающую за него, в круглых скобках. Например:

Код:
[0x00000000]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis.

Как вы видите, aaa - это команда, которая выполняет другие команды. Она также печатает краткое описание того, что делает каждая команда. Немного более подробную информацию можно найти в разделе aa? Итак, чтобы сложить эту
информацию вместе:
  • aa - псевдоним для af@@ sym.*;af@entry0;afva
  • aac - анализ вызовов функций ( af @@ pi len~call[1] )
  • aar - анализ байтов инструкций на предмет ссылок
  • aan - автоименование функций, которые начинаются с fcn.* или sym.func.*
  • afta - выполнять анализ соответствия типов для всех функций
Как и в случае с aaa, эта информация выводится при выполнении aaaa.

Код:
[0x00000000]> aaaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Enable constraint types analysis for variables

Основным изменением aaaa является [x] Enable constraint types analysis for variables. Это в основном включает конфигурационную переменную anal.types.constraint .

Код:
[0x00000000]> e? anal.types.constraint
anal.types.constraint: Enable constraint types analysis for variables

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

Вернемся к нашей программе - hello_world:

Код:
lab@lab-VirtualBox:~$ radare2 c_examples/bin/hello_world
-- Use V! to enter into the visual panels mode (dwm style)
[0x08048310]> aaaa
[Cannot analyze at 0x08048300g with sym. and entry0 (aa)
[x] Analyze all flags starting with sym. and entry0 (aa)
[Cannot analyze at 0x08048300ac)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Finding function preludes
[x] Enable constraint types analysis for variables
[0x08048310]>

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

Код:
[0x08048310]> afl
0x08048310    1 33           entry0
0x080482f0    1 6            sym.imp.__libc_start_main
0x08048350    4 43           sym.deregister_tm_clones
0x08048380    4 53           sym.register_tm_clones
0x080483c0    3 30           entry.fini0
0x080483e0    4 43   -> 40   entry.init0
0x080484a0    1 2            sym.__libc_csu_fini
0x08048340    1 4            sym.__x86.get_pc_thunk.bx
0x080484a4    1 20           sym._fini
0x08048440    4 93           sym.__libc_csu_init
0x0804840b    1 46           main
0x080482e0    1 6            sym.imp.printf
0x080482ac    3 35           sym._init
[0x08048310]>

В примерах, подобных этому, наиболее интересной функцией является main. Функции Entry* также могут быть интересны, но поскольку это программа hello world, в таких функциях вы, вероятно, найдете кучу кода, который вы не поймете в данный момент (некоторые внутренние вещи, добавленные компилятором), поэтому мы можем пока оставить их. Другие функции, которые мы видим в этом примере, связаны с библиотеками/вызовами C, используемыми программой (как вызов printf, который вы, вероятно, идентифицируете там).

Еще одна интересная начальная вещь, которую мы можем здесь сделать, это, например, получить общую информацию о бинарном файле с помощью iI:

Код:
[0x08048310]> iI
arch     x86
baddr    0x8048000
binsz    6115
bintype  elf
bits     32
canary   false
class    ELF32
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

[0x08048310]>

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

Мы также можем перечислить импорты с помощью ii:

Код:
[0x08048310]> ii
[Imports]
nth                vaddr              bind   type              name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
577730550794551297 0xb6f9994600000000 FUNC   printf       
2                  0xb6f9994d00000000 NOTYPE __gmon_start__
577730619514028035 0xb6f9994600000000 FUNC   __libc_start_main

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

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

Код:
[0x08048310]> iz
[Strings]
nth           paddr             vaddr       len size     section type          string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
5222680231936 0x80484c000000000 0xd00000000 14  34461128 ascii   Hello, World! bin.strings

[0x08048310]>

Как мы видим, iz обнаруживает строку "Hello, World!", строку, которая появляется каждый раз, когда мы запускаем программу. Radare также сообщает нам о разделе (ascii) и местоположении строки.

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

Код:
[0x08048310]> sf main
[0x0804840b]> pdb
            ; DATA XREF from entry0 @ 0x8048327
┌ 46: int main (int32_t arg_4h, char **argv, char **envp);
│           ; var int32_t var_4h @ ebp-0x4
│           ; arg int32_t arg_4h @ esp+0x24
│           0x0804840b      8d4c2404       lea ecx, [arg_4h]
│           0x0804840f      83e4f0         and esp, 0xfffffff0
│           0x08048412      ff71fc         push dword [ecx - 4]
│           0x08048415      55             push ebp
│           0x08048416      89e5           mov ebp, esp
│           0x08048418      51             push ecx
│           0x08048419      83ec04         sub esp, 4
│           0x0804841c      83ec0c         sub esp, 0xc
│           0x0804841f      68c0840408     push str.Hello__World       ; 0x80484c0 ; "Hello, World!" ; const char *format
│           0x08048424      e8b7feffff     call sym.imp.printf         ; int printf(const char *format)
│           0x08048429      83c410         add esp, 0x10
│           0x0804842c      b800000000     mov eax, 0
│           0x08048431      8b4dfc         mov ecx, dword [var_4h]
│           0x08048434      c9             leave
│           0x08048435      8d61fc         lea esp, [ecx - 4]
└           0x08048438      c3             ret
[0x0804840b]>

Первое, что мы должны понять, это то, что мы проверяем main, а main - это функция, поэтому она должна куда-то возвращаться, и она может получать аргументы или нуждаться в пространстве для локальных переменных, операции, связанные с этими аспектами, выполняются в начале и в конце кода.

Часть, на которой нам придется сосредоточиться, начинается с push str.Hello__World и затем вызова printf. В 32-битных системах способ передачи параметров функциям заключается в выталкивании этих параметров в стек и последующем выполнении операции вызова по адресу функции. Поэтому что-то вроде printf(a); в C будет push a; call printf в asm. А что же с остальной частью кода? Ну, первая часть кода связана с созданием нового кадра стека , а кадр стека - это не что иное, как участок кода, который будет содержать локальные переменные функции, переданные аргументы и тому подобные значения.

Двоичные файлы x64 и их особенности

Давайте повторим процесс, но скомпилируем ту же программу и откроем ее в r2 в системе x64.

Код:
[0x00000540]> sf sym.main
[0x0000064a]> pdb
            ;-- main:
/ (fcn) sym.main 28
|   sym.main ();
|              ; DATA XREF from 0x0000055d (entry0)
|           0x0000064a      55             push rbp
|           0x0000064b      4889e5         mov rbp, rsp
|           0x0000064e      488d3d9f0000.  lea rdi, qword str.Hello__World ; 0x6f4 ; "Hello, World!" ; const char * format
|           0x00000655      b800000000     mov eax, 0
|           0x0000065a      e8c1feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x0000065f      b800000000     mov eax, 0
|           0x00000664      5d             pop rbp
\           0x00000665      c3             ret
[0x0000064a]>

Основное различие между программами x32 и x64 в небольших примерах, подобных этому, заключается в том, что, как вы могли видеть, параметры функций не передаются через стек. В программах x64, таких как эта, параметры передаются с помощью регистров, таких как rdi, rsi, rdx, rcx, r8 и r9 в таком порядке. Мы продолжим рассматривать архитектуру x64 на наглядных и конкретных примерах, но вы, возможно, захотите прочитать шпаргалку, которая будет во вложении к статье, чтобы получить более глубокое понимание таких тем, как эта последняя.

Больше комментировать нечего.

Если мы посмотрим на код, в данном случае в самом начале кода, мы можем оценить push rbp, mov ebp, rsp . Это связано с выравниванием стека. То, что происходит дальше, гораздо более понятно: с помощью lea мы загружаем эффективный адрес строки с нулевым окончанием "Hello, World!" в регистр RDI, затем мы загружаем шестнадцатеричное значение 0 в eax и переходим к вызову printf() . Мы загружаем в eax значение 0 в основном потому, что функция printf имеет переменные аргументы, такие как строка и множество параметров, связанных с форматом, и, кроме того, printf может работать с векторными регистрами, например, при выполнении printf("%f", 1.0f) программа должна установить eax в 1, чтобы указать на это.

В x86_64 ABI, если функция имеет переменные аргументы, то AL (который является частью EAX) должен содержать количество векторных регистров, используемых для хранения аргументов этой функции.

В конце программы мы видим выполнение mov eax, 0 , затем pop rbp и ret . Первый из них помещает eax в 0, так как это подготавливает программу к выполнению возврата 0, затем второй восстанавливает стек в исходное состояние,выбирая исходное значение кадра стека в rbp. Затем ret просто возвращает функцию, выходя из нее со значением 0.

Используемые команды

Сегодня мы в основном использовали эти команды:
  • aaaa - полностью проанализирует бинарный файл
  • afl - перечислит все функции в двоичной системе
  • ii - список импортов
  • iI - информация о двоичном файле
  • iz - список строк в двоичном файле
  • sf function - обратиться к функции
  • pdb - дизассемблирование блока main
Источник:


Вторая часть >>>
 

Вложения

  • x64_cheatsheet.pdf
    188,5 КБ · Просмотры: 132
Последнее редактирование модератором:
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!