Защита от хакеров корпоративных сетей
Шрифт:
// class_tres1.cpp : Defines the entry point for the console
// application.
#include <stdio.h>
#include <string.h>
class test1
{
public:
char name[10];
virtual ~test1;
virtual void run;
};
class test2
{
public:
char name[10];
virtual ~test2;
virtual void run;
};
int main(int argc, char* argv[])
{
class test1 *t1 = new class test1;
class test1 *t5 = new class test1;
class test2 *t2 = new class test2;
class test2 *t3 = new class test2;
//////////////////////////////////////
// overwrite t2”s virtual function
// pointer w/ heap address
// 0x00301E54 making the destructor
// appear to be 0x77777777
// and the run function appear to
// be 0x88888888
//////////////////////////////////////
strcpy(t3->name,
XXXXXXXXXX”\ “XXXXXXXXXX XXXXXXXXXX XXXXXXXXXX
XXXX\x54\x1E\x30\x00");
delete t1;
delete t2; // causes destructor 0x77777777 to be called
delete t3;
return 0;
}
void test1::run
{
}
test1::~test1
{
}
void test2::run
{
puts(“hey”);
}
test2::~test2
{
}На рисунке 8.24 приведены пояснения к примеру. Близость между объектами динамически распределяемой памяти позволяет во время переполнения буфера подменить указатель виртуальных функций соседнего объекта динамически распределяемой памяти. Подмененный указатель начинает указывать на контролируемый буфер с новой таблицей виртуальных функций. При попытке вызова функций класса будут вызываться функции, на которые указывают указатели в новой таблице виртуальных функций. Лучше всего подменить указатель на деструктор класса, поскольку он всегда вызывается при удалении объекта из памяти.
Новаторские принципы построения программного кода полезной нагрузки
Изученные хитроумные способы переполнения буфера дополняют новаторские принципы построения программного кода полезной нагрузки, позволяющие ему успешно выполняться в разных средах. В секции приведены современные сведения о построении программного кода полезной нагрузки, которые позволяют повысить функциональные возможности и гибкость управляющего кода.
Программы переполнения буфера предполагают легкость модификации. Каждая часть программы переполнения буфера, будь то инициализация буфера, выбор точки перехода или другие компоненты программного кода полезной нагрузки, должна быть адаптирована к конкретной ситуации. В конечном счете программа переполнения буфера должна быть оптимизирована для работы в условиях ограниченности доступной памяти, прессинга со стороны систем обнаружения вторжения или проникновения в ядро операционной системы.
Использование того, что у вас есть
Даже простые программы часто загружают в память больше программных модулей, чем им действительно нужно. При установке связи с динамически подключаемой библиотекой программа определяет, когда загружать библиотеку: при запуске программы или во время ее выполнения. К сожалению, при использовании динамически подключаемой библиотеки DLL или совместно используемой библиотеки в системе UNIX в память загружается программный код всей библиотеки, а не только необходимые функции. Это означает, что в программу включается не только необходимый программный код, но и масса дополнительных функций. Современные операционные системы и мощные компьютеры не видят в этом ничего плохого, поскольку лишний программный код никогда не будет выполнен и, следовательно, он не окажет никакого воздействия на работу программы.
Но у злоумышленника другое отношение к дополнительному никогда не выполняющемуся коду. Для него он может оказаться чрезвычайно полезным. Его можно использовать для поиска не только точек перехода, но и уже загруженных в память полезных битов и фрагментов программного кода. При условии частой загрузки динамически подключаемых библиотек можно использовать загруженные неиспользуемые функции.
Статическая компоновка библиотек может уменьшить количество добавляемого в программу выполнимого кода до минимума, но на практике это часто не делается. Подобно библиотекам динамической связи, статические библиотеки обычно содержат большой объем программного кода на все случаи жизни,
Например, если библиотека kernel32.dll загружена, то можно использовать любую ее функцию, даже не используемую явно программой. Функцию можно использовать, потому что она, как и все другие компоненты библиотеки, уже загружена в память. Другими словами, при установлении связи с любой динамически подключаемой библиотекой DLL загружается гораздо больше программного кода, чем это кажется на первый взгляд.
Другой пример использования имеющегося под рукой кода относится к UNIX-системам. Речь идет о трюке, который использовался исследователями безопасности для преодоления защиты ранних патчей ядра Linux и модификаций ядра в рамках проекта PAX. Впервые этот трюк применила Solar Designer. Он заключался в записи в стек сначала параметров функции execve, а затем подмены хранимого в стеке содержимого регистра EIP на адрес функции execve. Стек оказывался настроен таким же образом, как и при вызове функции execve. По завершении функции команда ret восстанавливала подмененное содержимое регистра EIP и передавала управление на функцию execve. Следовательно, при подозрении взлома защиты выполнения программ из стека можно заблокировать выполнение программ из стека.
Загрузка новых динамически подключаемых библиотек
Наиболее современные операционные системы поддерживают концепцию совместно используемых библиотек. Они предназначены для уменьшения расхода памяти и многократного использования кода. Уже упоминалось о возможности использования в своих интересах программного кода, загруженного в память, но иногда может потребоваться то, что еще не загружено.
Аналогично обычной программе, программный код полезной нагрузки может при необходимости загрузить динамическую библиотеку и использовать ее функции, как это было показано в примере программы переполнения буфера для Windows NT.
В Windows NT есть пара функций, которыми всегда может воспользоваться программа: LoadLibrary и GetProcAddress. Они позволяют загрузить любую динамически подключаемую библиотеку DLL и вызвать функцию. В системе UNIX для этих целей служат функции dlopen и dlsym.
Перечисленные функции делятся на две группы: функции загрузки библиотеки и функции определения адреса экспортируемой функции. Краткое пояснение каждой функции позволит лучше понять их предназначение.
Функции загрузки библиотеки LoadLibrary или dlopen загружают совместно используемую часть кода в доступную программе память. Совсем не обязательно, что загружаемый код будет выполняться, но после загрузки он доступен для использования. В основном загружаемый код впоследствии выполняется.
Функции GetProcAddress и dlsym определяют в таблице функций динамически подключаемой библиотеки адрес экспортируемой функции. Для поиска в таблице функций используются символические имена и, возможно, необязательные порядковые целые числа – индексы. Входным параметром этих функций является имя искомой функции или ее индекс, а выходным – адрес искомой функции.
Как правило, перечисленные функции загружают в память всякую динамически подключаемую библиотеку DLL. После загрузки библиотеки можно получить адрес любой из ее функций по имени. Поэтому пока доступна динамически подключаемая библиотека, программисту предоставляется очень гибкий и удобный инструмент написания программ.
Известны два основных способа поиска функции при использовании динамических библиотек. Можно или жестко запрограммировать адреса функций, или найти их в таблице импортируемых символов (таблице перехода) атакованного процесса во время его выполнения.