Содержание статьи
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Идентификация указателя this
Указатель this — это настоящий золотой ключик или, если угодно, спасательный круг, позволяющий не утонуть в бурном океане ООП. Именно благодаря this можно определять принадлежность вызываемой функции к тому или иному классу. Поскольку все невиртуальные функции объекта вызываются непосредственно — по фактическому адресу, объект как бы расщепляется на составляющие его функции еще на стадии компиляции. Не будь указателей this, восстановить иерархию функций было бы принципиально невозможно!
Таким образом, правильная идентификация this очень важна. Единственная проблема: как отличить его от указателей на массивы и структуры? Ведь экземпляр класса идентифицируется по указателю this (если на выделенную память указывает this, это экземпляр класса), однако сам this по определению — это указатель, ссылающийся на экземпляр класса. Замкнутый круг! К счастью, есть одна лазейка... Код, манипулирующий указателем this, весьма специфичен, что и позволяет отличить this от всех остальных указателей.
Вообще‑то у каждого компилятора свой почерк, который настоятельно рекомендуется изучить, дизассемблируя собственные программы на C++, но существуют и универсальные рекомендации, применимые к большинству реализаций. Поскольку this — это неявный аргумент каждой функции — члена класса, то логично отложить разговор о его идентификации до раздела «Идентификация аргументов функций». Здесь же мы обсудим, как реализуют передачу указателя this самые популярные компиляторы.
Здесь мы, конечно, говорим об архитектуре x64. На 32-битной платформе параметры, выровненные до 32-битного размера, передаются через стек. С другой стороны, на 64-битной платформе дела обстоят интереснее: первые четыре целочисленных аргумента передаются в регистрах RCX, RDX, R8, R9. Если целочисленных аргументов больше, остальные размещаются в стеке. Аргументы, имеющие значения с плавающей запятой, передаются в регистрах XMM0, XMM1, XMM2, XMM3. При этом 16-битные аргументы передаются по ссылке. Замечу, все это касается соглашения о вызовах в операционных системах Microsoft (Microsoft ABI), в Unix-подобных системах дела обстоят по‑другому. Но не будем распылять на них свое внимание.
Оба протестированных мною компилятора, Visual C++ 2019 и C++Builder 10.3, независимо от соглашения вызова функции (__cdecl, __clrcall, __stdcall, __fastcall, __thiscall) передают указатель this в регистре RCX, что соответствует его природе: this — целочисленный аргумент.
Идентификация операторов new и delete
Операторы new и delete транслируются компилятором в вызовы библиотечных функций, которые могут быть распознаны точно так же, как и обычные библиотечные функции. Автоматически распознавать библиотечные функции умеет, в частности, IDA Pro, снимая эту заботу с плеч исследователя. Однако IDA Pro есть не у всех и далеко не всегда в нужный момент находится под рукой, да к тому же не все библиотечные функции она знает, а из тех, что знает, не всегда узнает new и delete... Словом, причин идентифицировать их вручную предостаточно.
Реализация new и delete может быть любой, но Windows-компиляторы в большинстве своем редко реализуют функции работы с кучей самостоятельно. Зачем это? Намного проще обратиться к услугам операционной системы. Однако наивно ожидать вместо new появление вызова HeapAlloc, а вместо delete — HeapFree. Нет, компилятор не так прост! Разве он может отказать себе в удовольствии «вырезания матрешек»? Оператор new транслируется в функцию new, вызывающую для выделения памяти malloc, malloc же, в свою очередь, обращается к HeapAlloc (или ее подобию — в зависимости от реализации библиотеки работы с памятью) — своеобразной «обертке» одноименной Win32 API-процедуры. Картина с освобождением памяти аналогична.
Углубляться в дебри вложенных вызовов слишком утомительно. Нельзя ли new и delete идентифицировать как‑нибудь иначе, с меньшими трудозатратами и без лишней головной боли? Разумеется, можно! Давай вспомним все, что мы знаем о new:
- new принимает единственный аргумент — количество байтов выделяемой памяти, причем этот аргумент в подавляющем большинстве случаев вычисляется еще на стадии компиляции, то есть является константой;
-
если объект не содержит ни данных, ни виртуальных функций, его размер равен единице (минимальный блок памяти, выделяемый только для того, чтобы было на что указывать указателю
this); отсюда будет очень много вызовов типаmov ecx, 1 ; sizecall XXXгде
XXXи есть адресnew! Вообще же, типичный размер объектов составляет менее сотни байтов... ищи часто вызываемую функцию с аргументом‑константой меньше ста байтов; функция
new— одна из самых популярных библиотечных функций, ищи функцию с «толпой» перекрестных ссылок;самое характерное:
newвозвращает указательthis, аthisочень легко идентифицировать даже при беглом просмотре кода (обычно он возвращается в регистреRCX);возвращенный
newрезультат всегда проверяется на равенство нулю (операторами типаtest,RCX RCX), и, если он действительно равен нулю, конструктор (если он есть) не вызывается.
«Родимых пятен» у new более чем достаточно для быстрой и надежной идентификации, тратить время на анализ кода этой функции совершенно ни к чему! Единственное, о чем следует помнить: new используется не только для создания новых экземпляров объектов, но и для выделения памяти под массивы (структуры) и изредка — под одиночные переменные (типа int , что вообще маразм, но некоторые так делают). К счастью, отличить два этих способа очень просто — ни у массивов, ни у структур, ни у одиночных переменных нет указателя this!
Сложнее идентифицировать delete. Каких‑либо характерных признаков эта функция не имеет. Да, она принимает единственный аргумент — указатель на освобождаемый регион памяти, причем в подавляющем большинстве случаев это указатель this. Но помимо нее, this принимают десятки, если не сотни других функций! Раньше в эпоху 32-битных камней у исследователя была удобная зацепка за то, что delete в большинстве случаев принимал указатель this через стек, а остальные функции — через регистр. В настоящее же время, как мы уже неоднократно убеждались, любые функции принимают параметры через регистры:
mov rcx, [rsp+58h+block] ; blockcall operator delete(void *,unsigned __int64)В данном случае IDA без замешательств распознала delete.
К тому же delete ничего не возвращает, но мало ли функций поступают точно так же? Единственная зацепка — вызов delete следует за вызовом деструктора (если он есть), но, поскольку конструктор как раз и идентифицируется как функция, предшествующая delete, образуется замкнутый круг!
Ничего не остается, кроме как анализировать содержимое функции: delete рано или поздно вызывает HeapFree (хотя тут возможны и варианты: так, Borland/Embarcadero содержит библиотеки, работающие с кучей на низком уровне и освобождающие память вызовом VirtualFree). К счастью, IDA Pro в большинстве случаев опознает delete и самостоятельно напрягаться не приходится.
А что произойдет, если IDA не распознает delete? Код будет выглядеть примерно так:
mov rcx, [rsp+58h+block] ; blockcall XXXcmp [rsp+58h+block], 0jnz short loc_1400010B0Неглубокий анализ показывает: в первой строчке в регистр RCX, очевидно для передачи в качестве параметра, помещается блок памяти. Похоже, это указатель на сущность. А после вызова XXX выполняется сравнение этого блока памяти с нулем и, если блок не обнулен, происходит переход по адресу. Таким несложным образом мы можем легко идентифицировать delete, даже если IDA его не определяет.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»
