Категория > Новости > Фундаментальные основы хакерства. Определяем «почерк» компилятора по вызовам функций - «Новости»
Фундаментальные основы хакерства. Определяем «почерк» компилятора по вызовам функций - «Новости»18-07-2021, 00:01. Автор: Борислав |
предыдущей статье – пример на Visual C++):void MyFunc(double, struct XT) proc neararg_0 = qword ptr 8arg_8 = qword ptr 10hIDA распознала два аргумента, передаваемых функции. Однако не стоит безоговорочно этому доверять, если один аргумент (например, Впрочем, и здесь не все гладко! Ведь никто не мешает вызываемой функции залезать в стек материнской так далеко, как она захочет! Может быть, нам не передавали никаких аргументов вовсе, а мы самовольно полезли в стек и стянули что‑то оттуда. Хотя это случается в основном вследствие программистских ошибок из‑за путаницы с прототипами, считаться с такой возможностью необходимо. Когда‑нибудь вы все равно с этим встретитесь, так что будьте начеку. Число, стоящее после Извлекаем переданные аргументы из регистров процессора и размещаем их в памяти (при этом вспоминаем, что передавали из вызывающей функции): mov [rsp+arg_8], rdx ; указатель на буферmovsd [rsp+arg_0], xmm0 ; значение с плавающей запятойДалее инициализируем стек, подготавливаем регистры к работе, производим необходимые вычисления, затем кладем в регистры значения для передачи параметров функции movОбрати внимание, перед вызовом А вот затем идет последний спецификатор cdecl MyFunc(double a, struct B b)Тип вызова cdecl означает, что стек вычищает вызывающая функция. Вот только, увы, подлинный порядок передачи аргументов восстановить невозможно. C++ Builder, кстати, так же вычищал стек вызывающей функцией, но самовольно изменял порядок передачи параметров. Может показаться, что если программу собирали в C++ Builder, то мы просто изменяем порядок аргументов на обратный, вот и все. Увы, это не так просто. Если имело место явное преобразование типа функции в cdecl, то C++ Builder без лишней самодеятельности поступил бы так, как ему велели, и тогда бы обращение порядка аргументов дало бы неверный результат! Впрочем, подлинный порядок следования аргументов в прототипе функции не играет никакой роли. Важно лишь связать передаваемые и принимаемые аргументы, что мы и сделали. Обратите внимание: это стало возможно лишь при совместном анализе и вызываемой, и вызывающей функций! Анализ лишь одной из них ничего бы не дал! infoНикогда не следует безоговорочно полагаться на достоверность строки спецификаторов. Поскольку спецификаторы формируются «вручную» самим программистом, тут возможны ошибки, подчас весьма трудноуловимые и дающие после компиляции чрезвычайно загадочный код! Далее деинициализируем стек и закругляемся. add rsp, 28hretn Соглашения о вызовахКое‑какие продвижения уже есть — мы уверенно восстановили прототип нашей первой функции. Но это только начало. Еще много миль предстоит пройти… Если устал — передохни, тяпни кваса, поболтай с кем‑нибудь и продолжим на свежую голову. Мы приступаем еще к одной очень важной теме — сравнительному анализу разных типов вызовов функций и их реализации в разных компиляторах. stdcallНачнем с изучения стандартного соглашения о вызове — stdcall. Рассмотрим следующий пример. Исходник примера stdcall #include <string.h>int __stdcall MyFunc(int a, int b, const char* c){ return a + b + strlen(c);}int main(){ printf("%xn", MyFunc(0x666, 0x777, "Hello,World!"));}Вот, как должен выглядеть результат его компиляции в Visual C++ с отключенной оптимизацией, то есть ключом main proc nearsub rsp, 28hIDA хорошо нам подсказывает, что константа lea r8, c ; "Hello,World!"mov edx, 777h ; bmov ecx, 666h ; aСледом помещаем в два 32-битных регистра EDX и ECX значения двух переменных call MyFunc(int,int,char const *)Обрати внимание, после вызова функции отсутствуют команды очистки стека от занесенных в него аргументов. Если компилятор не схитрил и не прибегнул к отложенной очистке, то, скорее всего, стек очищает сама вызываемая функция, значит, тип вызова — stdcall (что, собственно, и требовалось доказать). mov edx, eaxТеперь, передаем возвращенное функцией значение следующей функции, как аргумент. Эта следующая функция call printfxor eax, eaxadd rsp, 28hretnmain endpТеперь рассмотрим функцию ; int __fastcall MyFunc(int a, int b, const char *c)int MyFunc(int, int, char const *) proc nearIDA пытается самостоятельно восстановить прототип функции и… обламывается. Иными словами, делает это не всегда успешно. Например, «Ида» ошибочно предположила тип вызова var_18 = qword ptr -18hvar_10 = qword ptr -10harg_0 = dword ptr 8arg_8 = dword ptr 10harg_10 = qword ptr 18hПереданные аргументы из регистров помещаются в память, затем после инициализации стека происходит размещение числовых значений в регистрах, где происходит их сложение: mov [rsp+arg_10], r8mov [rsp+arg_8], edxmov [rsp+arg_0], ecxsub rsp, 18hmov eax, [rsp+18h+arg_8]mov ecx, [rsp+18h+arg_0]add ecx, eaxmov eax, ecxПреобразование двойного слова (EAX) в учетверенное (RAX): cdqeКопирование из стека указателя в строку, в регистр RCX и в переменную mov rcx, [rsp+18h+arg_10]mov [rsp+18h+var_10], rcxmov [rsp+18h+var_18], 0FFFFFFFFFFFFFFFFhloc_140001111:И действительно, на следующем шаге она увеличивается на единицу. К тому же здесь мы видим метку безусловного перехода. Похоже, вместо того, чтобы узнать длину строки посредством вызова библиотечной функции inc [rsp+18h+var_18]Значения переменных вновь копируются в регистры для проведения операций над ними. mov rcx, [rsp+18h+var_10]mov rdx, [rsp+18h+var_18]cmp byte ptr [rcx+rdx], 0Значения регистров RCX и RDX складываются, а сумма сравнивается с нулем. В случае, если выражение тождественно, то флаг ZF устанавливается в единицу, в обратном случае – в ноль. Инструкция jnz short loc_140001111Когда флаг ZF равен единице, осуществляется выход из цикла и переход на следующую за ним инструкцию, которая накопленное в переменной‑счетчике число записывает в регистр RCX. После этого происходит сложение значений в регистрах RCX и RAX, как помним, в последнем содержится сумма двух переданных числовых аргументов. mov rcx, [rsp+18h+var_18]add rax, rcxВ завершении функции происходит деинициализация стека: add rsp, 18hretnint MyFunc(int, int, char const *) endpВозвращение целочисленного аргумента на платформе x64 предусмотрено в регистре RAX. На вывод программа печатает: Вывод программы stdcall Легко проверить:
Перейти обратно к новости |