Автор: (c)Крис Касперски ака мыщъх
Чем глубже в Windows, тем больше дыр, поток которых прекращаться не собирается, открывая весьма соблазнительные перспективы для хакерских атак - как локальных, так и удаленных, к которым ни Microsoft, ни сторонние разработчики не готовы и которым придется конкретно напрячься, чтобы разрулить ситуацию, а мы к тому времени нароем новые дыры, так что никто без работы не останется.
Экспериментируя с программами потокового аудио/видео-вещания (главным образом, с VideoLAN), мыщъх с удивлением обнаружил, что его любимый SyGate Personal Firewall 4.5 в упор не видит ни входящего, ни исходящего unicast/multicast трафика и, соответственно, не может заблокировать его, что очень странно и подозрительно, особенно в случае с unicast-трафиком, работающим поверх IP и с этой точки зрения ничем не отличающимся от прочих IP-пакетов. Но тем не менее - факт! Очень упрямый и трудно объяснимый. Беглое расследование показало, что начиная еще с NT 4.0 и NT 3.51 SP2 обработка unicast/multicast-потоков выделена в отдельное "делопроизводство" внутри сетевой подсистемы. Мотивы вполне ясны и особенно хорошо ощутимы на "тонких" каналах связи. "Выхватывая" unicast/multicast-пакеты из общего сетевого трафика, операционная система уделяет им максимум внимания, оттесняя весь остальной TCP/IP-трафик на второй план. Другими словами, чтобы не реализовывать сетевой ввод/вывод с приориетами, разработчики Windows сделали исключение лишь для unicast/multicast-трафика, обрабатываемого с максимальным приоритетом. Кстати, чтобы выяснить это, совершенно необязательно иметь секс с отладчиком и дизассемблером, достаточно раскурить MSDN: technet2.microsoft.com/windowsserver/en/library/3da7c55f-cb91-406a-8596-7b120ebf10f81033.mspx?mfr=true, там же можно нарыть и примеры создания IP-фильтров, учитывающих весь трафик: www.microsoft.com/technet/prodtechnol/windows2000serv/reskit/intwork/inae_ips_neez.mspx?mfr=true, в том числе и unicast/multicast, и тогда ни один пакет не пройдет незамеченным. Увы! Далеко не все разработчики персональных брандмауэров учитывают это обстоятельство, что позволяет хакерам генерировать unicast-трафик и пускать его в обход брандмауэра.
NT 3.51 SP2 и выше, SyGate Personal Firewall 4.5 и некоторые другие брандмауэры.
В качестве "тестера", определяющего способность брандмауэра распознавать и блокировать различные виды unicast/multicast-трафика, можно использовать беспоатную программу VidoeLAN, кстати говоря, распространяемую в исходных текстах: www.videolan.org.
Использовать в качестве шлюза для доступа в Сеть любую Linux или BSD-подобную систему, чей штатный брандмауэр убивает любой unicast/mulicast-трафик.
Рисунок 1. Внешний вид программы VideoLAN.
В Висте появилась рандомизация адресного пространства, существенно затрудняющая внедрение зловредного кода в "доверенные" процессы типа explorer.exe, которым разрешен выход в сеть. Классическая схема внедрения (VirtualAllocEx, WriteProcessMemory, SetThreadContext) распознается практически всеми антивирусами и персональными брандмауэрами, написанными еще много лет тому назад, поэтому хакеры усовершенствовали методику, отказавшись от функции SetThreadContext, посредством которой они ранее изменяли регистр EIP так, чтобы он указывал на внедренный код. В новой схеме передача управления осуществлялась путем заполнения стека главного потока (благо его местоположение вплоть до Висты оставалось постоянным) указателями на внедренный код. Поскольку комбинация команд VirtualAllocEx/WriteProcessMemory довольно распространена среди "честных" программ и представляет собой совершенно легальный механизм межпроцессорного взаимодействия, то никакие защиты на нее не ругаются. Но с появлением Висты ситуация изменилась и базовый адрес стека стал располагаться по случайным адресам, что должно было положить конец хакерству, но... так и не положило, поскольку существует такая замечательная API-функция как VirtualQueryEx, возвращающая карту памяти целевого процесса и не менее замечательная API-функция VirtualProtectEx, сообщающая атрибуты страницы. Так вот, стек представляет собой блок памяти, на вершине которого лежит страница с атрибутами PAGE_GUARD, что является его характерной чертой, позволяющей отличать стек от всех остальных регионов памяти (примечание: некоторые программы также пользуются флагом PAGE_GUARD для динамического выделения памяти, но очень и очень немногие). Важно понять, что PAGE_GUARD определяет не текущее значение регистра ESP, а самое высокое положение указателя вершины стека, когда-либо достигнутое потоком в процессе его существования. Реальное же значение ESP, как правило, намного ниже, но что нам стоит заполнить указателями на внедренный нами код весь блок от PAGE_GUARD и до его конца?! Кстати говоря, поскольку операционная система выделяет стек постранично и делает это через общий с кучей менеджер памяти, то функцией VirtualFreeEx мы можем освобождать страницы, принадлежащие стеку одного из потоков целевого процесса, возвращая их в общий пул свободной памяти и тогда... куча окажется прямо в стеке! И программа, пытаясь прочитать локальные переменные или стянуть адрес возврата из функции, встретит что-то очень неожиданное и скорее всего рухнет, если, конечно, мы не подложим в строго определенные места заданные указатели, передающие управление на внедренный нами код. При желании можно придумать и другие разновидности атак на эту тему, но уже и без того ясно, что ASLR - никакая не защита, а так... пугало для пионеров.
Виста/Server 2008 (в более ранних системах рандомизация адресного пространства отсутствует, но данная атака прекрасно совместима с ними, включая линейку 9x).
Не требуется, любой отладчик (например, Olly) без труда найдет стек основного потока в целевом процессе по карте памяти.
Отсутствует.
Рисунок 2. На вершине блока памяти, выделенного потоку, гордо возлегает страница с атрибутами PAGE_GUARD (ну, или “Guarded” - в терминах OllyDbg).
В W2K (с большой задержкой против UNIX) наконец-то появилась поддержка квотирования дискового пространства, позволяющая администраторам умерять "аппетит" прожорливых пользователей. Ну, а кому понравится, когда ограничивают его свободу? Вот хакеры и взбунтовались и начали пакостить, обходя ограничения и поглощая все доступное дисковое пространство, приводящее к невозможности создания новых файлов и, как следствие - краху системы еще на ранних стадиях загрузки (при условии, что пользователям разрешено создавать файлы хотя бы в одном из каталогов системного тома, например, Documents-n-Settings или C:\WINDOWS\TEMP). Разработчики Windows, казалось бы, предусмотрели все, считая сколько физических кластеров занимают все созданные данными пользователем файлы (а для упакованных файлов берется их полный, а не сжатый размер). Но один маленький финт ушами они все-таки пропустили. Вопрос, мучивший хакеров еще со времен MS-DOS - сколько занимает файл нулевой длины? Ноль байт? Один кластер? Или... На самом деле, система не настолько глупа, чтобы выделять дисковое пространство файлу с нулевой длиной и потому формально их можно создавать сколько угодно. Вот только... У файла есть имя, атрибуты, дата и время создания, идентификатор владельца - словом достаточно большое количество информации, которую где-то надо хранить. В NTFS оно хранится в специальном служебном файле с именем $MFT, где на каждый файл заведена специальная файловая запись - структура данных, известная как FILE_RECORD, размер которой обычно занимает 1 Кб ("обычно", потому что из этого правила слишком много исключений, которые лень перечислять, да и на исход дела они никак не влияют, так зачем же углубляться в ненужные технические подробности?). К тому же, для ускорения типовых файловых операций содержимое директорий проиндексировано, а каждый индекс тоже пространства хочет (правда, не 1 Кб, а намного меньше, но все-таки...). Создание пустых файлов в бесконечном цикле вызывает рост $MFT файла, размер которого в пользовательских квотах не учитывается и через некоторое (впрочем, довольно продолжительное) время $MFT поглощает все свободное пространство на диске, затем кончаются файловые записи, принадлежащие удаленным файлам и... все. Чтобы создать еще хоть один файл, нужно удалить что-нибудь и успеть опередить хакерский цикл, упорно пытающийся создавать новые файлы...
W2K и выше (в NT 3.x/4.x нет квот, но данная схема атаки применима и для них).
Ниже приведен исходный код боевого exploit'а, написанного на языке Си и создающего файлы нулевого размера в бесконечном цикле:
Листинг 1. Exploit, обходящий систему дисковых квот в W2K и более старших системах.
Отсутствует.
Рисунок 3. Сколько байт занимает файл с нулевой длиной?
28 декабря 2007 года в 5:47 PM мыщъх получил от легендарного во всех отношениях хакера Юрия Харона следующее письмо (приводимое, естественно, с его разрешения):
"Нашел я ошибку в форточках. Слов нет, одни эмоции :( Добавляя новую "защиту", они умудрились (будут интересны подробности - расскажу) оставить непроинициализированные переменные (правда, в крайне экзотической ситуации), в результате чего отваливаем на BSOD при SEH в некоторых (старых) драйверах. Убббивать... Три дня угробил на поиск :(((
Теперь эта прошла и вылезла следующая. Которую я обнаружил совершенно случайно - не, ну вот как так можно?! Два варианта ntkrnlpa.exe. Версия одна и та же. Билд один и тот же. Но в version info присутствует строка (см. листинг 2) и разные они даже по размеру:
VALUE "FileVersion", "5.1.2600.3093 (xpsp_sp2_gdr.070227-2254) <- это в одном VALUE "FileVersion", "5.1.2600.3093 (xpsp_sp2_qfe.070227-2300) <- это в другом
Листинг 2. Разные строки FileVersion в одинаковых билдах ХРюши.
При этом (заметим в скобках) оба файлы получены с Windows Update, просто один обновился сразу же, как только вышел (в июне-июле), а второй только сейчас (на варю когда ставил). Файлы разные даже по размеру (не говоря уж про все остальное). И вот на том, который "сейчас", ошибка и вылезла. Причём, опять какая-то наведенка :(
Интересно сколько я её искать буду...". Мыщъх сказал, что подробности, разумеется, интересны и тут же получил ответ: "Напомни завтра(/ночью) - я щас уже офигел и спать пошел. И вообще – может, ты, напоминая, на старые вопросы ответишь :) Пока (чтобы писать меньше) почитай про ключ /SAFESEH в текущем ms-link (не столько про ключ, сколько про то, зачем он) - тогда будет проще объяснить".
Рисунок 4. Описание ключа /SAFESEH линкера MS-LINK на MSDN.
А пока Харон спит (то есть, теперь он, конечно, не спит, хотя... никаких гарантий на этот счет ни у кого нет), мы отправимся по ссылке, ведущей на Хароновский ftp-сервер: ftp://ftp.styx.cabel.net/, где в директории pub лежит замечательный (и бесплатный для некоммерческого использования линкер UniLink). Открываем файл whatsnew_ru.txt и втыкаем:
Рисунок 5. В гостях у Юрия Харона, где на FTP-сервере лежит последняя версия линкера UniLink, обходящего многие ошибки Windows, с которыми другие линкеры не справляются
+ Добавлен ключ -RS для "защиты" от инжекций SEH-обработчиков (этот механизм работает только в Vista и XP+SP2), Поведение несколько отличается от ключа /SAFESEH ms-link (v8 или старше): - Отсутствие ключа - аналог /SAFESEH:NO - При указании ключа коллекционируется информация об обработчиках (аналог отсутствия ключа у ms-link), однако при отсутствии такой информации компоновка не отвергается (как у ms-link), а модуль маркируется как программа (dll), в которой запрещён SEH, При этом выдаётся информационное сообщение (из группы w-inf). - Строго в соответствии с документацией допустимы ссылки на "внешние" обработчики (handler), в отличие от ms-link, где такая ситуация приводит к ошибке. Это имеет значение при необходимости работы с "нестандартными" обработчиками и/или с библиотеками, в которых их назначение отсутствует. Ссылки на внешние обработчики, которые НЕ определены в компонуемом приложении, порождают 'Unresolved external'; - Для упрощения работы с библиотеками Borland (в которых нет информации об обработчиках) делается "автоматическое" назначение обработчиков (они у Borland стандартные) - проверялось для C/CPP bc v5 и bcb v5/v6/bds4. - Указание ключа -RS+ приводит к запрету ИИ в отношении библиотек Borland. Для остальных компиляторов можно использовать служебные файлы, используя директиву .safeseh ml. -------------------------------------------------------------------------------
Листинг 3. Фрагмент файла whatsnew_ru.txt из комплекта поставки линкера UniLink.
Раскурив MSDN (отправные точки для поиска: http://blogs.msdn.com/greggm/archive/2004/07/22/191544.aspx и http://msdn2.microsoft.com/en-us/library/9a89h429.aspx), можно узнать, что механизм SafeSEH, призванный предотвратить подмену обработчика структурных исключений при атаке на переполняющиеся буфера, в зачаточном виде появился еще в XP, но только в Висте он был доведен но минимально работающего состояния.
Рисунок 6. На блоге Microsoft, посвященному SafeSEH.
В чем его суть? Если раньше указатели на обработчики структурных исключений хранились в стеке, беспрепятственно доступном на запись/чтение, то теперь они переместились в специальные секции PE-файла (см листинг 4), доступные только на чтение и формируемые статическим образом еще на этапе сборки программы при активном участии со стороны линкера и компилятора.
extern PVOID __safe_se_handler_table[]; /* base of safe handler entry table */ extern BYTE __safe_se_handler_count; /* absolute symbol whose address is the count of table entries */ typedef struct { DWORD Size; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD GlobalFlagsClear; DWORD GlobalFlagsSet; DWORD CriticalSectionDefaultTimeout; DWORD DeCommitFreeBlockThreshold; DWORD DeCommitTotalFreeThreshold; DWORD LockPrefixTable; // VA DWORD MaximumAllocationSize; DWORD VirtualMemoryThreshold; DWORD ProcessHeapFlags; DWORD ProcessAffinityMask; WORD CSDVersion; WORD Reserved1; DWORD EditList; // VA DWORD_PTR *SecurityCookie; PVOID *SEHandlerTable; DWORD SEHandlerCount; } IMAGE_LOAD_CONFIG_DIRECTORY32_2. const IMAGE_LOAD_CONFIG_DIRECTORY32_2 _load_config_used = { sizeof(IMAGE_LOAD_CONFIG_DIRECTORY32_2), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, &__security_cookie, __safe_se_handler_table, (DWORD)(DWORD_PTR) &__safe_se_handler_count };
Листинг 4. Новые структуры PE-файла, отвечающие за поддержку SafeSEH.
Утром Харон проснулся и отписал: "Ты про SafeSEH прочитал? Тогда рассказываю. Как оказалось (хоть они и врали, что это только для Висты), это уже используется в XP SP2 (но не всех "подбилдах"!) для драйверов. А поскольку драйвера могут быть собраны как с этим ключом, так и без, то используется оно только в ситуации, когда назначено. Практически, если посмотреть в процедуру RtlIsValidHandle, то увидим, что когда RtlLookupFuncionTable возвращает NULL (т.е. нет таблиц), хандлер считается валидным (что правильно), при возврате INVALID_HANDLE_VALUE (возникает при IMAGE_DLLCHARACTERISTICS_NO_SEH) хандлер считается не валидным, а всё остальное рассматривается как описатель диапазона. Т.е. всё вроде как правильно.
Рисунок 7. Функция NTDLL.DLL!RtlIsValidHandle под микроскопом дизассемблера IDA Pro.
Теперь смотрим в RtlLookupFunctionTable и видим, что возвращаемое значение (точнее - 2 значения) берутся из описания модуля, в диапазон адресов которого попадает текущее исключение. Сиречь - опять же все правильно. А вот теперь идём в то место, где этот самый описатель модуля формируется (сиречь - MiCaptureImageExceptionValues, вызываемую из MmLoadSystemImage) и видим... подтверждение старого доброго правила - если обезьяне выдать пистолет, то ее обороноспособность понизится :)
Помнишь, сколько было жалоб (в том числе и моих) на тему, что MS некорректно обрабатывает, точнее говоря - не обрабатывает (во многих местах) NumRvaAndSize в заголовке PE'шника? Они решили исправиться. Но поручили это своим пионэрам :) И вот что получилось в результате (псевдокод):
If (peh->OptHdr.DllCharacteristics & ...NO_SEH) mdsc->SEHtable = mdsc->SEHcount = -1; else if (peh->NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG) { If (peh->DataDir[...] == NULL) mdsc->SEHtable = mdsc->SEHcount = 0; else { // init values } }
Листинг 5. Псевдокод, обрабатывающий поле NumRvaAndSize PE-заголовка.
Обращаем внимание, что при NumRvaAndSizes <= ...LOAD_CONFIG значения в таблице описания модуля остаются неинициализированными!
Теперь вспоминаем, что память под эти описания (при загрузке драйверов) берётся динамически из nonpagedpool и возвращаемся в обработку исключений. Что происходит, когда RtlLookupFunctionTable возвращает не 0 и не -1? Правильно, начинаем разбирать таблицу. Т.е. имеем (псевдокод) нечто вроде:
for (...) { ... If (.... && cuFunction >= mdsc->SEHtable[i]) return TRUE; }
Листинг 6. Псевдокод функции разбора таблицы исключений SEHtable.
...теперь вспоминаем, что SEHtable у нас не инициализирован (сиречь - содержит мусор) и получаем что? Правильно - GPF. А теперь вспоминаем, что это место мы проходим при обработке любого исключения в драйвере (в том числе - вполне штатного, со своими обработчиками), в том числе и на IRQL > DISP и получим что? Правильно - BSOD. Например, при DebugPrint в release build и отсутствии отладчиков :)"
Рисунок 8. BSOD, возникающий из-за ошибки, допущенной разработчиками Windows, оставивших неинициализированные данные в таблицах, ответственных за поддержку SafeSEH.