Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #111, МАРТ 2008 г.

Обзор эксплойтов

Крис Касперски

Хакер, номер #111, стр. 111-046-1

Парад дыр в NT продолжается! Как и в прошлом выпуске, я делюсь своими находками, впрочем, не претендуя на то, что первым их нашел именно я. Во всяком случае, их явного описания в сети не нашлось, да и так ли важно, кто первый сказал «гав»? Главное, что это реальные дыры, работающие на всех современных операционных системах Microsoft – от W2K по Висту включительно. Да будет свет! Тушите свечи!

Microsoft Windows – PE loader BSOD

Brief

Еще весной 2004 года я обнаружил гремучую уязвимость в загрузчике PE-файлов, роняющую W2K SP3 в BSOD с прикладного режима даже без прав администратора. И хотя дыра была описана в «Системном администраторе», русском издании «Компьютерных вирусов снаружи и изнутри», а также в «Shellcoder's programming uncovered», выпущенной на английском языке – уязвимость никуда не делась. Разработчики XP SP2 исправили большое количество дыр в PE-загрузчике, но эту пропустили. Не заметили ее и разработчики Висты, а я к настоящему времени уже вплотную подошел к созданию боевого exploit'а, позволяющего загружать shell-код в ядерное пространство, не требуя от него ни цифровой подписи, ни прав администратора. Позволю себе процитировать отрывок своей собственной статьи четырехлетней давности: «поля File Alignment и Section Alignment задают кратность выравнивания секций на диске и в памяти, соответственно. Официально о кратности выравнивая известно лишь то, что она представляет собой степень двойки, причем:

  • а) Section Alignment должно быть больше или равно 1000h байт;
  • б) File Alignment должно быть больше или равно 200h байт;
  • в) Section Alignment должно быть больше или равно File Alignment.

Если хотя бы одно из этих условий не соблюдается, файл не будет загружен. В Windows NT существует недокументированная возможность отключения выравнивания, основанная на том, что загрузку прикладных исполняемых файлов/динамических библиотек и системных драйверов обрабатывает один и тот же загрузчик. Если Section Alignment == File Alignment, то последнее поле может принимать любое значение, представляющее собой степень двойки и превышающее 10h. Условимся называть такие файлы «не выровненными». Хотя этот термин не вполне корректен, лучшего пока не придумали. К не выровненным файлам предъявляется следующее, достаточно жесткое требование – виртуальные и физические адреса всех секций обязаны совпадать, то есть страничный имидж должен полностью соответствовать своему дисковому образу. Впрочем, никакое правило не обходится без исключений, и виртуальный размер секций может быть меньше их физического размера, но не более чем Section Alignment – 1 байт (секция все равно будет выровнена в памяти). Самое интересное, что физический размер последней секции «вылетает» за пределы загружаемого файла, операционная система выбрасывает голубой экран смерти».

Targets

NT, W2K, XP, Server 2003, Server 2008, Виста.

Exploit

Демонстрационный exploit выложен на моем сервере по адресу http://nezumi.org.ru/souriz/hack/PE_BSOD.

Solution

Отсутствует.

Microsoft Vista – отключение DEP, ASLR, SafeSEH, etc

Brief

Начиная с XP, парни из Microsoft всерьез озаботились безопасностью, и уже в XP SP2 появился DEP (поддержка NX/XD битов страниц, препятствующих выполнению кода на куче и в стеке) и механизм SafeSEH, ну а Виста явила нам рандомизацию адресного пространства, она же ASLR. Хакерам сразу стало интересно, как отключить все эти хорошие вещи, затрудняющие атаки. Возник вопрос: как же в Висте ухитряются работать старые программы, использующие антиотладочные трюки, противоречащие политике защитных механизмов. Всем известна плохая совместимость Висты с программным обеспечением, написанным до нее – однако, для популярных защитных пакетов (ASPack, Start Force) в NTDLL.DLL была оставлена специальная «нычка», распознающая запротекченные файлы и молчаливо отключающая защитные механизмы Висты, препятствующие их функционированию. Опознание происходит по именам секций PE-файла, и, если это '.sforce', '.pcle' или '.aspack', файл автоматически получает «иммунитет». Сброс поля COFF-заголовка «characteristics» в 010Eh (210E для динамических библиотек) также отключает множество защит (даже тех, что еще не появились на свет), причем SFC (система автоматической проверки целостности системных файлов) не контролирует поле «characteristics» и потому отключение защиты можно осуществлять даже для системных файлов: exe, dll, ocx. Естественно, удаленно этого не сделать и для реализации атаки необходимо найти дыру в каком-нибудь приложении, но актуальности угрозы это не снижает.

Targets

XP, Server 2003, Server 2008, Виста.

Exploit

Не требуется.

Solution

Отсутствует.

Microsoft Windows – EFS на страже малвари

Brief

Начиная с W2K, в Windows появилась поддержка шифрованной файловой системы (Encrypting File System или EFS), реализованной в виде расширения к NTFS и архитектурно находящейся на один уровень ниже ее, в результате чего все операции с зашифрованными файлами протекают абсолютно прозрачно как для пользователя, так и для прикладных приложений. Удобно! Но что скрывается за этим уродством, тьфу, простите, оговорился, удобством? Для каждого пользователя произвольным образом генерируется пара асимметричных ключей: публичный ключ (доступный всем) и приватный ключ, который по теории не должен быть доступен никому, кроме юзера. Публичный ключ копируется в каталог \documents-n-settings\user-name\application data\microsoft\systemcertificates\user-name\certificates\, а приватный помещается в \documents-n-settings\user-name\application data\microsoft\crypto\rsa\. Однако, поскольку асимметричная криптография — тормозная штука, Microsoft придумала шифровать файлы симметричным ключом пользователя (File Encryption Key или FEK), также генерируемым произвольным образом. Содержимое файла шифруется FEK-ключом по алгоритму DESX (W2K) или AES (XP и выше), а сам FEK-ключ шифруется публичным ключом и в зашифрованном виде сохраняется в файле в отдельном именованном NTFS-потоке $EFS. Специальный компонент системы, называемый агентом восстановления, считывает приватный ключ пользователя, извлекает из $EFS потока зашифрованный FEK-ключ, расшифровывает его, после чего расшифровывает FEK-ключом содержимое самого зашифрованного файла.

Достоинство W2K в том, что в ней имеется агент восстановления по умолчанию, представленный в лице администратора системы, в хранилище сертификатов которого автоматически копируются все приватные пользовательские ключи, а потому администратор может расшифровать файл, зашифрованный любым пользователем, даже если тот угробит свою учетную запись. Начиная с XP, агент восстановления по умолчанию ушел в отставку и, хотя администратор по-прежнему может расшифровать файлы, зашифрованные пользователями, ему необходимо вручную скопировать их приватные ключи в свое хранилище сертификатов. Это была предыстория. А теперь — уязвимость! Поскольку антивирусное программное обеспечение, как правило, работает с правами администратора (или из-под учетной записи спец пользователя), чтобы видеть все файлы, то зловредному ПО ничего не стоит скрыться от его глаз. Достаточно просто присвоить своим файлам атрибут «encrypted» и все! А сертификат, кстати говоря, можно динамически удалять/добавлять из хранилища, используя его только на время активной фазы работы — это предотвратит его ручное копирование администратором. Правда, если жертва сидит под «админом», то... хм, ну и это не преграда, ведь малварь может создать для себя отдельного пользователя, запуская файлы через runas, и антивирус их никак не захавает. Конечно, существование самих файлов не скрыть и при попытке доступа к ним антивирус получит ошибку отказа в доступе, но… это все-таки не то же самое, что антивирусная тревога!

Targets

XP, Server 2003, Server 2008, Виста.

Exploit

Не требуется.

Solution

Отсутствует.

Честная кража процессорного времени

В многозадачных (и особенно многопользовательских) системах очень важно знать, сколько процессорного времени «скушала» та или иная задача, чтобы планировать загрузку ЦП, не допуская нежелательных тормозов. Большинство коммерческих провайдеров накладывают жесткие ограничения на предельное время исполнения скрипта, зачастую приостанавливая хостинг при его превышении, а некоторые вычислительные центры напрямую торгуют машинным временем (они бы еще и воздухом торговали, блин). Спрос рождает предложение, и возникает естественная потребность — заплатить поменьше, а получить побольше, другими словами, украсть машинное время. Это актуально не только для суперкомпьютеров, но и обычных серверов, поскольку, большинство малвари палится как раз потому, что не умеет правильно воровать.

Все операционные системы линейки NT (как сервера, так и рабочие станции) поддерживают развитую систему мониторинга счетчиков производительности, отображая в реальном времени загрузку ЦП и выводя полное время, проведенное каждым процессом в пользовательском режиме и в режиме ядра. Увидеть его можно как с помощью «Системного монитора», так и «Диспетчера Задач». Разницы между ними никакой, так как они считают показания одних и тех же счетчиков. Утилиты сторонних разработчиков (например, «Process Explorer» Марка Руссиновича) также выводят данную информацию, причем в более наглядной форме. И ведь многие ей верят.

Лишь немногие задумываются, насколько точны эти показания, и можно ли их подделать, а если можно, то как? Достопочтенный «Червь Морриса» периодически расщеплял себя надвое, уничтожая материнский процесс. Счетчик потребления процессорного времени дочернего процесса при этом, естественно, устанавливался в ноль, что позволило Моррису избежать «накопления» времени ЦП. Однако подобная активность слишком заметна, что не есть гуд.

Разумеется, это грубая схема. Существуют намного более изощренные способы хищения процессорного времени, впервые продемонстрированные и теоретически обоснованные известным экспертом по безопасности Tsutomu Shimomura (тем самым, который ловил Митника) в далеком 1980 году. Тогда на них не обратили никакого внимания. Лишь в 2007 году (двадцать семь лет спустя!) два студента Израильского университета «School of Computer Science and Engineering The Hebrew University» Yoav Etsion и Dror G. Feitelson в соавторстве с Dan'ом Tsafrir — сотрудником корпорации IBM – опубликовали статью «Secretly Monopolizing the CPU Without Superuser Privileges», показывающую, что за минувшие годы ничего не изменилось и кража процессорного времени по-прежнему остается возможной во всех операционных системах: Linux, BSD, NT, etc.

К тем же самым выводам (приблизительно в то же самое время) пришел и я, исследующий устройство счетчиков производительности и алгоритм определения загрузки ЦП/процессорного времени в W2K, Server 2003 и Висте. Выяснилось, что система измеряет не загрузку ЦП как таковую, а готовность системного планировщика предоставить процессорное время потоку по первому требованию. Это очень грубый показатель, а методы его измерения вообще таковы, что вызывают шевеление волос в разных местах. Если представить все в упрощенном виде, то происходит это так. Имеется рабочий (который работает или, что тоже возможно, не работает). И специальный «инспектор», который через регулярные промежутки времени (например, каждый час) приходит и проверяет, чем рабочий в данный момент занимается. Если тот вкалывает, как пара Карло, то ему ставится «зачот» за весь отчетный период и, соответственно, наоборот.

Вообразим вполне вероятную ситуацию, при которой рабочий устраивает себе пятиминутный перекур каждый час. Как нетрудно рассчитать, его «загрузка» составит ~90%, но если перекуры совпадут с приходом инспектора, мы получим нулевую загрузку! Шутки в сторону, господа! При желании можно написать программу, потребляющую свыше 90% времени ЦП, но «Диспетчер Задач» (а вместе с ним и «Системный Монитор») будут осциллировать в переделах абсолютного нуля. Для этого достаточно отдавать остаток кванта времени за несколько миллисекунд до его истечения. Планировщик, обнаружив, что поток не исполняется, ошибочно пропишет ему «ноль» в графе «Использование процессорного времени».

Длительность одного кванта в NT-подобных системах составляет порядка 100 мс. Отдать остаток неиспользуемого кванта можно при помощи API-функции Sleep(0), которая соответствует функции nanosleep() в UNIX-системах. Самое сложное — это синхронизовать отдачу остатка кванта с приходом «инспектора», тогда, даже отдавая ничтожный остаток кванта, мы снизим потребление процессорного времени до абсолютного нуля (а точнее, украдем все процессорное время). Поскольку алгоритмы планировки потоков меняются от одной версии системы к другой, написать переносимую программу довольно сложно, но, к счастью, и не нужно. Куда легче отдавать существенный остаток кванта функцией Sleep(1), вгоняющей поток в сон на 1 мс (на самом деле, 10 мс, что связано с дискретностью системного таймера), гарантированно обеспечивая здоровый сон потока к моменту прихода инспектора. А поскольку при пробуждении поток получает полный квант, открывающий новый «отчетный период», это естественным образом синхронизует его с приходом «инспектора». КПД программы, конечно, упадет ( часть времени она будет спать вместо того, чтобы работать), но куда нам спешить?

Напишем несложную тестовую утилиту и проведем с ней несколько познавательных экспериментов:

Cheater.c – программа, демонстрирующая честную кражу процессорного времени

// total - общее количество вычислений некоторого типа (неважно каких)

#define total 100000000

// steal - количество вычислений, выполняемых перед отдачей остатка кванта

#define steal 30000

main()

{

// локальные переменные, которые мы будем использовать

int a, b, c, sum = 1; DWORD A1;

// засекаем время начала выполнения вычислительного цикла

A1=GetTickCount();

for (b = 0; b < total/steal; b++) // мотаем главный цикл

{

// мотаем цикл, исполняющийся в пределах одного кванта

for (a = 0;a < steal; a++) sum+=sum % 3 + 1;

// …а остаток кванта мы спим, как сурки

//Sleep(1);

}

// выводим _реальное_ время, потраченное на вычисления

printf("==%d sec\n", (GetTickCount() - A1)/1000);

// возвращаем sum назад, чтобы оптимизирующий компилятор

// не принял ее за неиспользуемую переменную и не выкинул

// результаты вычислений, исказив результаты экспериментов

return sum;

}

Транслируем программу компилятором «Microsoft Visual C++» со следующими ключами командной строки: «cl.exe /Ox cheater.c» (где /Ox – максимальная оптимизация) и запускаем полученный файл cheater.exe на выполнение.

Поскольку функция Sleep(1) пока закомментирована, программа использует все доступное процессорное время. Поэтому загрузка процессора (на однопроцессорной машине без поддержки Hyper-Threading или многоядерности) вплотную приближается к 100%, а полное время выполнения (по показаниям самой программы) на P-III 733 MHz составляет 7 сек. Эти показания вполне соответствуют счетчику производительности, оценившему потребление процессорного времени в 6 сек. В данном случае счетчику производительности можно верить, поскольку он вычисляет из общего времени выполнения то время, которое программа была вынуждена разделять с другими — в фоне играл Winamp, пара файлов качалась из сети.

А теперь раскомментируем Sleep(1), перекомпилируем программу и запустим ее по новой. Как нетрудно видеть, загрузка процессора существенно снизилась, достигнув в максимуме ~80%, а полное время выполнения программы увеличилось на одну секунду (8 сек.). Однако показания «Диспетчера Задач» радикально изменились – он показал всего 3 сек., что, очевидно, не соответствует действительности! Объем вычислений-то не изменился! И потому подлинное значение потребления процессорного времени никак не могло упасть. А ведь упало. Причем в два раза!

ОК, уменьшаем значение переменной steal с 30000 до 20000 и вновь перекомпилируем программу. На этот раз загрузка процессора падает до ~50% (а ширина «холмика», соответственно увеличивается). Полное время выполнения (вследствие падения КПД) составляет уже 11 сек., но показания счетчика производительности продолжают уменьшаться и колеблются между 2 и 3 сек., что дает нам среднее значение в 2,5 сек.

А теперь смертельный номер! Уменьшаем steal с 20000 до 10000, чтобы поток существенную часть своего времени проводил во сне. И что же? Загрузка процессора упала настолько, что его «холмик» практически сравнялся с «шумом» фоновых выплесков. Полное время выполнения программы увеличилось до 13 сек., (что вдвое больше первоначального результата с закомментированной функцией Sleep), но счетчик производительности показывает 0 сек. потребления процессорного времени (или 1 сек. — в худшем случае).

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

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

Содержание