Содержание статьи
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
в настоящий «мясной рулет», доверху нашпигованный всевозможными операциями отношений. Давай откомпилируем следующий код компилятором 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"); }}
Теперь посмотрим в IDA на результат дизассемблирования.
main 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:
:
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
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»