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

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


12-06-2022, 00:01. Автор: Григорий
стра­нице авто­ра.

 

Ищем операторы switch-case-break в бинарном коде


Для улуч­шения читабель­нос­ти прог­рамм в язык C был вве­ден опе­ратор мно­жес­твен­ного выбора — switch. В Delphi с той же самой задачей справ­ляет­ся опе­ратор CASE, более гиб­кий, чем его C-ана­лог, но об их раз­личи­ях мы погово­рим поз­днее.


Лег­ко показать, что switch экви­вален­тен такой конс­трук­ции:


IF (a == x1) THEN опе­ратор1
ELSE IF (a == X2) THEN опе­ратор2
IF (a == X2) THEN опе­ратор2
IF (a == X2) THEN опе­ратор2
ELSE ... опе­ратор по умол­чанию


Ес­ли изоб­разить это вет­вле­ние в виде логичес­кого дерева, то обра­зует­ся харак­терная «косич­ка».


Тран­сля­ция опе­рато­ра switch в общем слу­чае

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


Од­нако в реаль­ной жиз­ни все про­исхо­дит сов­сем не так. Ком­пилято­ры (даже неоп­тимизи­рующие) тран­сли­руют switch в нас­тоящий «мяс­ной рулет», довер­ху наш­пигован­ный все­воз­можны­ми опе­раци­ями отно­шений. Давай откомпи­лиру­ем сле­дующий код ком­пилято­ром Microsoft Visual C++ 2022:


#include <stdio.h>
int main()
{
int a = 0x666;
switch (a)
{
case 0:
printf("a == 0");
break;
case 1:
printf("a == 1");
break;
case 2:
printf("a == 2");
break;
case 0x666:
printf("a == 666h");
break;
default:
printf("Default");
}
}
Вы­вод при­ложе­ния switch_cases

Те­перь пос­мотрим в IDA на резуль­тат дизас­сем­бли­рова­ния.


Де­рево рас­пусти­ло вет­ки во все сто­роны. МОЖ­НО сде­лать однознач­ный вывод: в дизас­сем­бли­руемой прог­рамме при­сутс­тву­ет опе­ратор мно­жес­твен­ного выбора switch-case
mainproc near ; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdаta:0000000140004018↓o
; Объявляем две локальные переменные,
; но почему две, если в исходном коде объявлена только одна?
var_18= dword ptr -18h
var_14= dword ptr -14h
; Резервируем место для локальных переменных
sub rsp, 38h
; Инициализируем локальные переменные:
; var_14 присваиваем значение 0х666, следовательно, это переменная a
mov [rsp+38h+var_14], 666h
mov eax, [rsp+38h+var_14]

Пе­ремен­ной var_18 прис­ваиваем это же зна­чение. Обра­ти вни­мание: ее соз­дает опе­ратор switch для собс­твен­ных нужд. Зна­чит, мы опре­дели­ли, для чего в прог­рамме объ­явле­на вто­рая локаль­ная перемен­ная! Она нуж­на для хра­нения пер­воначаль­ного зна­чения. Таким обра­зом, даже если зна­чение срав­нива­емой перемен­ной var_14 в каком‑то ответ­вле­нии CASE будет изме­нено, это не пов­лияет на резуль­тат выборов, пос­коль­ку зна­чение перемен­ной var_18 не поменя­ется!


mov [rsp+38h+var_18], eax
; Сравниваем значение var_18 с нулем
cmp [rsp+38h+var_18], 0
; Если сравнение успешно, переходим в блок кода, выводящий в консоль "a == 0"
; Этот код получен трансляцией ветки case 0: printf("a == 0");
; Иначе продолжаем выполнение
jzshort loc_140001115
; Сравниваем значение var_18 с 1
cmp [rsp+38h+var_18], 1
; В случае успеха прыгаем внутрь блока кода для вывода "a == 1"
; Этот код получен трансляцией ветки case 1: printf("a == 1");
; Иначе продолжаем выполнение
jzshort loc_140001123
; Сравниваем значение var_18 с 2
cmp [rsp+38h+var_18], 2
; В случае равенства выводим "a == 2"
; Этот код получен трансляцией ветки case 2: printf("a == 2");
; Иначе продолжаем выполнение
jzshort loc_140001131
; Сравниваем var_18 и 0x666
cmp [rsp+38h+var_18], 666h
; Если равно, выводим "a == 666h"
; Этот код получен трансляцией ветки case 0x666: printf("a == 666h");
jzshort loc_14000113F

Ес­ли мы досюда доб­рались, зна­чит, ни одно усло­вие не сра­бота­ло, поэто­му выпол­няем дефол­тное дей­ствие: дела­ем безус­ловный переход в блок кода для вывода строч­ки Default.


Этот код получен тран­сля­цией вет­ки default: printf("Default");:


jmp short loc_14000114D
; ------------------------------------------------
loc_140001115:; CODE XREF: main+19↑j
; printf("a == 0");
lea rcx, _Format ; "a == 0"
call printf

А вот этот безус­ловный переход, вынося­щий управле­ние за пре­делы switch — в конец прог­раммы, есть опе­ратор break, находя­щий­ся в кон­це каж­дой вет­ки. Если бы его не было, то начали бы выпол­нять­ся все осталь­ные вет­ки case, незави­симо от того, к какому зна­чению var_18 они при­над­лежат!


jmp short loc_140001159 ; break
; ------------------------------------------------
loc_140001123:; CODE XREF: main+20↑j
; printf("a == 1");
lea rcx, aA1; "a == 1"
call printf
jmp short loc_140001159 ; break
; ------------------------------------------------
loc_140001131:; CODE XREF: main+27↑j
; printf("a == 2");
lea rcx, aA2; "a == 2"
call printf
jmp short loc_140001159 ; break
; ------------------------------------------------
loc_14000113F:; CODE XREF: main+31↑j
; printf("a == 666h");
lea rcx, aA666h ; "a == 666h"
call printf
jmp short loc_140001159 ; break
; ------------------------------------------------
loc_14000114D:; CODE XREF: main+33↑j
; printf("Default");
lea rcx, aDefault ; "Default"
call printf
loc_140001159: ; Конец SWITCH ; CODE XREF: main+41↑j
; main+4F↑j ...
; Возвращаем 0
xor eax, eax
; Восстанавливаем стек
add rsp, 38h
retn
mainendp

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


Для срав­нения взгля­нем, какой код пос­тро­ит C++Builder 10 на осно­ве этой же прог­раммы:


public main
mainproc near ; DATA XREF: __acrtused+29↑o
; Как много локальных переменных!
var_38= dword ptr -38h
var_34= dword ptr -34h
var_30= dword ptr -30h
var_2C= dword ptr -2Ch
var_28= dword ptr -28h
var_24= dword ptr -24h
var_20= dword ptr -20h
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
; Открываем кадр стека
push rbp
; Резервируем место для локальных переменных
sub rsp, 60h
; В RBP сохраняем указатель на дно стека
lea rbp, [rsp+60h]
; Инициализируем локальные переменные:
mov [rbp+var_4], 0
mov [rbp+var_8], ecx
mov [rbp+var_10], rdx
; var_14 присваиваем значение 0х666, следовательно, это переменная a
mov [rbp+var_14], 666h
; В ECX помещаем значение var_14
mov ecx, [rbp+var_14]
; Следующим элегантным образом сравниваем значение var_14 с нулем
test ecx, ecx

Ко­ман­да TEST не меня­ет зна­чение опе­ран­дов, поэто­му прис­ваиваем перемен­ной var_18 зна­чение 0х666. Выходит, var_18 — авто­мати­чес­кая перемен­ная, соз­данная switch для сво­ей работы, что­бы при изме­нении var_14 внут­ри какой‑либо вет­ки кода это не пов­лияло на даль­нейший выбор пути выпол­нения.


mov [rbp+var_18], ecx


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