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

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

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

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

 

Ищем операторы 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 не допус­кает).

Од­нако в реаль­ной жиз­ни все про­исхо­дит сов­сем не так. Ком­пилято­ры (даже неоп­тимизи­рующие) тран­сли­руют 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
Вы­вод при­ложе­ния switch_cases

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

Дерево распустило ветки во все стороны. МОЖНО сделать однозначный вывод: в дизассемблируемой программе присутствует оператор множественного выбора switch-case
Де­рево рас­пусти­ло вет­ки во все сто­роны. МОЖ­НО сде­лать однознач­ный вывод: в дизас­сем­бли­руемой прог­рамме при­сутс­тву­ет опе­ратор мно­жес­твен­ного выбора switch-case
main proc near ; CODE XREF: __scrt_common_main_seh+107↓p
; DATA XREF: .pdata: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");
; Иначе продолжаем выполнение
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 printf
loc_140001159: ; Конец SWITCH ; CODE XREF: main+41↑j
; main+4F↑j ...
; Возвращаем 0
xor eax, eax
; Восстанавливаем стек
add rsp, 38h
retn
main endp

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

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

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

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

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

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

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

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


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

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

    Подписаться

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