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

 

Механизмы защиты целостности ядра

Одна из концепций, которая позволяет поддерживать механизм защиты ядра, называется «монитор обращений». Монитор контролирует и разграничивает доступ к ресурсам системы. В последнее время появилось несколько механизмов, позволяющих обеспечить целостность и защиту ядра от посягательств извне. Основная идея, лежащая в основе таких механизмов, заключается в том, что ядро должно быть защищено от инжекта кода руткитами из пользовательского режима. Одна из самых распространенных идей, которые были реализованы в этом направлении, — это цифровая подпись модулей ядра (хоть она раньше и была неоднократно скомпрометирована). Если в операционной системе включена такая опция, то каждый модуль, загружаемый в ядро, должен содержать в себе валидную цифровую подпись, которая, в свою очередь, должна быть проверена и загружена в корневой каталог сертификации (root certification authority, СА). Если такая проверка не пройдет, то модуль, соответственно, не будет загружен.

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

Был разработан другой метод, направленный на предотвращение эксплуатации потенциальных уязвимостей программного обеспечения во время выполнения. Уверен, что ты слышал об этом подходе, который в первую очередь касается виртуальных адресов: страница памяти не может быть одновременно доступной на запись и выполнение кода (говоря кодерским языком, странице памяти не могут быть установлены атрибуты WRITABLE | EXECUTABLE). Данная техника была впервые внедрена в OpenBSD 3.3, а позже похожие системы защиты появились и в других ОС — например, PaX и ExecShield в Linux. В семействе ОС Windows эта система защиты более известна как Data Execution Prevention (DEP), которая была впервые реализована в Windows XP SP 2 и Windows Server 2003.

DEP реализуется на аппаратном и программном уровне. Начиная с пакета обновления 2 (SP2) для Windows XP 32-разрядная версия Windows использует один из следующих методов: функцию no-execute page-protection (NX), разработанную компанией AMD, и функцию Execute Disable Bit (XD), разработанную компанией Intel. Основным преимуществом, которое обеспечивает функция DEP, является возможность предотвратить запуск кода из областей данных (таких как куча, стек или пул памяти). Подробнее о технологии и возможностях DEP можно прочитать здесь.


Частота встречаемости байт после инструкции косвенного перехода

На этом программистские умы не успокоились, и была изобретена технология, которая позволяла поддерживать целостность ядра в режиме выполнения. Она получила название «memory shadowing». Эта технология реализована как монитор виртуальной машины, которая поддерживает раздельно существующие так называемые «регионы теневой памяти». Теневая память недоступна для гостевой учетной записи VM и содержит только копии некоторых участков основной памяти гостевой учетной записи. Новый исполняющийся код (то есть код, который исполняется в первый раз) аутентифицируется в системе путем сравнения значений простого криптографического хеша и только затем копируется в теневой участок памяти. Так как монитор виртуальной памяти в данной модели является доверенным по умолчанию, то это гарантирует невозможность неавторизованных модификаций в теневой памяти. Таким образом, код, запускающийся из-под гостевой записи, никогда не сможет получить доступа к привилегированной теневой памяти. В настоящее время технологию теневой памяти поддерживают win-системы начиная с Win2k и ядра Linux начиная с 2.4. Кроме того, эта технология реализована в виртуальных машинах QEMU, VMware и VirtualBox.

 

Возвратно-ориентированное программирование

Уф, сам не знаю, что это такое, но судя по всему — термоядерная штука. В самом ближайшем будущем ждите на экранах планеты руткиты, которые в своей основе будут использовать фичи возвратно-ориентированного кодинга (ВОК). ВОК позволяет атакующему эксплуатировать ошибки памяти в таргет-программах (как правило, уязвимости типа переполнения буфера и тому подобное) без фактического инжекта кода в адресное пространство этой программы. Не дошло? Повторю. Раньше для того, чтобы заэкплойтить программу/систему, нужно было искать в установленных программах ошибки, благодаря которым можно переписать адрес возврата из функции на нужный адрес и таким образом получить контроль над программой. Но найти такие случаи — большая редкость. Так вот, теперь этого делать не нужно. В атаке на систему злоумышленник должен организовать короткие последовательности инструкций процессора в целевой программе. Через выбор этих последовательностей злоумышленник окажется в состоянии обеспечить нужное поведение целевой программы. Обычно последовательность действий выбирается таким образом, чтобы каждое действие оканчивалось инструкцией процессора return( __asm ret), что (при условии контроля за стеком) позволит злоумышленнику контролировать ход выполнения программы. Кстати, именно по имени инструкции процессора — return — эта техника программирования и получила название возвратно-ориентированной.

Организационный блок инструкций в ВОП называется гаджетом и содержит в себе последовательность команд, адресов и данных, которые при запуске заставляют программу вести себя вполне прогнозируемым и нужным злоумышленнику образом. Инструкция ret обладает двумя особенностями. Во-первых, ей нужно выделить 4 байта на вершине стека и присвоить указателю инструкции EIP это значение. Во-вторых, она увеличивает указатель стека ESP на 4 таким образом, что вершина стека будет являться новым словом (2 байта) над тем словом, которому раньше был определен EIP. Эти особенности и используются для изменения последовательности инструкций, так как каждая последовательность может быть записана в стек. Когда исполняется последовательность команд, достигается исполнение инструкции ret, которая их заканчивает и обеспечивает переход к дальнейшей последовательности команд.

Тут обязательно стоит упомянуть, что присутствие инструкции return в гаджете вовсе не обязательно. Тот, кто мало-мальски разбирается в ASM’е и умеет на нем кодить, знает, что return-подобной инструкцией является выражение вида POP EAX; JMP EAX. Здесь, в отличие от вышеуказанных особенностей команды ret, достаточно только переписать регистр EAX и глупый процессор безмятежно прыгнет по указанному нами адресу. При этом return-подобные инструкции не ограничиваются только этим вариантом — при желании вариации на тему можно сильно разнообразить.


Небольшой экскурс в историю

Интересно, а какова вероятность встретить команду ret или иные return-подобные команды в стандартной программе? Очень велика. На х86-машинах команда возврата ret занимает один байт (с3) и теоретически будет встречаться с частотой 1/256. Хотя на самом деле — гораздо чаще, потому что среднестатистические (то есть, все законные) программы всегда будут использовать эту инструкцию, так что встретить ее в потоке байт будет несложно. Вместе с тем непрямые переходы с использованием регистров занимают два байта, и поэтому вероятность встретить их в программе будет гораздо ниже.

 

Pro & Cons

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

 

Ошибка #1: Переполнение буфера

Ну что тут сказать? Если ты не знаешь, что это такое, то вообще удивительно, каким образом ты дочитал статью до этого места :). Для устранения пробелов в образовании могу лишь посоветовать почитать К. Касперски, он тему переполнения буфера распилил от и до. Суть переполнения буфера заключается в появлении возможности переписать указатель следующей инструкции EIP в стеке какой-либо функции и таким образом получить контроль над ее выполнением. При реализации атаки ВОП это будет первая инструкция в гаджете, который будет выложен в стек. При этом указатель стека ESP будет указывать на следующее слово в стеке, которое также находится под контролем злоумышленника. Для того, чтобы воспользоваться переполнением буфера при реализации ВОП, злоумышленник должен переписать кадры стека таким образом, чтобы избежать изменения любых сохраненных значений EIP.

 

Ошибка #2: перезапись указателя vtable в С++

Опытные разработчики C++ знают, что можно контролировать память путем манипуляций с объектами виртуальных таблиц (vtable). В нашем случае, если появилась возможность контролировать vtable, то почему бы не переписать его так, чтобы он указывал на подконтрольный регион памяти? В зависимости от кода, который компилятор создает для виртуального вызова метода, наша последовательность команд будет эксплойтить один или несколько регистров, которые используются vtable и указывают на объект, виртуальную таблицу или сразу на них обоих. Злоумышленник должен просто использовать такие указатели для изменения указателя стека. Справедливости ради отмечу, что потенциальная возможность использовать виртуальные таблицы для перезаписи указателя зависит от версии компилятора и флагов. Посему гаджет ВОП легко окажется в пролете, ибо способ изменения vtable, оказывается, ненадежен.

 

Ошибка #3: перезапись указателя на функцию

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

 

Ошибка #4: перезапись буфера функции setjmp

И вот благочестивый Linux неожиданно оказался под ударом. Виной всему — функции setjmp и longjmp, которые реализуют нелокальные переходы goto. Кошмарные, на мой взгляд, функции — с точки зрения не только системного кодинга, но и кодинга вообще… При вызове функции setjmp программа выделяет место в структуре jmp_buf, которая хранит значения регистров EBX, EDI, ESI, EBP, ESP и EIP. Здорово, правда? В сохраненный в буфере указатель инструкций EIP с помощью инструкции CALL записывается текущее значение, а также сохраняется значение ESP. При выходе из setjmp в регистре EAX будет хранится нуль. Позднее вызывается функция longjmp. Эта функция восстанавливает прежнее значение основных регистров, заносит в регистр EAX второй аргумент, переданный функции longjmp, устанавливает регистр ESP и прыгает по этому адресу.

Пример использования функций setjmp/longjmp:

struct foo
{
char buffer[160];
jmp_buf jb;
};
int main( int argc, char **argv )
{
struct foo f = malloc( sizeof(f));
if( setjmp(f->jb) )
return 0;
strcpy( f->buffer, argv[1] );
longjmp( f->jb, 1 );
}

Обрати внимание на строку strcpy( f->buffer, argv[1] ). По-моему, ее даже пояснять не нужно. И пусть *nix-кодеры теперь решат для себя, стоит ли использовать эти функции в своих программах :).

 

Заключение

Итак, что мы получили? На мой взгляд, ВОП является крайне коварной вещью для обеспечения общей безопасности системы. Ведь стоит злоумышленнику пропатчить ядро, какой-нибудь драйвер, системный файл — и все! Для этого даже не нужно искать способы нелегального внедрения в нулевое кольцо, чтобы поставить систему на колени. Все оказывается гораздо проще. И ведь уже существуют практические наработки, которые реализуют возможность контролировать систему, используя вышеописанные методы ВОП.

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

Удачного компилирования и да пребудет с тобой Сила!

 

Links

Как показывает моя практика, для айтишника, занятого вопросами безопасности компьютерных систем, наиболее значимыми оказываются вовсе не официальные сайты каких-либо системных контор (хотя MSDN это не касается). Самые ценные крупицы информации собираются на блогах активистов андеграунда, кодокопателей и прочих. В качестве примера назову ресурсы blog.threatexpert.com и alex-ionescu.com.

Оставить мнение

Check Also

Выносим всё! Какие данные о нас хранит Google и как их вернуть себе через Takeout

Как известно, Google хранит огромное количество данных о своих пользователях, чем его непр…