Се­год­ня мы с тобой научим­ся находить любые типы цик­лов в коде реаль­ных прог­рамм на язы­ках высоко­го уров­ня — пос­тро­енных с опти­миза­цией и без нее. Это шаг, с которым ты стол­кнешь­ся при обратной раз­работ­ке при­ложе­ний. При этом опре­деле­ние цик­ла — это толь­ко пол­дела. Далее нуж­но разоб­рать­ся в его механиз­ме, выделить исполь­зуемые дан­ные и кор­рек­тно деком­пилиро­вать, прев­ратив в строй­ный код на язы­ке высоко­го уров­ня. Без это­го точ­ное понима­ние алго­рит­ма прог­раммы невоз­можно.

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

Фундаментальные основы хакерства

Пят­надцать лет назад эпи­чес­кий труд Кри­са Кас­пер­ски «Фун­дамен­таль­ные осно­вы хакерс­тва» был нас­толь­ной кни­гой каж­дого начина­юще­го иссле­дова­теля в области компь­ютер­ной безопас­ности. Одна­ко вре­мя идет, и зна­ния, опуб­ликован­ные Кри­сом, теря­ют акту­аль­ность. Редак­торы «Хакера» попыта­лись обно­вить этот объ­емный труд и перенес­ти его из вре­мен Windows 2000 и Visual Studio 6.0 во вре­мена Windows 10 и Visual Studio 2019.

Ссыл­ки на дру­гие статьи из это­го цик­ла ищи на стра­нице авто­ра.

 

Циклы while/do

 

Visual C++ 2022 с отключенной оптимизацией

Для зак­репле­ния прой­ден­ного в прош­лой статье матери­ала рас­смот­рим нес­коль­ко живых при­меров. Нач­нем с самого прос­того — иден­тифика­ции цик­лов while/do:

#include <stdio.h>
int main()
{
int a = 0;
while (a++ < 10)
printf("Оператор цикла while\n");
do {
printf("Оператор цикла do\n");
} while (--a > 0);
}

От­компи­лиру­ем этот код с помощью Visual C++ 2022 с отклю­чен­ной опти­миза­цией.

Результат выполнения примера while-do
Ре­зуль­тат выпол­нения при­мера while-do

Ре­зуль­тат ком­пиляции дол­жен выг­лядеть при­мер­но так:

; int __cdecl main(int argc, const char **argv, const char **envp)
main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdata:0000000140004018↓o
var_18 = dword ptr -18h
var_14 = dword ptr -14h
; Резервируем память для двух локальных переменных,
; Только откуда взялась вторая?
sub rsp, 38h
; Заносим в переменную var_18 значение 0
; Следовательно, это переменная a
mov [rsp+38h+var_18], 0

Ни­же сле­дует перек­рес­тная ссыл­ка — loc_1400010EC, нап­равлен­ная вниз. Это говорит нам о том, что перед нами начало цик­ла. Пос­коль­ку перек­рес­тная ссыл­ка нап­равле­на вниз, то переход, ссы­лающий­ся на этот адрес, будет нап­равлен вверх!

loc_1400010EC: ; CODE XREF: main+31↓j
; Загружаем в EAX значение переменной a (var_18)
mov eax, [rsp+38h+var_18]
; Загружаем в var_14 значение переменной a, вот, мы нашли,
; где используется вторая переменная
mov [rsp+38h+var_14], eax
; Зачем-то снова загружаем то же значение в регистр EAX
mov eax, [rsp+38h+var_18]
; Увеличение значения в регистре EAX на 1
inc eax
; Загружаем значение из регистра EAX в переменную var_18 ("a")
mov [rsp+38h+var_18], eax
; Сравниваем старое (до обновления) значение переменной a,
; ранее сохраненное в var_14, с числом 0xA
cmp [rsp+38h+var_14], 0Ah

Ес­ли (var_14 >= 0xA), дела­ем пры­жок «впе­ред», непос­редс­твен­но за инс­трук­цию безус­ловно­го перехо­да, нап­равлен­ного «назад». Если выпол­няет­ся пры­жок «назад», зна­чит, это цикл, а пос­коль­ку усло­вие выхода из цик­ла про­веря­ется в его начале, то это цикл с пре­дус­лови­ем.

Для его отоб­ражения на цикл while необ­ходимо инверти­ровать усло­вие выхода из цик­ла на усло­вие про­дол­жения цик­ла, дру­гими сло­вами, испра­вить >= на <.

Сде­лав это, мы получа­ем: while (a++ < 0xA)....

jge short loc_140001113
; Начало тела цикла:
; заносим ссылку на строку "Оператор цикла while\n"
lea rcx, _Format ; _Format
; Выводим на консоль
call printf
; Безусловный переход, направленный назад, на метку loc_1400010EC в начало цикла,
; в область подготовки переменных для проверки
jmp short loc_1400010EC

Меж­ду loc_1400010EC и jmp short loc_1400010E есть толь­ко одно усло­вие выхода из цик­ла: jge short loc_140001113. Зна­чит, исходный код цик­ла выг­лядел так:

while (a++ < 0xA) printf("Оператор цикла while\n");

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

loc_140001113: ; CODE XREF: main+23↑j
; main+4E↓j

Ага, никако­го усло­вия в начале цик­ла не при­сутс­тву­ет, зна­чит, это цикл с усло­вием в кон­це или в середи­не.

; Заносим ссылку на строку "Оператор цикла do\n"
lea rcx, byte_140002278 ; _Format
; Печатаем в консоли
call printf
; Тело цикла
; Загружаем в EAX значение переменной var_18 ("a")
mov eax, [rsp+38h+var_18]
; Уменьшаем значение в EAX на 1
dec eax
; Возвращаем значение из EAX в переменную a var_18
mov [rsp+38h+var_18], eax
; Сравниваем переменную a с нулем
cmp [rsp+38h+var_18], 0
; Если (a > 0), делаем переход в начало цикла
jg short loc_140001113

Пос­коль­ку усло­вие рас­положе­но в кон­це цик­ла, это цикл do:

do printf("Оператор цикла do\n");
while (--a > 0);
; Возвращаем 0
xor eax, eax
; Восстанавливаем стек
add rsp, 38h
retn
main endp
 

Visual C++ 2022 с включенной оптимизацией

Сов­сем дру­гой резуль­тат получит­ся, если вклю­чить опти­миза­цию. Откомпи­лиру­ем тот же самый при­мер с клю­чом /O2 (мак­сималь­ная опти­миза­ция: при­ори­тет ско­рос­ти) и пос­мотрим на резуль­тат, выдан­ный ком­пилято­ром:

; int __cdecl main(int argc, const char **argv, const char **envp)
main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdata:000000014000400C↓o
; Сохраняем регистр в стеке
push rbx
; Подготавливаем стек, ни одной локальной переменной не объявлено
sub rsp, 20h
; В EBX кладем число 0xA. Для чего, пока неясно
mov ebx, 0Ah
nop dword ptr [rax+rax+00h]
; Судя по следующей перекрестной ссылке, направленной вниз, это цикл!
loc_140001080: ; CODE XREF: main+20↓j
; Заносим в регистр RCX ссылку на строку "Оператор цикла while\n"
lea rcx, _Format ; _Format
; Выводим строку на терминал
call printf
; Если это тело цикла, то где же предусловие?!
; Вычитаем из RBX число 1
sub rbx, 1
; Получается, что число 0xA, помещенное в EBX ранее, было начальным значением

Инс­трук­ция SUB подоб­но CMP изме­няет сос­тояние фла­га нуля. Если в резуль­тате вычита­ния получа­ется 0, флаг нуля воз­водит­ся в еди­ницу. Сле­дующая инс­трук­ция совер­шает пры­жок назад, ког­да флаг не воз­веден, то есть в резуль­тате вычита­ния регистр RBX не стал равен нулю.

jnz short loc_140001080

Ком­пилятор в порыве опти­миза­ции прев­ратил неэф­фектив­ный цикл с пре­дус­лови­ем в более ком­пак­тный и быс­трый цикл с пос­тусло­вием. Имел ли он на это пра­во? А почему нет?! Про­ана­лизи­ровав код, ком­пилятор понял, что этот цикл выпол­няет­ся по край­ней мере один раз. Сле­дова­тель­но, скор­ректи­ровав усло­вие про­дол­жения, его про­вер­ку мож­но вынес­ти в конец цик­ла.

Так­же в исходном тек­сте был инкре­мент счет­чика цик­ла от нуля до 0xA, а в под­готов­ленном тран­сля­тором коде мы видим обратный эффект: дек­ремент счет­чика от 0xA до нуля. Таким обра­зом, ком­пилятор while ((int a=0)+1) < 10) printf(...) заменил do printf(...) while ((int a=10)-1) > 0).

При­чем, что инте­рес­но, он не срав­нивал перемен­ную цик­ла с кон­стан­той, а помес­тил кон­стан­ту в регистр и умень­шал его до тех пор, пока тот не стал равен нулю! Зачем? А затем, что так короче, да и работа­ет быс­трее.

Хо­рошо, но как нам деком­пилиро­вать этот цикл? Непос­редс­твен­ное отоб­ражение на язык C/C++ дает сле­дующую инс­трук­цию:

var_RBX = 0xA;
do {
printf("Оператор цикла while\n");
var_RBX--;
} while (var_RBX > 0);

Впол­не кра­сивый и опти­маль­ный цикл с одной перемен­ной.

; Значение 0xB помещаем в регистр EBX. Это подготовка к следующему циклу
; Этот код выполняется после завершения предыдущего цикла
mov ebx, 0Bh
nop word ptr [rax+rax+00000000h]
; Перекрестная ссылка, направленная вниз, говорит нам о том, что это начало цикла
loc_1400010A0: ; CODE XREF: main+40↓j
; Предусловия нет, значит, это цикл do
; Заносим в регистр RCX ссылку на строку "Оператор цикла do\n"
lea rcx, byte_140002278 ; _Format
; Выводим строку на терминал
call printf
; Уменьшаем значение, загруженное в EBX, на единицу
dec ebx
; Проверяем EBX на равенство нулю
test ebx, ebx
; Продолжаем выполнение цикла, пока EBX > 0
jg short loc_1400010A0

Этот цикл пря­миком отоб­ража­ется в конс­трук­цию язы­ка C/C++:

var_EBX = 0xB;
do { printf("Оператор цикла do\n"); }
while (--var_EBX > 0);
; Возвращаем ноль
xor eax, eax
; Восстанавливаем стек
add rsp, 20h
; Восстанавливаем регистр
pop rbx
retn
main endp
 

C++Builder 10 без оптимизации

Нес­коль­ко ина­че обра­баты­вает цик­лы ком­пилятор Embarcadero C++Builder 10.4. Смот­ри при­мер while-do_cb:

; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near ; DATA XREF: __acrtused+29↑o
; Объявляем шесть переменных
var_1C = dword ptr -1Ch
var_18 = dword ptr -18h
var_14 = dword ptr -14h
var_10 = qword ptr -10h
var_8 = dword ptr -8
var_4 = dword ptr -4
; Сохраняем в стеке RBP
push rbp
; Резервируем память для локальных переменных
sub rsp, 40h
; Помещаем в RBP указатель на дно стека
lea rbp, [rsp+40h]
; Инициализируем переменные:
; в var_4 записывает 0, вероятно, это переменная a из исходного кода
mov [rbp+var_4], 0
mov [rbp+var_8], ecx
mov [rbp+var_10], rdx
; Еще одна переменная, изначально равная нулю, возьмем на заметку
mov [rbp+var_14], 0
; Ниже перекрестная ссылка, направленная вниз, значит, это начало какого-то цикла
loc_40141F: ; CODE XREF: main+3E↓j
; В начале цикла условие не обнаружено, видимо, цикл с постусловием,
; хотя не будем спешить с выводами
; В регистр EAX копируем значение из переменной var_14
mov eax, [rbp+var_14]
; Копирование EAX в ECX
mov ecx, eax
; Увеличиваем значение в регистре ECX на 1
add ecx, 1
; Увеличенное значение из регистра ECX копируем в переменную var_14,
; из которой берется значение для счетчика в начале итерации
mov [rbp+var_14], ecx
; Сравнение неувеличенного значения с 0хА
cmp eax, 0Ah
; Если это значение больше константы или равно ей,
; выполняем прыжок за пределы цикла в область старших адресов
jge short loc_401440
; В случае продолжения выполнения помещаем ссылку на строку в регистр
; и выводим ее на консоль
lea rcx, aOperatorIklaWh ; "Оператор цикла while\n"
call printf
; Зачем-то сохраняем текущее значение регистра EAX в переменной var_18...
mov [rbp+var_18], eax
; ... и выполняем безусловный переход в начало цикла
jmp short loc_40141F

Вот так‑то C++Builder опти­мизи­ровал код! Началь­ный цикл с пре­дус­лови­ем выпол­нения он прев­ратил в бес­конеч­ный цикл с усло­вием выхода посере­дине (за под­робнос­тями обра­тись к прош­лой статье)! Как мы можем деком­пилиро­вать этот цикл? Нап­рашива­ется такой вари­ант:

int var_14 = 0;
do {
int var_EAX = var_14;
int var_ECX = var_EAX;
var_ECX++;
var_14 = var_ECX;
if (var_EAX >= 0xA) break;
printf("Оператор цикла while\n");
} while (TRUE);

Этот вари­ант кар­диналь­но отли­чает­ся от пер­воначаль­ного, и я очень сом­нева­юсь, что в луч­шую сто­рону! Что ж, издер­жки про­изводс­тва...

; --------------------------------
loc_401440: ; CODE XREF: main+2D↑j
; Сюда происходит переход при выходе из предыдущего цикла
; Как мы знаем, эта инструкция только переводит управление через себя
jmp short $+2
; --------------------------------
loc_401442: ; CODE XREF: main:loc_401440↑j
; main+5D↓j
; Новый цикл!
; Как видим, он начинается с вывода строки, нет условия, значит, цикл с постусловием
lea rcx, aOperatorIklaDo ; "Оператор цикла do\n"
call printf

Про­маты­ваем дизас­сем­блер­ный лис­тинг вверх, что­бы вспом­нить, какое зна­чение находит­ся в регис­тре EAX. Зна­чит, в этом мес­те прог­раммы зна­чение в регис­тре EAX рав­но 0хА. Записы­ваем это зна­чение в перемен­ную var_1C (непонят­но, для каких целей, ведь в будущем она не исполь­зует­ся). Выходит, локаль­ную перемен­ную a исходной прог­раммы пред­став­ляет регис­тро­вая перемен­ная EAX.

mov [rbp+var_1C], eax
; Записываем в регистр EAX значение переменной var_14
; А в ней содержится значение на 1 больше, чем в EAX! То есть 0xB
mov eax, [rbp+var_14]
; Какой хитрый C++Builder!
; Вместо реального вычитания он прибавляет к значению в EAX -1
add eax, 0FFFFFFFFh
; Присваивает результат переменной var_14
mov [rbp+var_14], eax
; И сравнивает уменьшенное значение с нулем
cmp eax, 0
; Если (EAX > 0), то мы прыгаем назад к началу «нового цикла»
; и осуществлению очередной итерации
jg short loc_401442

Во что C++Builder прев­ратил изна­чаль­ный цикл с пос­тусло­вием? В целом никаких изме­нений он не внес, оста­вив все на сво­их мес­тах. И деком­пилиро­ван­ный лис­тинг это­го цик­ла дол­жен выг­лядеть при­мер­но так:

var var_14 = 0xB;
do
{
int var_EAX = var_14;
var_EAX--;
var_14 = var_EAX;
printf("Оператор цикла while\n");
} while (var_EAX > 0);
; В ином случае, когда (EAX <= 0), пропускаем переход
; и продолжаем выполнение кода программы
mov [rbp+var_4], 0
; Возвращаем ноль
mov eax, [rbp+var_4]
; Восстанавливаем стек
add rsp, 40h
; Восстанавливаем регистр
pop rbp
retn
main endp

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    2 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии