Подобно сохранению указателей функций в стеке, указатели функции могут быть сохранены в динамически распределяемой памяти.
Разрушение указателя функции
Основная уловка, применяемая к динамически распределяемой памяти, – разрушение указателя функции. Для этого существует много способов. Для начала можно попробовать подменить один объект из динамически распределяемой памяти на другой из соседней «кучи». Объекты класса и структуры часто хранятся в динамически распределяемой памяти, поэтому такая возможность существует. Например, для этого можно воспользоваться простым для понимания способом, известным под названием «нарушение границы» или «посягательство на объект» (trespassing).
Нарушение границы динамически распределяемой памятиВ приведенном ниже примере два объекта класса размещены в динамически распределяемой памяти. При переполнении статического буфера одного из них нарушаются границы соседнего объекта. В результате во втором объекте перезаписывается указатель
vtable– указатель таблицы виртуальных функций
(virtual-function table pointer).Перезапись указателя виртуальных функций во втором объекте приводит к тому, что он начинает указывать на заранее подготовленный буфер – заготовку Троянской таблицы, куда затем записываются новые адреса функций класса. Один из них – адрес деструктора. Перезапись адреса деструктора приводит к вызову нового деструктора при удалении объекта. Указанным способом можно управлять любой программой по своему усмотрению – достаточно изменить указатель деструктора таким образом, чтобы он указывал на программный код полезной нагрузки. Единственное, что может помешать, – это нулевой указатель в списке адресов объектов динамически распределяемой памяти. Тогда программный код полезной нагрузки должен быть или размещен в области, указатель на которую не равен нулю, или следует воспользоваться одним из ранее изученных способов работы со стеком для загрузки в регистр EIP адреса перехода на нужную программу. Этот способ демонстрируется следующей программой.
// class_tres1.cpp : Defines the entry point for the console
// application.
#include
#include
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, «\x77\x77\x77\x77\x88\x88\x88\x88XX
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 приведены пояснения к примеру. Близость между объектами динамически распределяемой памяти позволяет во время переполнения буфера подменить указатель виртуальных функций соседнего объекта динамически распределяемой памяти. Подмененный указатель начинает указывать на контролируемый буфер с новой таблицей виртуальных функций. При попытке вызова функций класса будут вызываться функции, на которые указывают указатели в новой таблице виртуальных функций. Лучше всего подменить указатель на деструктор класса, поскольку он всегда вызывается при удалении объекта из памяти.
Рис. 8.24. Нарушение границы динамически распределяемой памяти
Новаторские принципы построения программного кода полезной нагрузки
Изученные хитроумные способы переполнения буфера дополняют новаторские принципы построения программного кода полезной нагрузки, позволяющие ему успешно выполняться в разных средах. В секции приведены современные сведения о построении программного кода полезной нагрузки, которые позволяют повысить функциональные возможности и гибкость управляющего кода.
Программы переполнения буфера предполагают легкость модификации. Каждая часть программы переполнения буфера, будь то инициализация буфера, выбор точки перехода или другие компоненты программного кода полезной нагрузки, должна быть адаптирована к конкретной ситуации. В конечном счете программа переполнения буфера должна быть оптимизирована для работы в условиях ограниченности доступной памяти, прессинга со стороны систем обнаружения вторжения или проникновения в ядро операционной системы.
Читать дальше
Конец ознакомительного отрывка
Купить книгу