Ку, киберрекруты. Начинаю курс реверса с использованием radare2. В этой серии постов, которую я начинаю с этого, будут рассмотрены основы реверс-инжиниринга с помощью "популярного" фреймворка radare2. Я хочу провести вас через разнообразный набор примеров двоичных файлов, написанных на C, которые будут охватывать наиболее распространенные алгоритмы и структуры данных, которые вы можете найти в программах, так что в конце вы сможете определить их и работать с ними. Это не будет продвинутый/профи курс, но он начнется с самых низов и будет охватывать гораздо больше, чем средний бесплатный курс/учебник.
В этой серии статей мы начнем с исходного файла / алгоритма на языке 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, как предлагает сама веб-страница, это клонировать его из репозиториев:
Вы также можете его установить из apt/rpm/yaourt, в большинстве дистрибутивов используя примерно такая конструкция:
А если вы хотите использовать его на Windows, вы можете загрузить установку для windows с его
Hello world
Каждый путь программирования начинается с классической программы hello world, и этот будет не хуже. А hello world на языке C будет выглядеть именно так:
Программу можно скомпилить так:
И если мы запустим ее, то в основном получим "Hello, World!" на экране и все, очень просто, но надо же с чего-то начинать
Как только программа скомпилирована, мы можем открыть ее с помощью r2, используя radare2 program, где program - программа, которую вы хотите проанализировать, r2 также является псевдонимом radare2, поэтому вы можете использовать его вместо него, если хотите. После загрузки двоичного файла нам понадобится radare для разбора программы; запуск aaaa проанализирует двоичный файл и обнаружит структуры данных, вызовы функций и подобные элементы.
Когда вы выполняете команду aaa, radare показывает вам, какие шаги она выполняет. Каждый шаг содержит команду, отвечающую за него, в круглых скобках. Например:
Как вы видите, aaa - это команда, которая выполняет другие команды. Она также печатает краткое описание того, что делает каждая команда. Немного более подробную информацию можно найти в разделе aa? Итак, чтобы сложить эту
информацию вместе:
Основным изменением aaaa является [x] Enable constraint types analysis for variables. Это в основном включает конфигурационную переменную anal.types.constraint .
От себя лично, я бы посоветовал не использовать aaaa, так как иногда он довольно глючен и, вероятно, в нем нет необходимости.
Вернемся к нашей программе - hello_world:
После загрузки и разбора программы с ней можно выполнить множество действий, таких как листинг строк, дизассемблирование блоков кода и многое другое. Одна из самых распространенных вещей, которую вы захотите сделать, получив в руки неизвестный двоичный файл - это получить список его функций. Это можно сделать с помощью команды afl.
В примерах, подобных этому, наиболее интересной функцией является main. Функции Entry* также могут быть интересны, но поскольку это программа hello world, в таких функциях вы, вероятно, найдете кучу кода, который вы не поймете в данный момент (некоторые внутренние вещи, добавленные компилятором), поэтому мы можем пока оставить их. Другие функции, которые мы видим в этом примере, связаны с библиотеками/вызовами C, используемыми программой (как вызов printf, который вы, вероятно, идентифицируете там).
Еще одна интересная начальная вещь, которую мы можем здесь сделать, это, например, получить общую информацию о бинарном файле с помощью iI:
Это подскажет такие значения, как архитектура или компилятор. Эти значения всегда интересны, так что мы можем знать, с чем имеем дело, и начать выбирать стратегии, сосредоточившись на том, что важно.
Мы также можем перечислить импорты с помощью ii:
В данном конкретном случае, по знанию, мы должны быть уверены, что имеем дело с простой программой, написанной на языке C.
Еще одной командой, представляющей для нас интерес, может быть команда iz. Она выведет список всех строк, содержащихся в секции данных программы (izz выведет список строк во всем файле). Эта команда особенно полезна при работе с простыми крэками (так мы можем определить закодированные пароли). В целом, знание строк внутри программы полезно, когда нужно получить общее представление о том, "о чем эта программа".
Как мы видим, iz обнаруживает строку "Hello, World!", строку, которая появляется каждый раз, когда мы запускаем программу. Radare также сообщает нам о разделе (ascii) и местоположении строки.
Теперь, когда мы знаем некоторые из самых основных основ, давайте на самом деле развернем программу и попытаемся понять, что и как она делает.
Первое, что мы должны понять, это то, что мы проверяем main, а main - это функция, поэтому она должна куда-то возвращаться, и она может получать аргументы или нуждаться в пространстве для локальных переменных, операции, связанные с этими аспектами, выполняются в начале и в конце кода.
Часть, на которой нам придется сосредоточиться, начинается с push str.Hello__World и затем вызова printf. В 32-битных системах способ передачи параметров функциям заключается в выталкивании этих параметров в стек и последующем выполнении операции вызова по адресу функции. Поэтому что-то вроде printf(a); в C будет push a; call printf в asm. А что же с остальной частью кода? Ну, первая часть кода связана с созданием нового кадра стека , а кадр стека - это не что иное, как участок кода, который будет содержать локальные переменные функции, переданные аргументы и тому подобные значения.
Двоичные файлы x64 и их особенности
Давайте повторим процесс, но скомпилируем ту же программу и откроем ее в r2 в системе x64.
Основное различие между программами 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.
Используемые команды
Сегодня мы в основном использовали эти команды:
Вторая часть >>>
В этой серии статей мы начнем с исходного файла / алгоритма на языке 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 - выполнять анализ соответствия типов для всех функций
Код:
[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
Ссылка скрыта от гостей
Вторая часть >>>
Вложения
Последнее редактирование модератором: