Содержание статьи
Классические приемы антиотладки, которые должен знать каждый
Хакер #177. Радиохакинг: что такое SDR?
Удивительный парадокс: те, кому положено свои программы отлаживать, с удовольствием кормят нас забагованными релизами, которые начинают нормально работать только после третьего обновления. Зато те, которых отлаживать программы совсем не просят, с удовольствием в чужом творчестве ковыряются :). Вопросы антиотладки интересны не только исследователям малвари (стоящим по обе стороны баррикад), но и кодерам, заботящимся о безопасности своих коммерческих поделок, поэтому, надеюсь, эта статья будет полезна каждому читателю ][.
Специально обученные API-функции
В неисчерпаемых недрах Windows, помимо всем известной функции IsDebuggerPresent, есть еще парочка API, с помощью которых можно довольно просто определить факт запуска программы под отладчиком. Это CheckRemoteDebuggerPresent:
... CheckRemoteDebuggerPresent(hProcess, &DbgDetect); if(DbgDetect) return true; else return false
и NtQueryInformationProcess, вызываемая с ProcessInformationClass = ProcessDebugPort (0x7):
... _asm { push 0 push 0x4 push FlagPointer // ProcessInformationClass = ProcessDebugPort push 0x7 push ProcessID call NtQueryInformationProcess } if (Flag) return true; else return false; ...
Однако к популярным отладчикам написано множество плагинов и скриптов, нейтрализующих действия этих API. В большинстве случаев это достигается их перехватом, поэтому перед применением этих функций желательно проверить их код на целостность и отсутствие перехвата.
Обработка исключений
Достаточно известный и применяемый способ. Некоторые API-функции, команды или последовательности команд процессора вызывают исключения, и, если программа не запущена под отладчиком, управление передается заранее установленному обработчику исключений. В то же время если запустить такую программу под отладчиком, то эти же самые функции, команды или последовательности никаких исключений вызывать не будут.
Общий принцип применения способа:
... __try { ... // В этом месте нужно написать, что-нибудь, // что может вызвать нужное нам исключение ... return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } ...
В качестве нужных команд или API сгодятся:
- int 0x3 (одним байтом 0xCC);
- int 0x3 (двумя байтами 0xCD, 0x03);
- int 0x2d;
- int 0x2c (работает только под Win Vista и выше);
- так называемая точка заморозки (команда с опкодом 0xf1);
- API-функция DebugBreak (или DbgBreakPoint из ntdll.dll);
- API-функция RaiseException с некоторыми входными значениями;
- флаг трассировки (trap flag).
Код для trap flag:
... __asm { pushfd or word ptr[esp], 0x100 popfd nop } ...
Замер времени выполнения команд
Когда отладчик присутствует и выполняет пошаговую трассировку, появляется существенная задержка между выполнением отдельных команд по сравнению с обычным выполнением. В системе есть довольно много способов измерения временных промежутков. Вот некоторые из них:
- команда RDTSC;
- API-функция GetTickCount;
- API-функция timeGetTime (из winmm.dll);
- API-функция QueryPerformanceCounter;
- API-функция GetSystemTimeAsFileTime;
- API-функция GetProcessTimes;
- API-функция KiGetTickCount (или вызов прерывания int 0x2A);
- API-функция NtQueryInformationProcess (ProcessInformationClass = ProcessTimes (0x04);
- API-функция NtQueryInformationThread (ThreadInformationClass = ThreadTimes (0x01);
- поля структуры KUSER_SHARED_DATA.
... DWORD TimeStart = GetTickCount(); ... DWORD TimeEnd = GetTickCount(); if ((TimeEnd - TimeStart) > TimeLimit) return true; else return false; ...
Команду RDTSC могут перехватывать и нейтрализовать некоторые плагины для OllyDbg. Побороть этот перехват можно, замерив какой-нибудь большой (примерно 10 секунд) промежуток времени и проконтролировав регистр edx до и после выполнения команды. Если его содержимое не изменилось, то налицо перехват RDTSC. Эту проверку лучше засунуть в отдельный поток, чтобы не задерживать выполнение основного.
Перехват GetTickCount тоже может присутствовать в плагинах, и его легко определить таким вот кусочком кода:
... DWORD TimeStart = GetTickCount(); Sleep(100); DWORD TimeEnd = GetTickCount(); ...
Если разница между TimeEnd и TimeStart меньше законной сотни, то GetTickCount явно перехвачена и висит под контролем какого-нибудь плагина. Все перечисленное поможет выявить отладчик при трассировке программы. Для выявления отладчика при обычном выполнении команд может помочь API NtQueryInformationProcess в паре с API GetSystemTimeAsFileTime:
... _asm { push 0 push 0x20 push TimeStartPointer // ProcessInformationClass = ProcessTimes push 0x4 push ProcessID call NtQueryInformationProcess } GetSystemTimeAsFileTime(LPFILETIME (&TimeEnd)); if ((TimeEnd.dwLowDateTime – TimeStart.CreateTime.LowPart) > TimeLimit) return true; else return false; ...
Кроме этого, в Win Vista и выше можно воспользоваться особенностью выполнения функции DbgPrintEx, ход выполнения которой под отладкой отличается от обычного, и функция выполняется дольше (для этих целей также отлично подойдет и OutputDebugString, но бывает иногда так, что ее контролируют антиотладочные плагины).
Отладочные регистры
Ненулевое значение специальных отладочных регистров процессора запросто может послужить фактом, подтверждающим отладку программы. Просто так узнать их содержимое не получится, и для этого можно либо вызвать исключение и прочитать контекст потока, либо воспользоваться функцией GetThreadContext:
... GetThreadContext(ThreadHandle, &ThreadContext) if ((ThreadContext.Dr0 != 0)||(ThreadContext.Dr1 != 0)||(ThreadContext.Dr2 != 0)||(ThreadContext.Dr3 != 0)) return true; else return false; ...
Флаги отладки, кучи и прочее
Наверное, только ленивый не проверял флаг BeingDebugged в своих программах, пытаясь защитить их от незадачливого взломщика. Более того, не секрет, что IsDebuggerPresent использует его для своей работы, поэтому на различных флагах, состояние которых указывает на наличие отладчика, долго останавливаться не будем.
Значение FLG_ HEAP_ ENABLE_ TAIL_CHECK в NtGlobalFlag указывает диспетчеру кучи, что идет процесс отладки и в конце каждого блока выделяемого из кучи нужно помещать сигнатуру в виде восьми байт 0xAB для контроля за переполнением. Это также может быть использовано для обнаружения отладчика:
... _asm { mov eax, fs:[0x30] mov ebx, [eax + 0x18] // Адрес начала кучи mov StartHeapAddr, ebx mov ecx, [ebx + 0x38] // Адрес конца кучи mov EndHeapAddr, ecx } Heap = (DWORD*)StartHeapAddr; // Сканируем кучу на предмет наличия 0xabababab for(DWORD index = 0; index <= EndHeapAddr - StartHeapAddr; index++) { if (Heap[index / 4] == 0xabababab) return true; } ...
Если так поступить с кучей, выделяемой для процесса по умолчанию, то можно обойти корректировку NtGlobalFlag, производимую некоторыми плагинами для OllyDbg (в частности Olly Advanced и Hide Debugger). Кроме всего этого, можно использовать API-функции RtlQueryProcessHeapInformation или RtlQueryProcessDebugInformation, с помощью которых можно посмотреть значения флагов кучи. Флаги KdDebuggerEnabled KdDebuggerNotPresent легко проверить с помощью NtQuerySystemInformation, вызвав ее с InformationClass, равным 0x23.
SEH (VEH) обработчики
Если установить свой обработчик событий с помощью функций SetUnhandledExceptionFilter или AddVectoredExceptionFilter, а затем сделать в программе какое-нибудь исключение, то под отладчиком установленный нами обработчик вызываться не будет, и эта особенность может помочь нам зафиксировать факт отладки. Используя контекст, передаваемый в обработчик, можно подправить счетчик команд и после выхода из обработчика продолжить выполнение команд в нужном нам направлении (код, который позволит это сделать, смотри на диске).
Особенности отладочного режима Windows
Очень многие API-функции (или их последовательности) по-разному себя ведут при обычном выполнении или при выполнении под отладчиком. К примеру, уже упомянутая функция DbgPrintEx (или OutputDebugString) при выполнении под отладчиком вызывает API ZwQueryDebugFilterState, а без отладчика вызова этой функции не происходит (за счет чего она, собственно, под отладчиком дольше и выполняется). Перехватив и отследив вызов этой функции, можно засечь факт отладки. Благодаря возможности подгружать файлы с отладочными символами вместе с загружаемой библиотекой в отладчике WinDbg и встроенном в Visual Studio отладчике можно легко распознать их наличие:
... HMODULE hLibrary = LoadLibrary(L"ntdll.dll"); if (int(CreateFileA("ntdll.dll",GENERIC_READ,0,0,3,0,0)) == -1) return true; ... // Или вот так HMODULE hLibrary = LoadLibrary(L"ntdll.dll"); HANDLE hRes = BeginUpdateResourceA("ntdll.dll",0); if (EndUpdateResourceA(hRes,0) == 0) return true; ...
Суть этого кода заключается в том, что под отладкой при открытии библиотеки, к примеру ntdll.dll, отладчик ждет загрузки файла с символами и других манипуляций с файлом открываемой библиотеки некоторое время делать не позволяет. Если для существующего файла сделать CreateFile с параметром OPEN_ EXISTING, затем установить для него HANDLE_ FLAG_ PROTECT_ FROM_CLOSE с помощью SetHandleInformation и далее попытаться закрыть все это дело с помощью CloseHandle, то под отладчиком вылезет исключение, чем мы с успехом можем воспользоваться:
... _try { HANDLE hFile = CreateFileA("d:/x_files.txt", 0, 0, 0, OPEN_EXISTING, 0, 0); SetHandleInformation(hFile, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); CloseHandle(hFile); return false; } __except(EXCEPTION_EXECUTE_HANDLER) { return true; } ...
Если учитывать, что отлаживаемый процесс должен иметь привилегии отладчика, да и отладчик, как правило, без административных прав запускается редко, то мы можем поиметь нужный нам результат. Это можно сделать, попробовав открыть системный процесс csrss.exe. Если удалось, значит, у испытуемого процесса с привилегиями отладчика все в порядке:
... _asm { call CsrGetProcessId // Получаем Id csrss.exe mov CSRSSId, eax } // Пытаемся открыть csrss.exe if (OpenProcess(PROCESS_ALL_ACCESS, 0, CSRSSId) != 0) return true; ...
Для этих же целей можно задействовать функцию DbgSetDebugFilterState, которая возвращает ненулевое значение, если процесс выполняется с админскими правами и имеет отладочные привилегии:
... _asm { call DbgSetDebugFilterState mov Flag, eax } if (Flag == 0) return true; ...
Еще можно попытаться отсоединить отлаживаемый поток от отладчика функцией NtSetInformationThread с параметром ThreadInformationClass равным 0x11 (ThreadHideFromDebugger):
... _asm { push 0 push 0 push 0x11 push -2 call NtSetInformationThread } ...
Процессы, окна, библиотеки и другие отладочные артефакты
Как нетрудно догадаться из заголовка, любой отладчик имеет какое-нибудь имя процесса, открытые окна, загруженные библиотеки и драйверы. Все это достаточно легко выявить, задействовав некоторые функции, любезно предоставленные в наше распоряжение операционной системой.
Это, к примеру, CreateToolhelp32Snapshot совместно с Process32First и Process32Next для поиска нужного процесса или FindWindow для поиска нужного окна.
Если надумаешь использовать этот прием, то следует учесть наличие плагинов к OllyDbg, которые контролируют эти функции. В некоторых случаях можно использовать функцию GetLastError после вызова нужных функций. Дело в том, что некоторые плагины просто имитируют неудачное срабатывание этих функций и несоответствие возвращенного ошибочного значения со значением, возвращаемым GetLastError, поможет это выявить.
Использовав функцию NtQueryInformationProcess 0x1f (ProcessDebugObjectHandle) или 0x1e (ProcessDebugFlags), можно определить факт отладки по наличию различных объектов отладки (соответствующий код лежит на диске).
Кроме всего этого, можно определить имя родительского процесса, и, если оно отлично от explorer.exe или cmd.exe, возможно, стоит заподозрить неладное.
Заключение
Конечно же, это не все. Это далеко не все. Тема антиотладки глубока, обширна и бесконечна, а журнал и объем статьи, к сожалению, такими свойствами не обладают. Что ж, думаю, у меня будет возможность еще чем-нибудь с тобой поделиться по этой теме в следующих номерах.
От эксперта: Вячеслав Закоржевский, «Лаборатория Касперского»
Чем знаменит: крутой парень, настоящий человек-дизассемблер
В статье приводится хорошая классификация антиотладочных приемов и соответствующие примеры. Подобные техники активно используются в разном программном обеспечении. Однако стоит отметить, что в использовании антиотладки больше заинтересованы те, кто хотят защитить код программы. И это далеко не всегда вирусописатели. Множество техник защиты от отладки используется в коммерческих упаковщиках, которые применяются для защиты легального ПО от реверса. Злоумышленники, конечно, тоже не против усложнить разбор «начинки» своего поделья, но так как для них это не несет прямого ущерба (в отличие от софта, для которого можно разобрать, например, генерацию кода активации), то и много времени на это не тратится. Вирусописатели в первую очередь думают о монетизации и защите детектирования своих продуктов, поэтому акценты при написании защитных механизмов несколько смещены в сторону обфускации и прочего.
От эксперта: Александр Матросов, Senior virus researcher, ESET
Чем знаменит: очень крутой парень
Антиотладочные приемы встречаются практически в любой вредоносной программе. И если раньше разработчики вредоносных программ стремились главным образом усложнить антивирусным компаниям анализ и тем самым замедлить выход вакцины, то на сегодняшний день засилье разношерстных троянцев не требует зачастую досконального изучения вредоносной программы для добавления ее в антивирусные базы. И сегодня основная цель разработчиков вредоносных программ состоит уже не в противостоянии с аналитиком, а в противодействии различным авторизированным системам для анализа новых образцов вредоносных программ, так называемым песочницам. Да и сами антиотладочные трюки ведут себя порой не очень стабильно на различных ОС или зависят от конкретной версии сервис-пака или ядра. Поэтому в современных вредоносных программах мы чаще встречаем что-нибудь интересное с точки зрения обнаружения выполнения на виртуальных машинах, нежели хитрые антиотладочные трюки.