Категория > Новости > Фундаментальные основы хакерства. Идентифицируем возвращаемое функцией значение - «Новости»
Фундаментальные основы хакерства. Идентифицируем возвращаемое функцией значение - «Новости»8-03-2022, 00:00. Автор: Peterson |
|
странице автора. Традиционно под возвращаемым функцией значением понимается то, что передает оператор int xdiv(int a, int b, int *c=0) { if (!b) return -1; if (c) c[0] = a % b; return a / b;}Функция Популярные издания склонны упрощать проблему идентификации возвращенного функцией значения, рассматривая один лишь частный случай с оператором Мы же рассмотрим следующие механизмы:
Вообще‑то к этому списку не помешало бы добавить возврат значений через дисковые и проецируемые в память файлы, но это выходит за рамки обсуждаемой темы (хотя, рассматривая функцию как «черный ящик» с входом и выходом, нельзя не признать, что вывод функцией результатов своей работы в файл фактически и есть возвращаемое ею значение). Возврат значения оператором returnПо общепринятому соглашению на платформе x86-64 значение, возвращаемое оператором А как возвращаются типы, занимающие более 8 байт? Скажем, некая функция возвращает структуру, состоящую из сотен байтов, или объект сопоставимого размера. Ни то ни другое в регистры не запихнешь! Оказывается, если возвращаемое значение не может быть втиснуто в регистры, компилятор скрыто от программиста передает функции неявный аргумент — ссылку на локальную переменную, в которую и записывается возвращенный результат. Таким образом, функции Давай проверим наши предположения. Откомпилируй следующий код с отключенной оптимизацией в Visual C++: #include <iostream>// Структура включает три переменные типа doublestruct mystruct { double d_var1; double d_var2; double d_var3;};mystruct MyFunc1(double a, double b, double c) { mystruct my; my.d_var1 = a; my.d_var2 = b; my.d_var3 = c; return my;}void MyFunc2(struct mystruct* my, double a, double b, double c) { my->d_var1 = a; my->d_var2 = b; my->d_var3 = c;}int main() { mystruct my; my = MyFunc1(1.001, 2.002, 3.003); std::cout << my.d_var1 << " " << my.d_var2 << " " << my.d_var3 << std::endl; MyFunc2(&my, 3.004, 5.005, 6.006); std::cout << my.d_var1 << " " << my.d_var2 << " " << my.d_var3 << std::endl;}![]() Теперь открой экзешник в IDA и откажись от загрузки отладочной информации, ведь при отладке в боевых условиях никакой дебажной инфы не будет. ![]() На следующей иллюстрации я привел обе функции для сравнения в одном окне VS Code. VS Code Хотя по объему функции различаются, выполняемые ими действия схожи. И без отладочной информации понять их различия непросто. Между тем, если приглядеться, можно обнаружить явные намеки, в какой функции происходит работа над ссылочным типом, а где выполняется возврат по значению. Смотри: sub_140001000 proc near; CODE XREF: main+33↓pvar_38= qword ptr -38hvar_30= qword ptr -30hvar_28= qword ptr -28harg_0= qword ptr 8arg_8= qword ptr 10harg_10= qword ptr 18harg_18= qword ptr 20h; Инициализация переменных-аргументовmovsd [rsp+arg_18], xmm3movsd [rsp+arg_10], xmm2movsd [rsp+arg_8], xmm1mov [rsp+arg_0], rcx ; В RCX первый аргумент целочисленный; Указатель на структуру; Открываем кадр стекаpush rsipush rdisub rsp, 28h; Заполнение полей структурыmovsd xmm0, [rsp+38h+arg_8]movsd [rsp+38h+var_38], xmm0movsd xmm0, [rsp+38h+arg_10]movsd [rsp+38h+var_30], xmm0movsd xmm0, [rsp+38h+arg_18]movsd [rsp+38h+var_28], xmm0lea rax, [rsp+38h+var_38]mov rdi, [rsp+38h+arg_0]mov rsi, raxmov ecx, 18hrep movsb ; Побайтно копируем структуруmov rax, [rsp+38h+arg_0] ; В RAX возвращаем указатель на структуру; Закрываем кадр стекаadd rsp, 28hpop rdipop rsiretnsub_140001000 endpВ начале функции значения копируются из регистров в память. Видно, что первый аргумент целочисленный. Можно предположить, что это указатель на структуру. Остальные аргументы передаются в регистрах Далее копируется 24 байта ( mystruct Func(double a, double b, double c)Разберем дизассемблерный листинг второй функции. sub_140001060 proc near; CODE XREF: main+DD↓parg_0= qword ptr 8arg_8= qword ptr 10harg_10= qword ptr 18harg_18= qword ptr 20h; Инициализация переменных-аргументовmovsd [rsp+arg_18], xmm3movsd [rsp+arg_10], xmm2movsd [rsp+arg_8], xmm1mov [rsp+arg_0], rcx; Модификация полей структурыmov rax, [rsp+arg_0]movsd xmm0, [rsp+arg_8]movsd qword ptr [rax], xmm0mov rax, [rsp+arg_0]movsd xmm0, [rsp+arg_10]movsd qword ptr [rax+8], xmm0mov rax, [rsp+arg_0]movsd xmm0, [rsp+arg_18]movsd qword ptr [rax+10h], xmm0retnsub_140001060 endpНачало такое же, как в прошлый раз. Но в этой функции не открывается кадр стека, что может указывать на работу с существующей структурой. Далее одно за другим выполняется замещение значений полей структуры. В конце функция ничего не возвращает. Отсюда можно вывести такой прототип: void Func(struct mystruct* my, double a, double b, double c)Нам удалось определить различия функций только после их глубокого анализа. Теперь взглянем на самодеятельность компилятора C++Builder. Сравнение функций MyFunc1 и MyFunc2, транслированных компилятором C++Builder 10.4 В этом случае при беглом взгляде разница совсем незаметна. В обеих функциях присутствует кадр стека. Между тем и здесь есть зацепка: после открытия кадра стека только в mystruct MyFunc1(double a, double b, double c) Определение типа возвращаемого значенияТип возвращаемого значения можно приблизительно определить по размеру регистра, в котором он возвращается. Если возвращаемое значение помещается в Если функция при выходе явно присваивает одному из перечисленных выше регистров некоторое значение, значит, оно возвращается в вызывающую функцию. Если же эти регистры остаются неопределенными, то, скорее всего, возвращается тип Для закрепления изученного рассмотрим следующий пример, демонстрирующий механизм возвращения основных типов значений: // Демонстрация возвращения переменной типа char оператором returnchar char_func(char a, char b) { return a + b;}// Демонстрация возвращения переменной типа int оператором returnint int_func(int a, int b) { return a + b;}// Демонстрация возвращения переменной типа int64 оператором return__int64 int64_func(__int64 a, __int64 b) { return a + b;}// Демонстрация возвращения указателя на int оператором return// Демонстрация возвращения значения через аргументы, переданные по ссылкеint* near_func(int* a, int* b) { int* c = new int; c[0] = a[0] + b[0]; return c;}int main() { int a; int b; a = 0x666; b = 0x777; printf("%I64xn",Результат его компиляции в Microsoft Visual C++ 2019 с отключенной оптимизацией будет выглядеть так: char char_func(char, char) proc near; Два аргумента размером в байтarg_0= byte ptr 8arg_8= byte ptr 10h; Инициализация переменных значениями из регистров, переданных в параметрахmov [rsp+arg_8], dlmov [rsp+arg_0], clИз памяти оба значения копируются в 32-битные регистры. Вместе с тем они преобразуются из типа movsx eax, [rsp+arg_0]movsx ecx, [rsp+arg_8]Сложение значений, находящихся в регистрах. Сумма накапливается в регистре add eax, ecxК сожалению, достоверно определить тип возвращаемого значения невозможно. Он с равным успехом может представлять собой retnchar char_func(char, char) endpint int_func(int, int) proc near; Два аргумента размером в 4 байтаarg_0= dword ptr 8arg_8= dword ptr 10h; Инициализация переменных значениями из регистров, переданных в параметрахmov [rsp+arg_8], edxmov [rsp+arg_0], ecx; Копирование значений из памяти в 32-битные регистрыmov eax, [rsp+arg_8]mov ecx, [rsp+arg_0]; Сложение значений из регистровadd ecx, eaxКопирование в регистр mov eax, ecxretnint int_func(int, int) endp__int64 int64_func(__int64, __int64) proc near; Два аргумента размером в четыре слова (8 байт)arg_0= qword ptr 8arg_8= qword ptr 10h; Загружаем в память значения из 64-битных регистровmov [rsp+arg_8], rdxmov [rsp+arg_0], rcx; Копируем значения из памяти в более удобные регистрыmov rax, [rsp+arg_8]mov rcx, [rsp+arg_0]; Выполняем суммированиеadd rcx, rax; Переносим результат в регистр RAX, который и будет возвращенmov rax, rcxСледовательно, возвращаемый тип имеет размер 64 бита, то есть retn__int64 int64_func(__int64, __int64) endpint * near_func(int *, int *) proc near; Две переменные размером в четыре слова (8 байт)var_18= qword ptr -18hvar_10= qword ptr -10h; Два аргумента размером в четыре слова (8 байт)arg_0= qword ptr 8arg_8= qword ptr 10h; Загружаем в память значения из 64-битных регистровmov [rsp+arg_8], rdxmov [rsp+arg_0], rcx; Открываем кадр стекаsub rsp, 38hmov ecx, 4; size; Выделяем 4 байта из кучиcall operator new(unsigned __int64); Заносим указатель на выделенную память в переменную var_10mov [rsp+38h+var_10], raxmov rax, [rsp+38h+var_10]mov [rsp+38h+var_18], raxСледующий витиеватый код используется, чтобы определить смещение внутри массива. Стоп! Но у нас же нет никакого массива. А как же строка mov eax, 4imul rax, 0mov ecx, 4imul rcx, 0; Готовим аргументы для сложенияmov rdx, [rsp+38h+arg_0]mov eax, [rdx+rax]mov rdx, [rsp+38h+arg_8]; Суммируемadd eax, [rdx+rcx]; Снова определяем смещениеmov ecx, 4imul rcx, 0mov rdx, [rsp+38h+var_18]mov [rdx+rcx], eax; Помещаем в RAX возвращаемое значениеmov rax, [rsp+38h+var_18]; Закрываем кадр стекаadd rsp, 38hretn int * near_func(int *, int *) endpmainproc nearvar_38= dword ptr -38hvar_30= qword ptr -30hvar_28= qword ptr -28hb= dword ptr -20ha= dword ptr -1Chvar_18= qword ptr -18h; __unwind { // __GSHandlerCheck; Открываем кадр стекаsub rsp, 58hmov rax, cs:__security_cookiexor rax, rsp; Инициализируем переменныеmov [rsp+58h+var_18], rax; В переменную a типа int заносим значение 0x666mov [rsp+58h+a], 666h; В переменную b типа int заносим значение 0x777mov [rsp+58h+b], 777h; В 8-битные регистры помещаем параметры типа char перед вызовом функцииmov dl, 2; bmov cl, 1; aВызываем функцию call char_func(char,char)Расширяем возвращенное функцией значение до movsx eax, al; Сохраняем результат в переменной var_38mov [rsp+58h+var_38], eax; На этот раз помещаем параметры в 32-битные регистры, следовательно, их тип intmov edx, 4; bmov ecx, 3; a; Вызываем функцию int_func(3,4), возвращающую значение типа intcall int_func(int,int)mov ecx, [rsp+58h+var_38]; Прибавляем результат к значению переменной var_38add ecx, eaxmov eax, ecxПреобразуем двойное слово, содержащееся в регистре cdqe ; Копируем расширенное четверное слово в переменную var_30mov [rsp+58h+var_30], rax ; Готовим 32-битные параметрыmov edx, 6; bmov ecx, 5; aВызываем функцию call int64_func(__int64,__int64)mov rcx, [rsp+58h+var_30]; Прибавляем результат, возвращенный функцией int64_func, к четверному словуadd rcx, raxmov rax, rcx; Сохраняем результат предыдущего сложения в переменной var_28mov [rsp+58h+var_28], raxДля передачи в качестве параметров загружаем в регистры leaКак мы видим, в идентификации типа значения, возвращенного оператором #include <string.h>struct XT { char s0[4]; int x;};// Функция возвращает значение типа "структура XT" по значениюstruct XT MyFunc(const char* a, int b) { struct XT xt; strcpy_s(&xt.s0[0], 4, a); xt.x = b; return xt;}int main() { struct XT xt; xt = MyFunc("Hello, Sailor!", 0x666); printf("%s %xn", &xt.s0[0], xt.x);}Внимание! Не запускай откомпилированную программу. Мало того что она содержит ошибку (в буфер вместимостью четыре символа помещается строка размером в 14 символов), так еще некоторые антивирусы считают такую программу малварью. Перейти обратно к новости |