EP-abusers

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

крис касперски ака мыщъх, ака nezumi, aka souriz, no email

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

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

Рисунок 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, но… хакерская общественность не оценила предложенный трюк по достоинству, в результате чего мыщъх потерял к этой затее всякий интерес.

Рисунок 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», предложила мыщъху зачитать там доклад по теме. А что?! И зачитаю. Это же Индия, Дели, там трава такая… монументальная. И архитектура вполне готическая. В смысле безбашенная.

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

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

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

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

ep-abusers_image_3.jpg

Рисунок 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_off0x3C PE magic word raw offset #define EP_off0x28 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 = *1); ep_off = *2); ep_adr = base_x + ep_off; RVA to VA return ep_adr; } Листинг 1 классический способ определения точки входа в файл Конечно, «промышленная» реализация функции GetEP()выглядит чуть сложнее, поскольку осуществляет множество проверок, опущенный в листинге 1 для простоты понимания. Но не в проверках дело. А в концепции. Дырявой естественно. Отладчики ставят сюда бряк, в надежде, что он сработает. Но он не сработает, если адрес точки входа изменен. Но стоп! Мыщъх опять устроил кавардак. Еще раз. Медленно и по порядку. Когда отладчик порождает отладочный процесс и запускает его на выполнение, то первым срабатывает бряк, засунутый системой в функцию NTDLL!DbgBreakPoint, сигнализирующую о том, что отлаживаемый файл спроецирован в адресное пространство и с ним можно работать как со своим собственным. В частности, считывать PE-заголовок и устанавливать бряк на точку останова. Дело в том, что операционная система никак не информирует отладчик о передаче управления на точку входа и отслеживать этот процесс отладчик должен самостоятельно. ntdll!DbgBreakPoint: 77f9193c ccint 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 функций не только не документированы, но еще и варьируются в широких пределах, а потому в долгосрочной перспективе закладываться на них нельзя. Стартовый адрес потока представляет собой редкое исключение из правил. Он _абсолютно_ недокументирован и в то же самое время _неизбежно_ следует из документированных функций и особенностей работы системного загрузчика, а системный загрузчик в грубом приближении работает так: - проецирует PE-файл в память; - считывает PE-заголовок, извлекает оттуда значение точки останова, представляющее собой относительный виртуальный адрес (RVA) и переводит его в линейный адрес (VA) путем сложения с базовым адресом, так же хранящимся в PE-заголовке; - VA адрес точки входа передается функции CreateRemoteThread() в качестве одного из аргументов, а передается он через стек, поскольку 32-разряные версии Windows используют stdcall-соглашение для всех API-функций без исключения (64-разрядные версии Windows используют fastcall-соглашение, передавая аргументы через регистры и стартовый адрес потока в стек не попадает); - стартовый адрес базового потока (указывающий на точку входа) после прохождения всех проверок заталкивается в стек и больше к PE-заголовку система не обращается (очень важный момент!); - CreateRemoteThread() выполняет инициализацию потока в процессе которой отрабатываются функции Dllmain статически прилинкованных DLL, а так же имеющиеся TLS-callback'и (если они, конечно, есть); - на завершающем этапе инициализации PE-файла, CreateRemoteThread() зовет недокументированную, не экспортируемую, сугубо внутреннюю функцию функцию BaseProcess(), передавая ей стартовый адрес потока, почерпнутый из стека в качестве аргумента; - BaseProcess() передает управление по указанному адресу. Какая интересная картина получается! Точка входа представляет собой аргумент API-функции CreateRemoteThread(), имеющей документированный прототип, который Microsoft совершенно не собирается изменять. Причем, этот аргумент попадает на стек _до_ отработки Dllmain/TLS-callback, а извлекается из стека _после_ того, как они возвратят управление. Причем, никакие проверки валидности стартового адреса потока _не_ выполняются, а это значит, что Dllmain/TLS-callback могут изменять стартовый адрес потока по своему усмотрению. Собственного говоря, это справедливо для всех потоков, а не только для базового, просто, адрес базового потока прописан в точке входа в PE-файл, а адреса остальных потоков передаются как аргументы функции Create[Remote]Thread(). Рисунок 5 адрес точки входа в файл, лежащий на дне стека базового потока (создается ложное впечатление, что Ольга знает в каком именно двойном слове лежит стартовый адрес, но это не так, она просто тупо сравнивает все константы с известными величинами, автоматически подставляя наиболее вероятных кандидатов) Другими словами, точка входа в файл попадает в стек не случайно, а согласно логике работы системного загрузчика, которая справедлива для всей 32-разрядной линейке NT-подобных систем и потому замечательно работает как на W2K так и на Server 2008. Единственная проблема в том, что положение аргумента функции CreateRemoteThread() системно-зависимо. Множество внутренних функций с недокументированными прототипами используют стек, складируя туда свои аргументы, что затрудняет нашу задачу. Разработчики обозначенной малвари пошли по пути наименьшего сопротивления. Они просто протестировали несколько версий операционных систем, определив смещение стартового адреса базового потока относительно дна стека, после чего загнали их в одну таблицу. Попав на чужой компьютер, малварь определяет версию оси, извлекая соответствующее «магическое смещение» из сопутствующей таблицы. Это в том случае, если данная версия Windows поддерживается малварью. Ну а если нет… Мыщъх пошел по другому пути. Менее надежному (и допускающему ложные срабатывания), но зато намного более универсальному. Все просто! Считываем PE-заголовок, извлекаем оттуда подлинную точку входа и затем сканируем стек на предмет поиска подходящих кандидатов. Естественно, точка входа может совпасть со значением, хранящемся в стеке, но не имеющей к точке входа никакого отношения. Подобная ситуация называется коллизией и ничего хорошего она в себе не несет. Нет никакой возможности определить какое именно значение следует изменить, а какое — лучше не трогать. Тем не менее, мыщъх решил изменять все значения, совпадающие с точкой входа, ну, а чтобы уменьшить количество ложных срабатываний, выбирать точку входа, хранящуюся в PE-файле так, чтобы она была ни на что не похожа, то есть представляла собой уникальное значение, не совпадающие ни с одним из прочих полей PE-файла. Рисунок 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 VirtualQuery3); BaseAddress = 4) if 5)) (*(DWORD*)(BaseAddress + RegionSize)) = NEW_EP ^ EP_KEY, (*(DWORD*)(BaseAddress + RegionSize)) ^= EP_key; return 1; } Листинг 5 подмена точки входа в файл из функции Dllmain статически прилинкованной DLL (для ослепления эвристических анализаторов, автоматически распознающих указатели на код по их значению, мыщъх шифрует точку входа магическим ключом) ===== охотники за привидениями ===== Сложность эмуляции работы системного загрузчика связана с «плавающим» смещением точки входа. Даже если антивирус и положит ее в стек, то у него нет никаких гарантий, что вирус ее там найдет, а не попытается изменить соседнюю ячейку. Антивирус не знает какую версию операционной системы «поддерживает» анализируемый вирус и каким образом он ее определяет. Помимо тупого вызова GetVersionEx, вирус может обратиться к реестру или посчитать контрольную сумму определенных системных библиотек. Короче, возможных вариантов намного больше одного… Конечно, мыщъхиный код (в «канонической» форме) легко попадется на ловушки расставленные антивирусами, поскольку даже не пытается отличить подлинный стартовый адрес потока, засунутый в стек системой, от его имитации антивирусом. Но, во-первых, антивирусы ничего об этом трюке пока не знают, а, во-вторых, мыщъх преследовал совсем иные цели и его код замечательно работает в защитных механизмах в силу того, что совместим со всеми системами, а не только со строго определенными версиями. ===== заключение ===== Пока малварь, изменяющая точку входа, только-только начинает появляться и предсказать пути ее дальнейшего развития весьма затруднительно. На данный момент ясно только одно — это бомба, реальная бомба. Первый серьезный вызов антивирусам за последние несколько лет. Интересно наблюдать за реакцией их разработчиков, варьирующейся от ужаса до тупого непонимания проблемы. Что ж! Если данная вирусная технология получит развитие, то кое-кто очень скоро вылетит с рынка. ===== »> врезка ссылки к статье ===== - https://www.openrce.org/forums/posts/275__: - открытое письмо Марку Руссиновичу, в котором мыщъх впервые высказал идею довольно надежного способа детекции малвари путем определения истинного стартового адреса потока, лежащего непосредственного в стеке и представляющего собой аргумент API-функции CreateRemoteThread; - https://www.openrce.org/repositories/users/nezumi/quux-crackme.zip__: - мыщъхный crackme демонстрирующий технику подмены точки входа в файл посредством изменения стартового адреса потока, лежащего в стеке и базирующегося на коде утилиты, предназначенной для поиска малвари, написанной им же и доступной по вышеупомянутой ссылке; - https://www.openrce.org/forums/posts/808__: - обсуждение мыщъхиного crackme на форуме OpenRCE; - http://www.aavar.org/avar2008/index.htm__: - офицальный сайт конференции AVAR-2008 на которой мыщъх планирует выступить с сабжевым докладом; ===== »> врезка ===== при определенных обстоятельствах системных загрузчик нагло игнорирует точку входа, прописанную в PE-файле и потому она может указывать на безобидный код (типа немедленный ExitProcess), в то время как зловредный код расположен совсем в другом месте. отладчики и дизассемблеры так же обламываются, выдавая неверный результат

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); EP is KERNEL32!CreateRemoteThread() function argument, and this argument is near to the bottom of the stack for(; RegionSize > 0; RegionSize -= sizeof(DWORD
5)
(DWORD)ep_adr) == ( *(DWORD*)(BaseAddress+RegionSize