Ку, киберрекруты. В предыдущей главе этого курса по radare2 мы рассмотрели базовую структуру исполняемого двоичного файла, написанного на языке C, уделив внимание вызовам функций ввода/вывода, таким как printf, и базовым структурам данных, таким как переменные, также были рассмотрены базовые меры контроля потока выполнения, такие как операторы if - else. Сегодня мы немного углубимся в эти меры контроля потока выполнения, представим базовое использование оператора case, объявим и используем собственные функции и в конце проанализируем циклы с while и for.
Давайте немного освежим предыдущие понятия с помощью следующего кода, который объявляет функцию для обнаружения положительных чисел и переходит к ней.
Мы можем скомпилировать этот код и перейти к нему в radare2
После запуска начального afl, на этот раз здесь мы можем обнаружить пару интересных мест, на которые стоит обратить внимание. Как обычно, у нас есть sym.main, но на этотраз у нас также есть sym.func2, который выглядит не совсем обычно, поэтому может быть интересно взглянуть на него. Другой начальной подсказкой здесь является наличие scanf, getchar, printf или puts, которые дают общее представление о том, что должна делать программа.
Мы начнем с перехода к основной функции:
В основном функция main выполняет основные операции выравнивания стека и быстро переходит к функции func2, после выполнения которой она вызывает getchar(), возможно, для поддержания окна открытым (если программа, например, запускается под windows).
Итак, интересной функцией здесь является func2, давайте перейдем к ней.
Давайте пройдемся по шагам.
Поскольку мы здесь играем на преимуществе, мы уже знаем, что первое, что делает программа на этом этапе, это объявляет числовую переменную, которая будет использоваться для хранения пользовательского ввода. Первые две строки здесь могут показаться немного запутанными и, на мой взгляд, не так уж сильно связаны с реальным "алгоритмом", который мы пытаемся проанализировать, первая строка присутствует во многих for'ах в большом количестве вызовов функций и в основном выделяет место в стеке для работы там с переменными и структурами данных, поскольку мы используем здесь переменные, программа "резервирует" некоторое пространство, конечно, мы углубимся в это позже, но вы и так знаете, что это нормально.
Затем мы находим эту странную инструкцию mov rax, qword, которая немного выходит за рамки этой статьи. Она вставлена туда нашим компилятором gcc, и делает это для того, чтобы установить проверку защиты стека от потенциальных уязвимостей переполнения буфера. Если мы присмотримся к коду, то увидим операцию XOR в том же самом месте в конце процедуры. В общих чертах, коротко описанных, программа проверяет, не был ли поврежден стек, если да, то запускает __stack_chk_fail для безопасного решения проблемы. Чтобы узнать поподробнее о переполнении буффера, можете прочитать статью Mogen - Эксплуатация бинарных уязвимостей (PWN) для почти начинающих: простое переполнение буфера [0x01].Переменная, помеченная как local_8h, будет использоваться для хранения Canary.
Идем дальше
Как мы уже знаем, на системах x64 параметры или аргументы передаются функциям через регистры, поэтому при выполнении lea rdi, "enter a number" мы загружаем в регистр эффективный адрес, по которому строка "Enter a number:" хранится в памяти. Мы не передаем функции строку целиком, вместо этого мы передаем функции ссылку на ее местоположение, эта концепция очень важна, потому что если функция каким-то образом модифицирует строку, ее исходное значение также будет изменено, если мы передаем копию строки, а не ссылку, любые операции, выполняемые внутри функции с копией, не повлияют на оригинал.
Немного о SEE и AVX
Итак, мы видим, что строка для печати передается по ссылке в качестве параметра функции printf, а затем регистр eax обнуляется. В ABI x86_64, если функция (например, printf) имеет переменные аргументы, то AL (который является частью EAX) должен содержать количество векторных регистров (SSE, AVX), используемых для хранения аргументов этой функции, в нашем случае это число равно нулю.
SSE и AVX имеют по 16 регистров. В SSE они обозначаются как XMM0-XMM15, а в AVX - YMM0-YMM15. Регистры XMM имеют длину 128 бит, а YMM - 256 бит.
SSE добавляет три типизации: m128 , m128d и __m128i. Float, double (d) и integer (i) соответственно.
AVX добавляет три типизации: m256 , m256d и __m256i. Float, double (d) и integer (i) соответственно.
После передачи этих параметров вызывается функция printf, которая должна вывести строку на stdout (экран).
ПРИМЕЧАНИЕ: XMM и YMM перекрываются! Регистры XMM рассматриваются как нижняя половина соответствующего регистра YMM. Это может вызвать некоторые проблемы с производительностью при смешивании SSE и AVX кода.
У типов данных с плавающей запятой (m128, m128d, m256 и m256d) есть только один вид структуры данных. Из-за этого GCC позволяет обращаться к компонентам данных в виде массива. То есть: это допустимо:
m128i и m256i являются союзами, поэтому на тип данных нужно ссылаться. Я не нашел подходящего способа получить объявление союза, поэтому я использую функции _mm_extract_epiXX() для получения отдельных значений данных из целочисленных векторов.
Пример работы AVX
Когда выполняется инструкция AVX, процесс происходит следующим образом:
Все операции выполняются одновременно. С точки зрения производительности, стоимость выполнения одной операции Add над плавающей точкой аналогична выполнению VAdd над 8 плавающими точками в AVX. В таблицах инструкций Агнера Фога есть больше информации о задержках и пропускной способности инструкций. На архитектуре Sandy Bridge VADDPS/D имеет задержку 3 и пропускную способность 1, как и FADD(P).
Вернемся обратно к теме
Сразу после вызова мы видим ссылку на тег local_ch, который загружается сначала в rax, а затем в rsi. local_ch ссылается на выделенное пространство, обычно на переменную, в данном случае это переменная "number". После этого мы видим, что "%d" также загружается в качестве параметра, в данном случае в rdi, затем eax обнуляется, и программа вызывает функцию scanf.
В общепринятых терминах этот блок кода означает printf("Enter a number:"), а затем scanf(%d, &number).
Следующий этап программы связан с проверкой значения для пользовательского ввода.
После выполнения scanf пользовательский ввод (local_ch) перемещается в eax, затем, как мы видели в предыдущем примере, выполняется сравнение с 0 с помощью test и jle. Если пользовательский ввод >0, выполнение продолжится и будет выведено "The number is positive", если это условие не выполняется, программа переходит в конец блока.
После этого мы можем сказать, что наш простой анализ закончен, давайте продолжим с дополнительным шагом.
Попробуем теперь сделать дополнительный шаг, добавим условие else и посмотрим, как с ним справится компилятор.
Теперь мы можем перейти непосредственно к делу
Как мы видим, в данном случае, если число не положительное, мы будем считать, что оно отрицательное, и выведем соответствующее сообщение вместо того, чтобы сразу перейти к getchar, в остальной части программы все работает так же.
Внутри radare2 есть более чистый способ проверки бифуркаций кода, таких как та, которую мы только что видели. Если вы напечатаете VV внутри функции, вы сможете визуально проверить ее.
SWITCH CASE
Случай switch относится к более продвинутому сценарию, чем if else. Оператор if else отлично работает, если у нас есть пара-тройка способов перенаправить поток выполнения программы, но если у нас много разных случаев, мы можем захотеть
использовать что-то более продвинутое, например, switch case.
Мы будем работать со следующим кодом:
Как мы видим, код довольно прост, как обычно, все волшебство происходит внутри функции func2. Здесь программа считывает символ со стандартного ввода и передает его в функцию switch. Затем значение символа будет проанализировано через все case'ы. Если введенным символом является пробел, то будет вызвана функция print, а затем проверка завершится инструкцией break, то есть в этом случае программа сразу выйдет из блока switch.
Эти следующие утверждения case можно интерпретировать как очень длинное предложение if, каждое из которых представляет собой что-то вроде if key == 'X', за которым следует символ and (&&), а затем следующее условие. Это означает следующее: если входной сигнал изменяется от '0' до '9', то это означает, что пользователь ввел цифру, и будет напечатано "Digit.\n", будет выполнен break и поток программы будетнаправлен за пределы блока switch. Если условие case не выполняется, программа выполнит то, что было в случае по умолчанию, “Neither space nor digit”.
Теперь, когда мы знаем, что делает эта программа, давайте перейдем непосредственно к radare2. На этот раз мы проанализируем корзину как статически, так и динамически
Как мы видим, мы запустили программу с флагом -d, чтобы мы могли ее отлаживать. То, что мы хотим проанализировать, находится внутри функции func2, поэтому мы можем начать с нее.
Как мы уже говорили, этот блок начинается с механизма защиты стека, который мы представили ранее. Как мы видим, управление потоком выполнения здесь немного сложнее. Это то, что иногда случается с вами, когда вы занимаетесь ctfs или даже
реальными проектами реверс-инжиниринга. Есть два обычных решения, которые можно применить в таких ситуациях. Мы можем либо перейти в визуальный режим, либо проверить строки или интересные вызовы функций. Здесь мы можем легко обнаружить строки Space, Digit и Neither space or digit, что говорит о многом и в принципе решает проблему, поскольку мы легко определили три основных случая.
Магия здесь начинается со scanf
В этом примере мы начинаем с передачи нескольких параметров функции scanf через регистры, с помощью %c мы указываем, что собираемся прочитать символ, а local_9h будет местом расположения этого символа в памяти.
После считывания значения символа с пользовательского ввода программа готовит его к сравнению с набором символов в первом операторе case. Поскольку символ в C хранится в одном (1) байте, программе нужно только содержимое AL (RAX/EAX нижний). После загрузки "правильного" значения программа сравнивает его с 0x20, которое представляет собой значение для "пробела" в соответствии с таблицей ASCII.
Как видите, если содержимое eax (ввод) равно значению space, программа перейдет на 0x560c23c18805 и там выдаст запрос "space", после чего выйдет из блока, перейдя сразу на 0x560c23c1882d (nop).
Самое интересное здесь происходит сразу после этого. После этого первого сравнения, если условие перехода не выполнено, программа снова сравнит значение с пробелом, на этот раз проверяя, меньше ли значение пробела, если меньше пробела, программа перейдет к "Neither space nor digit" и выйдет из блока, почему? Это простой трюк для компиляторов, чтобы проверить, находится ли какой-либо символ вне диапазона символов. Как вы сами можете проверить, все цифры от 0 до 9 имеют значения выше 0x20 в таблице ascii, поэтому все, что ниже 0x20, не должно быть цифрой.
Затем выполняется последнее сравнение:
Программа вычитает 0x30 из eax и сравнивает его с 0x9, почему? Ну, это еще один вычислительно простой способ для компилятора сделать это, ascii значение из char '9' равно 0x39, так что минус 0x30 возвращает 0x9. После сравнения выполняется ja для перехода к printf "Digit", если условие (key = '9') выполнено, в противном случае программа переходит в зону "neither space or digit" и выходит.
Давайте рассмотрим это более подробно, отладив программу, вводящую цифру.
Начнем с установки нескольких точек останова здесь и там
Затем мы запускаем выполнение до первой точки останова
Таким образом, значение '5' было загружено в переменную и теперь находится в AL (RAX)
Итак, rax = 0x35, что соответствует '5' в ascii. '5' будет сравниваться с ' ', и поскольку это не одно и то же число, флаг нуля останется нулевым.
После этого поток выполнения пойдет дальше, и мы встретим другой cmp.
cmp работает следующим образом с флагами
Поскольку ничего из этого не выполнено, флаги останутся нулевыми. После этого из
нашего значения будет вычтено 0x30, так что 0x35 - 0x30 = 0x5.
Затем значение будет сравниваться с 9, поэтому флаги будут выглядеть следующим образом
А поскольку ja = Jump short if выше (CF=0 и ZF=0), поток выполнения перейдет прямо к printf("Digit. \n").
На данный момент мы уже знаем, чем закончится программа, поэтому перейдем к нашему последнему примеру с switch-case.
Мы можем легко проверить его с помощью визуального режима radare2
Как вы можете видеть здесь, программа считывает выбор пользователя и затем сравнивает значение с одним из вариантов, если нет, то продолжает сравнивать, пока не будут оценены все условия. Вы должны быть в состоянии выполнить анализ
самостоятельно без каких-либо серьезных проблем.
Циклы While и For
Другой случай очень распространенной динамики, которую вы увидите почти в каждом бинарнике, - это циклы, обычно представленные операторами while и for. Код, расположенный внутри цикла while, будет продолжать циклическое выполнение до техпор, пока условие цикла истинно, код, расположенный внутри for, будет выполняться N раз.
Внутри radare2 функция func2 будет выглядеть следующим образом:
Как видите, логика программы здесь в общих чертах похожа на предыдущие блоки if-else. Разница лишь в том, что от инструкции, расположенной по адресу 0x55b75b1ba83b, вверх идет стрелка, которая отражает суть цикла. Давайте теперь пройдем шаг за шагом.
Прежде всего, программа устанавливает некоторое пространство и, как мы видим, использует пару переменных, одна из которых связана с защитой стека, а другая, вероятно, связана с пользовательским вводом.
Затем программа запрашивает ввод пользователя и сохраняет его в переменной local_ch.
Мы можем даже переименовать эту переменную, чтобы сделать все это более читабельным.
Давайте продолжим. Сразу после scanf мы выполняем прыжок в самом низу этого блока кода, вот так:
Если мы помним, условием выхода из цикла было равенство входного значения нулю, поэтому в данном случае, если входное значение не равно нулю, сработает jne и вернет нас в начало цикла.
Следующий блок кода? Мы уже знаем его
Программа проверяет входные данные, положительные или отрицательные, выполняя тест и переход.
И сразу после этого она снова запрашивает входные данные и проверяет условие выхода в нижней части блока
И это практически все.
Другой распространенный способ создания циклов - использование for, как показано здесь:
А дизазм radare2 будет выглядеть следующим образом:
Обратите внимание, что мы не видим здесь никакой защиты стека, можете сказать почему? Наверное, потому что мы не получаем здесь никаких входных данных!
Итак, программа начинается с установки значения в ноль (счетчик = 0), затем она устанавливает значение в 1, чтобы запустить процесс for
Затем он сравнивает значение с 10 (0xA).
И когда значение меньше его, он возвращается в начало, чтобы выполнить код цикла.
Код в основном печатает значение переменной и прибавляет к нему 1
Легко, да?
Ну, это все, о чем я хотел поговорить здесь. В следующей части мы начнем изучать некоторые структуры данных, такие как массивы и матрицы.
Источник:
Давайте немного освежим предыдущие понятия с помощью следующего кода, который объявляет функцию для обнаружения положительных чисел и переходит к ней.
C:
#include <stdio.h>
func2(){
int num;
printf("Enter a number: ");
scanf("%d", &num);
if(num>0) printf("The number is positive.\n");
getchar();
}
void main(){
func2();
getchar();
}
Мы можем скомпилировать этот код и перейти к нему в radare2
Код:
[0x000006a0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x000006a0]>
После запуска начального afl, на этот раз здесь мы можем обнаружить пару интересных мест, на которые стоит обратить внимание. Как обычно, у нас есть sym.main, но на этотраз у нас также есть sym.func2, который выглядит не совсем обычно, поэтому может быть интересно взглянуть на него. Другой начальной подсказкой здесь является наличие scanf, getchar, printf или puts, которые дают общее представление о том, что должна делать программа.
Код:
[0x000006a0]> afl
0x00000000 3 72 -> 73 sym.imp.__libc_start_main
0x00000618 3 23 sym._init
0x00000640 1 6 sym.imp.puts
0x00000650 1 6 sym.imp.__stack_chk_fail
0x00000660 1 6 sym.imp.printf
0x00000670 1 6 sym.imp.getchar
0x00000680 1 6 sym.imp.__isoc99_scanf
0x00000690 1 6 sub.__cxa_finalize_248_690
0x000006a0 1 43 entry0
0x000006d0 4 50 -> 40 sym.deregister_tm_clones
0x00000710 4 66 -> 57 sym.register_tm_clones
0x00000760 4 49 sym.__do_global_dtors_aux
0x000007a0 1 10 entry1.init
0x000007aa 5 111 sym.func2
0x00000819 1 26 sym.main
0x00000840 4 101 sym.__libc_csu_init
0x000008b0 1 2 sym.__libc_csu_fini
0x000008b4 1 9 sym._fini
[0x000006a0]>
Мы начнем с перехода к основной функции:
Код:
[0x000006a0]> s main
[0x00000819]> pdb
;-- main:
/ (fcn) sym.main 26
| sym.main ();
| ; DATA XREF from 0x000006bd (entry0)
| 0x00000819 55 push rbp
| 0x0000081a 4889e5 mov rbp, rsp
| 0x0000081d b800000000 mov eax, 0
| 0x00000822 e883ffffff call sym.func2
| 0x00000827 e844feffff call sym.imp.getchar ; int getchar(void)
| 0x0000082c b800000000 mov eax, 0
| 0x00000831 5d pop rbp
\ 0x00000832 c3 ret
[0x00000819]>
В основном функция main выполняет основные операции выравнивания стека и быстро переходит к функции func2, после выполнения которой она вызывает getchar(), возможно, для поддержания окна открытым (если программа, например, запускается под windows).
Итак, интересной функцией здесь является func2, давайте перейдем к ней.
Код:
[0x00000819]> s sym.func2
[0x000007aa]> pdf
/ (fcn) sym.func2 111
| sym.func2 ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x00000822 (sym.main)
| 0x000007aa 55 push rbp
| 0x000007ab 4889e5 mov rbp, rsp
| 0x000007ae 4883ec10 sub rsp, 0x10
| 0x000007b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x1a08 ; '('
| 0x000007bb 488945f8 mov qword [local_8h], rax
| 0x000007bf 31c0 xor eax, eax
| 0x000007c1 488d3dfc0000. lea rdi, qword str.Enter_a_number: ; 0x8c4 ; "Enter a number: "
| 0x000007c8 b800000000 mov eax, 0
| 0x000007cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x000007d2 488d45f4 lea rax, qword [local_ch]
| 0x000007d6 4889c6 mov rsi, rax
| 0x000007d9 488d3df50000. lea rdi, qword [0x000008d5] ; "%d"
| 0x000007e0 b800000000 mov eax, 0
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
| 0x000007ea 8b45f4 mov eax, dword [local_ch]
| 0x000007ed 85c0 test eax, eax
| ,=< 0x000007ef 7e0c jle 0x7fd
| | 0x000007f1 488d3de00000. lea rdi, qword str.The_number_is_positive. ; 0x8d8 ; "The number is positive."
| | 0x000007f8 e843feffff call sym.imp.puts ; int puts(const char *s)
| | ; JMP XREF from 0x000007ef (sym.func2)
| `-> 0x000007fd e86efeffff call sym.imp.getchar ; int getchar(void)
| 0x00000802 90 nop
| 0x00000803 488b55f8 mov rdx, qword [local_8h]
| 0x00000807 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x00000810 7405 je 0x817
| | 0x00000812 e839feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| | ; JMP XREF from 0x00000810 (sym.func2)
| `-> 0x00000817 c9 leave
\ 0x00000818 c3 ret
[0x000007aa]>
Давайте пройдемся по шагам.
Код:
| 0x000007ae 4883ec10 sub rsp, 0x10
| 0x000007b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x1a08 ; '('
| 0x000007bb 488945f8 mov qword [local_8h], rax
| 0x000007bf 31c0 xor eax, eax
Поскольку мы здесь играем на преимуществе, мы уже знаем, что первое, что делает программа на этом этапе, это объявляет числовую переменную, которая будет использоваться для хранения пользовательского ввода. Первые две строки здесь могут показаться немного запутанными и, на мой взгляд, не так уж сильно связаны с реальным "алгоритмом", который мы пытаемся проанализировать, первая строка присутствует во многих for'ах в большом количестве вызовов функций и в основном выделяет место в стеке для работы там с переменными и структурами данных, поскольку мы используем здесь переменные, программа "резервирует" некоторое пространство, конечно, мы углубимся в это позже, но вы и так знаете, что это нормально.
Затем мы находим эту странную инструкцию mov rax, qword, которая немного выходит за рамки этой статьи. Она вставлена туда нашим компилятором gcc, и делает это для того, чтобы установить проверку защиты стека от потенциальных уязвимостей переполнения буфера. Если мы присмотримся к коду, то увидим операцию XOR в том же самом месте в конце процедуры. В общих чертах, коротко описанных, программа проверяет, не был ли поврежден стек, если да, то запускает __stack_chk_fail для безопасного решения проблемы. Чтобы узнать поподробнее о переполнении буффера, можете прочитать статью Mogen - Эксплуатация бинарных уязвимостей (PWN) для почти начинающих: простое переполнение буфера [0x01].Переменная, помеченная как local_8h, будет использоваться для хранения Canary.
Идем дальше
Код:
| 0x000007c1 488d3dfc0000. lea rdi, qword str.Enter_a_number: ; 0x8c4 ; "Enter a number: "
| 0x000007c8 b800000000 mov eax, 0
| 0x000007cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x000007d2 488d45f4 lea rax, qword [local_ch]
| 0x000007d6 4889c6 mov rsi, rax
| 0x000007d9 488d3df50000. lea rdi, qword [0x000008d5] ; "%d"
| 0x000007e0 b800000000 mov eax, 0
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
Как мы уже знаем, на системах x64 параметры или аргументы передаются функциям через регистры, поэтому при выполнении lea rdi, "enter a number" мы загружаем в регистр эффективный адрес, по которому строка "Enter a number:" хранится в памяти. Мы не передаем функции строку целиком, вместо этого мы передаем функции ссылку на ее местоположение, эта концепция очень важна, потому что если функция каким-то образом модифицирует строку, ее исходное значение также будет изменено, если мы передаем копию строки, а не ссылку, любые операции, выполняемые внутри функции с копией, не повлияют на оригинал.
Немного о SEE и AVX
Итак, мы видим, что строка для печати передается по ссылке в качестве параметра функции printf, а затем регистр eax обнуляется. В ABI x86_64, если функция (например, printf) имеет переменные аргументы, то AL (который является частью EAX) должен содержать количество векторных регистров (SSE, AVX), используемых для хранения аргументов этой функции, в нашем случае это число равно нулю.
SSE и AVX имеют по 16 регистров. В SSE они обозначаются как XMM0-XMM15, а в AVX - YMM0-YMM15. Регистры XMM имеют длину 128 бит, а YMM - 256 бит.
SSE добавляет три типизации: m128 , m128d и __m128i. Float, double (d) и integer (i) соответственно.
AVX добавляет три типизации: m256 , m256d и __m256i. Float, double (d) и integer (i) соответственно.
После передачи этих параметров вызывается функция printf, которая должна вывести строку на stdout (экран).
ПРИМЕЧАНИЕ: XMM и YMM перекрываются! Регистры XMM рассматриваются как нижняя половина соответствующего регистра YMM. Это может вызвать некоторые проблемы с производительностью при смешивании SSE и AVX кода.
У типов данных с плавающей запятой (m128, m128d, m256 и m256d) есть только один вид структуры данных. Из-за этого GCC позволяет обращаться к компонентам данных в виде массива. То есть: это допустимо:
Код:
__m256 myvar = _mm256_set1_ps(6.665f); //Set all vector values to a single float
myvar[0] = 2.22f; //This is valid in GCC compiler
float f = (3.4f + myvar[0]) * myvar[7]; //This is valid in GCC compiler
m128i и m256i являются союзами, поэтому на тип данных нужно ссылаться. Я не нашел подходящего способа получить объявление союза, поэтому я использую функции _mm_extract_epiXX() для получения отдельных значений данных из целочисленных векторов.
Пример работы AVX
Когда выполняется инструкция AVX, процесс происходит следующим образом:
Все операции выполняются одновременно. С точки зрения производительности, стоимость выполнения одной операции Add над плавающей точкой аналогична выполнению VAdd над 8 плавающими точками в AVX. В таблицах инструкций Агнера Фога есть больше информации о задержках и пропускной способности инструкций. На архитектуре Sandy Bridge VADDPS/D имеет задержку 3 и пропускную способность 1, как и FADD(P).
Вернемся обратно к теме
Сразу после вызова мы видим ссылку на тег local_ch, который загружается сначала в rax, а затем в rsi. local_ch ссылается на выделенное пространство, обычно на переменную, в данном случае это переменная "number". После этого мы видим, что "%d" также загружается в качестве параметра, в данном случае в rdi, затем eax обнуляется, и программа вызывает функцию scanf.
В общепринятых терминах этот блок кода означает printf("Enter a number:"), а затем scanf(%d, &number).
Следующий этап программы связан с проверкой значения для пользовательского ввода.
Код:
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
| 0x000007ea 8b45f4 mov eax, dword [local_ch]
| 0x000007ed 85c0 test eax, eax
| ,=< 0x000007ef 7e0c jle 0x7fd
| | 0x000007f1 488d3de00000. lea rdi, qword str.The_number_is_positive. ; 0x8d8 ; "The number is positive."
| | 0x000007f8 e843feffff call sym.imp.puts ; int puts(const char *s)
| | ; JMP XREF from 0x000007ef (sym.func2)
| `-> 0x000007fd e86efeffff call sym.imp.getchar ; int getchar(void)
После выполнения scanf пользовательский ввод (local_ch) перемещается в eax, затем, как мы видели в предыдущем примере, выполняется сравнение с 0 с помощью test и jle. Если пользовательский ввод >0, выполнение продолжится и будет выведено "The number is positive", если это условие не выполняется, программа переходит в конец блока.
После этого мы можем сказать, что наш простой анализ закончен, давайте продолжим с дополнительным шагом.
Попробуем теперь сделать дополнительный шаг, добавим условие else и посмотрим, как с ним справится компилятор.
C:
#include <stdio.h>
func2(){
int num;
printf("Enter a number: ");
scanf("%d", &num);
if(num>0) printf("The number is positive.\n");
else printf("The number is negative.\n");
getchar();
}
main(){
func2();
getchar();
}
Теперь мы можем перейти непосредственно к делу
Код:
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
| 0x000007ea 8b45f4 mov eax, dword [local_ch]
| 0x000007ed 85c0 test eax, eax
| ,=< 0x000007ef 7e0e jle 0x7ff
| | 0x000007f1 488d3df00000. lea rdi, qword str.The_number_is_positive. ; 0x8e8 ; "The number is positive."
| | 0x000007f8 e843feffff call sym.imp.puts ; int puts(const char *s)
| ,==< 0x000007fd eb0c jmp 0x80b
| || ; JMP XREF from 0x000007ef (sym.func2)
| |`-> 0x000007ff 488d3dfa0000. lea rdi, qword str.The_number_is_negative. ; 0x900 ; "The number is negative."
| | 0x00000806 e835feffff call sym.imp.puts ; int puts(const char *s)
| | ; JMP XREF from 0x000007fd (sym.func2)
| `--> 0x0000080b e860feffff call sym.imp.getchar ; int getchar(void)
Как мы видим, в данном случае, если число не положительное, мы будем считать, что оно отрицательное, и выведем соответствующее сообщение вместо того, чтобы сразу перейти к getchar, в остальной части программы все работает так же.
Внутри radare2 есть более чистый способ проверки бифуркаций кода, таких как та, которую мы только что видели. Если вы напечатаете VV внутри функции, вы сможете визуально проверить ее.
Код:
| test eax, eax |
| jle 0x7ff;[gc] |
`--------------------------------------------'
| |
| '--------------------.
.----------------------------' |
| |
| |
.------------------------------------------------. .------------------------------------------------.
| 0x7f1 ;[gg] | | 0x7ff ;[gc] |
| ; 0x8e8 | | ; JMP XREF from 0x000007ef (sym.func2) |
| ; "The number is positive." | | ; 0x900 |
| lea rdi, qword str.The_number_is_positive. | | ; "The number is negative." |
| call sym.imp.puts;[ge] | | lea rdi, qword str.The_number_is_negative. |
| jmp 0x80b;[gf] | | call sym.imp.puts;[ge] |
`------------------------------------------------' `------------------------------------------------'
| |
'----------------------------. |
.------------------------'
|
|
.---------------------------------------------.
SWITCH CASE
Случай switch относится к более продвинутому сценарию, чем if else. Оператор if else отлично работает, если у нас есть пара-тройка способов перенаправить поток выполнения программы, но если у нас много разных случаев, мы можем захотеть
использовать что-то более продвинутое, например, switch case.
Мы будем работать со следующим кодом:
C:
#include <stdio.h>
func2(){
printf("Enter a key and then press enter: ");
char key;
scanf("%c",&key);
switch(key){
case ' ':
printf("Space. \n");
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0':
printf("Digit.\n");
break;
default: printf("Neither space nor digit.\n");
}
}
main(){
func2();
getchar();
}
Как мы видим, код довольно прост, как обычно, все волшебство происходит внутри функции func2. Здесь программа считывает символ со стандартного ввода и передает его в функцию switch. Затем значение символа будет проанализировано через все case'ы. Если введенным символом является пробел, то будет вызвана функция print, а затем проверка завершится инструкцией break, то есть в этом случае программа сразу выйдет из блока switch.
Эти следующие утверждения case можно интерпретировать как очень длинное предложение if, каждое из которых представляет собой что-то вроде if key == 'X', за которым следует символ and (&&), а затем следующее условие. Это означает следующее: если входной сигнал изменяется от '0' до '9', то это означает, что пользователь ввел цифру, и будет напечатано "Digit.\n", будет выполнен break и поток программы будетнаправлен за пределы блока switch. Если условие case не выполняется, программа выполнит то, что было в случае по умолчанию, “Neither space nor digit”.
Теперь, когда мы знаем, что делает эта программа, давайте перейдем непосредственно к radare2. На этот раз мы проанализируем корзину как статически, так и динамически
Код:
red@blue:~/c/chapter3$ radare2 -d case
Process with PID 7901 started...
= attach 7901 7901
bin.baddr 0x560c23c18000
Using 0x560c23c18000
asm.bits 64
[0x7ff6a3b32090]> aaa
[ WARNING : block size exceeding max block size at 0x560c23e18fe0
[+] Try changing it with e anal.bb.maxsize
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
= attach 7901 7901
7901
[0x7ff6a3b32090]>
Как мы видим, мы запустили программу с флагом -d, чтобы мы могли ее отлаживать. То, что мы хотим проанализировать, находится внутри функции func2, поэтому мы можем начать с нее.
Код:
[0x7ff6a3b32090]> s sym.func2
[0x560c23c187aa]> pdf
/ (fcn) sym.func2 154
| sym.func2 ();
| ; var int local_9h @ rbp-0x9
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x560c23c1884d (sym.main)
| 0x560c23c187aa 55 push rbp
| 0x560c23c187ab 4889e5 mov rbp, rsp
| 0x560c23c187ae 4883ec10 sub rsp, 0x10
| 0x560c23c187b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x560c23c187bb 488945f8 mov qword [local_8h], rax
| 0x560c23c187bf 31c0 xor eax, eax
| 0x560c23c187c1 488d3d200100. lea rdi, qword str.Enter_a_key_and_then_press_enter: ; 0x560c23c188e8 ; "Enter a key and then press enter: "
| 0x560c23c187c8 b800000000 mov eax, 0
| 0x560c23c187cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x560c23c187d2 488d45f7 lea rax, qword [local_9h]
| 0x560c23c187d6 4889c6 mov rsi, rax
| 0x560c23c187d9 488d3d2b0100. lea rdi, qword [0x560c23c1890b] ; "%c"
| 0x560c23c187e0 b800000000 mov eax, 0
| 0x560c23c187e5 e896feffff call sym.imp.__isoc99_scanf
| 0x560c23c187ea 0fb645f7 movzx eax, byte [local_9h]
| 0x560c23c187ee 0fbec0 movsx eax, al
| 0x560c23c187f1 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
| | 0x560c23c187f6 83f820 cmp eax, 0x20 ; 32
| ,==< 0x560c23c187f9 7c26 jl 0x560c23c18821
| || 0x560c23c187fb 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
| |||`-> 0x560c23c18805 488d3d020100. lea rdi, qword str.Space. ; 0x560c23c1890e ; "Space. "
| ||| 0x560c23c1880c e82ffeffff call sym.imp.puts ; int puts(const char *s)
| |||,=< 0x560c23c18811 eb1a jmp 0x560c23c1882d
| |||| ; JMP XREF from 0x560c23c18803 (sym.func2)
| `----> 0x560c23c18813 488d3dfc0000. lea rdi, qword str.Digit. ; 0x560c23c18916 ; "Digit."
| ||| 0x560c23c1881a e821feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x560c23c1881f eb0c jmp 0x560c23c1882d
| |``--> 0x560c23c18821 488d3df50000. lea rdi, qword str.Neither_space_nor_digit. ; 0x560c23c1891d ; "Neither space nor digit."
| | | 0x560c23c18828 e813feffff call sym.imp.puts ; int puts(const char *s)
| | | ; JMP XREF from 0x560c23c18811 (sym.func2)
| | | ; JMP XREF from 0x560c23c1881f (sym.func2)
| `--`-> 0x560c23c1882d 90 nop
| 0x560c23c1882e 488b55f8 mov rdx, qword [local_8h]
| 0x560c23c18832 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x560c23c1883b 7405 je 0x560c23c18842
| | 0x560c23c1883d e80efeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x560c23c18842 c9 leave
\ 0x560c23c18843 c3 ret
[0x560c23c187aa]>
Как мы уже говорили, этот блок начинается с механизма защиты стека, который мы представили ранее. Как мы видим, управление потоком выполнения здесь немного сложнее. Это то, что иногда случается с вами, когда вы занимаетесь ctfs или даже
реальными проектами реверс-инжиниринга. Есть два обычных решения, которые можно применить в таких ситуациях. Мы можем либо перейти в визуальный режим, либо проверить строки или интересные вызовы функций. Здесь мы можем легко обнаружить строки Space, Digit и Neither space or digit, что говорит о многом и в принципе решает проблему, поскольку мы легко определили три основных случая.
Магия здесь начинается со scanf
Код:
| 0x560c23c187d2 488d45f7 lea rax, qword [local_9h]
| 0x560c23c187d6 4889c6 mov rsi, rax
| 0x560c23c187d9 488d3d2b0100. lea rdi, qword [0x560c23c1890b] ; "%c"
| 0x560c23c187e0 b800000000 mov eax, 0
| 0x560c23c187e5 e896feffff call sym.imp.__isoc99_scanf
| 0x560c23c187ea 0fb645f7 movzx eax, byte [local_9h]
| 0x560c23c187ee 0fbec0 movsx eax, al
| 0x560c23c187f1 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
В этом примере мы начинаем с передачи нескольких параметров функции scanf через регистры, с помощью %c мы указываем, что собираемся прочитать символ, а local_9h будет местом расположения этого символа в памяти.
После считывания значения символа с пользовательского ввода программа готовит его к сравнению с набором символов в первом операторе case. Поскольку символ в C хранится в одном (1) байте, программе нужно только содержимое AL (RAX/EAX нижний). После загрузки "правильного" значения программа сравнивает его с 0x20, которое представляет собой значение для "пробела" в соответствии с таблицей ASCII.
Код:
| 0x560c23c187f1 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
| | 0x560c23c187f6 83f820 cmp eax, 0x20 ; 32
| ,==< 0x560c23c187f9 7c26 jl 0x560c23c18821
| || 0x560c23c187fb 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
| |||`-> 0x560c23c18805 488d3d020100. lea rdi, qword str.Space. ; 0x560c23c1890e ; "Space. "
| ||| 0x560c23c1880c e82ffeffff call sym.imp.puts ; int puts(const char *s)
| |||,=< 0x560c23c18811 eb1a jmp 0x560c23c1882d
| |||| ; JMP XREF from 0x560c23c18803 (sym.func2)
| `----> 0x560c23c18813 488d3dfc0000. lea rdi, qword str.Digit. ; 0x560c23c18916 ; "Digit."
| ||| 0x560c23c1881a e821feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x560c23c1881f eb0c jmp 0x560c23c1882d
| |``--> 0x560c23c18821 488d3df50000. lea rdi, qword str.Neither_space_nor_digit. ; 0x560c23c1891d ; "Neither space nor digit."
| | | 0x560c23c18828 e813feffff call sym.imp.puts ; int puts(const char *s)
| | | ; JMP XREF from 0x560c23c18811 (sym.func2)
| | | ; JMP XREF from 0x560c23c1881f (sym.func2)
| `--`-> 0x560c23c1882d 90 nop
Как видите, если содержимое eax (ввод) равно значению space, программа перейдет на 0x560c23c18805 и там выдаст запрос "space", после чего выйдет из блока, перейдя сразу на 0x560c23c1882d (nop).
Самое интересное здесь происходит сразу после этого. После этого первого сравнения, если условие перехода не выполнено, программа снова сравнит значение с пробелом, на этот раз проверяя, меньше ли значение пробела, если меньше пробела, программа перейдет к "Neither space nor digit" и выйдет из блока, почему? Это простой трюк для компиляторов, чтобы проверить, находится ли какой-либо символ вне диапазона символов. Как вы сами можете проверить, все цифры от 0 до 9 имеют значения выше 0x20 в таблице ascii, поэтому все, что ниже 0x20, не должно быть цифрой.
Затем выполняется последнее сравнение:
Код:
| || 0x560c23c187fb 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
Программа вычитает 0x30 из eax и сравнивает его с 0x9, почему? Ну, это еще один вычислительно простой способ для компилятора сделать это, ascii значение из char '9' равно 0x39, так что минус 0x30 возвращает 0x9. После сравнения выполняется ja для перехода к printf "Digit", если условие (key = '9') выполнено, в противном случае программа переходит в зону "neither space or digit" и выходит.
Давайте рассмотрим это более подробно, отладив программу, вводящую цифру.
Начнем с установки нескольких точек останова здесь и там
Код:
[0x560c23c187aa]> db 0x560c23c187f1
[0x560c23c187aa]> db 0x560c23c187fb
[0x560c23c187aa]>
Затем мы запускаем выполнение до первой точки останова
Код:
[0x560c23c187aa]> dc
Enter a key and then press enter: 5
hit breakpoint at: 560c23c187f1
[0x560c23c187f1]> pdb
/ (fcn) sym.func2 154
| sym.func2 ();
| ; var int local_9h @ rbp-0x9
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x560c23c1884d (sym.main)
| 0x560c23c187aa 55 push rbp
| 0x560c23c187ab 4889e5 mov rbp, rsp
| 0x560c23c187ae 4883ec10 sub rsp, 0x10
| 0x560c23c187b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x560c23c187bb 488945f8 mov qword [local_8h], rax
| 0x560c23c187bf 31c0 xor eax, eax
| 0x560c23c187c1 488d3d200100. lea rdi, qword str.Enter_a_key_and_then_press_enter: ; 0x560c23c188e8 ; "Enter a key and then press enter: "
| 0x560c23c187c8 b800000000 mov eax, 0
| 0x560c23c187cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x560c23c187d2 488d45f7 lea rax, qword [local_9h]
| 0x560c23c187d6 4889c6 mov rsi, rax
| 0x560c23c187d9 488d3d2b0100. lea rdi, qword [0x560c23c1890b] ; "%c"
| 0x560c23c187e0 b800000000 mov eax, 0z
| 0x560c23c187e5 e896feffff call sym.imp.__isoc99_scanf
| 0x560c23c187ea 0fb645f7 movzx eax, byte [local_9h]
| 0x560c23c187ee 0fbec0 movsx eax, al
| ;-- rip:
| 0x560c23c187f1 b 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
[0x560c23c187f1]>
Таким образом, значение '5' было загружено в переменную и теперь находится в AL (RAX)
Код:
| 0x560c23c187f1 b 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
[0x560c23c187f1]> dr
rax = 0x00000035
Итак, rax = 0x35, что соответствует '5' в ascii. '5' будет сравниваться с ' ', и поскольку это не одно и то же число, флаг нуля останется нулевым.
Код:
[0x560c23c187f1]> ds
[0x560c23c187f1]> dr 1
cf = 0x00000000
pf = 0x00000000
af = 0x00000000
zf = 0x00000000
sf = 0x00000000
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x560c23c187f1]>
После этого поток выполнения пойдет дальше, и мы встретим другой cmp.
cmp работает следующим образом с флагами
Код:
Assume result = op1 - op2
CF - 1 if unsigned op2 > unsigned op1
OF - 1 if sign bit of OP1 != sign bit of result
SF - 1 if MSB (aka sign bit) of result = 1
ZF - 1 if Result = 0 (i.e. op1=op2)
AF - 1 if Carry in the low nibble of result
PF - 1 if Parity of Least significant byte is even
Поскольку ничего из этого не выполнено, флаги останутся нулевыми. После этого из
нашего значения будет вычтено 0x30, так что 0x35 - 0x30 = 0x5.
Код:
|| 0x560c23c187fb b 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ||| ;-- rip:
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
| |||`-> 0x560c23c18805 488d3d020100. lea rdi, qword str.Space. ; 0x560c23c1890e ; "Space. "
Затем значение будет сравниваться с 9, поэтому флаги будут выглядеть следующим образом
Код:
[0x560c23c187f1]> dr 1
cf = 0x00000001
pf = 0x00000001
af = 0x00000001
zf = 0x00000000
sf = 0x00000001
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x560c23c187f1]>
А поскольку ja = Jump short if выше (CF=0 и ZF=0), поток выполнения перейдет прямо к printf("Digit. \n").
Код:
| |||| ; JMP XREF from 0x560c23c18803 (sym.func2)
| `----> 0x560c23c18813 488d3dfc0000. lea rdi, qword str.Digit. ; 0x560c23c18916 ; "Digit."
| ||| 0x560c23c1881a e821feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x560c23c1881f eb0c jmp 0x560c23c1882d
На данный момент мы уже знаем, чем закончится программа, поэтому перейдем к нашему последнему примеру с switch-case.
C:
#include <stdio.h>
func2(){
printf("Enter a key and then press enter: ");
int val;
printf("Select a fruit: \n");
printf("1: Apple\n");
printf("2: Orange\n");
printf("3: Banana\n");
printf("4: Pear\n");
scanf("%d",&val);
switch(val){
case 1:
printf("Apple. \n");
break;
case 2:
printf("Orange. \n");
break;
case 3:
printf("Banana. \n");
break;
case 4:
printf("Pear. \n");
break;
default: printf("Nothing selected.\n");
}
}
main(){
func2();
getchar();
}
Мы можем легко проверить его с помощью визуального режима radare2
Код:
| cmp eax, 2 |
| je 0x854;[gd] |
`----------------------------------------------------------'
| |
| '--------------------------------------------.
.----------------------' |
| |
| |
.--------------------. .---------------------------------------------.
| 0x82e ;[gg] | | 0x854 ;[gd] |
| cmp eax, 2 | | ; JMP XREF from 0x0000082c (sym.func2) |
| jg 0x83a;[gf] | | ; 0x9ac |
`--------------------' | ; "Orange. " |
| | | lea rdi, qword str.Orange. |
| | | call sym.imp.puts;[gb] |
| | | jmp 0x88a;[gp] |
| | `---------------------------------------------'
| | |
| '---------------------. |
.-------------------------------------' | |
| | '------------------.
| | |
| | |
.--------------------. .---------------------------------------------. |
| 0x833 ;[gi] | | 0x83a ;[gf] | |
| cmp eax, 1 | | ; JMP XREF from 0x00000831 (sym.func2) | |
| je 0x846;[gh] | | cmp eax, 3 | |
`--------------------' | je 0x862;[gl] | |
| | `---------------------------------------------' |
| | | | |
| '--. | | |
.------------------' | | | |
| | | '--------------. |
| | .------' | |
| | | | |
| | | | |
.--------------------. .---------------------------------------------. .--------------------. .---------------------------------------------. |
| 0x838 ;[gk] | | 0x846 ;[gh] | | 0x83f ;[gn] | | 0x862 ;[gl] | |
| jmp 0x87e;[gj] | | ; JMP XREF from 0x00000836 (sym.func2) | | cmp eax, 4 | | ; JMP XREF from 0x0000083d (sym.func2) | |
`--------------------' | ; 0x9a4 | | je 0x870;[gm] | | ; 0x9b5 | |
| | ; "Apple. " | `--------------------' | ; "Banana. " | |
| | lea rdi, qword str.Apple. | | | | lea rdi, qword str.Banana. | |
| | call sym.imp.puts;[gb] | | | | call sym.imp.puts;[gb] | |
| | jmp 0x88a;[gp] | | | | jmp 0x88a;[gp] | |
| `---------------------------------------------' | | `---------------------------------------------' |
Как вы можете видеть здесь, программа считывает выбор пользователя и затем сравнивает значение с одним из вариантов, если нет, то продолжает сравнивать, пока не будут оценены все условия. Вы должны быть в состоянии выполнить анализ
самостоятельно без каких-либо серьезных проблем.
Циклы While и For
Другой случай очень распространенной динамики, которую вы увидите почти в каждом бинарнике, - это циклы, обычно представленные операторами while и for. Код, расположенный внутри цикла while, будет продолжать циклическое выполнение до техпор, пока условие цикла истинно, код, расположенный внутри for, будет выполняться N раз.
C:
#include <stdio.h>
func2(){
int num;
printf("Enter a num, (exit with 0):");
scanf("%d", &num);
while(num != 0){
if(num > 0) printf("Positive num\n");
else printf("Negative num\n");
printf("Enter another num (exit with 0):");
scanf("%d", &num);
}
}
main(){
func2();
getchar();
}
Внутри radare2 функция func2 будет выглядеть следующим образом:
Код:
[0x7fba89d68090]> s sym.func2
[0x55b75b1ba7aa]> pdf
/ (fcn) sym.func2 170
| sym.func2 ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x55b75b1ba85d (sym.main)
| 0x55b75b1ba7aa 55 push rbp
| 0x55b75b1ba7ab 4889e5 mov rbp, rsp
| 0x55b75b1ba7ae 4883ec10 sub rsp, 0x10
| 0x55b75b1ba7b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x55b75b1ba7bb 488945f8 mov qword [local_8h], rax
| 0x55b75b1ba7bf 31c0 xor eax, eax
| 0x55b75b1ba7c1 488d3d300100. lea rdi, qword str.Enter_a_num___exit_with_0_: ; 0x55b75b1ba8f8 ; "Enter a num, (exit with 0):"
| 0x55b75b1ba7c8 b800000000 mov eax, 0
| 0x55b75b1ba7cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x55b75b1ba7d2 488d45f4 lea rax, qword [local_ch]
| 0x55b75b1ba7d6 4889c6 mov rsi, rax
| 0x55b75b1ba7d9 488d3d340100. lea rdi, qword [0x55b75b1ba914] ; "%d"
| 0x55b75b1ba7e0 b800000000 mov eax, 0
| 0x55b75b1ba7e5 e896feffff call sym.imp.__isoc99_scanf
| ,=< 0x55b75b1ba7ea eb4a jmp 0x55b75b1ba836
| .--> 0x55b75b1ba7ec 8b45f4 mov eax, dword [local_ch]
| :| 0x55b75b1ba7ef 85c0 test eax, eax
| ,===< 0x55b75b1ba7f1 7e0e jle 0x55b75b1ba801
| |:| 0x55b75b1ba7f3 488d3d1d0100. lea rdi, qword str.Positive_num ; 0x55b75b1ba917 ; "Positive num"
| |:| 0x55b75b1ba7fa e841feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x55b75b1ba7ff eb0c jmp 0x55b75b1ba80d
| |`---> 0x55b75b1ba801 488d3d1c0100. lea rdi, qword str.Negative_num ; 0x55b75b1ba924 ; "Negative num"
| | :| 0x55b75b1ba808 e833feffff call sym.imp.puts ; int puts(const char *s)
| | :| ; JMP XREF from 0x55b75b1ba7ff (sym.func2)
| `----> 0x55b75b1ba80d 488d3d240100. lea rdi, qword str.Enter_another_num__exit_with_0_: ; 0x55b75b1ba938 ; "Enter another num (exit with 0):"
| :| 0x55b75b1ba814 b800000000 mov eax, 0
| :| 0x55b75b1ba819 e842feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x55b75b1ba81e 488d45f4 lea rax, qword [local_ch]
| :| 0x55b75b1ba822 4889c6 mov rsi, rax
| :| 0x55b75b1ba825 488d3de80000. lea rdi, qword [0x55b75b1ba914] ; "%d"
| :| 0x55b75b1ba82c b800000000 mov eax, 0
| :| 0x55b75b1ba831 e84afeffff call sym.imp.__isoc99_scanf
| :| ; JMP XREF from 0x55b75b1ba7ea (sym.func2)
| :`-> 0x55b75b1ba836 8b45f4 mov eax, dword [local_ch]
| : 0x55b75b1ba839 85c0 test eax, eax
| `==< 0x55b75b1ba83b 75af jne 0x55b75b1ba7ec
| 0x55b75b1ba83d 90 nop
| 0x55b75b1ba83e 488b55f8 mov rdx, qword [local_8h]
| 0x55b75b1ba842 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x55b75b1ba84b 7405 je 0x55b75b1ba852
| | 0x55b75b1ba84d e8fefdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x55b75b1ba852 c9 leave
\ 0x55b75b1ba853 c3 ret
[0x55b75b1ba7aa]>
Как видите, логика программы здесь в общих чертах похожа на предыдущие блоки if-else. Разница лишь в том, что от инструкции, расположенной по адресу 0x55b75b1ba83b, вверх идет стрелка, которая отражает суть цикла. Давайте теперь пройдем шаг за шагом.
Прежде всего, программа устанавливает некоторое пространство и, как мы видим, использует пару переменных, одна из которых связана с защитой стека, а другая, вероятно, связана с пользовательским вводом.
Код:
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x55b75b1ba85d (sym.main)
| 0x55b75b1ba7aa 55 push rbp
| 0x55b75b1ba7ab 4889e5 mov rbp, rsp
| 0x55b75b1ba7ae 4883ec10 sub rsp, 0x10
| 0x55b75b1ba7b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x55b75b1ba7bb 488945f8 mov qword [local_8h], rax
| 0x55b75b1ba7bf 31c0 xor eax, eax
Затем программа запрашивает ввод пользователя и сохраняет его в переменной local_ch.
Код:
| 0x55b75b1ba7cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x55b75b1ba7d2 488d45f4 lea rax, qword [local_ch]
| 0x55b75b1ba7d6 4889c6 mov rsi, rax
| 0x55b75b1ba7d9 488d3d340100. lea rdi, qword [0x55b75b1ba914] ; "%d"
| 0x55b75b1ba7e0 b800000000 mov eax, 0
| 0x55b75b1ba7e5 e896feffff call sym.imp.__isoc99_scanf
Мы можем даже переименовать эту переменную, чтобы сделать все это более читабельным.
Код:
[0x55b75b1ba7aa]> afvn local_ch input
[0x55b75b1ba7aa]> afvn
local_8h
input
[0x55b75b1ba7aa]>
Давайте продолжим. Сразу после scanf мы выполняем прыжок в самом низу этого блока кода, вот так:
Код:
| ,=< 0x55b75b1ba7ea eb4a jmp 0x55b75b1ba836
| .--> 0x55b75b1ba7ec 8b45f4 mov eax, dword [input]
Если мы помним, условием выхода из цикла было равенство входного значения нулю, поэтому в данном случае, если входное значение не равно нулю, сработает jne и вернет нас в начало цикла.
Код:
| :`-> 0x55b75b1ba836 8b45f4 mov eax, dword [input]
| : 0x55b75b1ba839 85c0 test eax, eax
| `==< 0x55b75b1ba83b 75af jne 0x55b75b1ba7ec
Следующий блок кода? Мы уже знаем его
Код:
| .--> 0x55b75b1ba7ec 8b45f4 mov eax, dword [input]
| :| 0x55b75b1ba7ef 85c0 test eax, eax
| ,===< 0x55b75b1ba7f1 7e0e jle 0x55b75b1ba801
| |:| 0x55b75b1ba7f3 488d3d1d0100. lea rdi, qword str.Positive_num ; 0x55b75b1ba917 ; "Positive num"
| |:| 0x55b75b1ba7fa e841feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x55b75b1ba7ff eb0c jmp 0x55b75b1ba80d
| |`---> 0x55b75b1ba801 488d3d1c0100. lea rdi, qword str.Negative_num ; 0x55b75b1ba924 ; "Negative num"
| | :| 0x55b75b1ba808 e833feffff call sym.imp.puts ; int puts(const char *s)
| | :| ; JMP XREF from 0x55b75b1ba7ff (sym.func2)
Программа проверяет входные данные, положительные или отрицательные, выполняя тест и переход.
И сразу после этого она снова запрашивает входные данные и проверяет условие выхода в нижней части блока
Код:
| `----> 0x55b75b1ba80d 488d3d240100. lea rdi, qword str.Enter_another_num__exit_with_0_: ; 0x55b75b1ba938 ; "Enter another num (exit with 0):"
| :| 0x55b75b1ba814 b800000000 mov eax, 0
| :| 0x55b75b1ba819 e842feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x55b75b1ba81e 488d45f4 lea rax, qword [input]
| :| 0x55b75b1ba822 4889c6 mov rsi, rax
| :| 0x55b75b1ba825 488d3de80000. lea rdi, qword [0x55b75b1ba914] ; "%d"
| :| 0x55b75b1ba82c b800000000 mov eax, 0
| :| 0x55b75b1ba831 e84afeffff call sym.imp.__isoc99_scanf
| :| ; JMP XREF from 0x55b75b1ba7ea (sym.func2)
| :`-> 0x55b75b1ba836 8b45f4 mov eax, dword [input]
| : 0x55b75b1ba839 85c0 test eax, eax
| `==< 0x55b75b1ba83b 75af jne 0x55b75b1ba7ec
И это практически все.
Другой распространенный способ создания циклов - использование for, как показано здесь:
C:
#include <stdio.h>
func2(){
int counter = 0;
for(counter=1; counter <=10; counter++){
printf("%d ", counter);
}
}
main(){
func2();
getchar();
}
А дизазм radare2 будет выглядеть следующим образом:
Код:
[0x0000068a]> pdf
/ (fcn) sym.func2 59
| sym.func2 ();
| ; var int input @ rbp-0x4
| ; CALL XREF from 0x000006ce (sym.main)
| 0x0000068a 55 push rbp
| 0x0000068b 4889e5 mov rbp, rsp
| 0x0000068e 4883ec10 sub rsp, 0x10
| 0x00000692 c745fc000000. mov dword [input], 0
| 0x00000699 c745fc010000. mov dword [input], 1
| ,=< 0x000006a0 eb1a jmp 0x6bc
| | ; JMP XREF from 0x000006c0 (sym.func2)
| .--> 0x000006a2 8b45fc mov eax, dword [input]
| :| 0x000006a5 89c6 mov esi, eax
| :| 0x000006a7 488d3db60000. lea rdi, qword [0x00000764] ; "%d "
| :| 0x000006ae b800000000 mov eax, 0
| :| 0x000006b3 e898feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x000006b8 8345fc01 add dword [input], 1
| :| ; JMP XREF from 0x000006a0 (sym.func2)
| :`-> 0x000006bc 837dfc0a cmp dword [input], 0xa ; [0xa:4]=0
| `==< 0x000006c0 7ee0 jle 0x6a2
| 0x000006c2 90 nop
| 0x000006c3 c9 leave
\ 0x000006c4 c3 ret
[0x0000068a]>
Обратите внимание, что мы не видим здесь никакой защиты стека, можете сказать почему? Наверное, потому что мы не получаем здесь никаких входных данных!
Итак, программа начинается с установки значения в ноль (счетчик = 0), затем она устанавливает значение в 1, чтобы запустить процесс for
Код:
| 0x00000692 c745fc000000. mov dword [input], 0
| 0x00000699 c745fc010000. mov dword [input], 1
| ,=< 0x000006a0 eb1a jmp 0x6bc
Затем он сравнивает значение с 10 (0xA).
Код:
| :`-> 0x000006bc 837dfc0a cmp dword [input], 0xa ; [0xa:4]=0
| `==< 0x000006c0 7ee0 jle 0x6a2
И когда значение меньше его, он возвращается в начало, чтобы выполнить код цикла.
Код в основном печатает значение переменной и прибавляет к нему 1
Код:
| .--> 0x000006a2 8b45fc mov eax, dword [input]
| :| 0x000006a5 89c6 mov esi, eax
| :| 0x000006a7 488d3db60000. lea rdi, qword [0x00000764] ; "%d "
| :| 0x000006ae b800000000 mov eax, 0
| :| 0x000006b3 e898feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x000006b8 8345fc01 add dword [input], 1
Легко, да?
Ну, это все, о чем я хотел поговорить здесь. В следующей части мы начнем изучать некоторые структуры данных, такие как массивы и матрицы.
Источник:
Ссылка скрыта от гостей
Последнее редактирование модератором: