Малварь нового поколения или надругательство над точкой входа

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

В сентябре 2008 датчики распределенных антивирусных сетей зафиксировали необычную активность - эпидемия малвари нового поколения начала свое распространение практически одновременно с Индонезии, Вьетнама и Таиланда, обходя антивирусные преграды на форсаже. Мыщъх с Алиской это дело раскрутили, написав пару POC'ов, демонстрирующих технику сокрытия истинной точки входа в файл, что актуально не только для зловредных программ, но и для легальных протекторов.

Введение

История эта началась еще несколько лет назад, когда мыщъх ковырял червей, обходивших персональные брандмауэры путем создания удаленного потока посредством вызова API-вызова CreateRemoteThread, которому в качестве стартового адреса передавался указатель на вредоносный код, впрыснутый в адресное пространство жертвы тем или иным способом. Например, выделением памяти на куче с последующим копированием shell-кода API-вызовом WriteProcessMemory. Логично - чтобы "выкурить" левые потоки, достаточно взглянуть на их стартовый адрес - у "нормальных" потоков он указывает в область страничного имиджа PE-файла, а у "рукотворных" - лежит в стеке, куче или еще непонятно где.

Баг-рапорт на Process Explorer

Рисунок 1. Баг-рапорт на Process Explorer Марка Руссиновича, опубликованный на OpenRCE 1 ноября 2006 года, где впервые был обозначен адрес точки входа, хранящейся в стеке потока.

Вот только ни Process Explorer Марка Руссиновича, ни даже Soft-Ice не желали отображать истинные стартовые адреса "рукотворных" потоков, высвечивая какой-то невменяемый адрес, ведущий в недра KERNEL32.DLL, что, конечно, весьма подозрительно, но на вещественное доказательство не тянет. Поковырявшись в системном загрузчике, мыщъх выяснил, что подлинный стартовый адрес потока находится практически на самом дне стека и если малварь не предпринимает дополнительных способов маскировки, "рукотворные" потоки обнаруживаются тривиальным сканером, который и был написан мыщъхем, что называется, по горячим следам и послан Марку Руссиновичу вместе с баг-рапортом на Process Explorer. Поскольку никакой реакции так и не последовало, рапорт был обнародован на вполне респектабельной хакерской туссовке OpenRCE.org, но и там он остался незамеченным.

Годом позже, разгребая завалы своих заметок, нацарапанных на клочках бумаги, мыщъх заинтересовался - на каком этапе загрузки файла стартовый адрес базового потока попадает в стек и можно ли его изменить из TLS-callback'а или функции DllMain статически прилинкованной DLL? Оказалась, что стартовый адрес формируется до инициализации TLS/DllMain, а после инициализации обращения к оригинальной точке входа, прописанной в PE-заголовке, уже не происходит - система использует адрес, сохраненный в стеке и этот адрес действительно может быть изменен. Отладчики (не говоря уже о дизассемблерах!) к такой "подлянке", разумеется, не готовы и они (в своей массе) просто теряют контроль над исполняемым файлом! Мыщъх тут же написал crackme, выложил его на OpenRCE, но... хакерская общественность не оценила предложенный трюк по достоинству, в результате чего мыщъх потерял к этой затее всякий интерес.

quux-crackme в файловом репозитории мыщъха

Рисунок 2. quux-crackme в файловом репозитории мыщъха на OpenRCE, датированный 16 мая 2008 года - crackme, основанный на подмене точки входа в файл из функции DllMain статически прилинкованной динамической библиотеки.

Тем временем, совершенно независимо от него, хакерская группировка (пожелавшая остаться в тайне, в смысле - не оставившая в коде никаких инициалов) переоткрыла технологию изменения точки входа, использовав ее в малвари нового поколения, которая статическими антивирусными сканерами не обнаруживается в принципе! Как минимум, нам нужен полноценный эмулятор ЦП с качественным эмулятором окружения Windows, учитывающим недокументированные особенности системного загрузчика PE-файлов (а именно на них и держится механизм подмены точки входа).

На сегодняшний момент, из всех существующих антивирусов малварь нового поколения могут обнаруживать только NOD32, F-Secure, Symantec, KAV, Dr.Web, да и то, только после усовершенствования эмулятора окружения Windows. Остальные же тихо курят в сторонке. Неудивительно, что рассылка POC'ов знакомым сотрудникам антивирусных компаний (Checkpoint, Symantec, F-Secure, K7Computing) вызывала конкретный резонанс, завершившийся посылом мыщъха... нет, туда меня послали горячие парни из KAV'а (в определенных кругах именуемого калом). K7Computing, будучи платиновым спонсором конференции "AVAR 2008 International Conference", предложила мыщъху зачитать там доклад по теме. А что?! И зачитаю. Это же Индия, Дели, там трава такая... монументальная. И архитектура вполне готическая. В смысле - безбашенная.

Конференция AVAR-2008

Рисунок 3. Конференция AVAR-2008, посвященная (анти)вирусным технологиям, на которой мыщъх собирается зачитать свой доклад о способах подмены точки входа в файл.

Ладно, это все шутки. А ситуация вполне серьезная. Уж не мыщъх ли спровоцировал рождение малвари нового поколения (уже раздается в народе)? Нет, и еще раз нет. Это совершенно независимая находка неизвестной хакерской группы, что элементарно доказывается анализом кода. Несмотря на то, что мыщъхиный crackme и обозначенная малварь исповедуют идентичные концепции, детали реализации совершенно различны. Позиция стартового адреса потока в стеке непостоянна и варьируется даже в рамках отдельно взятой версии операционной системы, что высадило мыщъх'а на разработку системно-независимого алгоритма, сканирующего стек на предмет поиска наиболее вероятных кандидатов на роль стартового адреса. В то же самое время пойманная малварь использует фиксированные локации и потому функционирует только под строго определенными версиями операционной системы. Логично, если бы хакеры увидели мыщъхиный crackme, они бы непременно позаимствовали системно-независимый алгоритм, но этого не произошло. Значит, у нас есть все основания предполагать, что это продукт параллельных исследований и мыщъх тут совсем не при чем, так что нечего тухлые яйца кидать!

В настоящее время компания Endeavor Security, Inc работает над армированием AMP (Active Malware Protection), присобачивая к ней перехаченный x86emu вместе с эмулятором окружения Windows, "осведомленным" о недокументированных возможностях системного загрузчика PE-файлов (за что отвечает мыщъх с Алиской). Так что, AMP имеет все шансы оказаться первым коммерческим продуктом, способным распознавать малварь нового поколения и не только распознавать, но и блокировать. Между нами говоря, Endeavor Security, Inc - фирма уникальная во многих отношениях. Во-первых, она производит уникальные продукты. Во-вторых, не помешана на секретности. Здесь царит атмосфера свободы и демократии, столь редкая в наши дни. Политика компании не препятствует обмену информацией с остальными компаниями и не накладывает лапу на открытые публикации, подробно описывающие суть проблемы со всеми выкладками и возможными путями ее решения.

А проблема действительно встает в полный рост и чем дальше, чем полнее. Какое-то время антивирусы не смогут обнаруживать малварь, изменяющую точку входа в PE-файл, и нам придется сражаться с ней вручную, а для этого нужно не только уверенно держать IDA-Pro/HIEW в руках, но и разбираться в недокументированных тонкостях работы системного загрузчика, о которых мы сейчас и поговорим.

Досье: Alice Chang

Alice Chang за работой

Рисунок 4. Alice Chang за работой.

Девушка. Реверсер. Хакер. Должность "Threat Analyst". Место работы - Endeavor Security, Inc, куда она перешла из Symantec Managed Security Systems, оставив должность "Information Security Analyst". До этого работала в Aerospace Corporation, а еще раньше - в "UCSB Mathematics Engineering Science Achievement", куда устроилась сразу после окончания Калифорнийского Технологического, дислоцированного в Санта-Барбаре.

В настоящее время хомячит вместе с мыщъхем над поиском и документированием уязвимостей, нехило программируя под никсами на Си/Си++ с дюжиной скриптовых языков, о которых мыщъх не имеет ни малейшего представления. Является действительным членом Symantec Alumni Group, UCSB Alumni Association и USENIX Association.

Точки входа - живые и мертвые

Спецификация на PE-файл от Microsoft описывает специальное поле в PE-заголовке, хранящее относительный виртуальный адрес (RVA) точки входа в файл, с которого как бы и начинается его выполнение. "Как бы", потому что точка входа (она же Entry Point) выполняется не первой, а... последней.

До передачи управления на Entry Point система загружает все статически прилинкованные динамические библиотеки, вызывая функции DllMain, исполняющиеся в контексте загрузившего их процесса и способные воздействовать на него любым образом. TLS-callback'и (да-да, те самые, о которых мы уже говорили в выпуске №5 "Энциклопедии антиотладочных приемов") также получают управление до выполнения в точку входа, которой, вообще-то, может и не быть. В самом деле! Если DllMain или TLS-callback "забудет" возвратить управление, точку входа вызывать уже будет некому и потому она может указывать на любой код!!! Но... не будем забегать вперед.

Рассмотрим классический алгоритм определения точки входа (см. листинг 1), взятый на вооружение многими антивирусами, отладчиками, дизассемблерами, определителями упаковщиков и т.д. и т.п. - лапы устали перечислять.

#define PE_off 0x3C // PE magic word raw offset
#define EP_off 0x28 // relative Entry Point filed offset

BYTE* GetEP()
{
        static BYTE* base_x, *ep_adr;
        static DWORD pe_off, ep_off;
        char buf [_MAX_PATH];

        // obtain exe base address
        GetModuleFileName(0, buf, _MAX_PATH);

        base_x = (BYTE*) GetModuleHandle(buf);
        pe_off = *((DWORD*)(base_x + PE_off));
        ep_off = *((DWORD*)(base_x + pe_off + EP_off));
        ep_adr = base_x + ep_off; // RVA to VA
        return ep_adr;
}

Листинг 1. Классический способ определения точки входа в файл.

Конечно, "промышленная" реализация функции GetEP() выглядит чуть сложнее, поскольку осуществляет множество проверок, опущенных в листинге 1 для простоты понимания. Но не в проверках дело. А в концепции. Дырявой, естественно.

Отладчики ставят сюда бряк в надежде, что он сработает. Но он не сработает, если адрес точки входа изменен. Но стоп! Мыщъх опять устроил кавардак. Еще раз. Медленно и по порядку. Когда отладчик порождает отладочный процесс и запускает его на выполнение, то первым срабатывает бряк, засунутый системой в функцию NTDLL!DbgBreakPoint, сигнализирующий о том, что отлаживаемый файл спроецирован в адресное пространство и с ним можно работать, как со своим собственным. В частности, считывать PE-заголовок и устанавливать бряк на точку останова. Дело в том, что операционная система никак не информирует отладчик о передаче управления на точку входа и отслеживать этот процесс отладчик должен самостоятельно.

ntdll!DbgBreakPoint:
77f9193c cc      int 3

Листинг 2. При порождении отладочного процесса система передает бразды управления отладчику посредством вызова функции NTDLL!DbgBreakPoint.

Дизассемблеры никаких бряков не устанавливают, а просто тупо начинают дизассемблирование с точки входа. Что же касается антивирусов, то... вообще-то тут действуют разные стратегии. Сигнатура может быть привязана не только к точке входа, но и к смещению относительно конца/начала файла (или секции). В этом случае изменение точки входа никак не повлияет на умственные способности антивируса. Однако прежде чем искать сигнатуру, файл необходимо распаковать, а в случае полиморфных вирусов - еще и прогнать их через эмулятор. И вот тут-то без правильного определения точки входа ну никак не обойтись.

Некоторые вирусы используют довольно хитрый трюк, совершая jump из TLS-callback'а, т.е. фактически выполняя TLS-callback без возврата управления, в результате чего оригинальная точка останова идет лесом и может содержать, что угодно. Конечно, в разумных пределах. Наглеть не стоит. В частности, начиная с XP системный загрузчик выполняет ряд проверок и файлы с точкой останова, вылетающей за пределы страничного образа, просто не загружаются в память! Но даже если бы они и загружались, с точки зрения антивируса такая точка останова выглядит слишком подозрительно и легко ловится эвристическим анализатором. Какой смысл палить себя на мелочах? Лучше засунуть в точку входа безобидный код, а из TLS-callback'а совершить переход на вирусное тело. В грубом приближении это выглядит так (см. листинг 3):

EntryPoint:
        XOR EAX, EAX
        PUSH EAX
        CALL d, ds:[ExitProcess]
        ...
VirusBody:
        ...
        ...
        ...
TLS_Callback1:
        JMP VirusBody

Листинг 3. Псевдокод малвари старого поколения, перекрывающий точку входа в PE-файл посредством блокировки возврата управления из TLS-callback'а.

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

А теперь посмотрим на псевдокод малвари нового поколения (см. листинг 4):

EntryPoint:
        XOR EAX, EAX
        PUSH EAX
        CALL d, ds:[ExitProcess]
        ...
VirusBody:
        ...
        ...
        ...
TLS_Callback1:
        ADD d, ds:[ESP+magic_offset], offset VirusBody - offset EntryPoint
        RETN 0Ch

Листинг 4. Псевдокод малвари нового поколения, изменяющий точку входа в PE-файл, чем и вводящий в заблуждение отладчики, дизассемблеры и антивирусы.

На первый взгляд - никакой разницы, но если подумать головой, то разница будет просто драматической. В первом случае мы имеем ничем не прикрытый бесстыдный jump из TLS-callback'а. И хотя его можно замаскировать с помощью самомодифицирующегося кода или запутанных математических преобразований, целевой адрес перехода декодируется однозначно и указывает на вирусное тело.

Во втором случае TLS-callback добавляет какое-то значение к некоторой ячейке памяти, лежащей в области стека и... возвращает управление. Системе. Человек-с-отладчиком чисто теоретически может трассировать тонны машинных инструкций, ответственных за инициализацию файла, дожидаясь момента передачи управления на точку входа или хотя бы область памяти, не принадлежащую системе, а находящуюся в границах PE-файла или одной из принадлежащих ему динамических библиотек. Тогда он с удивлением обнаружит, что точка входа каким-то магическим образом не получает управления!!! Вот только трассировать придется долго. И занятие это будет явно непродуктивно.

Антивирусам приходится еще хуже. Они не могут трассировать код, исполняющийся на живой операционной системе (и вряд ли даже стоит объяснять, почему). Откуда им знать, что именно находится в данной конкретной ячейке памяти? А что, собственно говоря, там находится и как оно туда попадает?! Попробуем разобраться!!!

Тайны системного загрузчика

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

Стартовый адрес потока представляет собой редкое исключение из правил. Он абсолютно недокументирован и в то же самое время неизбежно следует из документированных функций и особенностей работы системного загрузчика, а системный загрузчик в грубом приближении работает так:

Какая интересная картина получается! Точка входа представляет собой аргумент API-функции CreateRemoteThread(), имеющей документированный прототип, который Microsoft совершенно не собирается изменять. Причем, этот аргумент попадает на стек до отработки DllMain/TLS-callback, а извлекается из стека после того, как они возвратят управление. Причем никакие проверки валидности стартового адреса потока не выполняются, а это значит, что DllMain/TLS-callback могут изменять стартовый адрес потока по своему усмотрению. Собственного говоря, это справедливо для всех потоков, а не только для базового, просто адрес базового потока прописан в точке входа в PE-файл, а адреса остальных потоков передаются как аргументы функции Create[Remote]Thread().

Адрес точки входа в файл

Рисунок 5. Адрес точки входа в файл, лежащий на дне стека базового потока (создается ложное впечатление, что Ольга знает, в каком именно двойном слове лежит стартовый адрес, но это не так, она просто тупо сравнивает все константы с известными величинами, автоматически подставляя наиболее вероятных кандидатов).

Другими словами, точка входа в файл попадает в стек не случайно, а согласно логике работы системного загрузчика, которая справедлива для всей 32-разрядной линейки NT-подобных систем и потому замечательно работает как на W2K так и на Server 2008. Единственная проблема в том, что положение аргумента функции CreateRemoteThread() системно-зависимо. Множество внутренних функций с недокументированными прототипами используют стек, складируя туда свои аргументы, что затрудняет нашу задачу.

Разработчики обозначенной малвари пошли по пути наименьшего сопротивления. Они просто протестировали несколько версий операционных систем, определив смещение стартового адреса базового потока относительно дна стека, после чего загнали их в одну таблицу. Попав на чужой компьютер, малварь определяет версию оси, извлекая соответствующее "магическое смещение" из сопутствующей таблицы. Это в том случае, если данная версия Windows поддерживается малварью. Ну, а если нет...

Мыщъх пошел по другому пути. Менее надежному (и допускающему ложные срабатывания), но зато намного более универсальному. Все просто! Считываем PE-заголовок, извлекаем оттуда подлинную точку входа и затем сканируем стек на предмет поиска подходящих кандидатов. Естественно, точка входа может совпасть со значением, хранящимся в стеке, но не имеющем к точке входа никакого отношения. Подобная ситуация называется коллизией и ничего хорошего она в себе не несет. Нет никакой возможности определить, какое именно значение следует изменить, а какое - лучше не трогать. Тем не менее, мыщъх решил изменять все значения, совпадающие с точкой входа, а чтобы уменьшить количество ложных срабатываний, выбирать точку входа, хранящуюся в PE-файле так, чтобы она была ни на что не похожа, то есть представляла собой уникальное значение, не совпадающее ни с одним из прочих полей PE-файла.

Исходный текст POC'а

Рисунок 6. Исходный текст POC'а, написанного мыщъхем и протестированного Алиской.

Ниже (см. листинг 5) приводится законченный алгоритм подмены точки входа из DllMain/TLS-callback, протестированный на всем спектре Windows-систем и показавший хороший результат.

#define NEW_EP 0x0040100A     // new EP (SystemLoader'll ignore the old one)
#define EP_KEY 0xA11CE169     // simple immediate constant obfuscation...
unsigned int EP_key = EP_KEY; // ...anti IDA Pro trick

BOOL WINAPI dllmain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
        BYTE* ep_adr;
        DWORD RegionSize;
        BYTE* BaseAddress;
        MEMORY_BASIC_INFORMATION lpBuffer;
        
        ep_adr = GetEP();
        
        // get stack top allocated base address
        VirtualQuery((LPCVOID)&hinstDLL, &lpBuffer, sizeof(lpBuffer));
        BaseAddress = ((BYTE*)lpBuffer.BaseAddress);
        RegionSize = lpBuffer.RegionSize - sizeof(DWORD);
        
        // EP is KERNEL32!CreateRemoteThread() function argument,
        // and this argument is near to the bottom of the stack
        for (; RegionSize > 0; RegionSize -= sizeof(DWORD))
                if (((DWORD)ep_adr) == ( *(DWORD*)(BaseAddress+RegionSize)))
                        (*(DWORD*)(BaseAddress + RegionSize)) = NEW_EP ^ EP_KEY,
                                (*(DWORD*)(BaseAddress + RegionSize)) ^= EP_key;
        return 1;
}

Листинг 5. Подмена точки входа в файл из функции DllMain статически прилинкованной DLL (для ослепления эвристических анализаторов, автоматически распознающих указатели на код по их значению, мыщъх шифрует точку входа магическим ключом).

Охотники за привидениями

Сложность эмуляции работы системного загрузчика связана с "плавающим" смещением точки входа. Даже если антивирус и положит ее в стек, то у него нет никаких гарантий, что вирус ее там найдет, а не попытается изменить соседнюю ячейку. Антивирус не знает, какую версию операционной системы "поддерживает" анализируемый вирус и каким образом он ее определяет. Помимо тупого вызова GetVersionEx, вирус может обратиться к реестру или посчитать контрольную сумму определенных системных библиотек. Короче, возможных вариантов намного больше одного...

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

Заключение

Пока малварь, изменяющая точку входа, только-только начинает появляться и предсказать пути ее дальнейшего развития весьма затруднительно. На данный момент ясно только одно - это бомба, реальная бомба. Первый серьезный вызов антивирусам за последние несколько лет. Интересно наблюдать за реакцией их разработчиков, варьирующейся от ужаса до тупого непонимания проблемы. Что ж! Если данная вирусная технология получит развитие, то кое-кто очень скоро вылетит с рынка.

Ссылки к статье

Врезка

При определенных обстоятельствах системный загрузчик нагло игнорирует точку входа, прописанную в PE-файле и потому она может указывать на безобидный код (типа - немедленный ExitProcess), в то время как зловредный код расположен совсем в другом месте. Отладчики и дизассемблеры также обламываются, выдавая неверный результат.