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

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

  • Ищем операторы switch-case-break в бинарном коде
  • Отличия switch от оператора case языка Pascal
  • Обрезка длинных деревьев
  • Сложные случаи балансировки, или оптимизирующая балансировка
  • Ветвления в case-обработчиках
  • Итоги

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

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

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

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

 

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

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

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

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

Тран­сля­ция опе­рато­ра 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-casemain proc near ; CODE XREF: __scrt_common_main_seh+107↓p ; DATA XREF: .pdata:0000000140004018↓o; Объявляем две локальные переменные,; но почему две, если в исходном коде объявлена только одна?var_18 = dword ptr -18hvar_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");; Иначе продолжаем выполнение jz short loc_140001115; Сравниваем значение var_18 с 1 cmp [rsp+38h+var_18], 1; В случае успеха прыгаем внутрь блока кода для вывода "a == 1"; Этот код получен трансляцией ветки case 1: printf("a == 1");; Иначе продолжаем выполнение jz short loc_140001123; Сравниваем значение var_18 с 2 cmp [rsp+38h+var_18], 2; В случае равенства выводим "a == 2"; Этот код получен трансляцией ветки case 2: printf("a == 2");; Иначе продолжаем выполнение jz short loc_140001131; Сравниваем var_18 и 0x666 cmp [rsp+38h+var_18], 666h; Если равно, выводим "a == 666h"; Этот код получен трансляцией ветки case 0x666: printf("a == 666h"); jz short 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 printfloc_140001159: ; Конец SWITCH ; CODE XREF: main+41↑j ; main+4F↑j ...; Возвращаем 0 xor eax, eax; Восстанавливаем стек add rsp, 38h retnmain endp

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

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

public mainmain proc near ; DATA XREF: __acrtused+29↑o; Как много локальных переменных!var_38 = dword ptr -38hvar_34 = dword ptr -34hvar_30 = dword ptr -30hvar_2C = dword ptr -2Chvar_28 = dword ptr -28hvar_24 = dword ptr -24hvar_20 = dword ptr -20hvar_1C = dword ptr -1Chvar_18 = dword ptr -18hvar_14 = dword ptr -14hvar_10 = qword ptr -10hvar_8 = dword ptr -8var_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

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

Ответить

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