Категория > Новости > Фундаментальные основы хакерства. Изучаем условные переходы в обработке современными компиляторами - «Новости»

Фундаментальные основы хакерства. Изучаем условные переходы в обработке современными компиляторами - «Новости»


26-05-2022, 00:02. Автор: Jacobson
стра­нице авто­ра.

Се­год­няшняя статья — про­дол­жение пре­дыду­щей, если ее не про­читать, разоб­рать­ся в коде будет труд­новато. Что ж, про­дол­жим наш зап­лыв в оке­ан усло­вий, вет­вле­ний, отно­шений и ком­про­мис­сов.


 

Идентификация тернарного оператора


Конс­трук­ция a=(условие)?do_it:continue язы­ка C в общем слу­чае тран­сли­рует­ся так: IF (условие) THEN a=do_it ELSE a=continue, одна­ко резуль­тат ком­пиляции обе­их конс­трук­ций, воп­реки рас­простра­нен­ному мне­нию, не всег­да иден­тичен. Опе­ратор «?:» зна­читель­но лег­че под­дает­ся опти­миза­ции, чем вет­вле­ние IF THEN ELSE. Покажем это на сле­дующем при­мере:


#include <iostream>
int main() {
int a = -2;
int b = 2;
a = (a > 0)? 1 :-1; // Тернарный оператор
if (b > 0) // Ветвление
b = 1;
else
b = -1;
std::cout <<"a + b = " << a+ b;
}

Ес­ли про­пус­тить эту прог­рамму сквозь ком­пилятор Microsoft Visual C++ 2022 с отклю­чен­ной опти­миза­цией, на выходе получим такой код:


; int __cdecl main(int argc, const char **argv, const char **envp)
mainproc near ; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdаta:ExceptionDir↓o
; Четыре локальные переменные
var_18= dword ptr -18h
var_14= dword ptr -14h
var_10= dword ptr -10h
var_C= dword ptr -0Ch
; Резервируем место для локальных переменных
sub rsp, 38h
; Инициализация локальных переменных:
; var_14 присваиваем -2, компилятор выразил отрицательное число
; сверхбольшим положительным
mov [rsp+38h+var_14], 0FFFFFFFEh
; var_18 присваиваем 2, здесь все прямолинейно
mov [rsp+38h+var_18], 2
; Сравниваем переменную var_14 с 0
cmp [rsp+38h+var_14], 0
; Переход по метке, если в результате предыдущего сравнения первый операнд меньше второго или равен ему
jle short loc_140001025 ; В нашем случае все сходится, делаем прыжок
mov [rsp+38h+var_10], 1
jmp short loc_14000102D
; --------------------------------------
loc_140001025:; CODE XREF: main+19↑j
; Делаем переход сюда
mov [rsp+38h+var_10], 0FFFFFFFFh ; var_10 присваиваем -1
loc_14000102D:; CODE XREF: main+23↑j
mov eax, [rsp+38h+var_10]
mov [rsp+38h+var_14], eax ; var_14 = -1
; Сравниваем var_18 с 0, припоминаем или прокручиваем экран дизассемблера вверх,
; в var_18 помещено значение 2
cmp [rsp+38h+var_18], 0
; Переход по метке, если в результате предыдущего сравнения первый операнд меньше второго или равен ему
jle short loc_140001046 ; В данном случае карты не сходятся, поэтому продолжаем выполнение
mov [rsp+38h+var_18], 1 ; Переменной var_18 присваиваем значение 1
jmp short loc_14000104E ; Безусловный переход
; --------------------------------------
loc_140001046:; CODE XREF: main+3A↑j
mov [rsp+38h+var_18], 0FFFFFFFFh
loc_14000104E:; CODE XREF: main+44↑j
; Прыгаем сюда
mov eax, [rsp+38h+var_18] ; EAX = 1
mov ecx, [rsp+38h+var_14] ; ECX = -1
add ecx, eax; -1 + 1
mov eax, ecx
mov [rsp+38h+var_C], eax ; Переменной var_C присваиваем сумму значение 0
; Готовим параметры
lea rdx, _Val; "a + b = "
mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout ; _Ostr
; Вызываем оператор << для вывода строки
call std::operator<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char const *)
mov ecx, [rsp+38h+var_C]
mov edx, ecx
mov rcx, rax
; Вызываем оператор << для вывода числа
call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(int)
xor eax, eax ; Возвращаем 0
add rsp, 38h ; Восстанавливаем стек
retn
mainendp

Во дела! Ком­пилятор оба условных опе­рато­ра ском­пилил в оди­нако­вый код! Теперь переве­дем ком­пилятор в режим мак­сималь­ной опти­миза­ции с при­ори­тетом по ско­рос­ти — /O2, про­изве­дем ком­пиляцию, откро­ем резуль­тат в дизас­сем­бле­ре и пос­мотрим на резуль­тат:


mainproc near
sub rsp, 28h
mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout ; _Ostr
call std::operator<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char const *)
mov rcx, rax
xor edx, edx
call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(int)
xor eax, eax
add rsp, 28h
retn
mainendp

Что за чер­товщи­на?! Здесь не оста­лось никаких условнос­тей! Даже если вклю­чить режим опти­миза­ции с при­ори­тетом раз­мера — /O1, ничего не изме­нит­ся. Ком­пилятор заоп­тимизи­ровал все что мож­но!


А что нам про­демонс­три­рует C++Builder? Сна­чала без опти­миза­ции:


; int __cdecl main(int argc, const char **argv, const char **envp)
public main
mainproc near; DATA XREF: __acrtused+29↑o
var_20= qword ptr -20h
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
; Открываем кадр стека
push rbp
; Выделяем память под локальные переменные
sub rsp, 40h
; Запоминаем адрес дна стека
lea rbp, [rsp+40h]
mov eax, 0FFFFFFFFh ; EAX = -1
mov r8d, 1; R8D = 1
; Инициализация локальных переменных
mov [rbp+var_4], 0
mov [rbp+var_8], ecx
mov [rbp+var_10], rdx
mov [rbp+var_14], 0FFFFFFFEh ; var_14 = -2
mov [rbp+var_18], 2; var_18 = 2
mov ecx, [rbp+var_14]; ECX = -2
; Сравниваем ECX с 0
cmp ecx, 0

Ага! Исполь­зует­ся коман­да условной пересыл­ки (за под­робнос­тями обра­тись к пре­дыду­щей статье): G зна­чит Greater. Дру­гими сло­вами, дан­ные копиру­ются, если пер­вый опе­ранд боль­ше вто­рого (в таком слу­чае фла­ги ZF и SF рав­ны 0). В нашем слу­чае усло­вие не выпол­няет­ся, так как -2 < 0, поэто­му зна­чение из регис­тра R8D в регистр EAX не копиру­ется.


cmovg eax, r8d
mov [rbp+var_14], eax ; var_14 = -1
; Сравнение var_18 с 0
cmp [rbp+var_18], 0
; Переход по метке, если в результате предыдущего сравнения первый операнд меньше второго или равен ему
jle short loc_4013ED ; В данном случае карты не сходятся (2 > 0), поэтому продолжаем выполнение
mov [rbp+var_18], 1 ; var_18 = 1
jmp short loc_4013F4 ; Безусловный прыжок
; --------------------------------------
loc_4013ED:; CODE XREF: main+42↑j
mov [rbp+var_18], 0FFFFFFFFh
loc_4013F4:; CODE XREF: main+4B↑j
; Безусловный прыжок сюда
; Подготовка параметров для вызова оператора вывода
lea rcx, a008_free_list
lea rdx, aAB; "a + b = "
; Вывод строки оператором <<
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc ; std::operator<<<std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &,char const*)
mov r8d, [rbp+var_14] ; R8D = -1
add r8d, [rbp+var_18] ; R8D = -1 + 1
; Готовим параметры для вызова оператора вывода
mov rcx, rax
mov edx, r8d
; Вывод числа (суммы) оператором <<
call _ZNSolsEi; std::ostream::operator<<(int)
mov [rbp+var_4], 0
mov [rbp+var_20], rax
; Возвращаем 0
mov eax, [rbp+var_4]
; Восстанавливаем стек
add rsp, 40h
; Закрываем кадр стека
pop rbp
retn
mainendp

Бы­ло инте­рес­но уви­деть резуль­тат работы C++Builder, в осо­бен­ности исполь­зование коман­ды условной пересыл­ки, которая поз­волила умень­шить количес­тво условных перехо­дов с двух до одно­го. За под­робнос­тями обра­тись к под­готов­ленно­му Visual C++ неоп­тимизи­рован­ному дизас­сем­блер­ному лис­тингу, что­бы уви­деть оба условных перехо­да в дей­ствии.



Перейти обратно к новости