Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Polnaya_metodichka

.pdf
Скачиваний:
30
Добавлен:
11.05.2015
Размер:
2.79 Mб
Скачать

15. Методы защиты программного обеспечения от обратного проектирования и модификации.

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

Исследование логики работы может выполняться в статическом и динамическом режиме. Сущность статического режима заключается в изучении листинга исходного текста программы на языке Assembler, IL и т.д. Для его получения выполняемый программный модуль подвергают дизассемблированию. Динамический режим изучения алгоритма предполагает выполнение трассировки программы, т. е. запуск с использованием специальных средств, позволяющих выполнять программу в пошаговом режиме, получать доступ к регистрам процессора, областям памяти и т. д. Обычно трассировка производится в автоматическом режиме, после чего взломщик исследует ее результаты. Для защиты программ от изучения необходимо иметь средства противодействия как дизассемблированию, так и трассировке.

Наиболее распространенными методами защиты от обратного проектирования и модификации являются:

1.Обфускация (obfuscation – затемнение) − изменение программного кода таким образом, чтобы он был труден для изучения и модификации третьими лицами, при сохранении его функциональности. Подходы:

1.1.Лексическая обфускация – удаление комментариев, отступов, замена имен идентификаторов, добавление мусорных операций и т.д.

1.2.Обфускация данных – объединение переменных (например 4хCHAR в INT), реструктурирование массивов (разделение на подмассивы или объединение), изменение иерархии наследования классов и т. д.

1.3.Обфускация управления – запутывание последовательности выполнения программного кода. Используются непрозрачные предикаты (последовательности операций с неизвестным результатом), добавление недостижимого кода, встраивание функций в место вызова и т.д.

1.4.Превентивная обфускация – использование недостатков наиболее распространенных деобфускаторов.

2.Вынесение критического программного кода в защищенный модуль. Таким модулем может быть внешний физический носитель (эффективно при небольшом количестве пользователей программы) или доверенный сервер в сети.

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

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

16. Атаки на переполнение буфера. Переполнение буфера в стеке. Переполнение буфера в области динамической памяти.

Переполнение буфера (buffer overflow) – наверное, одна из самых интересных и широко распространённых уязвимостей программного обеспечения. Небольшая ошибка программиста может (при особых обстоятельствах) позволить злоумышленнику сделать практически что угодно на компьютере пользователя программы. Ошибка заключается в том, что в каком-либо месте программы происходит копирование данных из одного участка памяти в другой без проверки того, достаточно ли для них места там, куда их копируют. Область памяти, куда копируются данные, принято называть буфером. Таким образом, если данных слишком много, то часть их попадает за границы буфера - происходит "переполнение буфера". Умелое использование того, куда попадают "лишние данные" может позволить злоумышленнику выполнить любой код на компьютере, где произошло переполнение. Существуют различные варианты данной уязвимости. Рассмотрим самую распространённую из них, связанную с искажением адреса возврата функции (т.н. "переполнение стека" - stack overflow или "срыв стека" - smashing the stack).

Переполнение стека.

Рассмотрим следующую программу:

#include <stdio.h>

void show_array(int arrlen, char array[])

{

char buffer[32]; int i;

for (i = 0; i < arrlen; i++) buffer[i] = array[i]; printf(buffer);

}

int main()

{

char mystr[] = "To be, or not to be..."; show_array(23, mystr);

return 0;

}

Функция show_array получает в качестве параметров размер массива символов и сам массив, копирует этот массив в локальную переменную buffer и выводит buffer на экран. Итак, где же здесь ошибка? Ошибка в процедуре show_array. В ней переданный в качестве параметра массив array "слепо" копируется в переменную buffer. Возможность того, что array окажется больше 32 байт (т. е. arrlen > 32) просто не учтена. Да, конечно, в нашей программе никто и не передаёт этой процедуре неподходящих данных, но ведь это только "модель" реальной программы, и на самом деле массив mystr мог бы быть введен и как строка символов с клавиатуры. Просто в демонстрационных целях удобнее задавать его прямо внутри программы. Это позволит нам использовать в строке различные непечатаемые символы, в том числе байт 0. По этой же причине (нам понадобится наличие в строке байта

0) мы не используем функции strlen и strcpy, а задаем длину строки (здесь это число 23) вручную.

Итак, что же будет, если mystr длиннее 32-х байт? Проверим. Меняем функцию main следующим образом:

int main()

{

char mystr[] =

"11111222223333344444555556666677777888889999900000";

show_array(51, mystr);

return 0;

}

Компилируем, выполняем,… видим крах программы. Давайте разберёмся, почему так получилось. Для этого необходимо понять, как происходит вызов и выполнение процедуры show_array. Для вызова show_array(51, mystr), её аргументы (51 и адрес строки mystr) пихаются на стек в обратном порядке, и затем управление передаётся процедуре с помощью инструкции CALL show_array. Примерно так:

PUSH mystr

PUSH 51

CALL show_array

Перед тем, как передать управление процедуре show_array, инструкция CALL добавляет на стек значение регистра EIP, т. н. адрес возврата. Поэтому перед выполнением show_array стек выглядит следующим образом:

Далее управление переходит к show_array и перед началом собственно работы функции выполняется приблизительно следующая последовательность инструкций:

PUSH EBP

MOV EBP, ESP

ADD ESP, -36

Т. е. сначала на стеке сохраняется значение EBP, затем в EBP переносится значение ESP и, наконец, от ESP вычитается 36. Операции с EBP нас здесь не интересуют; достаточно сказать, что относительно EBP адресуются локальные переменные. Интересует же нас строчка ADD ESP, -36. Тем самым функция резервирует на стеке место для своих локальных переменных. Их у неё две - char buffer[32] и int i. Массив buffer занимает 32 байта, целое число i - 4 байта. Итого 36 байт. В результате стек вылядит вот так:

Теперь должно быть понятно, куда попадают байты, не поместившиеся в буфер. Они записываются на место сохранённого ранее EBP, переписывают адрес возврата и так далее пока их хватит. Самое интересное же происходит при возврате из функции. Он происходит следующим образом:

MOV ESP, EBP

POP EBP

RET

Т. е. освобождается место, занятое ранее локальными переменными, затем из стека восстанавливается сохранённое в начале значение EBP и, наконец, инструкция RET достаёт со стека адрес возврата и передаёт управление по нему. Вспомним наш пример. Мы попробовали скопировать в буфер строку "11111222... 9900000". При этом 32 байта из неё ("11111...6666677") попали по назначению, следующие 4 байта ("7778") переписали сохранённый EBP, ещё 4 байта ("8888") попали на адрес возврата, а остаток ("9999900000") попал на место параметров и далее. При возврате из функции были, таким образом, неверно восстановлены регистры EBP и EIP, и, так как по адресу 0x38383838 исполнимых инструкций не нашлось, произошла ошибка, которую мы и имели удовольствие наблюдать

(примечание: 0х38 – код символа ‘8’ в таблице ASCII).

Но ведь тот адрес, по которому произошёл возврат из функции, полностью зависит от того, какую строку мы передали функции. Значит если бы на месте байтов "8888", переписавших адрес возврата, был бы какой-нибудь реально существующий адрес, управление перешло бы по нему. Следовательно, правильно подобрав строку, которую мы передаём функции, мы можем перенаправить ход выполнения программы по нашему усмотрению. Здесь есть 2 варианта:

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

вотладчике адрес нужной функции и вписать его в строку вместо "8888".

2.Внедрение собственного исполняемого кода. Учитывая то, что наш код будет находиться

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

вотладчике, посмотреть, по какому адресу будет находиться наша строка во время выполнения программы, и указать в качестве адреса возврата, например, адрес начала строки. Потом мы сможем записать туда необходимый нам код. У этого метода есть, правда, один недостаток. Необходимый нам адрес возврата будет "слишком маленьким", скорее всего меньше чем 0x00ffffff. А это значит, что один из байтов в строке будет нулём, и это нехорошо (т.к. в реальных программах для копирования используется функция strcpy, которая завершает работу при достижении нуль-терминанта). Избежать

этого можно следующим образом: очевидно, что после выполнения возврата из процедуры, регистр ESP будет указывать на тот "хвост" строки, который остался на стеке. Поэтому, если передать управление по адресу [ESP], то начнёт выполняться программа, записанная в этом "хвосте". Следовательно, нас бы устроила возможность выполнить инструкцию JMP [ESP] или CALL [ESP]. Такая инструкция скорее всего найдётся в одной из динамически загружаемых библиотек (DLL), которые использует программа. Так как DLL обычно загружаются на достаточно высокие адреса в памяти, то в качестве адреса возврата мы и укажем адрес одной из этих инструкций в DLL. Выполнение произойдёт тогда следующим образом

RET --> CALL [ESP] --> код в "хвосте" строки

Одна из DLL, которые использует наша программа - KERNEL32.DLL. Попробуем найти в ней инструкцию CALL [ESP] или JMP [ESP]. Положим что инструкция CALL [ESP] нашлась по адресу 0xbff794b3 (В общем, этот адрес зависит от используемой версии KERNEL32.DLL). Вот это число мы и укажем в качестве адреса возврата, а прямо за ним в строке последует исполняемый код. Например:

int main()

{

// часть строки, заполняющая буфер

char mystr[] = "111112222233333444445555566666777778"

"\xb3\x94\xf7\xbf"

// адрес

возврата

 

 

// -----------

код -----------

 

"\xff\x15\xe8\xf0\x40\x00";

// CALL

[KERNEL32.ExitProcess]

 

show_array(47, mystr);

 

return 0;

 

}

 

Очевидно, что вместо безобидного KERNEL32.ExitProcess можно написать что угодно.

Примечание: если вам все-таки понадобятся инструкции, содержащие нуль-терминант (что неприемлемо в случае атак на strcpy), можно использовать следующий трюк: передать на исполнение «закодированную» строку, например, сделав XOR каждого символа строки с каким-нибудь байтом, например 0х80, а затем в начале внедренного кода «раскодировать» строку, выполнив побайтовый XOR еще раз.

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

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

1. Рассмотрим пример перезаписи переменной:

#include <stdio.h> #include <stdlib.h> #include <string.h>

struct mystruct

{

unsigned char buffer[16]; unsigned long cookie;

};

int main(int argc, char **argv)

{

struct mystruct *s;

if ((s = malloc(sizeof(struct mystruct))) == NULL) { perror("malloc");

exit(EXIT_FAILURE);

}

s->cookie = 0;

if (argc > 1) strcpy(s->buffer, argv[1]);

if (s->cookie == 0x42424242) { printf("Congratulations! You won a cookie!\n"); exit(EXIT_SUCCESS);

}

printf("Hello world!\n");

exit(EXIT_SUCCESS);

}

Код не проверяет данные, предоставленные пользователем, при их копировании в элемент buffer предварительно выделенной структуры struct mystruct при помощи функции strcpy , в результате чего происходит переполнение буфера кучи.

$ ./example1 AAAAAAAAAAAAAAAABBBB Congratulations! You won a cookie!

Примечание: 0х42 – код символа ‘B’ в таблице ASCII.

2. Рассмотрим пример перезаписи указателя:

#include <stdio.h> #include <stdlib.h> #include <string.h>

struct mystruct {

unsigned char buffer[16];

int (*myfunc)(const char *format, ...);

};

int main(int argc, char **argv)

{

struct mystruct *s;

if ((s = malloc(sizeof(struct mystruct))) == NULL) { perror("malloc");

exit(EXIT_FAILURE);

}

s->myfunc = printf;

if (argc > 1) strcpy(s->buffer, argv[1]);

s->myfunc("Hello world!\n");

exit(EXIT_SUCCESS);

}

Работу процесса можно изменить перезаписью и последующим выполнением указателя функции myfunc являющегося элементом структуры struct mystruct, расположенного в памяти справа после буфера.

17.Атаки на переполнение буфера. Целочисленное переполнение. Выполнение кода злоумышленника.

Атаки на переполнение буфера.

Переполнение буфера (Buffer Overflow) — явление, возникающее, когда компьютерная программа записывает данные за пределами выделенного в памяти буфера. Обычно возникает из-за неправильной работы с данными, полученными извне, и памятью, при отсутствии жесткой защиты со стороны подсистемы программирования (компилятор или интерпретатор) и операционной системы. В результате переполнения могут быть испорчены данные, расположенные следом за буфером (или перед ним). Переполнение буфера может вызывать аварийное завершение или зависание программы, ведущее к отказу обслуживания (denial of service, DoS). Отдельные виды переполнений, например переполнение в стековом кадре, позволяют злоумышленнику загрузить и выполнить произвольный машинный код от имени программы и с правами учетной записи, от которой она выполняется.

Виды атак:

1. ”Переполнение стэка” (Stack buffer overflow) или ”Срыв стэка” (Smashing the stack). Возникает, когда в стеке вызовов хранится больше информации, чем он может держать.

Обычно ёмкость стека задаётся при старте программы/потока. Когда указатель стека выходит за границы, программа аварийно завершает работу

Типы атак:

a) Перезапись локальной переменной.

б) Перезапись обратного адреса (return address) в стэк фрэйме.

в) Перезапись указателя на функцию или обработчика исключений. 2. ”Переполнение кучи” (Heap overflow).

Термин переполнение кучи (heap overflow) применяется для описания многих уязвимостей. Типичный пример переполнения кучи — выделение буфера нулевого размера и копирование в него большого числа. В этом смысле переполнение кучи происходит при любом нарушении целостности памяти, не находящейся в стеке. Из-за разнообразия конкретных способов нарушения от них практически невозможно защититься внесением исправлений в компилятор. Кроме того, к категории переполнения кучи относятся дефекты двойного вызова функции free().

Большинство уязвимостей переполнения кучи основаны на общих принципах: куча (как и стек) содержит данные и служебную информацию, управляющую «восприятием» данных программой. Трюк заключается в том, чтобы заставить реализацию malloc() или free() сделать то, что требуется нападающему — как правило, записать одно-два слова по нужному адресу памяти.

Целочисленное переполнение

Переменной типа Integer, отводится в памяти 4 байта (для x86). Переполнение происходит при попытке записать в переменную значение превышающее максимально возможное число. Поведение программы в таких ситуациях полностью зависят от используемого компилятора. Потому, как согласно стандарту ISO C99 каждый компилятор при таком переполнении может делать все что угодно, от игнорирования до аварийного завершения программы. В большинства компиляторах какраз ничего и не делается :) Этот вид атаки опасен еще и тем, что приложение не может определить произошло переполнение или

нет. Переполнения целых чисел можно использовать для влияния на значения некоторых критических данных, например размера буфера, индекса элемента массива, и т.д. Но на практике можно столкнутся с трудностью использования этой уязвимости. Все дело в том это переполнение в большинстве случаев не может непосредственно переписать область памяти(в отличии от Buffer Overflow и Heap overflow). Но применение этой уязвимости может легко вызвать и ошибки другого класса. В большинстве случаев возникает возможность переполнения буфера (Buffer Overflow). По словам ISO C99 уязвимость исчезает при использовании в вычислениях Unsigned Integer. Обьявим две переменные A и B, типа

Unsigned Integer. Далее занесем в А максимальное значение Unsigned Integer - 4294967295, а в В -1:

unsigned int A=0xFFFFFFFF; unsigned int B=0x1;

В результате выполнения операции (А+В) полученное значение, согласно стандарту ISO, не вмещается в 32 бита. В таком случае результатом будет (А+В) mod 0x100000000. mod - остаток от деления, например (8 mod 2 =0), а (8 mod 3=2). Следовательно, наш результат будет равен выражению:

result=(A+B) % 0x100000000;

подставив наши значения получим следующее:

result=(0xffffffff + 0x1) % 0x100000000; //result=(0x100000000) % 0x100000000; //result=0;

Как видно результат вышел равен нулю. Этот эффект называют "wrap around", тоесть вращение вокруг нуля.

Выполнение кода злоумышленника.

Кажется, если атакующий может перезаписывать адрес возврата (или любой другой указатель на функцию), то проблема передачи управления на shell-код решается сама собой, но все не так просто! Допустим, переполняющийся буфер расположен в стеке, а стек, как известно, растет снизу вверх (или сверху вниз - это уж кому как привычнее) и точное положение указателя вершины стека неизвестно. Следовательно, неизвестна и локация shellкода. Так куда же передавать управление?!

Рисунок 1. Иллюстрация NOP SLED техники.

Одно из решений проблемы (известное под именем техники NOP SLED) заключается в дописывании в конец буфера большого количества незначащих инструкций NOP (которым на x86 процессорах соответствует опкод 90h, тождественный операции XCHG EAX,EAX - обмен содержимого регистра EAX с регистром EAX), в конце которых стоит команда относительного (relative) перехода на начало shell-кода, не требующая знания абсолютных адресов (неизвестных атакующему).

При этом NOP'ы оказываются расположены как до адреса возврата, так и после. Естественно, если управление будет передано "вперед", то цепочка управления, докатившись до адреса возврата, попытается интерпретировать его как машинную команду, со всеми вытекающими отсюда последствиями типа непредсказуемого поведения, поэтому перед адресом возврата вставляется еще одна команда относительного перехода (см. рис. 1).

Однако для реализации NOP SLED хакеру должен быть известен хотя бы приблизительный адрес буфера с shell-кодом, а известен он далеко не всегда и тогда приходится прибегать к другой технике, передающей управление на вершину стека через команду JMP ESP, в x86-процессорах представляющую собой двухбайтовую машинную инструкцию с опкодом FFh E4h. Вся хитрость в том, чтобы найти такую последовательность байт в памяти и подсунуть ее адрес на место адреса возврата из функции. Тогда в момент стягивания последнего со стека, регистр ESP будет смотреть на двойное слово, следующее за адресом возврата, где может быть либо сам shell-код, либо команда перехода к нему.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]