Обзор эксплойтовКрис Касперски Хакер, номер #114, стр. 042 Этот необычный во всех отношениях обзор посвящен ошибкам отладчиков, приводящим к утрате контроля над отлаживаемым приложением еще на стадии загрузки exe-файла в память. Для исследователей малвари это зачастую заканчивается катастрофой – только хотел взглянуть на очередного зверька под отладчиком на своей основной машине, как отладчик проскакивает точку входа и живчик поселяется на компьютере, ищи его потом! Syser – проскок EP на сетевых и сменных носителяхBriefРаботая с отладчиком Syser версии 1.95.1900.0894, выпущенной в начале 2008 года, я обратил внимание, что при загрузке программ с сетевых дисков и сменных носителей (типа дискет) отладчик проскакивает точку входа в файл (она же Entry Point или, сокращенно, EP). Он передает подопытной программе бразды правления и утрачивает над ней всякий контроль (впрочем, глобальные точки останова на API-функции срабатывают нормально, если, конечно, не забыть заблаговременно их поставить). Мне пришлось отослать разработчикам баг-репорт, получив подтверждение об ошибке вкупе с обещанием ее исправить, но версии Syser'а продолжают выходить одна за другой, а ошибка как была, так и осталась. Почему же она вообще возникает? Откуда берется и отчего никуда не девается? Дело в том, что механизм загрузки файлов с жестких дисков и сменных носителей принципиально различен. Исполняемые файлы (и динамические библиотеки), расположенные на винчестере, операционная система просто проецирует в память, что (грубо говоря) превращает их в «файл подкачки, доступный только на чтение». Подкачка страниц с диска в оперативную память происходит только по мере обращения к ним, а при недостатке памяти немодифицированные страницы не вытесняются в настоящий файл подкачки, а просто высвобождаются под новые данные. Действительно, какой смысл записывать их в своп? Проще вновь обратиться к исполняемому файлу – он же никуда не денется за это время. Вернее, с жесткого диска не денется (и потому система блокирует к нему доступ на время выполнения), а вот с дискеты или сетевого диска... там, во-первых, скорость намного ниже, чем у винчестера, а во-вторых, сеть в любой момент может лечь, а диск могут вынуть. Разработчики Windows учли такую возможность и решили проблему путем предварительной загрузки файла в оперативную память. За это отвечает совсем другой компонент загрузчика, игнорируемый Syser'ом со всеми отсюда вытекающими. TargetsВсе существующие в данный момент версии Syser'а. exploitНе требуется, достаточно попробовать загрузить любой исполняемый файл с сетевого диска, дискеты или CD/DVD, как результат все скажет сам за себя. SolutionВсегда копируй файлы с сетевых дисков (сменных носителей) на жесткий диск перед их загрузкой в Syser. Syser: BSOD на файлах определенных типовBriefЭкспериментируя со штатным линкером от Microsoft на предмет создания предельно компактных исполняемых файлов, я получил от одного из бета-тестеров баг-репорт. В нем говорилось, что на его машине при активном Syser'е (активном – значит, просто запущенном, загрузки файла в отладчик не требуется) мои файлы уводят XP SP2 в BSOD с указанием на драйвер Syser.sys, принадлежащий, как и следует из названия, сабжевому отладчику. Я попробовал воспроизвести данный эффект на своей горячо любимой W2K и получил тот же самый BSOD. Вот тебе, бабушка, и оптимизация! Зато, какой шикарный способ борьбы с активным Syser'ом! На Soft-Ice эффект не распространяется, однако Soft-Ice на хакерских машинах встречается все реже и реже, особенно на новых системах (которые он вообще не поддерживает). Разработчикам Syser'а был отправлен очередной баг-репорт, но ответа не последовало (интересно, какую чудо-траву курят разработчики? – Прим. Forb). Когда они исправят дефект в отладчике — неизвестно. Подробнее об этой ошибке можно прочитать на блоге: souriz.wordpress.com/2008/05/09/syser-causes-bsod. TargetsВсе существующие версии Syser. ExploitГотовую бинарную сборку файла, вызывающего BSOD (вместе с исходными текстами и командными файлами для сборки в среде MS VC) я выложил на свой сервер: http://nezumi.org.ru/souriz/TF-bug.zip. Впрочем, сам исполняемый файл тут не причем (он может быть любым), главное – это опции линкера для его сборки, которые выглядят следующим образом: $link.exe %NIK%.obj /FIXED /ENTRY:nezumi /SUBSYSTEM:CONSOLE /ALIGN:16 /MERGE:.rdata=.text /STUB:stub KERNEL32.LIB Значения ключей компиляции ты можешь найти на нашем DVD. В целях оптимизации я использовал «голый» MS-DOS old-exe заголовок с отрезанным телом файла. В результате этих ухищрений размер файла (с полезным кодом, намного более функциональным чем «hello, world2) составил 624 байта, и это при том, что он написан на языке Си (пускай и не без ассемблерных вставок) и собран штатными средствами! Какой именно из этих параметров привел к падению Syser'а — я не знаю, а экспериментировать, роняя свою систему в BSOD, – этим пусть разработчики Syser'а занимаются. Кстати, если загрузить такой файл в Syser, а не просто запустить его при активном Syser'е, то все будет нормально, и BSOD не появится. SolutionВыгружать Syser перед запуском потенциально небезопасных файлов (благо, Syser, в отличие от Soft-Ice, поддерживает возможность выгрузки из памяти «на лету»). IDA-Pro: проскок EP на файлах с Image Base равной нулюBriefКак известно, IDA-Pro с некоторых времен не только дизассемблер, но еще и отладчик. Отладчик не то, чтобы сильно мощный (намного слабее Ольги), но все-таки более удобный, чем постоянное переключение между дизассемблером и внешним отладчиком, а потому активно используемый хакерами наряду с исследователями малвари. И все бы хорошо, но если «скормить» дизассемблеру файл с нулевым базовым адресом, он нормально загрузит его по этому адресу, но при попытке запуска отладчика мы получим ругательство: «IDA Pro couldn't automatically determine if the program should be rebased in the database because the database format is too old and doesn't contain enough information. Create a new database if you want automated rebasing to work properly. Notice you can always manually rebase the program by using the Edit, Segments, Rebase program command» («IDA-Pro не может автоматически определить: должна ли программа быть перемещена в базе, поскольку формат базы очень старый и не содержит достаточно информации. Создайте новую базу, если вы хотите автоматизировать перемещение. Примечание: вы можете переместить базу и самостоятельно: Edit -> Segment -> Rebase program»). После чего нам предлагают нажать «ОК», чтобы согласиться. Но, чтобы мы не нажали – ОК или Esc, IDA-Pro запускает процесс, полностью утрачивая контроль и мерзко игнорируя все ранее установленные точки останова. Вот такой замечательный анализ малвари! К слову сказать, файл с нулевым базовым адресом загрузки при наличии в нем таблицы перемещаемых элементов совершенно законен и операционная система переместит его автоматически. А вот IDA-Pro – нет. Я послал баг-рапорт Ильфаку, и тот сказал, что будет исправлять, так что следи за новостями: souriz.wordpress.com/2008/05/14/773-bug-in-ida-pro-fails-to-debug-zero-base-pe. TargetВсе существующие на данный момент версии IDA-Pro (проверялось на 4.7 и 5.2). ExploitИсходный текст программы, демонстрирующей уязвимость (вместе с ключами сборки), приведен ниже: #include <stdio.h> main() { printf("hello, world!\n"); } $cl /c hello-world.c $link hello-world.obj /FIXED:NO /BASE:0 SolutionПеред запуском отладчика удостоверься, что программа находится по «правильным» адресам, в противном случае перемести ее на новое место (например, по адресу 400000h) через Edit -> Segment -> Rebase program, или же с помощью утилиты ms editbin.exe (в последнем случае потребуется перезагрузка файла в IDA-Pro с потерей всех предыдущих результатов анализа). Full-disclose: универсальный способ выхода из-под отладчикаBriefВ процессе реализации проекта по переносу Soft-Ice на Висту и Server 2008 (при финансовой поддержке фирмы K7) я исследовал Soft-Ice вместе с кучей других отладчиков и с удивлением обнаружил, что все они спроектированы неправильно. Отлаживаемая программа может вырваться из-под контроля еще до начала трассировки – непосредственно в процессе загрузки файла в отладчик. Чтобы выяснить, почему так происходит, нам необходимо разобраться как вообще отладчики «стопорят» программу, а делают они это приблизительно так: сначала процесс загружается в память, отладчик отслеживает этот момент и, считывая из PE-заголовка точку входа в файл, устанавливает по этому адресу программную или (реже) аппаратную точку останова. Затем возвращает бразды правления операционной системе, которая посредством функции KiUserApcDispatcher создает первичный поток. Прототип функции приведен на рисунке, откуда видно, что стартовый адрес потока передается как аргумент по NTAPI соглашению, то есть через пользовательский стек, после чего система начинает подгружать статически прилинкованные динамические библиотеки, вызывая функцию DllMain (если, конечно, DLL имеет точку входа) и DLL. Если DllMain возвращает ноль, система сообщает об ошибке загрузки приложения и завершает процесс. В противном же случае (когда все динамические библиотеки рапортуют, что инициализация прошла успешно) управление передается первичному потоку процесса, где находится точка останова, установленная отладчиком. При попытке ее выполнения процессор генерирует исключение типа breakpoint exception, отлавливаемое операционной системой и передающее отладчику бразды правления. Таким образом, вырисовывается не очень-то приятная для отладчиков и исследователей малвари картина:
Конечно, теоретики от программирования скажут, мол, в чем проблема-то? Операционная система уведомляет отладчик о загрузке всех динамических библиотек еще до начала выполнения DllMain и ничего не стоит исследовать их на вшивость. Многие отладчики даже имеют специальную опцию – останавливаться на DllMain. Вот только ни у одного из них она по умолчанию не включена, а анализировать DllMain в отладчике – гиблое дело, особенно если она вызывается из стартового библиотечного кода. Но, даже отлаживая DllMain, мы можем не заметить (или не понять), как она меняет аргумент функции KiUserApcDispatcher или снимает точку останова, поскольку для этого необходимо знать, что именно у нас находится в памяти и зачем оно там находится. С точки зрения не очень опытного реверсера это выглядит вполне невинно. Ну, меняет что-то такое DLL в стеке и завершается, но ведь это не страшно, правда? Мы ведь все равно вернемся в отладчик при начале выполнение процесса, верно? А вот и нет! Уже не вернемся! Так что проблема на самом деле очень серьезна. Подробнее ее планируется осветить на моем блоге (souriz.wordpress.com). TargetsMS VS, WinDbg, OllyDbg, ImmDbg, Syser, Soft-Ice, IDA-Pro и многие другие.ExploitДемонстрационный пример (вместе с полными исходными текстами, правда, без комментариев) выложен на OpenRCE в мой репрозиторий: https:// openrce.org/repositories/users/nezumi/quux-crackme.zip. Пример занимает в упакованном виде меньше 2х килобайт, совместим с Вистой/Server 2008 и не конфликтует с Syser'ом (в смысле не «выбивает» из него BSOD, но ускользает из-под вышеперечисленных отладчиков). SolutionОтсутствует. Ну, не то, чтобы совсем, – но общих решений нет, и помимо DllMain существует много всего, исполняющегося до «всплытия» отладчика. Взять хотя бы те же TLS-callback'и. А потому загрузка программы в отладчик – равносильна ее запуску, и потенциально опасные файлы можно использовать только на специальной (виртуальной) машине, на которой нет ничего, что было бы жалко потерять. Кстати, в Olly Advanced – популярном plug-in'е для Ольги – есть опция «Flexible Breakpoints», убирающая программную точку останова, что предотвращает обнаружение отладчика, но не в силах противостоять подмене стартового адреса первичного потока. Аналогичным образом дела обстоят и с другим популярным plug-in'ом – PhantOm, имеющим опцию «Remove EP Break», назначение которой говорит само за себя. Комментированные исходные тексты quux-crackmeДля облегчения понимания принципов работы отладчиков и способов «побега» из-под них я привожу комментированные исходные тексты quux-crackme, однако, прежде чем их читать, рекомендуется попробовать разобраться с crackme самостоятельно, благо он очень простой. Даже start-up убит и коду там – несколько сотен машинных команд без каких бы то ни было трюков или выкрутасов (комментарии к файлу quux-crackme.c, подгружающему библиотеку, ищи на DVD). Исходный текст динамической библиотеки quux-dll.c #include <windows.h> bar(); // функция foo, вызываемая из экспортируемой // функции baz, представленной далее по тексту __declspec(naked)foo() { __asm { CALL bar RETN } } // выводим пословицу на Малайском с оригинальным переводом bar() { MessageBox(0,"\n -> use the correct tool for the correct job <-\n\n", " -<* kee chang jahb thak-a-thaen *>-",0); } // экспортируем функцию baz(), // вызываемую исполняемым файлом // из той его части, что получает // управление, благодаря подмене // стартового адреса первичного потока __declspec(dllexport) int baz(){ return foo();} // точка входа в динамическую библиотеку // вызывающая при различных системных событиях, // например, создании потока, в том числе и первичного BOOL WINAPI dllmain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved) { // определяем смещения основных полей PE-файла #define PE_off 0x3C // смещение PE-заголовка в файле #define EP_off 0x28 // смещение поле Entry-Point, относительно PE
// опкод программной точки останова #define SW_BP 0xCC
// объявляем переменные BYTE* base_x; DWORD pe_off; DWORD ep_off; BYTE* ep_adr; DWORD* stackg; BYTE* BaseAddress; DWORD RegionSize; char buf[_MAX_PATH]; MEMORY_BASIC_INFORMATION lpBuffer;
// для отладки динамической библиотеки // раскомментирование этой строки приводит // к генерации исключения, передаваемому // отладчику, который тут же "всплывает" //__asm{ int 03}
// получаем имя exe-файла на тот случай // если кто-то его захочет переименовать // из quux-crackme.exe во что-то другое GetModuleFileName(0,buf,_MAX_PATH);
// определяем базовый адрес exe-файла base_x = (BYTE*) GetModuleHandle(buf);
// определяем смещение PE-заголовка в файле pe_off = *((DWORD*)(base_x + PE_off));
// определяем адрес точки входа, преобразуя // ее в указатель на двойное слово, что уже // не будет работать на 64-битных системах, // но на 64-битных системах по любому все // будет сильно иначе ep_off = *((DWORD*)(base_x + pe_off + EP_off));
// вычисляем линейный виртуальный адрес точки входа ep_adr = base_x + ep_off;
// проверяем: а не установлена ли точка останова на EntryPoint // если установлена – возвращаем системе ноль, сигнализируя // о провале инициализации DLL – или, как вариант, здесь мы бы // могли снять точку останова, чтобы выйти из-под контроля // отладчика, чем, собственно говоря, и занимается qux-crackme: // https://www.openrce.org/repositories/users/nezumi/crackme-qux.zip if (*ep_adr == 0xCC) return 0;
// передаем функции VirtualQuery адрес аргумента hinstDLL, // переданного процедуре dllmain через стек и, тем самым, // узнаем приблизительное значение регистра ESP без // всяких ассемблерных вставок - красота! VirtualQuery((LPCVOID)&hinstDLL, &lpBuffer, sizeof(lpBuffer));
// VirtualQuery возвращает нам базовый адрес блока, // выделенного под стек, который мы и будем ковырять // на предмет аргумента функции KiUserApcDispatcher BaseAddress = ((BYTE*)lpBuffer.BaseAddress); // определяем размер блока, чтобы не выйти за его границы // (на тот случай, если вдруг в стеке по каким-то причинам // искомого аргумента не окажется) RegionSize = lpBuffer.RegionSize-sizeof(DWORD); // ищем в стеке двойное слово, совпадающее // с адресом точки входа в исполняемый файл, // начиная поиск со дна стека и продвигаясь наверх // (искомый аргумент лежит почти на дне, но где именно: // заведомо неизвестно, вот и приходится рыскать) for(RegionSize;RegionSize>0;RegionSize-=sizeof(DWORD)) if (((DWORD)ep_adr)==( *(DWORD*)(BaseAddress+RegionSize))) // искомый аргумент (или нечто на него похожее) найден! // увеличиваем его значение на 0Dh, проскакивая не только // бряк, установленный отладчиком, на точку входа, но и // первый CALL c RETN, передавая управление сразу на // второй CALL, который IDA-Pro, не найдя ссылок // даже не удосужилась дизассемблировать (*(DWORD*)(BaseAddress+RegionSize)) +=0xD; // change EP // примечание: здесь по идее должен стоят break, // типа - раз нашли адрес, то чего шерстить по стеку?! // а вот мы продолжаем шерстить на тот случай, если // вдруг нашли что-то не то и адрес точки входа совпал // с чем-то другим, ес-но, это может привести к краху // программы и если искомый адрес встречается два и // более раз, следовало бы отказаться от его модификации // именно так и следует поступать в коммерческих защитах return 1; } Разумеется, это никакой не crackme (в обычном смысле), а настоящий exploit типа proof-of-concept, предназначенный для тестирования отладчиков на «вшивость» и написанный так, чтобы предельно облегчить его понимание, благодаря чему сломать его – плевое дело. Но если наворотить здесь хитрый код, то шансы на понимание резко снизятся, а шансы на «взлом» отладчика, соответственно, резко возрастут. Борьба с отладчиками на данном этапе не входила в мою задачу – этому посвящена отдельная колонка в журнале, а здесь мы говорим исключительно о дырах (прорыв сквозь отладчик – в первую очередь дефект проектирования отладчика, то есть дыра, и только потом – антиотладочный прием). |