Ку, киберрекруты. Продолжаю курс реверса и использованием radare2. Сегодня мы рассмотрим некоторые простые структуры данных, такие как переменные, поймем, как работают основные условные структуры кода внутри, а также научимся отлаживать с помощью radare2.
Первая часть
Переменные
Код
Что касается числовых переменных, то первая представляет собой int, вторая - float, третья - double, а также char 'a'. В конце эти значения складываются и результат выводится на печать. Мы видим, как первая переменная хранится непосредственно в регистре (общего назначения), вторая тоже хранится в памяти, но в другом месте, а третья работает так же, за исключением того, что ей требуется в 2 раза больше места, чем второй (double).
Как всегда, мы можем скомпилировать код с помощью gcc, ничего сложного.
Бинарь
Мы открываем двоичный файл и анализируем его содержимое, aaa должен работать нормально.
Поскольку мы компилировали двоичный файл с помощью GCC, мы можем определить некоторые "типичные" функции, связанные с инициализацией программы.
Единственная интересная функция здесь - main, поскольку она явно принадлежит к главной функции программы. Отсюда мы также можем определить, что в программе используется printf.
Просматривая основной код программы, мы видим здесь несколько новых инструкций, таких как fstp, fild, fadd и т.д. Путем чистой дедукции мы можем утверждать, что они могут быть связаны с операциями с плавающей точкой, поскольку здесь используются float и double. Мы также можем определить, как вызывается функция print, мы видим, как некоторые параметры заталкиваются в стек.
Поскольку здесь мы имеем дело с переменными, одна из вещей, которую мы можем захотеть сделать, это посмотреть, как radare2 идентифицирует переменные и, возможно, дать им красивое имя. Мы можем сделать это с помощью afv.
Поскольку мы явно идентифицируем char с radare2 (var char var_1...), мы можем переименовать эту переменную в char
Интересна переменная char. При использовании переменных char происходит шестнадцатеричное кодирование символов. В шестнадцатеричной кодировке 'a' соответствует 61 в таблице ascii. В нашем примере мы просто видим, как программа использует mov для перемещения байта 0x61 в позицию переменной.
Теперь, когда мы определили переменную char и уже должны быть в состоянии определить переменную int, давайте посмотрим, как программа работает с переменными с плавающей точкой.
Лучший способ проверить это - запустить программу в режиме отладки. В radare2 мы можем открыть программу в режиме отладки, используя опцию -d.
В режиме отладки мы можем использовать такие команды, как db memaddress для установки точки останова, dc для продолжения потока выполнения до этой/этих точек останова и dt для выполнения текущей инструкции и перехода к следующей сразу после нее.
В нашей программе мы можем установить несколько интересных точек перед fldz, flstp и т.п.
После достижения первой точки останова мы переходим к следующему пункту:
Мы можем определить, что значение 3 было перемещено в var_1ch, а затем выполняется какая-то странная инструкция fld dword. Инструкция fld загружает в стек 32-битное, 64-битное или 80-битное значение с плавающей точкой. Эта инструкция преобразует 32- и 64-битный операнд в 80-битное значение расширенной точности, прежде чем поместить значение в стек с плавающей точкой. Таким образом, если мы проверим, какое значение fld берет для загрузки, мы увидим что-то вроде:
0000 9040 0000 0000 0000 Теперь мы можем попытаться просмотреть содержимое той позиции в памяти, которая явно соответствует переменной
Мы можем сделать вывод немного более приятным и человекочитаемым (для нашего случая), добавив w и таким образом запустив pxw, когда w приходит из слова (два байта)
0x40900000 должно быть значением. Но это значение дает нам мало информации, по крайней мере, в этом формате. Поскольку мы подозреваем, что это число представляет собой закодированное число с плавающей точкой, мы можем опробовать использовать инструмент rax2 для его чтения.
И мы можем четко видеть, как это число соответствует значению нашей первой переменной с плавающей точкой.
Мы можем проделать то же самое со второй переменной, я оставлю это на ваше усмотрение, вы найдете значение 5.25, но так как это double значение, вместо одного слова у нас будет размер 32.
Наконец, мы видим, как эти параметры попадают в стек. Строка "The sum of..." заталкивается непосредственно в стек, просто выполнив команду push, а переменная sum вставляется в стек с помощью инструкции fstp. Инструкция FST копирует значение в регистре ST(0) в операндназначения, который может быть ячейкой памяти или другим регистром в стеке регистров FPU. При сохранении значения в памяти оно преобразуется в одно- или двухреальный формат.
Мы видим, как значение sum приходит из ebp-0x14 в результате предыдущей операции:
И это четко соответствует реальному результату:
И затем он появляется в стеке вместе с адресом, указывающим на строку, которая будет напечатана
заметьте, что здесь используется little endian и 0x4029800000000000 = 12.75 как double = sum
Условные структуры
Код
Мы начнем со следующего фрагмента кода. Он объявит переменную как целое знаковое число, затем прочитает введенные пользователем данные и сохранит их в этой переменной. После этого он проверит, является ли int положительным или отрицательным, и выведет сообщение.
Бинарь
Начнем, как обычно, с запуска программы с r2 -d, анализируя ее, переключимся на главную функцию и запустим pdf, чтобы заглянуть в нее:
Итак, здесь мы можем увидеть стрелки, движущиеся вверх и вниз по коду. Эти стрелки указывают на поток выполнения, которому может следовать программа в зависимости от определенных условий, поскольку у нас уже есть код, мы можем предположить, что эти направления потока каким-то образом связаны с частью кода IF-ELSE. Мы также видим, что в верхней части кода объявлены некоторые переменные.
Следующее, что может привлечь наше внимание - это вызов функции SCANF. Давайте рассмотрим его, установив несколько точек останова с помощью команды db:
Итак, после вызова PRINTF происходит некоторая корректировка стека, после чего мы видим, что адрес одной из переменных, которые мы видели в верхней части кода, загружается в eax (можно назвать ее как-нибудь вроде input с помощью afvn из чисто логических соображений), затем eax выталкивается в стек для передачи в качестве параметра (можно догадаться) функции SCANF, также в стек выталкивается адрес памяти, в данном случае мы видим, что этот адрес содержит строку ("please enter...:").
Если мы переместимся после первой точки останова, то сможем осмотреть регистры и увидеть, как загружается адрес:
Затем мы можем перейти к следующей точке останова и осмотреть стек, мы увидим, как там появляются эти два параметра:
Затем, если мы попадем в следующую точку останова еще раз, программа запросит значение, а затем попадет в последнюю точку останова:
Если мы проверим регистры, то увидим, что EAX установлен в значение 0x00000001, что означает, что вызов SCANF был выполнен правильно без каких-либо серьезных проблем.
Теперь мы можем посмотреть адрес переменной input и увидеть, как число (10 в нашем случае) хранится там в шестнадцатеричном формате.
Мы даже можем убедиться в этом, используя rax2 для преобразования в десятичную систему счисления.
В этой точке программы мы имеем правильно сохраненное входное значение, после чего программа должна будет проверить, является ли это значение положительным или отрицательным, и в зависимости от этого переместить выполнение в то или иное место. Поэтому мы можем установить еще одну точку останова прямо перед первой стрелкой, связанной с потоком выполнения. Поскольку стрелки могут появляться не всегда (это всегда зависит от используемого вами дизасма/фреймворка), другой способ обнаружить точки переключения в потоке выполнения - искать JLE, JE и подобные им инструкции.
Итак, мы видим, что значение 10, в десятичной системе, загружается в EAX, а затем выполняется тестовая инструкция. После выполнения тестовой инструкции состояние регистров флагов следующее:
По сути, инструкция TEST устанавливает флаги ZF(флаг нуля) и SF(флаг знака) на основе логического И (AND) между операндами и очищает флаг OF(флаг переполнения). Так что в данном случае эти флаги будут установлены в ноль. Тогда инструкция JLE означает JUMP (на адрес памяти, который указан сразу после) Если меньше чем и Меньше чем определяется как: ZF=1 или SF != OF
Таким образом, мы видим, что если мы перезагрузим программу и введем отрицательное число, то флаг SF активизируется:
После этого программа перейдет к адресу памяти, который напечатает "положительный" адрес или к другому, печатающему "отрицательный", затем перейдет или перепрыгнет (с помощью jmp) в конец программы, и таким образом программа завершится.
64-битная версия
64-битная версия практически такая же, можете отметить различия? В основном регистры (esi,rdi...) используются для передачи параметров вместо стека! Кроме того, адреса памяти 64-битные.
Успехов
Источник:
Первая часть
Переменные
Код
Что касается числовых переменных, то первая представляет собой int, вторая - float, третья - double, а также char 'a'. В конце эти значения складываются и результат выводится на печать. Мы видим, как первая переменная хранится непосредственно в регистре (общего назначения), вторая тоже хранится в памяти, но в другом месте, а третья работает так же, за исключением того, что ей требуется в 2 раза больше места, чем второй (double).
C:
#include <stdio.h>
int main() {
char ab ='a';
int a = 3;
float b = 4.5;
double c = 5.25;
float sum;
sum = a+b+c;
printf("The sum of a, b, and c is %f.", sum);
return 0;
}
Как всегда, мы можем скомпилировать код с помощью gcc, ничего сложного.
Бинарь
Мы открываем двоичный файл и анализируем его содержимое, aaa должен работать нормально.
Код:
-- sudo make me a pancake
[0x08048310]> aaa
[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.
[0x08048310]>
Поскольку мы компилировали двоичный файл с помощью GCC, мы можем определить некоторые "типичные" функции, связанные с инициализацией программы.
Код:
[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
0x080484f0 1 2 sym.__libc_csu_fini
0x08048340 1 4 sym.__x86.get_pc_thunk.bx
0x080484f4 1 20 sym._fini
0x08048490 4 93 sym.__libc_csu_init
0x0804840b 1 123 main
0x080482e0 1 6 sym.imp.printf
0x080482ac 3 35 sym._init
[0x08048310]>
Единственная интересная функция здесь - main, поскольку она явно принадлежит к главной функции программы. Отсюда мы также можем определить, что в программе используется printf.
Код:
[0x08048310]> sf main
[0x0804840b]> pdf
; DATA XREF from entry0 @ 0x8048327
┌ 123: int main (int32_t arg_4h, char **argv, char **envp);
│ ; var char var_1dh @ ebp-0x1d
│ ; var int32_t var_1ch @ ebp-0x1c
│ ; var int32_t var_4h @ ebp-0x4
│ ; arg int32_t arg_4h @ esp+0x4c
│ 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 83ec34 sub esp, 0x34
│ 0x0804841c c645e361 mov byte [var_1dh], 0x61 ; 'a' ; 97
│ 0x08048420 c745e4030000. mov dword [var_1ch], 3
│ 0x08048427 d9054c850408 fld dword [0x804854c]
│ 0x0804842d d95de8 fstp dword [ebp - 0x18]
│ 0x08048430 dd0550850408 fld qword [0x8048550]
│ 0x08048436 dd5df0 fstp qword [ebp - 0x10]
│ 0x08048439 d9ee fldz
│ 0x0804843b d95dec fstp dword [ebp - 0x14]
│ 0x0804843e db45e4 fild dword [var_1ch]
│ 0x08048441 d845e8 fadd dword [ebp - 0x18]
│ 0x08048444 dc45f0 fadd qword [ebp - 0x10]
│ 0x08048447 d95dec fstp dword [ebp - 0x14]
│ 0x0804844a d945ec fld dword [ebp - 0x14]
│ 0x0804844d 83ec04 sub esp, 4
│ 0x08048450 8d6424f8 lea esp, [esp - 8]
│ 0x08048454 dd1c24 fstp qword [esp]
│ 0x08048457 6810850408 push str.The_sum_of_a__b__and_c_is__f. ; 0x8048510 ; "The sum of a, b, and c is %f." ; const char *format
│ 0x0804845c e87ffeffff call sym.imp.printf ; int printf(const char *format)
│ 0x08048461 83c410 add esp, 0x10
│ 0x08048464 0fbe45e3 movsx eax, byte [var_1dh]
│ 0x08048468 83ec08 sub esp, 8
│ 0x0804846b 50 push eax
│ 0x0804846c 682e850408 push str.The_value_of_char_ab_is__c. ; 0x804852e ; "The value of char ab is %c." ; const char *format
│ 0x08048471 e86afeffff call sym.imp.printf ; int printf(const char *format)
│ 0x08048476 83c410 add esp, 0x10
│ 0x08048479 b800000000 mov eax, 0
│ 0x0804847e 8b4dfc mov ecx, dword [var_4h]
│ 0x08048481 c9 leave
│ 0x08048482 8d61fc lea esp, [ecx - 4]
└ 0x08048485 c3 ret
[0x0804840b]>
Просматривая основной код программы, мы видим здесь несколько новых инструкций, таких как fstp, fild, fadd и т.д. Путем чистой дедукции мы можем утверждать, что они могут быть связаны с операциями с плавающей точкой, поскольку здесь используются float и double. Мы также можем определить, как вызывается функция print, мы видим, как некоторые параметры заталкиваются в стек.
Поскольку здесь мы имеем дело с переменными, одна из вещей, которую мы можем захотеть сделать, это посмотреть, как radare2 идентифицирует переменные и, возможно, дать им красивое имя. Мы можем сделать это с помощью afv.
Код:
[0x0804840b]> afv
arg int32_t arg_4h @ esp+0x4c
var char var_1dh @ ebp-0x1d
var int32_t var_1ch @ ebp-0x1c
var int32_t var_4h @ ebp-0x4
[0x0804840b]>
Поскольку мы явно идентифицируем char с radare2 (var char var_1...), мы можем переименовать эту переменную в char
Код:
[0x0804840b]> afvn char1 var_1dh
[0x0804840b]> afvn
char1
var_1ch
var_4h
arg_4h
[0x0804840b]>
Интересна переменная char. При использовании переменных char происходит шестнадцатеричное кодирование символов. В шестнадцатеричной кодировке 'a' соответствует 61 в таблице ascii. В нашем примере мы просто видим, как программа использует mov для перемещения байта 0x61 в позицию переменной.
Код:
0x0804841c c645e361 mov byte [var_1dh], 0x61 ; 'a' ; 97
0x08048420 c745e4030000. mov dword [var_1ch], 3
Теперь, когда мы определили переменную char и уже должны быть в состоянии определить переменную int, давайте посмотрим, как программа работает с переменными с плавающей точкой.
Лучший способ проверить это - запустить программу в режиме отладки. В radare2 мы можем открыть программу в режиме отладки, используя опцию -d.
В режиме отладки мы можем использовать такие команды, как db memaddress для установки точки останова, dc для продолжения потока выполнения до этой/этих точек останова и dt для выполнения текущей инструкции и перехода к следующей сразу после нее.
В нашей программе мы можем установить несколько интересных точек перед fldz, flstp и т.п.
Код:
[0x0804840b]> db 0x08048427
[0x0804840b]> db 0x0804842d
[0x0804840b]> db 0x08048430
[0x0804840b]> dc
hit breakpoint at: 8048427
[0x08048427]>
После достижения первой точки останова мы переходим к следующему пункту:
Код:
│ 0x08048420 c745e4030000. mov dword [var_1ch], 3
│ 0x08048427 d9054c850408 fld dword [0x804854c]
│ 0x0804842d d95de8 fstp dword [ebp - 0x18]
Мы можем определить, что значение 3 было перемещено в var_1ch, а затем выполняется какая-то странная инструкция fld dword. Инструкция fld загружает в стек 32-битное, 64-битное или 80-битное значение с плавающей точкой. Эта инструкция преобразует 32- и 64-битный операнд в 80-битное значение расширенной точности, прежде чем поместить значение в стек с плавающей точкой. Таким образом, если мы проверим, какое значение fld берет для загрузки, мы увидим что-то вроде:
Код:
[0x08048427]> px 32 @ 0x804854c
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x0804854c 0000 9040 0000 0000 0000 1540 011b 033b ...@.......@...;
0x0804855c 2800 0000 0400 0000 78fd ffff 4400 0000 (.......x...D...
[0x08048427]>
0000 9040 0000 0000 0000 Теперь мы можем попытаться просмотреть содержимое той позиции в памяти, которая явно соответствует переменной
Код:
[0x08048430]> px @ ebp-0x18
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0xbfe7a7e0 0000 9040 a4a8 e7bf aca8 e7bf b184 0408 ...@............
0xbfe7a7f0 dc93 f6b7 10a8 e7bf 0000 0000 37f6 dcb7 ............7...
0xbfe7a800 0090 f6b7 0090 f6b7 0000 0000 37f6 dcb7 ............7...
Мы можем сделать вывод немного более приятным и человекочитаемым (для нашего случая), добавив w и таким образом запустив pxw, когда w приходит из слова (два байта)
Код:
[0x08048430]> pxw @ ebp-0x18
0xbfe7a7e0 0x40900000 0xbfe7a8a4 0xbfe7a8ac 0x080484b1 ...@............
0xbfe7a7f0 0xb7f693dc 0xbfe7a810 0x00000000 0xb7dcf637 ............7...
0x40900000 должно быть значением. Но это значение дает нам мало информации, по крайней мере, в этом формате. Поскольку мы подозреваем, что это число представляет собой закодированное число с плавающей точкой, мы можем опробовать использовать инструмент rax2 для его чтения.
Код:
[0xbfe7a7f0]> rax2 Fx40900000
4.500000f
И мы можем четко видеть, как это число соответствует значению нашей первой переменной с плавающей точкой.
Мы можем проделать то же самое со второй переменной, я оставлю это на ваше усмотрение, вы найдете значение 5.25, но так как это double значение, вместо одного слова у нас будет размер 32.
Наконец, мы видим, как эти параметры попадают в стек. Строка "The sum of..." заталкивается непосредственно в стек, просто выполнив команду push, а переменная sum вставляется в стек с помощью инструкции fstp. Инструкция FST копирует значение в регистре ST(0) в операндназначения, который может быть ячейкой памяти или другим регистром в стеке регистров FPU. При сохранении значения в памяти оно преобразуется в одно- или двухреальный формат.
Код:
| 0x08048447 d95dec fstp dword [ebp - 0x14]
│ 0x0804844a d945ec fld dword [ebp - 0x14]
│ 0x0804844d 83ec04 sub esp, 4
│ 0x08048450 8d6424f8 lea esp, [esp - 8]
│ 0x08048454 dd1c24 fstp qword [esp]
│ 0x08048457 6810850408 push str.The_sum_of_a__b__and_c_is__f. ; 0x8048510 ; "The sum of a, b, and c is %f." ; const char *format
│ 0x0804845c e87ffeffff call sym.imp.printf ; int printf(const char *format)
Мы видим, как значение sum приходит из ebp-0x14 в результате предыдущей операции:
Код:
[0x08048444]> pxw @ ebp-0x14
0xbf8e0ba4 0x00000000 0x00000000 0x40150000 0xb7f573dc ...........@.s..
И это четко соответствует реальному результату:
Код:
[0x0804844d]> rax2 Fx414c0000
12.750000f (as float)
И затем он появляется в стеке вместе с адресом, указывающим на строку, которая будет напечатана
Код:
[0x0804845c]> pxw @ esp
0xbf8e0b70 0x08048510 0x00000000 0x40298000 0xb7f711b0 ..........)@....
0xbf8e0b80 0x00008000 0xb7f57000 0xb7f55244 0xb7dbd0ec .....p..DR......
0xbf8e0b90 0x00000001 0x00000000 0x61dd3a50 0x00000003 ........P:.a....
заметьте, что здесь используется little endian и 0x4029800000000000 = 12.75 как double = sum
Условные структуры
Код
Мы начнем со следующего фрагмента кода. Он объявит переменную как целое знаковое число, затем прочитает введенные пользователем данные и сохранит их в этой переменной. После этого он проверит, является ли int положительным или отрицательным, и выведет сообщение.
C:
#include <stdio.h>
int main() {
signed int number;
printf("Enter an integer: ");
scanf("%d", &number);
if (number > 0) {
printf("You entered %d.\n", number);
}
else{
printf("You entered a negative number %d.\n", number);
}
printf("The if statement is easy.");
return 0;
Бинарь
Начнем, как обычно, с запуска программы с r2 -d, анализируя ее, переключимся на главную функцию и запустим pdf, чтобы заглянуть в нее:
Код:
[0x080484bb]> pdf
; DATA XREF from entry0 @ 0x80483d7
┌ 159: int main (int argc, char **argv, char **envp);
│ ; var int32_t var_10h @ ebp-0x10
│ ; var int32_t var_ch @ ebp-0xc
│ ; var int32_t var_4h @ ebp-0x4
│ ; arg int32_t arg_4h @ esp+0x34
│ 0x080484bb 8d4c2404 lea ecx, [arg_4h]
│ 0x080484bf 83e4f0 and esp, 0xfffffff0
│ 0x080484c2 ff71fc push dword [ecx - 4]
│ 0x080484c5 55 push ebp
│ 0x080484c6 89e5 mov ebp, esp
│ 0x080484c8 51 push ecx
│ 0x080484c9 83ec14 sub esp, 0x14
│ 0x080484cc 65a114000000 mov eax, dword gs:[0x14]
│ 0x080484d2 8945f4 mov dword [var_ch], eax
│ 0x080484d5 31c0 xor eax, eax
│ 0x080484d7 83ec0c sub esp, 0xc
│ 0x080484da 68e0850408 push str.Enter_an_integer: ; 0x80485e0 ; "Enter an integer: "
│ 0x080484df e88cfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x080484e4 83c410 add esp, 0x10
│ 0x080484e7 83ec08 sub esp, 8
│ 0x080484ea 8d45f0 lea eax, [var_10h]
│ 0x080484ed 50 push eax
│ 0x080484ee 68f3850408 push 0x80485f3
│ 0x080484f3 e8a8feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x080484f8 83c410 add esp, 0x10
│ 0x080484fb 8b45f0 mov eax, dword [var_10h]
│ 0x080484fe 85c0 test eax, eax
│ ┌─< 0x08048500 7e16 jle 0x8048518
│ │ 0x08048502 8b45f0 mov eax, dword [var_10h]
│ │ 0x08048505 83ec08 sub esp, 8
│ │ 0x08048508 50 push eax
│ │ 0x08048509 68f6850408 push str.You_entered__d. ; 0x80485f6 ; "You entered %d.\n"
│ │ 0x0804850e e85dfeffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x08048513 83c410 add esp, 0x10
│ ┌──< 0x08048516 eb14 jmp 0x804852c
│ │└─> 0x08048518 8b45f0 mov eax, dword [var_10h]
│ │ 0x0804851b 83ec08 sub esp, 8
│ │ 0x0804851e 50 push eax
│ │ 0x0804851f 6808860408 push str.You_entered_a_negative_number__d. ; 0x8048608 ; "You entered a negative number %d.\n"
│ │ 0x08048524 e847feffff call sym.imp.printf ; int printf(const char *format)
│ │ 0x08048529 83c410 add esp, 0x10
│ │ ; CODE XREF from main @ 0x8048516
│ └──> 0x0804852c 83ec0c sub esp, 0xc
│ 0x0804852f 682b860408 push str.The_if_statement_is_easy. ; 0x804862b ; "The if statement is easy."
│ 0x08048534 e837feffff call sym.imp.printf ; int printf(const char *format)
│ 0x08048539 83c410 add esp, 0x10
│ 0x0804853c b800000000 mov eax, 0
│ 0x08048541 8b55f4 mov edx, dword [var_ch]
│ 0x08048544 653315140000. xor edx, dword gs:[0x14]
│ ┌─< 0x0804854b 7405 je 0x8048552
│ │ 0x0804854d e82efeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x08048552 8b4dfc mov ecx, dword [var_4h]
│ 0x08048555 c9 leave
│ 0x08048556 8d61fc lea esp, [ecx - 4]
└ 0x08048559 c3 ret
Итак, здесь мы можем увидеть стрелки, движущиеся вверх и вниз по коду. Эти стрелки указывают на поток выполнения, которому может следовать программа в зависимости от определенных условий, поскольку у нас уже есть код, мы можем предположить, что эти направления потока каким-то образом связаны с частью кода IF-ELSE. Мы также видим, что в верхней части кода объявлены некоторые переменные.
Следующее, что может привлечь наше внимание - это вызов функции SCANF. Давайте рассмотрим его, установив несколько точек останова с помощью команды db:
Код:
│ 0x080484df e88cfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x080484e4 83c410 add esp, 0x10
│ 0x080484e7 83ec08 sub esp, 8
│ 0x080484ea b 8d45f0 lea eax, [input]
│ 0x080484ed 50 push eax
│ 0x080484ee 68f3850408 push 0x80485f3
│ 0x080484f3 b e8a8feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x080484f8 b 83c410 add esp, 0x10
│ 0x080484fb 8b45f0 mov eax, dword [input]
│ 0x080484fe 85c0 test eax, eax
Итак, после вызова PRINTF происходит некоторая корректировка стека, после чего мы видим, что адрес одной из переменных, которые мы видели в верхней части кода, загружается в eax (можно назвать ее как-нибудь вроде input с помощью afvn из чисто логических соображений), затем eax выталкивается в стек для передачи в качестве параметра (можно догадаться) функции SCANF, также в стек выталкивается адрес памяти, в данном случае мы видим, что этот адрес содержит строку ("please enter...:").
Если мы переместимся после первой точки останова, то сможем осмотреть регистры и увидеть, как загружается адрес:
Код:
[0x080484ed]> dr
eax = 0xbfa9a688
ebx = 0x00000000
ecx = 0x0910b01a
edx = 0xb7f0c870
esi = 0xb7f0b000
edi = 0xb7f0b000
esp = 0xbfa9a678
ebp = 0xbfa9a698
eip = 0x080484ed
eflags = 0x00000296
oeax = 0xffffffff
Затем мы можем перейти к следующей точке останова и осмотреть стек, мы увидим, как там появляются эти два параметра:
Код:
[0x080484ed]> dc
hit breakpoint at: 80484f3
[0x080484f3]> pxr @ esp
0xbfa9a670 0x080485f3 .... @esp (/home/lab/c_examples/bin/ifelse) (.rodata) program R X 'and eax, 0x6f590064' 'ifelse' (%d)
0xbfa9a674 0xbfa9a688 .... ([stack]) stack R W 0xbfa9a74c --> ([stack]) stack R W 0xbfa9c288 --> ([stack]) stack R W 0x5f474458 (XDG_VTNR=7) --> ascii ('X')
0xbfa9a678 0xb7d87a50 Pz.. (/lib/i386-linux-gnu/libc-2.23.so) library R X 'add ebx, 0x1835b0' 'libc-2.23.so'
0xbfa9a67c 0x080485ab .... (/home/lab/c_examples/bin/ifelse) (.text) sym.__libc_csu_init program R X 'add edi, 1' 'ifelse'
0xbfa9a680 0x00000001 .... 1 (.comment)
0xbfa9a684 0xbfa9a744 D... ([stack]) stack R W 0xbfa9c27f --> ([stack]) stack R W 0x66692f2e (./ifelse) --> ascii ('.')
Затем, если мы попадем в следующую точку останова еще раз, программа запросит значение, а затем попадет в последнюю точку останова:
Код:
[0x080484f3]> dc
Enter an integer: 10
hit breakpoint at: 80484f8
[0x080484f8]>
Если мы проверим регистры, то увидим, что EAX установлен в значение 0x00000001, что означает, что вызов SCANF был выполнен правильно без каких-либо серьезных проблем.
Код:
[0x080484f8]> dr
eax = 0x00000001
ebx = 0x00000000
ecx = 0x00000001
edx = 0xb7f0c87c
esi = 0xb7f0b000
edi = 0xb7f0b000
esp = 0xbfa9a670
ebp = 0xbfa9a698
eip = 0x080484f8
eflags = 0x00000246
oeax = 0xffffffff
[0x080484f8]>
Теперь мы можем посмотреть адрес переменной input и увидеть, как число (10 в нашем случае) хранится там в шестнадцатеричном формате.
Код:
[0x080484f8]> pxw @ 0xbfa9a688
0xbfa9a688 0x0000000a 0x630f3300 0xb7f0b3dc 0xbfa9a6b0 .....3.c........
Мы даже можем убедиться в этом, используя rax2 для преобразования в десятичную систему счисления.
Код:
[0x080484f8]> rax2 0xa
10
В этой точке программы мы имеем правильно сохраненное входное значение, после чего программа должна будет проверить, является ли это значение положительным или отрицательным, и в зависимости от этого переместить выполнение в то или иное место. Поэтому мы можем установить еще одну точку останова прямо перед первой стрелкой, связанной с потоком выполнения. Поскольку стрелки могут появляться не всегда (это всегда зависит от используемого вами дизасма/фреймворка), другой способ обнаружить точки переключения в потоке выполнения - искать JLE, JE и подобные им инструкции.
Код:
[0x080484f8]>
│ 0x080484f8 b 83c410 add esp, 0x10
│ 0x080484fb 8b45f0 mov eax, dword [input]
│ 0x080484fe 85c0 test eax, eax
│ ┌─< 0x08048500 7e16 jle 0x8048518
│ │ 0x08048502 8b45f0 mov eax, dword [input]
│ │ 0x08048505 83ec08 sub esp, 8
Итак, мы видим, что значение 10, в десятичной системе, загружается в EAX, а затем выполняется тестовая инструкция. После выполнения тестовой инструкции состояние регистров флагов следующее:
Код:
[0x08048500]> dr 1
cf = 0x00000000
pf = 0x00000001
af = 0x00000000
zf = 0x00000000
sf = 0x00000000
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
По сути, инструкция TEST устанавливает флаги ZF(флаг нуля) и SF(флаг знака) на основе логического И (AND) между операндами и очищает флаг OF(флаг переполнения). Так что в данном случае эти флаги будут установлены в ноль. Тогда инструкция JLE означает JUMP (на адрес памяти, который указан сразу после) Если меньше чем и Меньше чем определяется как: ZF=1 или SF != OF
Таким образом, мы видим, что если мы перезагрузим программу и введем отрицательное число, то флаг SF активизируется:
Код:
[0x080484bb]> db 0x08048500
[0x080484bb]> dc
Enter an integer: -20
hit breakpoint at: 8048500
[0x08048500]> dr 1
cf = 0x00000000
pf = 0x00000000
af = 0x00000000
zf = 0x00000000
sf = 0x00000001
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x08048500]>
And one more time, as we enter a positive number the SF flag will not be activated.
[0x080484bb]> db 0x08048500
[0x080484bb]> dc
Enter an integer: 20
hit breakpoint at: 8048500
[0x08048500]> dr 1
cf = 0x00000000
pf = 0x00000001
af = 0x00000000
zf = 0x00000000
sf = 0x00000000
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x08048500]>
После этого программа перейдет к адресу памяти, который напечатает "положительный" адрес или к другому, печатающему "отрицательный", затем перейдет или перепрыгнет (с помощью jmp) в конец программы, и таким образом программа завершится.
64-битная версия
64-битная версия практически такая же, можете отметить различия? В основном регистры (esi,rdi...) используются для передачи параметров вместо стека! Кроме того, адреса памяти 64-битные.
Код:
[0x7f584b7b9090]> sf main
[0x561302d2871a]> pdf
;-- main:
/ (fcn) main 161
| main ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; DATA XREF from 0x561302d2862d (entry0)
| 0x561302d2871a 55 push rbp
| 0x561302d2871b 4889e5 mov rbp, rsp
| 0x561302d2871e 4883ec10 sub rsp, 0x10
| 0x561302d28722 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x561302d2872b 488945f8 mov qword [local_8h], rax
| 0x561302d2872f 31c0 xor eax, eax
| 0x561302d28731 488d3d100100. lea rdi, qword str.Enter_an_integer: ; 0x561302d28848 ; "Enter an integer: "
| 0x561302d28738 b800000000 mov eax, 0
| 0x561302d2873d e89efeffff call sym.imp.printf ; int printf(const char *format)
| 0x561302d28742 488d45f4 lea rax, qword [local_ch]
| 0x561302d28746 4889c6 mov rsi, rax
| 0x561302d28749 488d3d0b0100. lea rdi, qword [0x561302d2885b] ; "%d"
| 0x561302d28750 b800000000 mov eax, 0
| 0x561302d28755 e896feffff call sym.imp.__isoc99_scanf
| 0x561302d2875a 8b45f4 mov eax, dword [local_ch]
| 0x561302d2875d 85c0 test eax, eax
| ,=< 0x561302d2875f 7e18 jle 0x561302d28779
| | 0x561302d28761 8b45f4 mov eax, dword [local_ch]
| | 0x561302d28764 89c6 mov esi, eax
| | 0x561302d28766 488d3df10000. lea rdi, qword str.You_entered__d. ; 0x561302d2885e ; "You entered %d.\n"
| | 0x561302d2876d b800000000 mov eax, 0
| | 0x561302d28772 e869feffff call sym.imp.printf ; int printf(const char *format)
| ,==< 0x561302d28777 eb16 jmp 0x561302d2878f
| |`-> 0x561302d28779 8b45f4 mov eax, dword [local_ch]
| | 0x561302d2877c 89c6 mov esi, eax
| | 0x561302d2877e 488d3deb0000. lea rdi, qword str.You_entered_a_negative_number__d. ; 0x561302d28870 ; "You entered a negative number %d.\n"
| | 0x561302d28785 b800000000 mov eax, 0
| | 0x561302d2878a e851feffff call sym.imp.printf ; int printf(const char *format)
| | ; JMP XREF from 0x561302d28777 (main)
| `--> 0x561302d2878f 488d3dfd0000. lea rdi, qword str.The_if_statement_is_easy. ; 0x561302d28893 ; "The if statement is easy."
| 0x561302d28796 b800000000 mov eax, 0
| 0x561302d2879b e840feffff call sym.imp.printf ; int printf(const char *format)
| 0x561302d287a0 b800000000 mov eax, 0
| 0x561302d287a5 488b55f8 mov rdx, qword [local_8h]
| 0x561302d287a9 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x561302d287b2 7405 je 0x561302d287b9
| | 0x561302d287b4 e817feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x561302d287b9 c9 leave
\ 0x561302d287ba c3 ret
[0x561302d2871a]>
Успехов
Источник:
Ссылка скрыта от гостей
Последнее редактирование модератором: