Exploits review (выпуск 0x18)

Автор: (c)Крис Касперски ака мыщъх

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

Syser - проскок EP на сетевых и сменных носителях

brief

Работая с отладчиком Syser версии 1.95.1900.0894, выпущенной в начале 2008 года, мыщъх обратил внимание, что при загрузке программ с сетевых дисков и сменных носителей (типа дискет) отладчик проскакивает точку входа в файл (она же Entry Point или, сокращенно, EP), передавая подопытной программе бразды правления и утрачивая над ней всякий контроль (впрочем, глобальные точки останова на API-функции срабатывают нормально, если, конечно, не забыть заблаговременно их поставить). Мыщъх отослал разработчикам баг-репорт, получив подтверждение об ошибке вкупе с обещанием ее исправить, но... версии Syser'а продолжают выходить одна за другой, а ошибка как была, так и осталась. Почему же она вообще возникает? Откуда берется и почему никуда не девается? Дело в том, что механизм загрузки файлов с жестких дисков и сменных носителей принципиально различен. Исполняемые файлы (и динамические библиотеки), расположенные на винчестере, операционная система просто проецирует в память, что (грубо говоря) превращает их в "файл подкачки, доступный только на чтение". Подкачка страниц с диска в оперативную память происходит только по мере обращения к ним, а при недостатке памяти немодифицированные страницы не вытесняются в настоящий файл подкачки, а просто высвобождаются под новые данные. Действительно - какой смысл записывать их в своп? Проще вновь обратиться к исполняемому файлу - он же никуда не денется за это время. Ну, это с жесткого диска он не денется (и потому система блокирует к нему доступ на время выполнения), а вот с дискеты или сетевого диска... Там, во-первых, скорость намного ниже, чем у винчестера, а во-вторых, сеть в любой момент может лечь, а диск - быть вынутым. Разработчики Windows учли такую возможность развития событий и решили проблему путем предварительной загрузки файла в оперативную память, за что отвечает совсем другой компонент загрузчика, игнорируемый Syser'ом со всеми отсюда вытекающими...

targets

Все существующие в данный момент версии Syser'а.

exploit

Не требуется, достаточно попробовать загрузить любой исполняемый файл с сетевого диска, дискеты или CD/DVD, как результат все скажет сам за себя.

solution

Всегда копируйте файлы с сетевых дисков (сменных носителей) на жесткий диск перед их загрузкой в Syser.

Попытка загрузки файла

Рисунок 1. Попытка загрузки файла с сетевого диска в Syser ведет к "проскоку" точки входа в файл и утрате контроля за отлаживаемым приложением.

Syser: BSOD на файлах определенных типов

brief

Экспериментируя со штатным линкером от Microsoft на предмет создания предельно компактных исполняемых файлов, мыщъх получил от одного из бета-тестеров баг-репорт, что на его машине мыщъх'иные файлы при активном Syser'е (активном - просто запущенном, загрузки файла в отладчик не требуется) роняют XP SP2 в BSOD с указанием на драйвер Syser.sys, принадлежащий, как и следует из его названия, сабжевому отладчику. Мыщъх попробовал воспроизвести данный эффект на своей горячо любимой W2K и... получил тот же самый BSOD. Вот тебе, бабушка, и оптимизация! Зато какой шикарный способ борьбы с активным Syser'ом! На Soft-Ice данный эффект не распространяется, однако Soft-Ice на хакерских машинах встречается все реже и реже, особенно на новых системах, которые Soft-Ice вообще не поддерживает. Разработчикам Syser'а был отправлен очередной баг-репорт, но никакого ответа от них не последовало и когда они исправят дефект в отладчике - неизвестно. Подробнее об этой ошибке можно прочитать на мыщъх'ном блоге: http://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

Листинг 1. Сборка компактного исполняемого файла, высаживающего Syser'а на полный BSOD.

Здесь: /ALIGN:16 - установить выравнивание секций в файле и в памяти по границе 10h, что намного меньше "официально" разрешенного значения (1000h - для выравнивания секций в памяти, что соответствует размеру одной страницы и 200h для выравнивания секций на диске, что соответствует размеру одного сектора), однако для драйверов такого ограничения нет, а грузит их один и тот же системный загрузчик, поэтому обозначенный трюк вполне законен для всех операционных систем из линейки NT, вплоть по Висту/Server 2008 включительно.

/MERGE:.rdata=.text - говорит линкеру объединить секцию .rdata (секция данных, доступная только на чтение) с секцией .text (содержащей код), в результате чего у нас образуется всего одна секция и мы экономим до 10h байт, которые в противном случае ушли бы на выравнивание второй секции.

/STUB:stub - приказывает линкеру использовать пользовательскую MS-DOS "затычку", вместо той, что по умолчанию вставляется в начало всякого PE-файла и выводит на экран известное сообщение, что программа жить не может без Windows. В целях оптимизации мыщъх использовал "голый" MS-DOS old-exe заголовок с отрезанным телом файла.

В результате всех этих ухищрений размер файла (с полезным кодом, намного более функциональным чем "hello, world") составил 624 байта и это при том, что файл написан на языке Си (пускай и не без ассемблерных вставок) и собран штатными средствами! Какой именно из этих параметров привел к падению Syser'а - мыщъх не знает, а экспериментировать, роняя свою систему в BSOD - этим пусть разработчики Syser'а занимаются. Кстати, если загрузить такой файл в Syser, а не просто запустить его при активном Syser'е, то все будет нормально и BSOD не появится.

solution

Выгружать Syser перед запуском потенциально небезопасных файлов (благо, Syser, в отличие от Soft-Ice, поддерживает возможность выгрузки из памяти "на лету").

BSOD

Рисунок 2. BSOD, вызываемый Syser'ом при запуске "оптимизированного" файла.

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"), после чего нам предлагают нажать на "ОК", чтобы согласиться. Но что бы мы не нажали - "ОК" или "Escape", IDA-Pro запускает процесс, полностью утрачивая контроль и мерзко игнорируя все ранее установленные точки останова. Вот такой замечательный анализ малвари! К слову сказать, файл с нулевым базовым адресом загрузки при наличии в нем таблицы перемещаемых элементов совершенно законен и операционная система переместит его в памяти автоматически. А вот IDA-Pro - нет. Мыщъх послал баг-рапорт Ильфаку и тот сказал, что “будем посмотреть”, так что следите за новостями: http://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");
}

Листинг 2. "hello-world.c" - вполне типичная программа на Си.

$cl /c hello-world.c
$link hello-world.obj /FIXED:NO /BASE:0

Листинг 3. Сборка программы с нулевым базовым адресом загрузки.

solution

  Перед запуском отладчика удостовериться, что программа находится по "правильным" адресам, в противном случае переместить ее на новое место (например, по адресу 400000h) через Edit -> Segment -> Rebase program, или же с помощью утилиты ms editbin.exe (в последнем случае потребуется перезагрузка файла в IDA-Pro с потерей всех предыдущих результатов анализа).

Реакция IDA-Pro

Рисунок 3. Реакция IDA-Pro на попытку отладки файла с нулевым базовым адресом загрузки.

Full disclose: Универсальный способ выхода из-под отладчика

brief

В процессе реализации проекта по переносу Soft-Ice на Висту и Server 2008 (при финансовой поддержке фирмы K7), мыщъх исследовал Soft-Ice вместе с кучей других отладчиков и с удивлением обнаружил, что все они спроектированы неправильно и отлаживаемая программа может вырваться из-под контроля еще до начала трассировки - непосредственно в процессе загрузки файла в отладчик. Чтобы выяснить - почему так происходит, нам необходимо разобраться - как вообще отладчики "стопорят" программу, а делают они это приблизительно так - сначала процесс загружается в память, отладчик отслеживает этот момент и, считывая из PE-заголовка точу входа в файл, устанавливает по этому адресу программную или (реже) аппаратную точку остановка, после чего возвращает бразды правления операционной системе, которая посредством функции KiUserApcDispatcher создает первичный поток. Прототип функции приведен на рис. 4, откуда видно, что стартовый адрес потока передается как аргумент по NTAPI-соглашению, то есть через пользовательский стек, после чего система начинает подгружать статически прилинкованные динамические библиотеки, вызывая функцию DllMain (если, конечно, DLL имеет точку входа) и каждой DLL.

Прототип функции

Рисунок 4. Прототип функции KiUserApcDispatcher, с которой начинает жизнь любой поток.

Если DllMain возвращает ноль, система сообщает об ошибке загрузки приложения (см. рис. 5) и завершает процесс. В противном же случае (когда все динамические библиотеки рапортуют, что инициализация прошла успешно) управление передается первичному потоку процесса, где находится точка останова, установленная отладчиком. При попытке ее выполнения процессор генерирует исключение типа breakpoint exception, отлавливаемое операционной системой и передающей отладчику бразды правления.

Сообщение операционной системе

Рисунок 5. Сообщение операционной системе о неудачной инициализации динамической библиотеки, ведущей к завершению процесса.

Таким образом, вырисовывается следующая (не очень-то приятная для отладчиков и исследователей малвари) картина:

Блог мыщъх'а

Рисунок 6. Блог мыщъх'а.

targets

MS VS, WinDbg, OllyDbg, ImmDbg, Syser, Soft-Ice, IDA-Pro и многие другие...

exploit

Демонстрационный пример (вместе с полными исходными текстами, правда, без комментариев) выложен на OpenRCE в репозиторий мыщъх'а: https://www.openrce.org/repositories/users/nezumi/quux-crackme.zip, занимающий в упакованном виде меньше 2-х килобайт, совместимый с Вистой/Server 2008 и не конфликтующий с Syser'ом (в смысле - не "выбивающий" из него BSOD), но ускользающий из-под всех вышеперечисленных отладчиков.

Файловый репозиторий

Рисунок 7. Файловый репозиторий мыщъх'а на OpenRCE.

solution

Отсутствует. Ну, не то, чтобы совсем отсутствует, но общих решений нет и помимо DllMain существует туча всего такого, исполняющегося до "всплытия" отладчика, взять хотя бы те же TLS-callback'и, например, а потому загрузка программы в отладчик равносильна ее запуску и потенциально опасные файлы можно использовать только на специальной (виртуальной) машине, на которой нет ничего такого, что было бы жалко потерять (примечание: кстати, в Olly Advanced - популярном plug-in'е для Ольги - есть опция "Flexible Breakpoints", убирающая программную точку останова, что предотвращает обнаружение отладчика, но не в силах противостоять подмене стартового адреса первичного потока. Аналогичным образом дела обстоят и с другим популярным plug-in'ом - PhantOm, имеющим опцию "Remove EP Break", назначение которой говорит само за себя).

Full disclose: Комментированные исходные тексты quux-crackme

Для облегчения понимая принципов работы отладчиков и способов "побега" из-под них, мыщъх приводит комментированные исходные тексты quux-crackme, однако прежде, чем их читать, рекомендуется попробовать разобраться с crackme самостоятельно, благо он очень простой, даже start-up убит для облегчения понимания и коду там - всего несколько сотен машинных команд, без каких бы то ни было трюков или выкрутасов.

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


#include <windows.h>

// импортируем из DLL функцию baz();
// просто импортируем! не вызываем!
// на самом деле мы вызываем DllMain

__declspec(dllimport) baz()

// объявляем прототип функции foo(),
// реализация которой представлена ниже

foo()

// точка входа в исполняемый файл;
// чтобы убить библиотечный start-up,
// пришлось отказаться от main(),
// указывая имя точки входа линкеру
// в параметре /ENTRY (для ms link)

__declspec(naked) nezumi()
{
__asm {
                NOP ; // сюда отладчик ставит точку останова;
                NOP ; // несколько подряд идущих инструкций NOP
                NOP ; // не имеют никакого особого смысла,
                NOP ; // и вставлены лишь для того, чтобы код
                NOP ; // не был уж совсем тривиальным
                CALL foo ; // зовем функцию foo
                RETN ; // завершаем свое выполнение
                                ; // функция foo, как легко видеть,
                                ; // сообщает о том, что обнаружен отладчик,
                                ; // и этот код получает управление только
                                ; // под отладчиком (после того, как вырвется
                                ; // из-под его контроля)
                
                NOP ; // а вот истинная точка входа(!),
                                ; // которой передается управление за счет
                                ; // подмены стартового адреса первичного потока
                                ; // из функции DllMain статически прилинкованной DLL;
                CALL ds:[baz] ; // зовем функцию baz из DLL, выдающую мудрость;
                RETN ; // завершаем свое выполнение (с чистой совестью!)
        }
}


foo()
{
        // эта функция вызывается только из-под отладчика, причем
        // не всякого, а OllyDbg, в котором есть ошибка - когда DLL
        // возвращает 0 (а она его возвращает, если есть Olly), то
        // отладчик должен прибить процесс, но Olly все-таки позволяет
        // нажать F9 и продолжить выполнение, получив это сообщение:
        MessageBox(0, "\ndebugger is detected!\n\n", "hello,hacker!", 0);
}

Листинг 4. Исходный код quux-crackme.c (головной исполняемый файл).

#include <windows.h>;

bar(); // объявляем функцию 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;        // рапортуем о нормальной инициализации
        
}

Листинг 5. Исходный текст динамической библиотеки quux-dll.c.

Разумеется, на самом деле это никакой не crackme (в обычном смысле этого слова), а настоящий exploit типа proof-of-concept, предназначенный для тестирования отладчиков на "вшивость" и написанный так, чтобы предельно облегчить его понимание, благодаря чему сломать его - плевое дело, но вот если наворотить здесь хитрый код, то шансы на его понимание резко снижаются, а шансы на "взлом" отладчика, соответственно, резко возрастают. Однако борьба с отладчиками на данном этапе не входила в задачу мыщх'а - этому посвящена отдельная колонка в "Хакере", а в этой мы говорим исключительно о дырах (прорыв сквозь отладчик - в первую очередь дефект проектирования отладчика, т.е. дыра, и только потом антиотладочный прием).