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

Содержание статьи

  • Идентификация оператора +
  • Идентификация оператора –
  • Идентификация оператора /
  • Оптимизированный вариант
  • C++ Builder
  • Идентификация оператора %
  • Идентификация оператора *
  • C++ Builder
  • Комплексные операторы
  • Выводы

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

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

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

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

 

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

В общем слу­чае опе­ратор + тран­сли­рует­ся либо в машин­ную инс­трук­цию ADD, «перема­лыва­ющую» целочис­ленные опе­ран­ды, либо, с уче­том наличия в про­цес­соре под­дер­жки SSE (а без нее про­цес­соры уже дав­ным‑дав­но не выпус­кают­ся), в инс­трук­цию ADDSS, обра­баты­вающую вещес­твен­ные зна­чения оди­нар­ной точ­ности, и ADDSD — двой­ной точ­ности.

Оп­тимизи­рующие ком­пилято­ры могут заменять ADD xxx, 1 более ком­пак­тной коман­дой INC xxx, а конс­трук­цию c = a + b + const тран­сли­ровать в машин­ную инс­трук­цию LEA c, [a + b + const]. Такой трюк поз­воля­ет одним махом скла­дывать нес­коль­ко перемен­ных, воз­вра­тив получен­ную сум­му в любом регис­тре обще­го наз­начения, — необя­затель­но в левом сла­гаемом, как это тре­бует мне­мони­ка коман­ды ADD. Одна­ко LEA не может быть непос­редс­твен­но деком­пилиро­вана в опе­ратор +, пос­коль­ку она исполь­зует­ся не толь­ко для опти­мизи­рован­ного сло­жения (что, в общем‑то, толь­ко побоч­ный про­дукт ее деятель­нос­ти), но и по сво­ему пря­мому наз­начению — для вычис­ления эффектив­ного сме­щения.

Рас­смот­рим при­мер demo_plus, демонс­три­рующий исполь­зование опе­рато­ра + со зна­чени­ями оди­нар­ной точ­ности:

#include <iostream>int main(){ float a = 0.7f, b = 1.4f, c; c = a + b; std::cout << c << std::endl; c = c + 0.3f; std::cout << c << std::endl;}

Ре­зуль­тат выпол­нения demo_plus

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

Ре­зуль­тат тран­сля­ции это­го при­мера ком­пилято­ром 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↓ovar_c = dword ptr -18hvar_a = dword ptr -14hvar_b = dword ptr -10h; Резервируем память для локальных переменных sub rsp, 38h; Загружаем в регистр XMM0 значение из сегмента данных только для чтения movss xmm0, cs:__real@3f333333; Перекладываем это значение из регистра в переменную var_a movss [rsp+38h+var_a], xmm0; Загружаем в регистр следующее по порядку значение movss xmm0, cs:__real@3fb33333; Перекладываем его в переменную var_b movss [rsp+38h+var_b], xmm0; Первое значение возвращаем в регистр XMM0 из переменной var_a movss xmm0, [rsp+38h+var_a]; Складываем содержимое XMM0 со значением переменной var_b addss xmm0, [rsp+38h+var_b]; Копируем сумму var_a и var_b в переменную var_c, следовательно, var_c = var_a + var_b movss [rsp+38h+var_c], xmm0; Готовим параметры для передачи оператору <<; Второй слева — переменная var_c movss xmm1, [rsp+38h+var_c]; Первый слева — формат вывода mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout; Собственно вызов оператора вывода строки call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(float); Плюс вывод символа новой строки lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) mov rcx, rax call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)); Загружаем в XMM0 значение переменной var_c... movss xmm0, [rsp+38h+var_c]; ...прибавляем к этому значению значение из сегмента данных только для чтения addss xmm0, cs:__real@3e99999a; Обновляем var_c: var_c = var_c + const movss [rsp+38h+var_c], xmm0; Готовим параметры для передачи оператору <<; Второй слева — переменная var_c movss xmm1, [rsp+38h+var_c]; Первый слева — формат вывода mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout; Собственно вызов оператора вывода строки call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(float); Плюс вывод символа новой строки lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) mov rcx, rax call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)) xor eax, eax add rsp, 38h retnmain endp

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

main proc near ; CODE XREF: __scrt_common_main_seh+107↓p ; DATA XREF: .pdata:ExceptionDir↓o sub rsp, 28h; Как ловко! Компилятор подсчитал сумму во время компиляции; и подставил ее непосредственно для вывода; С чего бы ему идти на такие хитрости без последствий?; Значения-то представляют собой константы,; которые хранятся в сегменте данных только для чтения movss xmm1, cs:__real@40066666 mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(float) mov rcx, rax lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)); Второе для вывода значение — с ним такая же история, как с первым значением movss xmm1, cs:__real@40199999 mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(float) mov rcx, rax lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)) xor eax, eax add rsp, 28h retnmain endp

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

Embarcadero C++Builder генери­рует похожий код, а в слу­чае опти­миза­ции — еще хуже. Поэто­му при­водить резуль­таты его тру­да бес­смыс­ленно — никаких новых «изю­минок» они в себе не несут.

 

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

В общем слу­чае опе­ратор – тран­сли­рует­ся либо в машин­ную инс­трук­цию SUB (если опе­ран­ды — целочис­ленные зна­чения), либо в инс­трук­цию SUBSS (если опе­ран­ды — вещес­твен­ные зна­чения оди­нар­ной точ­ности) или в SUBSD (ког­да опе­ран­ды двой­ной точ­ности). Опти­мизи­рующие ком­пилято­ры могут заменять SUB xxx, 1 более ком­пак­тной коман­дой DEC xxx, а конс­трук­цию SUB a, const тран­сли­ровать в ADD a, –const, которая ничуть не ком­пак­тнее и нис­коль­ко не быс­трей (и та и дру­гая укла­дыва­ется в один такт). Одна­ко хозя­ин (ком­пилятор) — барин.

По­кажем это на при­мере demo_minus, демонс­три­рующем исполь­зование опе­рато­ра – со зна­чени­ями двой­ной точ­ности:

#include <iostream>int main(){ double a = 3.1, b = 1.6, c; c = a - b; std::cout << c << std::endl; c = c - 10; std::cout << c << std::endl;}

Ре­зуль­тат выпол­нения demo_minus

Не­опти­мизи­рован­ный вари­ант будет выг­лядеть приб­лизитель­но так:

main proc near ; CODE XREF: __scrt_common_main_seh+107↓p ; DATA XREF: .pdata:ExceptionDir↓ovar_c = qword ptr -28hvar_a = qword ptr -20hvar_b = qword ptr -18h; Резервируем память для локальных переменных sub rsp, 48h; Загружаем в регистр XMM0 значение из сегмента данных только для чтения movsd xmm0, cs:__real@4008cccccccccccd; Перекладываем это значение из регистра в переменную var_a movsd [rsp+48h+var_a], xmm0; Загружаем в регистр, заменяя имеющееся там значение следующим по порядку значением movsd xmm0, cs:__real@3ff999999999999a; Перекладываем его в переменную var_b movsd [rsp+48h+var_b], xmm0; Из переменной var_a возвращаем значение в регистр XMM0 movsd xmm0, [rsp+48h+var_a]; Вычитаем из var_a значение переменной var_b, записывая результат в XMM0 subsd xmm0, [rsp+48h+var_b]; Записываем в var_c разность var_a и var_b:; var_c = var_a — var_b movsd [rsp+48h+var_c], xmm0 movsd xmm1, [rsp+48h+var_c] mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(double) lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) mov rcx, rax call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)); Загружаем в регистр XMM0 значение переменной var_c — готовим к вычислению movsd xmm0, [rsp+48h+var_c]; Вычитаем из var_c значение, взятое из сегмента данных только для чтения; При этом результат записываем в регистр XMM0 subsd xmm0, cs:__real@4024000000000000; Обновляем содержимое переменной var_c:; var_c = var_c — const movsd [rsp+48h+var_c], xmm0 movsd xmm1, [rsp+48h+var_c] mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(double) lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) mov rcx, rax call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)) xor eax, eax add rsp, 48h retnmain endp

А теперь рас­смот­рим опти­мизи­рован­ный вари­ант того же при­мера:

main proc near ; CODE XREF: __scrt_common_main_seh+107↓p ; DATA XREF: .pdata:ExceptionDir↓o sub rsp, 28h; Компилятор подсчитал разность во время трансляции и подготовил ее значение для вывода movsd xmm1, cs:__real@3ff8000000000000 mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(double) mov rcx, rax lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)); Результат второй разности также подсчитан во время компиляции movsd xmm1, cs:__real@c021000000000000 mov rcx, cs:std::basic_ostream<char,std::char_traits<char>> std::cout call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(double) mov rcx, rax lea rdx, std::endl<char,std::char_traits<char>>(std::basic_ostream<char,std::char_traits<char>> &) call cs:std::basic_ostream<char,std::char_traits<char>>::operator<<(std::basic_ostream<char,std::char_traits<char>> & (*)(std::basic_ostream<char,std::char_traits<char>> &)) xor eax, eax add rsp, 28h retnmain endp

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

 

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

В общем слу­чае опе­ратор / тран­сли­рует­ся либо в машин­ную инс­трук­цию DIV (без­зна­ковое целочис­ленное деление), либо в IDIV (целочис­ленное деление со зна­ком), либо в DIVSS (деление вещес­твен­ных зна­чений оди­нар­ной точ­ности) или DIVSD (деление вещес­твен­ных зна­чений двой­ной точ­ности).

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *