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

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

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

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

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

 

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

Конс­трук­ция 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)
main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdata: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
main endp

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

main proc 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
main endp

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

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

; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc 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
main endp

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

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

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

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

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

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


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

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

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии