Современные способы переполнения буфера

We use cookies. Read the Privacy and Cookie Policy

Современные способы переполнения буфера

После изучения обязательного минимума пришло время познакомиться с современными способами переполнения буфера. Одни из них применимы повсеместно, другие – в частных случаях. С течением времени злоумышленники узнают о переполнении буфера все больше, поэтому сегодня для успешной защиты от атак переполнения буфера необходимо знать изощренные способы извлечения из него пользы.

Фильтрация входных данных

Современные программисты, стремясь как можно лучше защититься от атак переполнения буфера, начинают создавать программы, которые перед записью входных данных в буфер проверяют, нет ли в них выполняемого программного кода. Подобные программы серьезно затрудняют деятельность злоумышленника, не позволяя ему запросто поместить злонамеренный код в программе. Большие неприятности сулят нулевые байты в буфере, поэтому современные программисты тщательно исследуют безопасность записываемых в буфер данных.

Способов анализа безопасности данных много, и каждый из них по-своему препятствует переполнению буфера.

Например, некоторые программисты проверяют значения входных данных. Если ожидается ввод чисел, то перед записью в буфер проверяется, является ли каждое введенное значение числом. В стандартной библиотеке языка C есть несколько функций, которые проверяют значение введенных данных. Ниже приведены некоторые из них для платформы Win32. Для работы в 16-битном стандарте кодирования символов Unicode существуют аналогичные функции проверки «широких» символов.

int isalnum( int c ); checks if it is in A-Z,a-z,0-9

int isalpha( int c ); checks if it is in A-Z,a-z

int __isascii( int c ); checks if it is in 0x00-0x7f

int isdigit( int c ); checks if it is in 0-9

isxdigit( int c ); checks if it is in 0-9, A-F

Подобные функции реализованы во многих библиотеках С для UNIX.

Хорошая программа переполнения буфера должна преодолевать фильтрацию входных данных. Для этого создаются специальные программы кодирования входных данных, позволяющие обмануть фильтрацию.

Было проведено много исследований в области создания программного кода полезной нагрузки, состоящего из алфавитно-цифровых символов и младших символов кода ASCII. При возможности реализации этим способом необходимых функциональных возможностей исследования увенчались успехом. В частности, был разработан способ кодирования полезной нагрузки по стандартам MIME (MIME – набор стандартов для передачи мультимедийной информации посредством электронной почты) или командой XOR, выполненной над последовательностью из нескольких байт. Применяемая кодировка позволяет маскировать странную последовательность байтов под программный код полезной нагрузки из ASCII-символов.

Другой способ преодоления проверки входных данных заключается в том, чтобы избежать фильтрации данных. Например, присвоив переменной окружения или переменной сессии двоичную строку программного кода полезной нагрузки, можно сократить число байтов, которые должны удовлетворять условиям проверки входных данных.

Частичное переполнение буфера и искажение данных

В последнее время значительно увеличилось число программистов, начавших использовать строковые функции с ограничениями, например функцию strncpy() вместо strcpy(). Этих программистов научили, что функции с ограничениями защищают от переполнения буфера. Как же они удивятся, когда узнают, что зачастую применяют их неправильно.

Широко известна общая ошибка использования функций с ограничениями, получившая название «минус один», когда максимальная длина записываемой в буфер строки приравнивается размеру буфера. При этом часто забывают об обязательном признаке конца строки – завершающем строку нулевом байте. Некоторые функции с ограничениями могут не включать в строку завершающего символа, позволяя строке незаметно слиться со строкой из рядом расположенного буфера. Если позднее обратиться к ней, то два буфера могут рассматриваться как один, способствуя переполнению буфера.

Рассмотрим пример:

[buf1 – 32 bytes ][buf2 – 32 bytes ]

После записи в буфер в bufl ровно 32 байтов два буфера выглядят следующим образом:

[buf1 – 32 bytes of data ][buf2 – 32 bytes ]

Любая последующая попытка переслать из буфера bufl данные может привести к копированию 64-байтной строки данных и переполнению буфера, в который записываются данные.

Другое часто встречающееся неверное использование функций с ограничениями заключается в ошибках программирования или в неправильном расчете контролирующих величин во время выполнения программы. Это может произойти из-за нелепой ошибки или несогласованных изменений в программе в процессе разработки, например в программе был определен буфер фиксированного размера, размер которого не был откорректирован в соответствии с внесенными в программу изменениями. Помните, что размер обрабатываемых данных должен быть согласован с размером буфера получателя информации, а не ее источника. Известны примеры использования в проверках функций strlen(), которые во время выполнения программы подсчитывали число байт в буфере, из которого данные копировались. Эта простая ошибка делает бесполезной любую проверку размеров буферов.

Опасно переполнение не только всего стека, но и так называемое частичное переполнение буфера, когда в стеке происходит подмена не всех, а только отдельных сохраненных значений. Месторасположение буфера в стеке и контроль адресов, по которым копируются в буфер данные, могут сделать невозможным запись в буфер такого количества данных, чтобы при переполнении буфера добраться до области хранения в стеке значения регистра EIP и подменить его. В этом случае при помощи команды ret нельзя передать управление нужной программе, но возможность контроля процессора сохраняется. Для этого можно попытаться подменить содержимое регистра EBP или доступные данные в стеке. Позднее этим можно воспользоваться для взятия под свой контроль атакуемой программы, чтобы заставить ее выполнить не предусмотренные в ней действия.

Например, на сайте www.phrack.org была опубликована статья, в которой рассказан способ получения контроля над вызванной функцией путем изменения единственного байта сохраненного в стеке содержимого регистра EBP. Познакомиться со статьей можно по адресу www.phrack.org/show.php?p=55&a=8.

Побочный эффект проявляется при переполнении буфера вблизи вершины стека, рядом с которым сначала находится область сохранения критических данных, а затем содержимое регистра EIP. При подмене этих данных предпочтительнее было бы завершить работу уязвимой программы, чем позволить злоумышленнику воспользоваться ею. Часто после подмены критических данных программа пытается выполниться с поврежденным стеком. Для противодействия подобным атакам переполнения буфера были придуманы, например, системы, защищенные проверочными величинами (canary-protected systems). В этих системах перед командой завершения функции ret проверяется целостность сохраненных в стеке проверочных величин. Если их целостность нарушена, то, как правило, программа завершается. Но и они не гарантируют полной защиты. Если проверочные величины не псевдослучайные величины, то их можно восстановить. При использовании неизменяемых проверочных величин, а для контроля целостности иногда используются и они, можно подменить данные стека при переполнении буфера, но при этом восстановить проверочные величины для обхода проверки.

Перезапись указателя функции в стеке

Иногда программисты сохраняют в стеке указатели функций и затем по мере необходимости используют их. Часто указатели используются там, где требуется динамически изменять часть программы. Машины сценариев (scripting engines; машина сценариев – приложение, способное выполнять сценарии (script), написанные наязыке сценариев, например VBScript или JavaScript) и программы синтаксического анализа часто пользуются этим приемом. Указатель функции – это адрес, по которому будет передано управление командой вызова функции call прямо или косвенно, основываясь на сохраненных в стеке данных. При подмене в стеке указателей можно будет управлять вызовами функций, не влияя на содержимое регистра EIP.

Чтобы воспользоваться указателем функции в стеке, следует вместо подмены содержимого регистра EIP подменить часть стека с сохраненным адресом функции. Подмена указателя вызываемой функции, как и перезапись области хранения содержимого регистра EIP, позволит выполнить нужный программный код. Нужно только выяснить содержимое регистров и написать программу переполнения буфера, что вполне возможно.

Переполнения области динамически распределяемой памяти

До сих пор в главе описывались атаки на буфер памяти, размещенный в стеке. Известны простые способы влияния на работу программы, если ее буфер данных расположен в стеке. Поэтому можно считать, что вопросы переполнения буфера хорошо изучены. Кроме стека, в программе используется еще один тип распределения памяти – область динамически распределяемой памяти («куча»).

Функции malloc- типа HeapAlloc(), malloc() и new() выделяют программе область динамически распределяемой памяти, а функции HeapFree(), free() и delete() освобождают ее. Управляет областью динамически распределяемой памяти компонента операционной системы, известная как менеджер кучи (heap manager), который выделяет динамически распределяемую память процессам, обеспечивая при необходимости увеличение ее размера.

Динамически распределяемая память отличается от памяти стека тем, что это постоянный объект, время жизни которого не ограничено временем выполнения создавшей и использующей его функции. Это означает, что распределенная функцией динамически распределяемая память остается распределенной, пока она не будет явно освобождена. Поэтому переполнение динамически распределяемой памяти может никак не отразиться на работе программы до тех пор, пока она не будет повторно использована. В динамически распределяемой памяти не хранится что-либо похожее на содержимое регистра EIP, но в ней часто хранятся не менее важные вещи.

Подобно сохранению указателей функций в стеке, указатели функции могут быть сохранены в динамически распределяемой памяти.

Разрушение указателя функции

Основная уловка, применяемая к динамически распределяемой памяти, – разрушение указателя функции. Для этого существует много способов. Для начала можно попробовать подменить один объект из динамически распределяемой памяти на другой из соседней «кучи». Объекты класса и структуры часто хранятся в динамически распределяемой памяти, поэтому такая возможность существует. Например, для этого можно воспользоваться простым для понимания способом, известным под названием «нарушение границы» или «посягательство на объект» (trespassing).

Нарушение границы динамически распределяемой памяти В приведенном ниже примере два объекта класса размещены в динамически распределяемой памяти. При переполнении статического буфера одного из них нарушаются границы соседнего объекта. В результате во втором объекте перезаписывается указатель vtable – указатель таблицы виртуальных функций (virtual-function table pointer). Перезапись указателя виртуальных функций во втором объекте приводит к тому, что он начинает указывать на заранее подготовленный буфер – заготовку Троянской таблицы, куда затем записываются новые адреса функций класса. Один из них – адрес деструктора. Перезапись адреса деструктора приводит к вызову нового деструктора при удалении объекта. Указанным способом можно управлять любой программой по своему усмотрению – достаточно изменить указатель деструктора таким образом, чтобы он указывал на программный код полезной нагрузки. Единственное, что может помешать, – это нулевой указатель в списке адресов объектов динамически распределяемой памяти. Тогда программный код полезной нагрузки должен быть или размещен в области, указатель на которую не равен нулю, или следует воспользоваться одним из ранее изученных способов работы со стеком для загрузки в регистр EIP адреса перехода на нужную программу. Этот способ демонстрируется следующей программой.

// 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, «x77x77x77x77x88x88x88x88XX

XXXXXXXXXX” “XXXXXXXXXX XXXXXXXXXX XXXXXXXXXX

XXXXx54x1Ex30x00");

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. Нарушение границы динамически распределяемой памяти

Данный текст является ознакомительным фрагментом.