exploit-review-0x18

exploits review\\ 18h выпуск

крис касперски ака мыщъх, a.k.a. nezumi, a.k.a elraton, a.k.a. souriz, no-email

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

brief:работая с отладчиком Syser версии 1.95.1900.0894, выпущенной в начале 2008 года, мыщъх обратил внимание, что при загрузке программ с сетевых дисков и сменных носителей (типа дискет) отладчик проскакивает точку входа в файл (она же Entry Point или, сокращенно, EP), передавая подопытной программе бразды правления и утрачивая над ней всякий контроль (впрочем, глобальные точки останова на API-функции срабатывают нормально, если, конечно, не забыть заблаговременно их поставить). мыщъх отослал разработчикам баг-репорт, получив подтверждение об ошибке вкупе с обещанием ее исправить, но… версии Syser'а продолжают выходить одна за другой, а ошибка как была, так и осталась. почему же она вообще возникает? откуда берется и зачем никуда не девается? дело в том, что механизм загрузки файлов с жестких дисков и сменных носителей принципиально различен. исполняемые файлы (и динамические библиотеки), расположенные на винчестере, операционная система просто проецирует в память, что (грубо говоря) превращает их в «файл подкачки, доступный только на чтение». подкачка страниц с диска в оперативную память происходит только по мере обращения к ним, а при недостатке памяти немодифицированные страницы не вытесняются в настоящий файл подкачке, а просто высвобождаются под новые данные. действительно, какой смысл записывать их в своп? проще вновь обратиться к исполняемому файлу — он же нм куда не денется за это время. ну, это с жесткого диска он не денется (и потому система блокирует к нему доступ на время выполнения), а вот с дискеты или сетевого диска… там во-первых, скорость _намного_ ниже, чем у винчестера, а во-вторых, сеть в любой момент может лечь, а диска — быть вынута. разработчики Windows учли такую возможность развития событий и решили проблему путем предварительной загрузки файла в оперативную память, за что отвечает совсем другой компонент загрузчика, игнорируемый Syser'ом со всеми отсюда вытекающими…

targets:все существующие в данный момент версии Syser'а;

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

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

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

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-DOSold-exe заголовок с отрезанным телом файла.

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

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

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

brief:как известно, IDA-Pro с некоторых времен не только дизассемблер, но еще и отладчик. отладчик не то, чтобы сильно мощный (_намного_ слабее Ольги), но все-таки намного более удобный, чем постоянное переключение между дизассемблером и внешним отладчиком, а потому активно используемый хакерами наряду с исследователями малвари. и все было бы хорошо, но если «скормить» дизассемблеру файл с нулевым базовым адресом, он нормально загрузит его по этому самому нулевому адресу, но вот при попытке запуска отладчика мы получим следующее ругательство: «IDA Pro couldn't automatically determine if the program should berebased in the database because the database format is too old anddoesn'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 theEdit, 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 с потерей всех предыдущих результатов анализа).

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

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

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

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

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

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

  1. функции DllMain всех статически прилинкованных библиотек отрабатывают еще до начала «всплытия» отладчика и в них может находиться все что угодно! В принципе, исполняемый файл может ограничиться процедурой-пустышкой, разместив зловредный код в одной из своих DLL и он будет выполнен еще до «всплытия» отладчика!
  2. функции DllMain выполняются уже после установки отладчиком точки останова на EntryPoint и, если это программная точка, представленная опкодом CCh, динамическая библиотека может обнаружить, что процесс находится под отладкой и либо сделать что-то нехорошее, либо же вызывать VirtualProtect, присвоить соответствующей странице памяти атрибут на запись, снять точку останова, восстановив оригинального содержимое первого байта (естественно, для этого его нужно где-то хранить или вычислять эвристическим путем, что не представляет никакой проблемы), тогда — точки останова уже не будет, процессор не сгенерирует исключение и отладчик не сможет перехватить управление;
  3. если же отладчик установил аппаратную точку останова, то… можно пойти другим путем, изменив из DllMain аргумент функции KiUserApcDispatcher, хранящий стартовый адрес первичного потока, тогда, по завершении инициализации всех динамических библиотек, поток начнет свое выполнение отнюдь не с точки входа, прописанной в PE-заголовке, а оттуда, откуда DLL пожелает, вырываясь из-под контроля отладчика. Естественно, чтобы менять аргумент — его еще необходимо найти, что не так-то просто, поскольку, в зависимости от версии операционной системы и прочих обстоятельств, он меняет свое местоположение в широких пределах, поэтому, приходится прибегать к различным эвристическим методикам, например, считывать из PE-заголовка оригинальную точку входа и искать в стеке двойное слово, совпадающее с ней.

Конечно, теоретики от программирования скажут, что мол, в чем проблема-то? Операционная система уведомляет отладчик о загрузке всех динамических библиотек еще до начала выполнения DllMain и ничего не стоит исследовать их на вшивость. Многие отладчики даже имеют специальную опцию — останавливаться на DllMain, вот только… ни у одного из них она по умолчанию не включена, а анализировать DllMain в отладчике — гиблое дело, особенно если она вызывается из стартового библиотечного кода. Но даже отлаживая DllMain мы можем не заметить (не понять) как она меняет аргумент функции KiUserApcDispatcher или снимает точку останова, поскольку, для этого необходимо _знать_ что именно у нас находится в памяти и _зачем_ оно там находится. С точки зрения не очень опытного реверсера это выглядит вполне невинно. Ну меняет что-то такое DLL в стеке и завершается, но ведь это не страшно, правда? Мы же ведь все равно вернемся в отладчик при начале выполнение процесса, верно? А вот и неверно!!! Уже не вернемся!!! Так что проблема на самом деле очень серьезна. Подробнее ее планируется осветить на мыщъх'ином блоге (http://souriz.wordpress.com/), но и в этой статье нарытой информации предоставлено не мало.

Рисунок 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_off0x3C смещение PE-заголовка в файле #define EP_off0x28 смещение поле Entry-Point, относительно PE опкод программной точки останова #define SW_BP0xCC объявляем переменные 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 = *1); определяем адрес точки входа, преобразуя ее в указатель на двойное слово, что уже не будет работать на 64-битных системах, но на 64-битных системах по любому все будет сильно иначе ep_off = *2); вычисляем линейный виртуальный адрес точки входа 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 без всяких ассемблерных вставок - красота! VirtualQuery3); VirtualQuery возвращает нам базовый адрес блока, выделенного под стек, который мы и будем ковырять на предмет аргумента функции KiUserApcDispatcher BaseAddress = 4) if 5)) искомый аргумент (или нечто на него похожее) найден! увеличиваем его значение на 0Dh, проскакивая не только бряк, установленный отладчиком, на точку входа, но и первый CALLc RETN, передавая управление сразу на второй CALL, который IDA-Pro, не найдя ссылок даже не удосужилась дизассемблировать (*(DWORD*)(BaseAddress+RegionSize)) +=0xD; change EP примечание: здесь по идее должен стоят break, типа - раз нашли адрес, то чего шерстить по стеку?! а вот мы продолжаем шерстить на тот случай, если вдруг нашли что-то не то и адрес точки входа совпал с чем-то другим, ес-но это может привести к краху программы и если искомый адрес встречается два и более раз, следовало бы отказаться от его модификации именно так и следует поступать в коммерческих защитах return 1; рапортуем о нормальной инициализации } Листинг 5 исходный текст динамической библиотеки quux-dll.c Разумеется, на самом деле это никакой не crackme (в обычном смысле этого слова), а настоящий exploit типа proof-of-concept, предназначенный для тестирования отладчиков на «вшивость» и написанный так, чтобы предельно облегчить его понимание, благодаря чего, сломать его — плевое дело, но вот если наворотить здесь хитрый код, то шансы на его понимание резко снижаются, а шансы на «взлом» отладчика, соответственно, резко возрастают.Однако, борьба с отладчиками на данном этапе не входила в задачу мыщх'а — этому посвящена отдельная колонка в «Хакере», а в этой — мы говорим исключительно о _дырах_ (прорыв сквозь отладчик — в первую очередь дефект проектирования отладчика, т. е. дыра, и только потом антиотладочный прием).

1)
DWORD*)(base_x + PE_off
2)
DWORD*)(base_x + pe_off + EP_off
3)
LPCVOID)&hinstDLL, &lpBuffer, sizeof(lpBuffer
4)
BYTE*)lpBuffer.BaseAddress); определяем размер блока, чтобы не выйти за его границы (на тот случай, если вдруг в стеке по каким-то причинам искомого аргумента не окажется) RegionSize = lpBuffer.RegionSize-sizeof(DWORD); ищем в стеке двойное слово, совпадающее с адресом точки входа в исполняемый файл, начиная поиск со дна стека и продвигаясь наверх (искомый аргумент лежит почти на дне, но где именно: заведомо неизвестно, вот и приходится рыскать как лосям) for(RegionSize;RegionSize>0;RegionSize-=sizeof(DWORD
5)
(DWORD)ep_adr)==( *(DWORD*)(BaseAddress+RegionSize