Категория > Новости > Фундаментальные основы хакерства. Учимся идентифицировать аргументы функций - «Новости»
Фундаментальные основы хакерства. Учимся идентифицировать аргументы функций - «Новости»30-05-2021, 00:02. Автор: Нинель |
странице автора. Идентификация аргументов функций — ключевое звено в исследовании дизассемблированных листингов. Поэтому приготовь чай и печеньки, разговор будет долгим. В сегодняшней статье мы рассмотрим список соглашений о передаче параметров, используемых в разных языках программирования и компиляторах. В довесок мы рассмотрим приложение, в котором можно отследить передачу параметров, а также определить их количество и тип. Это может быть весьма нетривиальной задачей, особенно если один из параметров — структура. Существует три способа передать аргументы функции: через стек, регистры и комбинированный — через стек и регистры одновременно. К этому списку вплотную примыкает и неявная передача аргументов через глобальные переменные. Сами же аргументы могут передаваться либо по значению, либо по ссылке. В первом случае функции передается копия соответствующей переменной, а во втором — указатель на саму переменную. Соглашения о передаче параметровДля успешной совместной работы вызывающая функция должна не только знать прототип вызываемой, но и «договориться» с ней о способе передачи аргументов: по ссылке или по значению, через регистры или через стек. Если через регистры — оговорить, какой аргумент в какой регистр помещен, а если через стек — определить порядок занесения аргументов и выбрать «ответственного» за очистку стека от аргументов после завершения вызываемой функции. Неоднозначность механизма передачи аргументов — одна из причин несовместимости различных компиляторов. Кажется, почему бы не заставить всех производителей компиляторов придерживаться какой‑то одной схемы? Увы, это принесет больше проблем, чем решит. Каждый механизм имеет свои достоинства и недостатки и, что еще хуже, тесно связан с самим языком. В частности, «сишные» вольности с соблюдением прототипов функций возможны именно потому, что аргументы из стека выталкивает не вызываемая, а вызывающая функция, которая наверняка помнит, что она передавала. Например, функции На паскале подобная выходка привела бы либо к ошибке компиляции, либо к краху программы, так как в нем стек очищает непосредственно вызываемая функция. Если она этого не сделает (или сделает неправильно, вытолкнув не то же самое количество машинных слов, которое ей было передано), стек окажется несбалансированным и все рухнет. Точнее, у материнской функции «слетит» вся адресация локальных переменных, а вместо адреса возврата в стеке окажется, что глюк на душу положит. Минусом «сишного» решения является незначительное увеличение размера генерируемого кода, ведь после каждого вызова функции приходится вставлять машинную команду (и порой не одну) для выталкивания аргументов из стека, а у паскаля эта команда внесена непосредственно в саму функцию и потому встречается в программе один‑единственный раз. Не найдя золотой середины, разработчики компиляторов решили использовать все доступные механизмы передачи данных, а чтобы справиться с проблемой совместимости, стандартизировали каждый из механизмов, введя ряд соглашений.
x64 Вместе с появлением архитектуры x64 для нее было изобретено только одно новое соглашение вызова, заменившее собой все остальные:
Однако благодаря обратной совместимости с x86 современные процессоры на базе x86_64 также поддерживают все перечисленные способы передачи параметров. Стоит отметить, что регистры Цели и задачиПри исследовании функции перед нами стоят следующие задачи: определить, какое соглашение используется для вызова, подсчитать количество аргументов, передаваемых функции (и/или используемых функцией), и, наконец, выяснить тип и назначение самих аргументов. Начнем? Тип соглашения грубо идентифицируется по способу вычистки стека. Если его очищает вызываемая функция, мы имеем дело c cdecl, в противном случае это либо stdcall, либо PASCAL. Такая неопределенность в отождествлении вызвана тем, что подлинный прототип функции неизвестен и, стало быть, порядок занесения аргументов в стек определить невозможно. Единственная зацепка: зная компилятор и предполагая, что программист использовал тип вызовов по умолчанию, можно уточнить тип вызова функции. Однако в программах под Windows широко используются оба типа вызовов: и PASCAL (он же WINAPI), и stdcall, поэтому неопределенность по‑прежнему остается. Впрочем, порядок передачи аргументов ничего не меняет: имея в наличии и вызывающую, и вызываемую функции, между передаваемыми и принимаемыми аргументами всегда можно установить взаимную однозначность. Или, проще говоря, если действительный порядок передачи аргументов известен (а он и будет известен, см. вызывающую функцию), то знать очередность расположения аргументов в прототипе функции уже ни к чему. Другое дело — библиотечные функции, прототип которых известен. Зная порядок занесения аргументов в стек, по прототипу можно автоматически восстановить тип и назначение аргументов! Определение количества и типа передачи аргументовКак уже было сказано выше, аргументы могут передаваться либо через стек, либо через регистры, либо и через стек, и через регистры сразу, а также неявно через глобальные переменные. Если бы стек был задействован только для передачи аргументов, подсчитать их количество было бы относительно легко. Увы, стек активно используется и для временного хранения регистров с данными. Поэтому, встретив инструкцию «заталкивания» Если функция следует соглашению stdcall (или PASCAL), она наверняка очищает стек командой Логично предположить, что количество занесенных в стек байтов равно количеству выталкиваемых, иначе после завершения функции стек окажется несбалансированным и программа рухнет (о том, что оптимизирующие компиляторы допускают дисбаланс стека на некотором участке, мы помним, но поговорим об этом потом). Отсюда следует: количество аргументов равно количеству переданных байтов, деленному на размер машинного слова. Под машинным словом понимается не только два байта, но и размер операндов по умолчанию, в 32-разрядном режиме машинное слово равно четырем байтам (двойное слово), в 64-разрядном режиме машинное слово — это учетверенное слово (восемь байтов). Верно ли это? Нет! Далеко не всякий аргумент занимает ровно один элемент стека. Взять тот же тип int, отъедающий только половину. Или символьную строку, переданную не по ссылке, а по непосредственному значению: она «скушает» столько байтов, сколько захочет. К тому же строка может засылаться в стек (как и структура данных, массив, объект) не командой Если я успел окончательно тебя запутать, то попробуем разложить по полочкам тот кавардак, что образовался в твоей голове. Итак, анализом кода вызывающей функции установить количество переданных через стек аргументов невозможно. Даже количество переданных байтов определяется весьма неуверенно. С типом передачи полный мрак. Позже мы к этому еще вернемся, а пока вот пример. Но не волнуйся, нам не пришли кранты — мы еще повоюем! Большую часть проблем решает анализ вызываемой функции. Выяснив, как она манипулирует переданными ей аргументами, мы установим и их тип, и количество! Для этого нам придется познакомиться с адресацией аргументов в стеке, но, прежде чем приступить к работе, рассмотрим в качестве небольшой разминки следующий пример: #include <string.h>struct XT { Вывод нашего приложенияРезультат его компиляции компилятором Microsoft Visual C++ с включенной поддержкой платформы x64 и в релизном режиме, но с выключенной оптимизацией (/Od) выглядит так:
main proc near
var_58 = byte ptr -58h
Dst= byte ptr -38h
var_24 = dword ptr -24h
var_20 = qword ptr -20h
; Инициализируем стек
push rsi
push rdi
Перейти обратно к новости |