Вок­руг перемен­ных и с их непос­редс­твен­ным учас­тием про­исхо­дят глав­ные события в любой прог­рамме. Поэто­му каж­дый кодоко­патель дол­жен уметь находить перемен­ные и опре­делять их тип. Имен­но этим мы сегод­ня и зай­мем­ся.

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

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

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

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

 

Идентификация локальных стековых переменных

Ло­каль­ные перемен­ные раз­меща­ются в сте­ке (его так­же называ­ют авто­мати­чес­кой памятью), а уда­ляет их отту­да вызыва­емая фун­кция, ког­да она завер­шится. Рас­смот­рим под­робнее, как это про­исхо­дит. Сна­чала в стек затяги­вают­ся аргу­мен­ты, переда­ваемые фун­кции (если они есть), а свер­ху на них кла­дет­ся адрес воз­вра­та, помеща­емый туда инс­трук­цией CALL, которая вызыва­ет эту фун­кцию. Получив управле­ние, фун­кция откры­вает кадр сте­ка — сох­раня­ет преж­нее зна­чение регис­тра RBP и уста­нав­лива­ет его рав­ным регис­тру RSP (регистр — ука­затель вер­шины сте­ка). «Выше» (то есть в более млад­ших адре­сах) RBP находит­ся сво­бод­ная область сте­ка, «ниже» — слу­жеб­ные дан­ные (сох­ранен­ный RBP, адрес воз­вра­та) и аргу­мен­ты.

Сох­ранность области сте­ка, рас­положен­ной выше ука­зате­ля вер­шины сте­ка (регис­тра RSP), не гаран­тирова­на от затира­ния и иска­жения. Ее бес­пре­пятс­твен­но могут исполь­зовать, нап­ример, обра­бот­чики аппа­рат­ных пре­рыва­ний, вызыва­емые в неп­ред­ска­зуемом мес­те в неп­ред­ска­зуемое вре­мя. Да и исполь­зование сте­ка самой фун­кци­ей (для сох­ранения регис­тров или переда­чи аргу­мен­тов) при­ведет к его иска­жению. Какой выход? При­нуди­тель­но перемес­тить ука­затель вер­шины сте­ка вверх, тем самым заняв дан­ную область сте­ка. Сох­ранность памяти, находя­щей­ся «ниже» RSP, гаран­тиру­ется (име­ется в виду гаран­тиру­ется от неп­редна­мерен­ных иска­жений). Оче­ред­ной вызов инс­трук­ции PUSH занесет дан­ные на вер­шину сте­ка, не затирая локаль­ные перемен­ные.

По окон­чании работы фун­кция обя­зана вер­нуть RSP на преж­нее мес­то, ина­че инс­трук­ция RET сни­мет со сте­ка отнюдь не адрес воз­вра­та, а вооб­ще невесть что (зна­чение самой «вер­хней» локаль­ной перемен­ной) и передаст управле­ние «в кос­мос»...

Механизм размещения локальных переменных в стеке
Ме­ханизм раз­мещения локаль­ных перемен­ных в сте­ке

На левой кар­тинке показа­но сос­тояние сте­ка на момент вызова фун­кции. Она откры­вает кадр сте­ка, сох­раняя преж­нее зна­чение регис­тра RBP, и уста­нав­лива­ет его рав­ным RSP. На пра­вой кар­тинке изоб­ражено резер­вирова­ние 0x14 байт сте­ковой памяти под локаль­ные перемен­ные. Резер­вирова­ние дос­тига­ется переме­щени­ем регис­тра RSP «вверх» — в область млад­ших адре­сов. Фак­тичес­ки локаль­ные перемен­ные раз­меща­ются в сте­ке так, как буд­то бы они были туда помеще­ны коман­дой PUSH. При завер­шении работы фун­кция уве­личи­вает зна­чение регис­тра RSP, воз­вра­щая его на преж­нюю позицию и осво­бож­дая тем самым память, занятую локаль­ными перемен­ными. Затем она стя­гива­ет со сте­ка и вос­ста­нав­лива­ет зна­чение RBP, зак­рывая тем самым кадр сте­ка.

 

Адресация локальных переменных

Ад­ресация локаль­ных перемен­ных очень похожа на адре­сацию сте­ковых аргу­мен­тов, толь­ко аргу­мен­ты рас­полага­ются «ниже» RBP, а локаль­ные перемен­ные — «выше». Дру­гими сло­вами, аргу­мен­ты име­ют положи­тель­ные сме­щения отно­ситель­но RBP, а локаль­ные перемен­ные — отри­цатель­ные, поэто­му их очень лег­ко отли­чить друг от дру­га. Нап­ример, [RBP+xxx] — аргу­мент, а [RBP-xxx] — локаль­ная перемен­ная.

Ре­гистр — ука­затель кад­ра сте­ка слу­жит как бы барь­ером: по одну сто­рону от него аргу­мен­ты фун­кции, по дру­гую — локаль­ные перемен­ные.

Адресация локальных переменных
Ад­ресация локаль­ных перемен­ных

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

 

Детали технической реализации

Су­щес­тву­ет мно­жес­тво вари­антов реали­зации выделе­ния и осво­бож­дения памяти под локаль­ные перемен­ные. Казалось бы, чем пло­хо оче­вид­ное SUB RSP,xxx на вхо­де и ADD RSP, xxx на выходе? А вот некото­рые ком­пилято­ры в стрем­лении отли­чать­ся ото всех осталь­ных резер­виру­ют память не умень­шени­ем, а уве­личе­нием RSP... Да, на отри­цатель­ное чис­ло, которое по умол­чанию боль­шинс­твом дизас­сем­бле­ров отоб­ража­ется как очень боль­шое положи­тель­ное. Опти­мизи­рующие ком­пилято­ры при отво­де неболь­шого количес­тва памяти изме­няют SUB на PUSH reg, что на нес­коль­ко бай­тов короче. Пос­леднее соз­дает оче­вид­ные проб­лемы иден­тифика­ции: поп­робуй раз­берись, то ли перед нами сох­ранение регис­тров в сте­ке, то ли переда­ча аргу­мен­тов, то ли резер­вирова­ние памяти для локаль­ных перемен­ных (под­робнее об этом см. раз­дел «Иден­тифика­ция механиз­ма выделе­ния памяти»).

Ал­горитм осво­бож­дения памяти так­же неод­нозна­чен. Помимо уве­личе­ния регис­тра ука­зате­ля вер­шины сте­ка инс­трук­цией ADD RSP, xxx (или в осо­бо извра­щен­ных ком­пилято­рах уве­личе­ния его на отри­цатель­ное чис­ло), час­то встре­чает­ся конс­трук­ция MOV RSP, RBP. Мы ведь пом­ним, что при откры­тии кад­ра сте­ка RSP копиро­вал­ся в RBP, а сам RBP в про­цес­се исполне­ния фун­кции не изме­нял­ся. Наконец, память может быть осво­бож­дена инс­трук­цией POP, вытал­кива­ющей локаль­ные перемен­ные одну за дру­гой в какой‑нибудь ненуж­ный регистр (понят­ное дело, такой спо­соб оправды­вает себя лишь на неболь­шом количес­тве локаль­ных перемен­ных).

Наиболее распространенные варианты реализации резервирования памяти под локальные переменные и ее освобождение
На­ибо­лее рас­простра­нен­ные вари­анты реали­зации резер­вирова­ния памяти под локаль­ные перемен­ные и ее осво­бож­дение
 

Идентификация механизма выделения памяти

Вы­деле­ние памяти инс­трук­циями SUB и ADD неп­ротиво­речи­во и всег­да интер­пре­тиру­ется однознач­но. Если же оно выпол­няет­ся коман­дой PUSH, а осво­бож­дение — POP, эта конс­трук­ция ста­новит­ся неот­личима от прос­того осво­бож­дения/сох­ранения регис­тров в сте­ке. Ситу­ация серь­езно осложня­ется тем, что в фун­кции при­сутс­тву­ют и нас­тоящие коман­ды сох­ранения регис­тров, сли­ваясь с коман­дами выделе­ния памяти. Как узнать, сколь­ко бай­тов резер­виру­ется для локаль­ных перемен­ных и резер­виру­ются ли они вооб­ще (может, в фун­кции локаль­ных перемен­ных и нет вов­се)?

От­ветить на этот воп­рос поз­воля­ет поиск обра­щений к ячей­кам памяти, лежащих «выше» регис­тра RBP, то есть с отри­цатель­ными отно­ситель­ными сме­щени­ями. Рас­смот­рим два при­мера.

В левом из них никако­го обра­щения к локаль­ным перемен­ным не про­исхо­дит вооб­ще, а в пра­вом наличес­тву­ет конс­трук­ция MOV [RBP-4],0x666, копиру­ющая зна­чение 0x666 в локаль­ную перемен­ную var_4. А раз есть локаль­ная перемен­ная, для нее кем‑то дол­жна быть выделе­на память. Пос­коль­ку инс­трук­ций SUB RSP, xxx и ADD RSP, xxx в теле фун­кций не наб­люда­ется, подоз­рение пада­ет на PUSH RCX, так как сох­ранен­ное содер­жимое регис­тра RCX рас­полага­ется в сте­ке на четыре бай­та «выше» RBP. В дан­ном слу­чае подоз­рева­ется лишь одна коман­да — PUSH RCX, пос­коль­ку PUSH RBP на роль «резер­ватора» не тянет. Но как быть, если подоз­рева­емых нес­коль­ко?

Оп­ределить количес­тво выделен­ной памяти мож­но по сме­щению самой «высокой» локаль­ной перемен­ной, которую уда­ется обна­ружить в теле фун­кции. То есть, отыс­кав все выраже­ния типа [RBP-xxx], выберем наиболь­шее сме­щение «xxx» — в общем слу­чае оно рав­но количес­тву бай­тов выделен­ной под локаль­ные перемен­ные памяти. В час­тнос­тях же встре­чают­ся объ­явленные, но не исполь­зуемые локаль­ные перемен­ные. Им выделя­ется память (хотя опти­мизи­рующие ком­пилято­ры прос­то выкиды­вают такие перемен­ные за ненадоб­ностью), но ни одно­го обра­щения к ним не про­исхо­дит, и опи­сан­ный выше алго­ритм под­сче­та объ­ема резер­виру­емой памяти дает занижен­ный резуль­тат. Впро­чем, эта ошиб­ка никак не ска­зыва­ется на резуль­татах ана­лиза прог­раммы.

Инициализация локальных переменных

Су­щес­тву­ет два спо­соба ини­циали­зации локаль­ных перемен­ных: прис­воение необ­ходимо­го зна­чения инс­трук­цией MOV (нап­ример, MOV [RBP-04], 0x666) и непос­редс­твен­ное затал­кивание зна­чения в стек инс­трук­цией PUSH (нап­ример, PUSH 0x777). Пос­леднее поз­воля­ет выгод­но ком­биниро­вать выделе­ние памяти под локаль­ные перемен­ные с их ини­циали­заци­ей (разуме­ется, толь­ко в том слу­чае, если этих перемен­ных нем­ного).

По­пуляр­ные ком­пилято­ры в подав­ляющем боль­шинс­тве слу­чаев выпол­няют опе­рацию ини­циали­зации с помощью MOV, а PUSH более харак­терен для ассем­блер­ных извра­щений, которые встре­чают­ся, нап­ример, в защитах, име­ющих задачу сбить хакеров с тол­ку. Ну, если такой при­ем и сму­тит хакера, то толь­ко начина­юще­го.

Размещение массивов и структур

Мас­сивы и струк­туры раз­меща­ются в сте­ке пос­ледова­тель­но в смеж­ных ячей­ках памяти, при этом мень­ший индекс мас­сива (эле­мент струк­туры) лежит по мень­шему адре­су, но — вни­мание — адре­сует­ся боль­шим модулем сме­щения отно­ситель­но регис­тра ука­зате­ля кад­ра сте­ка. Это не покажет­ся уди­витель­ным, если вспом­нить, что локаль­ные перемен­ные адре­суют­ся отри­цатель­ными сме­щени­ями, сле­дова­тель­но, [RBP-0x4] > [RBP-0x10].

Пу­тани­цу уси­лива­ет и то обсто­ятель­ство, что, давая локаль­ным перемен­ным име­на, IDA опус­кает знак минус. Поэто­му из двух имен, ска­жем var_4 и var_10, по мень­шему адре­су лежит то, чей индекс боль­ше! Если var_4 и var_10 — это два кон­ца мас­сива, то с неп­ривыч­ки воз­ника­ет неп­роиз­воль­ное желание помес­тить var_4 в голову, а var_10 в хвост мас­сива, хотя на самом деле все наобо­рот!

Выравнивание в стеке

В некото­рых слу­чаях эле­мен­ты струк­туры, мас­сива и даже прос­то отдель­ные перемен­ные тре­бует­ся рас­полагать по крат­ным адре­сам. Но ведь зна­чение ука­зате­ля вер­шины заранее не опре­деле­но и неиз­вес­тно ком­пилято­ру. Как же он, не зная фак­тичес­кого зна­чения ука­зате­ля, смо­жет выпол­нить это тре­бова­ние? Да очень прос­то — возь­мет и отки­нет млад­шие биты RSP!

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

Сбра­сыва­ет биты в подав­ляющем боль­шинс­тве слу­чаев инс­трук­ция AND. Нап­ример, AND RSP, FFFFFFF0 дела­ет RSP крат­ным шес­тнад­цати. Как было получе­но это зна­чение? Перево­дим 0xFFFFFFF0 в дво­ичный вид, получа­ем 11111111 11111111 11111111 11110000. Видишь четыре нуля на кон­це? Зна­чит, четыре млад­ших бита любого чис­ла будут мас­кирова­ны и оно раз­делит­ся без остатка на 24 = 16.

Хо­тя с локаль­ными перемен­ными мы уже неод­нократ­но встре­чались при изу­чении прош­лых при­меров, не помеша­ет сде­лать это еще один раз:

local_vars_identified
#include <stdio.h>
#include <stdlib.h>
int MyFunc(int a, int b) {
int c; // Локальная переменная типа int
char x[50]; // Массив (демонстрирует схему размещения массивов в памяти)
c = a + b; // Заносим в c сумму аргументов a и b
_itoa_s(c, &x[0], sizeof(x), 0x10); // Переводим сумму a и b в строку
printf("%x == %s == ", c, &x[0]); // Выводим строку на экран
return c;
}
int main() {
// Объявляем локальные переменные a и b для того, чтобы
// продемонстрировать механизм их инициализации компилятором.
// Такие извращения понадобились для того, чтобы запретить
// оптимизирующему компилятору помещать локальную переменную
// в регистр (см. «Идентификация регистровых переменных»)
int a = 0x666;
int b = 0x777;
int c[1];
// Так как функции printf передается указатель на с,
// а указатель на регистр быть передан не может,
// компилятор вынужден оставить переменную в памяти
c[0] = MyFunc(a, b);
printf("%x\n", &c[0]);
return 0;
}

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

Обрати внимание, так как третье значение в выводе — адрес, при каждом запуске приложения его значение будет меняться
Об­рати вни­мание, так как третье зна­чение в выводе — адрес, при каж­дом запус­ке при­ложе­ния его зна­чение будет менять­ся

Ре­зуль­тат ком­пиляции ком­пилято­ром Microsoft Visual C++ 2019 в режиме реаль­ного вре­мени с отклю­чен­ной опти­миза­цией дол­жен выг­лядеть так:

int MyFunc(int, int) proc near
Value = dword ptr -58h
DstBuf = byte ptr -50h
var_18 = qword ptr -18h

Ло­каль­ные перемен­ные рас­полага­ются по отри­цатель­ному сме­щению отно­ситель­но RBP, а аргу­мен­ты фун­кции — по положи­тель­ному. Выше мы обсужда­ли, что боль­шинс­тво ком­пилято­ров (в том чис­ле Visual C++) опти­мизи­руют код, осво­бож­дая регистр RBP, явля­ющий­ся ука­зате­лем кад­ра сте­ка, что­бы он исполнял роль допол­нитель­ного регис­тра обще­го наз­начения. При этом фун­кцию RBP начина­ет выпол­нять RSP (ука­затель на вер­шину сте­ка). Заметь так­же: чем выше рас­положе­на перемен­ная, тем боль­ше модуль ее сме­щения.

arg_0 = dword ptr 8
arg_8 = dword ptr 10h
; Инициализируем аргументы в памяти, загружая их значения из регистров
mov [rsp+arg_8], edx
; О том, что это аргументы, а не нечто иное, говорит их положительное смещение относительно регистра RBP
mov [rsp+arg_0], ecx
; Уменьшаем значение RSP на 0x78, резервируя 0x78 байт под локальные переменные
sub rsp, 78h
mov rax, cs:__security_cookie
xor rax, rsp
mov [rsp+78h+var_18], rax
; Вновь копируем аргументы в регистры для удобства последующих операций
mov eax, [rsp+78h+arg_8]
mov ecx, [rsp+78h+arg_0]
; Складываем значения аргументов
add ecx, eax
mov eax, ecx

А вот и пер­вая локаль­ная перемен­ная! На то, что это имен­но она и есть, ука­зыва­ет ее отри­цатель­ное сме­щение отно­ситель­но регис­тра RBP. Почему отри­цатель­ное? А пос­мотри, как IDA опре­дели­ла Value. По моему лич­ному мне­нию, было бы нам­ного наг­ляднее, если бы отри­цатель­ные сме­щения локаль­ных перемен­ных под­черки­вались более явно.

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

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

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

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

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


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

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

    Подписаться

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