Отладчик, пожалуй, наиболее часто используемый инструмент при реверс инжиниринге (следующий по распространенности инструмент после дизассемблера, такого как Interactive DisAssembler (IDA)). В результате, анти-отладочные трюки, вероятно, являются наиболее распространенной функцией кода, предназначенной для вмешательства в реверс-инжиниринг (а анти-дизассемблирующие конструкции являются следующими наиболее распространенными). Эти приемы могут просто обнаружить присутствие отладчика, отключить отладчик, вырваться из-под контроля отладчика или даже использовать уязвимость в отладчике. Присутствие отладчика может быть выведено косвенно, или может быть обнаружен определенный отладчик. Отключение или побег из-под контроля отладчика может быть достигнуто как общими, так и конкретными способами. Однако использование уязвимости достигается в отношении определенных отладчиков. Конечно, отладчик не обязательно должен присутствовать для того, чтобы попытаться использовать эксплоит.
Как правило, когда загружается отладчик, среда отладчика изменяется операционной системой, чтобы позволить отладчику взаимодействовать с программой (единственным исключением из этого является отладчик Obsidian). Некоторые из этих изменений более очевидны, чем другие, и по-разному влияют на работу отлаживаемого процесса. Среду также можно изменить по-разному, в зависимости от того, использовался ли отладчик для создания процесса или отладчик подключается к уже запущенному процессу.
Далее следует перечень известных методов, используемых для обнаружения присутствия отладчика, и в некоторых случаях защиты от них.
Примечание: Этот текст содержит несколько фрагментов кода в 32-битной и 64-битной версиях. Для простоты в 64-битных версиях предполагается, что все указатели стека и кучи и все дескрипторы соответствуют 32 битам. Они также полагаются на тот факт, что PEB всегда находится в нижней памяти.
1. NtGlobalFlag
Одно из самых простых изменений, которое вносит система, является также одним из наиболее неправильно понятых: поле NtGlobalFlag в Блоке Окружения Процесса. Поле NtGlobalFlag существует по смещению 0x68 в Блоке Окружения Процесса в 32-разрядных версиях Windows и по смещению 0xBC в 64-разрядных версиях Windows. Значение в этом поле по умолчанию равно нулю. Значение не изменяется, когда отладчик подключается к процессу. Однако значение может быть изменено до некоторой степени находясь под контролем процесса. Есть также два ключа реестра, которые можно использовать для установки определенных значений. В их отсутствие процесс, созданный отладчиком, будет иметь фиксированное значение в этом поле по умолчанию, но это конкретное значение можно изменить с помощью определенной переменной среды. Поле состоит из набора флагов. Для процесса, созданного отладчиком, будут установлены следующие флаги:
Таким образом, способ обнаружения присутствия отладчика состоит в проверке комбинации этих флагов. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Обратите внимание, что для 32-разрядного процесса в 64-разрядных версиях Windows существует отдельный Блок Окружения Процесса для 32-разрядной и 64-разрядной частей. Поля в 64-битной части затрагиваются так же, как и для 32-битной части.
Таким образом, существует еще одна проверка, которая использует этот 32-разрядный код для проверки 64-разрядной среды Windows:
Распространенная ошибка - использовать прямое сравнение, не маскируя сначала другие биты. В этом случае, если установлены другие биты, присутствие отладчика будет пропущено.
Способ победить эту технику для отладчика, заключается в том, чтобы изменить значение обратно на ноль, прежде чем возобновить процесс. Однако, как отмечено выше, начальное значение может быть изменено одним из четырех способов. Первый метод включает значение реестра, которое влияет на все процессы в системе. Это значение реестра является строковым значением "GlobalFlag" раздела реестра "HKLM\System\CurrentControlSet\Control\Session Manager". Здесь значение помещается в поле NtGlobalFlag, хотя оно может быть изменено позже в Windows. Изменение этого параметра реестра требует перезагрузки для вступления в силу. Это требование приводит к другому способу обнаружения присутствия отладчика, который также знает о значении реестра. Если отладчик копирует значение реестра в поле NtGlobalFlag, чтобы скрыть его присутствие, и если значение реестра изменяется, но система не перезагружается, тогда отладчик может быть обманут, используя это новое значение вместо истинного значения. Отладчик был бы обнаружен, если бы процесс знал, что истинное значение было чем-то отличным от того, что появляется в значении реестра. Один из способов определить истинное значение - запустить другой процесс, а затем запросить его значение NtGlobalFlag. Отладчик, который не знает о значении реестра, также раскрывается таким образом.
Второй метод также включает значение реестра, но оно влияет только на именованный процесс. Это значение реестра также является строковым значением "GlobalFlag", но в разделе реестра "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<имя файла>". "<Имя файла>" должно быть заменено именем исполняемого файла (не DLL), к которому будут применены флаги при выполнении файла. Как и выше, значение здесь помещается в поле NtGlobalFlag, хотя оно может быть изменено позже в Windows. Значение, установленное с помощью этого метода, объединяется со значением, которое применяется ко всем процессам, если таковые имеются.
Третий метод изменения значения основан на двух полях в Таблице Конфигурации Загрузки. В одном поле (GlobalFlagsClear) перечислены флаги для очистки, а в другом поле (GlobalFlagsSet) перечислены флаги, которые необходимо установить. Эти параметры применяются после того, как значения реестра GlobalFlag были применены, поэтому они могут переопределять значения, указанные в значениях реестра GlobalFlag. Однако они не могут переопределять значения, которые Windows устанавливает, когда определенные флаги остаются установленными (хотя они могут удалять флаги, которые устанавливаются, когда отладчик создает процесс). Например, установка флага FLG_USER_STACK_TRACE_DB (0x1000) заставляет Windows установить флаг FLG_HEAP_VALIDATE_PARAMETERS (0x40). Если флаг FLG_USER_STACK_TRACE_DB установлен в одном из значений реестра GlobalFlag, то даже если флаг FLG_HEAP_VALIDATE_PARAMETERS помечен для очистки в Таблице Конфигурации Загрузки, он все равно будет установлен Windows позже во время загрузки процесса.
Четвертый метод относится к изменениям, которые вносит Windows, когда отладчик создает процесс. При установке переменной среды "_NO_DEBUG_HEAP" три флага кучи не будут установлены в поле NtGlobalFlag из-за отладчика. Конечно, они все еще могут быть установлены значениями реестра GlobalFlag или полем GlobalFlagsSet в Таблице Конфигурации Загрузки.
2. Флаги кучи
Куча содержит два флага, которые инициализируются вместе с NtGlobalFlag. Значения в этих полях зависят от наличия отладчика, но также зависят от версии Windows. Расположение этих полей зависит от версии Windows. Два поля называются "Flags" и "ForceFlags". Поле "Flags" существует по смещению 0x0C в куче в 32-разрядных версиях Windows NT, Windows 2000 и Windows XP; и по смещению 0x40 в 32-разрядных версиях Windows Vista и более поздних. Поле "Flags" существует по смещению 0x14 в куче в 64-разрядных версиях Windows XP и по смещению 0x70 в куче в 64-разрядных версиях Windows Vista и более поздних версий. Поле ForceFlags существует по смещению 0x10 в куче в 32-разрядных версиях Windows NT, Windows 2000 и Windows XP; и по смещению 0x44 в 32-разрядных версиях Windows Vista и более поздних. Поле ForceFlags существует по смещению 0x18 в куче в 64-разрядных версиях Windows XP и по смещению 0x74 в куче в 64-разрядных версиях Windows Vista и более поздних версий.
Значение для поля Flags обычно устанавливается в HEAP_GROWABLE (2) во всех версиях Windows. Значение для поля ForceFlags обычно устанавливается равным нулю во всех версиях Windows. Однако оба эти значения зависят от версии подсистемы хост-процесса для 32-битного процесса (у 64-битного процесса такой зависимости нет). Значения полей соответствуют указанным, только если версия подсистемы равна 3.51 или выше. Если версия подсистемы 3.10-3.50, тогда флаг HEAP_CREATE_ALIGN_16 (0x10000) также будет установлен в обоих полях. Если версия подсистемы меньше 3.10, файл не будет работать вообще. Это особенно интересно, потому что общепринятым методом является размещение двух и нулевых значений в соответствующих полях, чтобы скрыть присутствие отладчика. Однако, если версия подсистемы не проверена, то это действие может выявить присутствие чего-либо, что пытается скрыть отладчик.
При наличии отладчика в поле Flags обычно устанавливается комбинация этих флагов в Windows NT, Windows 2000 и 32-разрядной Windows XP:
В 64-разрядных версиях Windows XP и Windows Vista и более поздних версиях поле Flags обычно устанавливается в комбинацию следующих флагов:
Когда присутствует отладчик, поле ForceFlags обычно устанавливается в комбинацию этих флагов:
Флаг HEAP_TAIL_CHECKING_ENABLED устанавливается в полях кучи, если в поле NtGlobalFlag установлен флаг FLG_HEAP_ENABLE_TAIL_CHECK. Флаг HEAP_FREE_CHECKING_ENABLED устанавливается в полях кучи, если в поле NtGlobalFlag установлен флаг FLG_HEAP_ENABLE_FREE_CHECK. Флаг HEAP_VALIDATE_PARAMETERS_ENABLED (и флаг HEAP_CREATE_ALIGN_16 (0x10000) в Windows NT и Windows 2000) устанавливается в полях кучи, если в поле NtGlobalFlag установлен флаг FLG_HEAP_VALIDATE_PARAMETERS.
Это поведение можно предотвратить в Windows XP и более поздних версиях, в результате чего вместо этого будут использоваться значения по умолчанию, создав переменную среды "_NO_DEBUG_HEAP".
Флаги кучи также могут управляться для каждого отдельного процесса через строковое значение "PageHeapFlags" раздела реестра "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<filename>".
Расположение кучи можно получить несколькими способами. Одним из способов является использование функции Get32cessHeap() для kernel32. Это эквивалентно использованию этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или используя этот 64-битный код для проверки 64-битной Среда Windows:
Как и в случае с Блоком Окружения, для 32-разрядного процесса в 64-разрядных версиях Windows существует отдельная куча для 32-разрядной и 64-разрядной частей. Поля в 64-битной части затрагиваются так же, как и для 32-битной части.
Таким образом, существует еще одна проверка, которая использует этот 32-разрядный код для проверки 64-разрядной среды Windows:
Другой способ заключается в использовании функции Get32cessHeaps() kernel32. Эта функция просто пересылается в функцию ntdll RtlGetProcessHeaps(). Функция возвращает массив куч процесса. Первая куча в списке такая же, как и та, которую возвращает функция kernel32 GetProcessHeap(). Запрос также может быть выполнен с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
или используя этот 32-битный код для проверки 64-битной среды Windows:
Таким образом, способ обнаружения присутствия отладчика состоит в проверке специальной комбинации флагов в поле Flags. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы находится (или может быть) в диапазоне 3.10-3.50:
или этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы составляет 3,51 или выше:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
или используя этот 32-битный код для проверки 64-битной среды Windows:
Вызов функции GetVersion() из kernel32 может быть еще более обфусцированн путем простого извлечения значения непосредственно из поля NtMajorVersion в структуре KUSER_SHARED_DATA со смещением 0x7ffe026c для конфигураций в 2 ГБ пространства пользователя. Это значение доступно во всех 32-разрядных и 64-разрядных версиях Windows.
Другой способ обнаружить присутствие отладчика - проверить наличие специальной комбинации флагов в поле ForceFlags. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы находится (или может быть) в диапазоне 3.10-3.50:
или использовать этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы составляет 3,51 или выше:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
или используя этот 32-битный код для проверки 64-битной среды Windows:
3. Куча
Когда куча инициализируется, проверяются флаги кучи, и в зависимости от того, какие флаги установлены, в среду могут вноситься дополнительные изменения. Если установлен флаг EAP_TAIL_CHECKING_ENABLED, то последовательность 0xABABABAB будет добавлена дважды в 32-разрядной среде Windows (четыре раза в 64-разрядной среде Windows) в конце выделенного блока. Если установлен флаг HEAP_FREE_CHECKING_ENABLED, то последовательность 0xFEEEFEEE (или ее часть) будет добавлена, если для заполнения свободного пространства до следующего блока требуются дополнительные байты. Таким образом, способ обнаружить присутствие отладчика - проверить эти значения. Если указатель кучи известен, то проверку можно выполнить путем непосредственного изучения данных кучи. Однако Windows Vista и более поздние версии используют защиту кучи как на 32-разрядной, так и на 64-разрядной платформах, с введением ключа XOR для кодирования размера блока. Использование этого ключа необязательно, но по умолчанию оно используется. Расположение служебного поля также отличается в Windows NT / 2000/XP и Windows Vista и более поздних версиях. Следовательно, версия Windows должна быть принята во внимание. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
Не существует эквивалента для 32-разрядного кода для проверки 64-разрядной среды Windows, поскольку 64-разрядная куча не может быть проанализирована 32-разрядной функцией кучи.
Если указатель неизвестен, его можно получить с помощью функции kernel32 HeapWalk() или функции ntdll RtlWalkHeap() (или даже функции kernel32 GetCommandLine()). Возвращенное значение размера блока декодируется автоматически, поэтому версия Windows больше не имеет значения в этом случае. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Не существует эквивалента для 32-разрядного кода для проверки 64-разрядной среды Windows, поскольку 64-разрядная куча не может быть проанализирована 32-разрядной функцией кучи.
4. TLS
Локальное хранилище потоков является одним из самых интересных методов борьбы с отладкой, которые существуют, потому что, несмотря на то, что они известны уже более десяти лет, все еще открываются новые способы их использования (и злоупотребления). Локальное хранилище потока существует для инициализации данных, специфичных для потока, до запуска этого потока. Поскольку каждый процесс содержит хотя бы один поток, это поведение включает в себя возможность инициализировать данные до запуска основного потока. Инициализация может быть выполнена путем указания статического буфера, который копируется в динамически распределенную память, и/или посредством выполнения кода в массиве обратных вызовов для динамической инициализации содержимого памяти. Чаще всего злоупотребляют массивом обратного вызова.
Массив обратных вызовов TLS может быть изменен (более поздние записи могут быть изменены) и/или расширен (новые записи могут быть добавлены) во время выполнения. Вновь добавленные или измененные обратные вызовы будут вызываться с использованием новых адресов. Нет ограничений на количество обратных вызовов, которые могут быть размещены. Расширение может быть сделано с помощью этого кода (идентичного для 32-битной и 64-битной) в 32-битной или 64-битной версии Windows:
Обратный вызов в l2 будет вызван, когда обратный вызов вернется в l1.
Адреса обратного вызова локального хранилища потоков могут указывать за пределы образа, например, на вновь загруженные библиотеки DLL. Это можно сделать косвенным путем, загрузив DLL и поместив возвращенный адрес в массив обратных вызовов TLS. Это также можно сделать напрямую, если известен адрес загрузки DLL. Значение imagebase может использоваться в качестве адреса обратного вызова, если DLL структурирована таким образом, чтобы победить DEP, если оно включено, или может быть получен и использован действительный адрес экспорта. Вызов может быть выполнен с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
В этом случае заголовок "MZ" файла с именем "tls2.dll" будет выполнен, когда обратный вызов вернется на l1. Кроме того, файл может ссылаться на себя, используя этот 32-разрядный код в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
В этом случае заголовок "MZ" текущего процесса будет выполнен, когда обратный вызов вернется в l1 . Адреса обратного вызова локального хранилища потока могут содержать RVA импортированных адресов из других библиотек DLL, если таблица адресов импорта изменяется, чтобы указывать на массив обратного вызова. Импорт разрешается до вызова обратных вызовов, поэтому импортированные функции будут вызываться нормально при достижении записи массива обратных вызовов.
Обратные вызовы TLS получают три параметра стека, которые могут быть переданы непосредственно в функции. Первый параметр - это ImageBase хост-процесса. Он может быть использован функцией kernel32 LoadLibrary() или функцией kernel32 WinExec(), например. Параметр ImageBase будет интерпретироваться функциями ядра LoadLibrary() или ядра WinExec() как указатель на имя файла для загрузки или выполнения. Создав файл с именем "MZ [некоторая строка]", где "[некоторая строка]" соответствует содержимому заголовка файла хоста, обратный вызов TLS получит доступ к файлу без какой-либо явной ссылки. Конечно, часть строки "MZ" также может быть заменена вручную во время выполнения, но многие функции полагаются непредсказуемыми.
Обратные вызовы локального хранилища потоков вызываются всякий раз, когда поток создается или уничтожается (если только процесс не вызывает kernel32 DisableThreadLibraryCalls() или функции ntdll LdrDisableThreadCalloutsForDll()). Это включает в себя поток, который создается Windows, когда отладчик подключается к процессу. Поток отладчика является особенным в том смысле, что его точка входа не указывает внутри образа. Вместо этого он указывает внутри kernel32.dll. Таким образом, простой метод обнаружения отладчика заключается в использовании обратного вызова TLS для запроса начального адреса каждого потока, который создается. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Поскольку обратные вызовы локального хранилища выполняются до того, как отладчик может получить контроль, обратный вызов может вносить другие изменения, такие как удаление точки останова, которая обычно размещается в точке входа . Патч можно сделать с помощью этого кода (идентичного для 32-битной и 64-битной) в 32-битной или 64-битной версии Windows:
Защита от этой техники очень проста и все более необходима. Это вопрос вставки точки останова в первый байт первого обратного вызова локального хранилища потока, а не в точку входа хоста. Это позволит отладчику получить контроль перед тем, как в процессе может выполняться любой код (конечно, за исключением загруженных DLL). Однако необходимо соблюдать осторожность в отношении адреса обратного вызова, поскольку, как отмечено выше, исходное значение по этому адресу может быть RVA импортируемой функции. Таким образом, адрес не может быть прочитан из файла. Это должно быть прочитано из памяти изображения.
Выполнение обратных вызовов TLS также зависит от платформы. Если исполняемый файл импортируется только из ntdll.dll или kernel32.dll, то обратные вызовы не будут вызываться во время события "on attach" при запуске в Windows XP и более поздних версиях. Когда процесс запускается, функция ntdll LdrInitializeThunk() обрабатывает список InLoadOrderModuleList. Список nLoadOrderModuleList содержит список библиотек DLL для обработки. Значение Flags в ссылочной структуре должно иметь бит LDRP_ENTRY_PROCESSED, очищенный хотя бы в одной DLL, для обратных вызовов TLS, вызываемых при присоединении.
Этот бит всегда устанавливается для ntdll.dll, поэтому при импорте файла из только ntdll.dll не будет выполняться обратный вызов локального хранилища потока при присоединении. В Windows 2000 и более ранних версиях была ошибка сбоя, если файл не импортировался из kernel32.dll, явно (то есть импортируется из kernel32.dll напрямую) или неявно (то есть импортировался из DLL, которая импортируется из kernel32.dll; или DLL, которая импортирует из DLL, которая импортирует из kernel32.dll, независимо от длины цепочки).
Эта ошибка была исправлена в Windows XP, заставляя ntdll.dll явно загружать kernel32.dll перед обработкой таблицы импорта хоста. Когда kernel32.dll загружен, он добавляется в InLoadOrderModuleList. Проблема в том, что это исправление внесло побочный эффект.
Побочный эффект возникает, когда ntdll.dll извлекает адрес экспортированной функции из kernel32.dll через функцию ntdll LdrGetProcedureAddressEx(). Побочный эффект может быть вызван в результате получения любой экспортируемой функции, но он запускается в данном конкретном случае, когда ntdll извлекает адрес одной из следующих функций: BaseProcessInitPostImport() (только для Windows XP и Windows Server 2003), BaseQueryModuleData () (Только для Windows XP и Windows Server 2003, если функция BaseProcessInitPostImport() не существует), BaseThreadInitThunk() (Windows Vista и более поздние версии) или BaseQueryModuleData() (Windows Vista и более поздние версии, если BaseThreadInitThunk() не существует ).
Побочным эффектом является то, что функция ntdll LdrGetProcedureAddressEx() устанавливает флаг LDRP_ENTRY_PROCESSED для записи kernel32.dll в списке InLoadOrderModuleList. В результате при импорте файла, импортируемого только из kernel32.dll, обратные вызовы локального хранилища потоков не выполняются. Это можно считать ошибкой в Windows.
Существует простой обходной путь для этой проблемы: импортировать что-либо из другой библиотеки DLL и при условии, что у библиотеки DLL есть ненулевая точка входа. Затем обратные вызовы TLS будут выполняться при присоединении. Обходной путь работает, потому что значение поля Flags будет иметь бит LDRP_ENTRY_PROCESSED, очищенный для этой DLL.
В Windows Vista и более поздних версиях динамически загружаемые библиотеки DLL также поддерживают локальное хранилище потоков. Оно находится в прямом противоречии с существующей документацией формата Portable Executable, в которой говорится, что "Статически объявленные объекты данных TLS", то есть обратные вызовы TLS, могут использоваться только в статически загружаемых файлах образа. Этот факт делает ненадежным использование статических данных локального хранилища потоков в DLL, если вы не знаете, что DLL или что-либо статически связанное с ней никогда не будет загружаться динамически с помощью функции API LoadLibrary". Кроме того, будут вызываться обратные вызовы TLS независимо от того, что присутствует в таблице импорта. Таким образом, DLL может импортировать из ntdll.dll или kernel32.dll или даже вообще без DLL (в отличие от случая .exe, описанного выше), и обратные вызовы будут вызваны!
5.Anti-Step-Over
Большинство отладчиков поддерживают пошаговое выполнение определенных инструкций, таких как последовательности "call" и "rep". В таких случаях программная точка останова часто помещается в поток команд, и затем процессу разрешается возобновить выполнение. Обычно отладчик снова получает управление, когда достигается программная точка останова. Однако в случае последовательности "rep" отладчик должен проверить, что инструкция, следующая за префиксом rep, действительно является инструкцией, к которой rep применяется по закону. Некоторые отладчики предполагают, что любой префикс rep предшествует строковой инструкции. Это создает уязвимость, когда инструкция, следующая за префиксом rep, является полностью другой инструкцией. В частности, проблема возникает, если эта инструкция удаляет программную точку останова, которая была бы помещена в поток, если команда перешагнула. В этом случае, когда инструкция перешагивает, а программная точка останова удаляется командой, выполнение возобновляется под полным контролем процесса и никогда не возвращается к отладчику. Пример кода выглядит так:
Если попытка перешагнуть на l1, то выполнение будет возобновлено свободно с l2.
Более общий метод использует строковые инструкции для удаления точки останова. Патч можно сделать с помощью этого 32-битного кода в 32-битной или 64-битной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
Существуют варианты этой техники, например, использование "rep movs" вместо "rep stos". Флаг направления может использоваться для изменения направления записи в память, так что перезапись может быть пропущена. Патч можно сделать с помощью этого 32-битного кода в 32-битной или 64-битной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
Решением этой проблемы является использование аппаратных точек останова во время перехода по строковым инструкциям. Это особенно важно, если учесть, что отладчик не может знать, является ли установленная им точка останова выполненной. Если процесс удаляет точку останова, он может впоследствии восстановить точку останова и затем выполнить точку останова, как обычно. Отладчик увидит ожидаемое исключение точки останова и будет вести себя как обычно. Это может быть выполнено с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
В этом примере, перешагнув инструкцию на l2, код достигнет l4, а затем вернется к l1. Это приведет к замене точки останова на l2 на втором проходе и выполнению на l3. Затем отладчик восстановит управление. В то время единственное очевидное отличие будет состоять в том, что регистр AL будет содержать значение 0xCC вместо ожидаемого 0x90. Это позволит достичь уровня l5 за один проход вместо двух. Конечно, возможны гораздо более тонкие варианты, включая выполнение совершенно разных путей кода.
Разновидность техники может использоваться для простого обнаружения присутствия отладчика. Проверка может быть выполнена с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
Этот код обнаружит точку останова, которая находится на l1. Он работает путем копирования значения в l1 через 90h в l1 + 1. Затем значение сравнивается на l2.
6.Аппаратура
A. Аппаратные точки останова
Когда возникает исключение, Windows создает контекстную структуру для передачи обработчику исключения. Структура будет содержать значения общих регистров, селекторов, управляющих регистров и регистров отладки. Если отладчик присутствует и передает исключение отладчику с использованием аппаратных точек останова, то регистры отладки будут содержать значения, которые показывают наличие отладчика. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Значения для регистров отладки также могут быть изменены до возобновления выполнения в 32-разрядных версиях Windows, что может привести к неконтролируемому выполнению, если программная точка останова не установлена в соответствующем месте.
B.Подсчет инструкций
Подсчет команд можно выполнить, зарегистрировав обработчик исключений, а затем установив аппаратные точки останова на определенных адресах. При нажатии на соответствующий адрес будет сгенерировано исключение EXCEPTION_SINGLE_STEP (0x80000004). Это исключение будет передано в обработчик исключений. Обработчик исключений может выбрать настройку указателя инструкции для указания новой инструкции, опционально установить дополнительные аппаратные точки останова на определенных адресах и затем возобновить выполнение. Для установки точек останова необходим доступ к контекстной структуре. Копию контекстной структуры можно получить, вызвав функцию kernel32 GetThreadContext(), которая позволяет при необходимости установить начальные значения для аппаратных точек останова. Впоследствии, когда возникает исключение, обработчик исключений автоматически получает копию структуры контекста. Отладчик будет мешать пошаговому выполнению, что приведет к другому количеству команд по сравнению с отсутствием отладчика. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
Поскольку в этом методе используется структурированный обработчик исключений, его нельзя использовать в 64-разрядных версиях Windows. Код можно легко переписать, чтобы вместо него использовать Vectored Exception Handler. Это требует создания потока и изменения его контекста, потому что регистры отладки не могут быть назначены внутри обработчика исключений с векторным управлением в 64-разрядных версиях Windows. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версии Windows XP или более поздней версии:
или этот 64-битный код для проверки 64-битной среды Windows:
C.Прерывание #3
Всякий раз, когда возникает исключение программного прерывания, адрес исключения и значение регистра EIP будут указывать на инструкцию после той, которая вызвала исключение. Исключение точки останова рассматривается как особый случай. Когда возникает исключение EXCEPTION_BREAKPOINT (0x80000003), Windows предполагает, что оно было вызвано однобайтовым кодом операции "CC" (инструкция "INT 3"). Windows уменьшает адрес исключения, указывая на предполагаемый код операции «CC», а затем передает исключение обработчику исключений. На значение регистра EIP это не влияет. Таким образом, если используется код операции "CD 03" (длинная инструкция "INT 03"), адрес исключения будет указывать на "03", когда обработчик исключения получает управление.
D. Прерывание 0x2d
Прерывание 0x2D является частным случаем. Когда оно выполняется, Windows использует текущее значение регистра EIP в качестве адреса исключения, а затем оно увеличивается на единицу значения регистра EIP. Однако Windows также проверяет значение в регистре EAX, чтобы определить, как настроить адрес исключения. Если регистр EAX имеет значение 1, 3 или 4 во всех версиях Windows или значение 5 в Windows Vista и более поздних версиях, то Windows увеличивает его на один адрес исключения. Наконец, он выдает исключение EXCEPTION_BREAKPOINT (0x80000003), если присутствует отладчик. Поведение прерывания 0x2D может вызвать проблемы для отладчиков. Проблема заключается в том, что некоторые отладчики могут использовать значение регистра EIP в качестве адреса для возобновления, в то время как другие отладчики могут использовать адрес исключения в качестве адреса для возобновления. Это может привести к пропуску однобайтовой инструкции или выполнению совершенно другой инструкции, поскольку первый байт отсутствует. Эти поведения могут использоваться, чтобы вывести присутствие отладчика. Проверка может быть выполнена с использованием этого кода (идентичного для 32-битной и 64-битной), чтобы проверить 32-битную или 64-битную среду Windows:
E. Прерывание 0x41
Прерывание 0x41 может отображать другое поведение, если присутствует отладчик режима ядра или нет. Дескриптор прерывания 0x41 обычно имеет нулевой DPL, что означает, что прерывание не может быть успешно выполнено из кольца зазиты 3. Попытка выполнить это прерывание напрямую приведет к тому, что процессор выдает общую ошибку защиты (прерывание 0x0D), что в итоге приводит к исключению EXCEPTION_ACCESS_VIOLATION (0xC0000005). Однако некоторые отладчики перехватывают прерывание 0x41 и устанавливают его DPL равным 3, чтобы прерывание можно было успешно вызывать из пользовательского режима. Этот факт можно использовать для определения наличия отладчика режима ядра. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows (хотя вряд ли он будет поддерживаться 64-битным отладчиком):
F.MOV SS
Существует простой трюк для определения одноступенчатых операций, который работал с самых ранних процессоров Intel. Он использовался довольно часто во времена DOS, но он все еще работает во всех версиях Windows. Трюк основан на том факте, что определенные инструкции приводят к отключению всех прерываний при выполнении следующей инструкции. В частности, загрузка регистра SS очищает прерывания, чтобы позволить следующей инструкции загрузить регистр ESP без риска повреждения стека. Однако не требуется, чтобы следующая инструкция загружала что-либо в регистр ESP. Любая инструкция может следовать за загрузкой регистра SS. Если для пошагового выполнения кода используется отладчик, то в образе EFLAGS будет установлен флаг T. Обычно это не видно, поскольку флаг T будет очищен в образе EFLAGS после доставки каждого события отладчика. Однако, если флаги сохраняются в стеке до доставки события отладчика, тогда флаг T станет видимым. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
Интересная ситуация возникает в VirtualPC при работе с Windows 2000, которая заключается в том, что инструкция CPUID ведет себя таким же образом. Неизвестно, почему это происходит.
Пример 64-битного кода отсутствует, поскольку в этой среде не поддерживается селектор SS.
7.API
Отладчик может быть обнаружен, отключен (в результате чего он теряет контроль над отладчиком), используя стандартные функции операционной системы.
A. Функции кучи
Единственное, что объединяет все эти функции, это то, что они вызывают функцию ntdll RtlFreeHeap().
The kernel32
Функции BasepFreeActivationContextActivationBlock() и SortCloseHandle() kernel32 существуют только в Windows 7 и более поздних версиях. Функция kernel32 BasepFreeAppCompatData() существует только в Windows Vista и более поздних версиях. Ядро FindVolumeMountPointClose() вызывает функцию ntdll RtlFreeHeap() только как особый случай (в частности, когда параметр hFindVolumeMountPoint является допустимым указателем на дескриптор, который может быть успешно закрыт)
Однако дело в том, что функция ntdll RtlFreeHeap() содержит функцию, которая предназначена для использования вместе с отладчиком, - вызов функции ntdll DbgPrint(). Проблема заключается в том, что способ реализации функции ntdll DbgPrint() позволяет приложению обнаруживать присутствие отладчика при вызове функции.
Когда вызывается функция dtgPrint() ntdll, она вызывает исключение DBG_PRINTEXCEPTION_C (0x40010006), но исключение обрабатывается особым образом, поэтому зарегистрированный структурированный обработчик исключений не увидит его. Причина в том, что Windows внутренне регистрирует свой собственный обработчик структурированных исключений, который использует исключение, если отладчик этого не делает. Однако в Windows XP и более поздних версиях любой зарегистрированный векторный обработчик исключений будет работать до структурированного обработчика исключений, который регистрирует Windows. Это может считаться ошибкой в Windows. Присутствие отладчика, который использует исключение, теперь может быть выведено из отсутствия исключения. Кроме того, другой обработчик доставляется в обработчик исключений если отладчик присутствует, но не использует исключение, или если отладчик вообще отсутствует. Если отладчик присутствует, но не использует исключение, Windows доставит исключение DBG_PRINTEXCEPTION_C (0x40010006). Если отладчик отсутствует, Windows доставит исключение EXCEPTION_ACCESS_VIOLATION (0xC0000005). Присутствие отладчика теперь может быть определено либо отсутствием исключения, либо значением исключения.
Существует еще один случай, который применяется, среди прочего, к функциям кучи и ресурсов, когда функции могут быть принудительно вызваны прерыванием отладки. Общим для них является проверка флага BeingDebugged в Блоке Окружения Процесса. Присутствие отладчика может быть подделано, чтобы вызвать исключение прерывания 3, и исключение должно быть видимым для отладчика. Таким образом, если исключение отсутствует (потому что отладчик использовал его), то присутствие отладчика раскрывается. Проверка может быть выполнена с использованием этого 32-разрядного кода, чтобы проверить 32-разрядную среду Windows в 32-разрядной версии для 64-разрядных версий Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Значение флага появляется дважды в каждом случае, потому что оно размещается в обоих возможных местах для поля флага, в зависимости от версии Windows. Это исключает необходимость проверки версии.
Обратите внимание, что в Windows Vista и более поздних версиях поведение изменилось незначительно. Ранее прерывание отладки было вызвано вызовом функции ntdll DbgBreakPoint(). Теперь это вызвано инструкцией прерывания 3, которая сохраняется непосредственно в потоке кода. Результат в обоих случаях одинаков.
Обнаружение также может быть немного расширено. LastErrorValue в блоке среды потока может быть установлен в ноль до вызова функции, либо напрямую, либо путем вызова функции kernel32 SetLastError(). Если исключение не произошло, то при возврате из функции значение в этом поле (также возвращаемое функцией GetLastError() ядра32) будет установлено в ERROR_INVALID_HANDLE (6).
B. Дескрипторы
1.Функция OpenProcess
Иногда утверждается, что функция kernel32 OpenProcess() (или функция ntdll NtOpenProcess()) обнаруживает присутствие отладчика при использовании в процессе "csrss.exe". Это неверно. Хотя это правда, что вызов функции будет успешным в присутствии некоторых отладчиков, это происходит из-за побочного эффекта поведения отладчика (в частности, получения привилегии отладки), а не из-за самого отладчика (это должно быть очевидно поскольку вызов функции не удается при использовании с некоторыми отладчиками). Все, что он показывает, - то, что учетная запись пользователя для процесса является членом группы администраторов и имеет привилегию отладки. Причина в том, что успех или неудача вызова функции ограничена только уровнем привилегий процесса. Если учетная запись пользователя процесса является членом группы администраторов и имеет привилегию отладки, то вызов функции будет успешным; если нет, то нет. Обычному пользователю недостаточно приобрести привилегию отладки, и администратор не может успешно вызвать функцию без нее. Идентификатор процесса csrss.exe можно получить с помощью функции ntdll CsrGetProcessId() в Windows XP и более поздних версиях (другие способы существуют для более ранних версий Windows и показаны позже в контексте поиска процесса "Explorer.exe"). ). Вызов может быть выполнен с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
Привилегию отладки можно получить с помощью этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
2.CloseHandle
Один из хорошо известных методов обнаружения отладчика включает функция kernel32 CloseHandle(). Если недопустимый дескриптор передается в функцию ядра 32 CloseHandle() (или непосредственно в функцию ntdll NtClose(), или функцию ядра 32 FindVolumeMountPointClose() в Windows 2000 и более поздних версиях (которая просто вызывает функцию ядра 32 CloseHandle ()), и присутствует отладчик, затем будет вызвано исключение EXCEPTION_INVALID_HANDLE (0xC0000008). Это исключение может быть перехвачено обработчиком исключения, и это указывает на то, что работает отладчик. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Однако существует второй случай, который предполагает использование защищенного дескриптора. Если защищенный дескриптор передается в функцию CloseHandle() (или непосредственно в функцию ntdll NtClose()) и присутствует отладчик, то будет вызвано исключение EXCEPTION_HANDLE_NOT_CLOSABLE (0xC0000235). Это исключение может быть перехвачено обработчиком исключения, и это указывает на то, что работает отладчик. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Попытка победить любой из этих методов проще всего в Windows XP и более поздних версиях, где обработчик исключений FirstHandler Vectored может быть зарегистрирован отладчиком, чтобы скрыть исключение и молча возобновить выполнение. Однако существует проблема прозрачного перехвата функции AddVectoredExceptionHandler() из kernel32, чтобы предотвратить регистрацию другого обработчика в качестве первого обработчика. Недостаточно перехватить попытку зарегистрировать первый обработчик, а затем сделать его последним обработчиком. Причина в том, что это будет обнаружено путем регистрации двух обработчиков и последующего исключения, поскольку обработчики будут вызываться в неправильном порядке. Существует также потенциальная проблема, заключающаяся в том, чтобы перехватить функцию и обнаружить такой измененный запрос, а затем просто снова изменить его. Другой способ чтобы исправить это состоит в том, чтобы дизассемблировать функцию, чтобы найти базовый указатель, который содержит заголовок списка, но это зависит от платформы. Конечно, поскольку функция возвращает указатель на структуру обработчика, список можно просмотреть, зарегистрировав обработчик и затем проанализировав структуру.
Эта ситуация все еще лучше, чем проблема прозрачного перехвата функции ntdll NtClose() в Windows NT и Windows 2000, чтобы зарегистрировать обработчик структурированных исключений, чтобы скрыть исключение.
Существует флаг, который может быть установлен для создания исключительного поведения, даже если нет отладчика. Устанавливая флаг FLG_ENABLE_CLOSE_EXCEPTIONS (0x400000) в значении реестра "HKLM\System\CurrentControlSet\Control\Session Manager\GlobalFlag", а затем перезагружая, функция CloseHandle() и функция ntdll NtClose() всегда будут вызывать исключение, если в функцию передан неверный или защищенный дескриптор. Эффект является общесистемным и поддерживается во всех версиях Windows для Windows NT, как 32-разрядных, так и 64-разрядных.
Есть другой флаг, который приводит к аналогичному поведению для других функций, которые принимают дескрипторы. Устанавливая флаг FLG_APPLICATION_VERIFIER (0x100) в значении реестра "HKLM\System\CurrentControlSet\Control\Session Manager\GlobalFlag" и затем перезагружая, функция ObtoferenceObjectByHandle() ntoskrnl всегда вызывает исключение, если недопустимый дескриптор передается в функцию (такую как функция SetEvent() kernel32), которая вызывает функцию ntoskrnl ObReferenceObjectByHandle(). Эффект также общесистемный.
Обратите внимание, что один из этих флагов в настоящее время задокументирован неправильно, а другой из этих флагов в настоящее время документирован не полностью. Flag "Enable close exception" задокументированокак вызывающее исключения для недопустимых дескрипторов, которые передаются функциям, отличным от функции ntoskrnl NtClose(), но это неверно; Flag "Enable bad handles detection" вызовет исключения для недопустимых дескрипторов, которые передаются в функции, отличные от функции ntoskrnl NtClose(), но это поведение не задокументировано.
3.Функция CreateFile
Немного ненадежный способ обнаружить присутствие отладчика - попытаться открыть исключительно файл текущего процесса. Когда присутствуют некоторые отладчики, это действие всегда будет неудачным. Причина в том, что при запуске процесса для отладки открывается дескриптор файла. Это позволяет отладчику читать отладочную информацию из файла (при условии, что она присутствует). Значение дескриптора сохраняется в структуре, которая заполняется, когда происходит событие CREATE_PROCESS_DEBUG_EVENT. Если этот дескриптор не закрыт отладчиком, файл не может быть открыт для монопольного доступа. Поскольку отладчик не открыл файл, было бы легко забыть закрыть его.
Конечно, если какое-либо другое приложение (например, шестнадцатеричный редактор) проверяет файл, то открытие также завершится ошибкой по той же причине. Вот почему метод считается ненадежным, но в зависимости от намерения такие ложные срабатывания могут оказаться приемлемыми. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows (но метод не работает для 64-битных процессов):
Функция kernel32 CreateFile() может также использоваться для обнаружения присутствия драйверов режима ядра, которые могут принадлежать отладчику (или любому другому интересующему инструменту). Инструментам, использующим драйверы режима ядра, также необходим способ связи с этими драйверами. Очень распространенным методом является использование именованных устройств. Таким образом, при попытке открыть такое устройство любой успех указывает на присутствие драйвера. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
Типичный список включает в себя следующие имена:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Однако в настоящее время нет 64-битного списка из-за нехватки отладчиков режима ядра, которые предлагают сервисы режима пользователя.
Обратите внимание, что имя драйвера "\\.\ NTICE" действительно только для SoftICE до версии 4.0. SoftICE v4.x не создает устройства с таким именем на платформах под управлением Windows NT. Вместо этого имя устройства - "\\.\NTICExxxx"), где «xxxx» - это четыре шестнадцатеричных символа. Источником символов являются 9-й, 7-й, 5-й и 3-й символы из данных в значении реестра "Serial". Это значение появляется в нескольких местах в реестре. Драйвер SoftICE использует значение реестра "HKLM\System\CurrentContrlSet\Services\NTice\Serial". Функция DevIO_ConnectToSoftICE() использует значение реестра "HKLM\Software\NuMega\SoftIce\Serial". Алгоритм, который использует SoftICE, состоит в том, чтобы перевернуть строку, а затем, начиная с третьего символа, взять каждый второй символ для четырех символов. Конечно, для этого есть более простой способ. Имя может быть сконструировано с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядных версиях Windows (SoftICE не работает в 64-разрядных версиях Windows):
4. Функция LoadLibrary
Функция LoadLibrary() в kernel32 - удивительно простой и эффективный способ обнаружения отладчика. Когда файл загружается в присутствии отладчика, используя функцию LoadLibrary() kernel32 (или любой из ее вариантов - функцию LoadLibraryEx() kernel32 или функцию ntdll LdrLoadDll()), открывается дескриптор файла. Это позволяет отладчику читать отладочную информацию из файла (при условии, что она присутствует). Значение дескриптора сохраняется в структуре, которая заполняется, когда происходит событие LOAD_DLL_DEBUG_EVENT. Если этот дескриптор не закрыт отладчиком (выгрузка DLL не закроет его), файл не может быть открыт для монопольного доступа. Поскольку отладчик не открыл файл, было бы легко забыть закрыть его. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Альтернативный метод заключается в вызове другой функции, которая вызывает внутреннюю функцию CreateFile() из kernel32 (или любую ее разновидность) - функцию ntdll NtCreateFile() или функцию ntdll NtOpenFile(). Одним из примеров являются функции обновления ресурсов, такие как функция kernel32 EndUpdateResource(). Причина, по которой работает функция kernel32 EndUpdateResource(), заключается в том, что она в конечном итоге вызывает функцию kernel32 CreateFile() для записи новой таблицы ресурсов. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows (но метод не работает для 64-битных процессов):
5. Функция ReadFile
Функция kernel32 ReadFile() может использоваться для автоматической модификации потока кода путем считывания содержимого файла в местоположение после вызова функции. Она также может использоваться для удаления программных точек останова, которые отладчик может поместить в поток кода, особенно сразу после вызова функции. Результатом в этом случае будет то, что код выполняется свободно. Вызов может быть выполнен с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
Один из способов преодолеть эту технику - использовать аппаратные контрольные точки вместо программных контрольных точек при переходе через вызовы функций.
C. Время выполнения
При наличии отладчика, который используется для пошагового выполнения кода, существует значительная задержка между выполнением отдельных инструкций по сравнению с собственным выполнением. Эта задержка может быть измерена с использованием одного из нескольких возможных источников времени. Эти источники включают инструкцию RDPMC (однако эта инструкция требует, чтобы в регистре CR4 был установлен флаг PCE, но это не настройка по умолчанию), инструкцию RDTSC , функцию kernel32 GetLocalTime(), функция GetSystemTime(), QueryPerformanceCounter(), GetTickCount(), функция ntoskrnl KiGetTickCount() (предоставляется через интерфейс прерываний 0x2A в 32-разрядных версиях Windows) и функция winmm timeGetTime(). Однако разрешение функции winmm timeGetTime() является переменным, в зависимости от того, является ли оно внутренним или внешнним по отношению к функции GetTickCount(), что делает очень ненадежным измерение небольших интервалов. Инструкция RDMSR также может использоваться в качестве источника времени, но ее нельзя использовать в пользовательском режиме. Проверка может быть выполнена для инструкции RDPMC с использованием этого кода для проверки 32-разрядной или 64-разрядной среды Windows:
Проверка может быть выполнена для инструкции RDTSC с использованием этого кода (идентичного для 32-битной и 64-битной) для проверки 32-битной или 64-битной среды Windows:
Проверка может быть выполнена для функции GetLocalTime() kernel32, использующей этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Проверка может быть выполнена для функции GetSystemTime() с использованием точно такого же кода, что и для функции GetLocalTime() , за исключением изменения имени функции.
Можно выполнить проверку для функции GetTickCount() kernel32, используя этот код (идентичный для 32-битной и 64-битной), чтобы проверить 32-битную или 64-битную среду Windows:
Проверка может быть выполнена для функции ntoskrnl KiGetTickCount(), использующей этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядных версиях Windows (прерывание не поддерживается в 64-разрядных версиях Windows):
Проверка может быть выполнена для функции kernel32 QueryPerformanceCounter(), использующей этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Можно выполнить проверку для функции winmm timeGetTime (), используя этот код (идентичный для 32-разрядного и 64-разрядного), чтобы проверить 32-разрядную или 64-разрядную среду Windows:
D.Process-level
1.Функция CheckRemoteDebuggerPresent
Функция kernel32 CheckRemoteDebuggerPresent() была введена в Windows XP с пакетом обновления 1 (SP1) для запроса значения, существовавшего со времени Windows NT. Remote в этом смысле означает отдельный процесс на одной машине. Функция устанавливает в 0xffffffff значение, на которое указывает аргумент pbDebuggerPresent, если присутствует отладчик (то есть присоединенный к текущему процессу). Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
2.Родительский процесс
Пользователи обычно запускают приложения, щелкая значок, отображаемый процессом оболочки (Explorer.exe). В результате родительский процесс исполняемого процесса будет Explorer.exe. Конечно, если приложение выполняется из командной строки, то родительский процесс исполняемого процесса будет процессом командной строки. Выполнение приложения путем отладки приведет к тому, что родительский процесс исполняемого процесса станет процессом отладчика.
Выполнение приложений из командной строки может вызвать проблемы для некоторых приложений, поскольку они ожидают, что родительский процесс будет Explorer.exe. Некоторые приложения проверяют имя родительского процесса, ожидая, что оно будет "Explorer.exe". Некоторые приложения сравнивают идентификатор родительского процесса с идентификатором Explorer.exe. Несоответствие в любом случае может привести к тому, что приложение будет думать, что оно отлаживается.
На этом этапе мы сделаем небольшой обход и представим тему, которая логически должна появиться позже. Самый простой способ получить идентификатор процесса Explorer.exe - вызвать функции user32 GetShellWindow() и user32 GetWindowThreadProcessId(). Это оставляет идентификатор процесса и имя родительского процесса текущего процесса, что можно получить, вызвав функцию ntdll NtQueryInformationProcess() с классом ProcessBasicInformation. Вызовы могут быть сделаны с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:
Однако этот код имеет серьезную проблему, заключающуюся в том, что в одном сеансе может быть несколько экземпляров Explorer.exe, если значение реестра "HKCU\Software\Microsoft\ Windows\CurrentVersion\Explorer\Advanced\SeparateProcess" (введено в Windows 2000) не равно нулю. Это приводит к запуску отдельной копии Explorer.exe для каждого открытого окна. В результате окно оболочки может не быть родительским процессом текущего процесса, и все же Explorer.exe является именем родительского процесса.
3. Функция CreateToolhelp32Snapshot
Идентификатор процесса Explorer.exe и родительского процесса текущего процесса, а также имя этого родительского процесса можно получить с помощью функции CreateToolhelp32Snapshot() и перечисления функции Process32Next (). Вызов может быть выполнен с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
Как правило, когда загружается отладчик, среда отладчика изменяется операционной системой, чтобы позволить отладчику взаимодействовать с программой (единственным исключением из этого является отладчик Obsidian). Некоторые из этих изменений более очевидны, чем другие, и по-разному влияют на работу отлаживаемого процесса. Среду также можно изменить по-разному, в зависимости от того, использовался ли отладчик для создания процесса или отладчик подключается к уже запущенному процессу.
Далее следует перечень известных методов, используемых для обнаружения присутствия отладчика, и в некоторых случаях защиты от них.
Примечание: Этот текст содержит несколько фрагментов кода в 32-битной и 64-битной версиях. Для простоты в 64-битных версиях предполагается, что все указатели стека и кучи и все дескрипторы соответствуют 32 битам. Они также полагаются на тот факт, что PEB всегда находится в нижней памяти.
1. NtGlobalFlag
Одно из самых простых изменений, которое вносит система, является также одним из наиболее неправильно понятых: поле NtGlobalFlag в Блоке Окружения Процесса. Поле NtGlobalFlag существует по смещению 0x68 в Блоке Окружения Процесса в 32-разрядных версиях Windows и по смещению 0xBC в 64-разрядных версиях Windows. Значение в этом поле по умолчанию равно нулю. Значение не изменяется, когда отладчик подключается к процессу. Однако значение может быть изменено до некоторой степени находясь под контролем процесса. Есть также два ключа реестра, которые можно использовать для установки определенных значений. В их отсутствие процесс, созданный отладчиком, будет иметь фиксированное значение в этом поле по умолчанию, но это конкретное значение можно изменить с помощью определенной переменной среды. Поле состоит из набора флагов. Для процесса, созданного отладчиком, будут установлены следующие флаги:
FLG_HEAP_ENABLE_TAIL_CHECK (0x10)
FLG_HEAP_ENABLE_FREE_CHECK (0x20)
FLG_HEAP_VALIDATE_PARAMETERS (0x40)
Таким образом, способ обнаружения присутствия отладчика состоит в проверке комбинации этих флагов. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov eax, fs:[30h] ;Process Environment Block
mov al, [eax+68h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged
или использовать этот 64-битный код для проверки 64-битной среды Windows:
push 60h
pop rsi
gs:lodsq ;Process Environment Block
mov al, [rsi*2+rax-14h] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged
Обратите внимание, что для 32-разрядного процесса в 64-разрядных версиях Windows существует отдельный Блок Окружения Процесса для 32-разрядной и 64-разрядной частей. Поля в 64-битной части затрагиваются так же, как и для 32-битной части.
Таким образом, существует еще одна проверка, которая использует этот 32-разрядный код для проверки 64-разрядной среды Windows:
mov eax, fs:[30h] ; Process Environment Block
;64-bit Process Environment Block
;follows 32-bit Process Environment Block
mov al, [eax+10bch] ;NtGlobalFlag
and al, 70h
cmp al, 70h
je being_debugged
Распространенная ошибка - использовать прямое сравнение, не маскируя сначала другие биты. В этом случае, если установлены другие биты, присутствие отладчика будет пропущено.
Способ победить эту технику для отладчика, заключается в том, чтобы изменить значение обратно на ноль, прежде чем возобновить процесс. Однако, как отмечено выше, начальное значение может быть изменено одним из четырех способов. Первый метод включает значение реестра, которое влияет на все процессы в системе. Это значение реестра является строковым значением "GlobalFlag" раздела реестра "HKLM\System\CurrentControlSet\Control\Session Manager". Здесь значение помещается в поле NtGlobalFlag, хотя оно может быть изменено позже в Windows. Изменение этого параметра реестра требует перезагрузки для вступления в силу. Это требование приводит к другому способу обнаружения присутствия отладчика, который также знает о значении реестра. Если отладчик копирует значение реестра в поле NtGlobalFlag, чтобы скрыть его присутствие, и если значение реестра изменяется, но система не перезагружается, тогда отладчик может быть обманут, используя это новое значение вместо истинного значения. Отладчик был бы обнаружен, если бы процесс знал, что истинное значение было чем-то отличным от того, что появляется в значении реестра. Один из способов определить истинное значение - запустить другой процесс, а затем запросить его значение NtGlobalFlag. Отладчик, который не знает о значении реестра, также раскрывается таким образом.
Второй метод также включает значение реестра, но оно влияет только на именованный процесс. Это значение реестра также является строковым значением "GlobalFlag", но в разделе реестра "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<имя файла>". "<Имя файла>" должно быть заменено именем исполняемого файла (не DLL), к которому будут применены флаги при выполнении файла. Как и выше, значение здесь помещается в поле NtGlobalFlag, хотя оно может быть изменено позже в Windows. Значение, установленное с помощью этого метода, объединяется со значением, которое применяется ко всем процессам, если таковые имеются.
Третий метод изменения значения основан на двух полях в Таблице Конфигурации Загрузки. В одном поле (GlobalFlagsClear) перечислены флаги для очистки, а в другом поле (GlobalFlagsSet) перечислены флаги, которые необходимо установить. Эти параметры применяются после того, как значения реестра GlobalFlag были применены, поэтому они могут переопределять значения, указанные в значениях реестра GlobalFlag. Однако они не могут переопределять значения, которые Windows устанавливает, когда определенные флаги остаются установленными (хотя они могут удалять флаги, которые устанавливаются, когда отладчик создает процесс). Например, установка флага FLG_USER_STACK_TRACE_DB (0x1000) заставляет Windows установить флаг FLG_HEAP_VALIDATE_PARAMETERS (0x40). Если флаг FLG_USER_STACK_TRACE_DB установлен в одном из значений реестра GlobalFlag, то даже если флаг FLG_HEAP_VALIDATE_PARAMETERS помечен для очистки в Таблице Конфигурации Загрузки, он все равно будет установлен Windows позже во время загрузки процесса.
Четвертый метод относится к изменениям, которые вносит Windows, когда отладчик создает процесс. При установке переменной среды "_NO_DEBUG_HEAP" три флага кучи не будут установлены в поле NtGlobalFlag из-за отладчика. Конечно, они все еще могут быть установлены значениями реестра GlobalFlag или полем GlobalFlagsSet в Таблице Конфигурации Загрузки.
2. Флаги кучи
Куча содержит два флага, которые инициализируются вместе с NtGlobalFlag. Значения в этих полях зависят от наличия отладчика, но также зависят от версии Windows. Расположение этих полей зависит от версии Windows. Два поля называются "Flags" и "ForceFlags". Поле "Flags" существует по смещению 0x0C в куче в 32-разрядных версиях Windows NT, Windows 2000 и Windows XP; и по смещению 0x40 в 32-разрядных версиях Windows Vista и более поздних. Поле "Flags" существует по смещению 0x14 в куче в 64-разрядных версиях Windows XP и по смещению 0x70 в куче в 64-разрядных версиях Windows Vista и более поздних версий. Поле ForceFlags существует по смещению 0x10 в куче в 32-разрядных версиях Windows NT, Windows 2000 и Windows XP; и по смещению 0x44 в 32-разрядных версиях Windows Vista и более поздних. Поле ForceFlags существует по смещению 0x18 в куче в 64-разрядных версиях Windows XP и по смещению 0x74 в куче в 64-разрядных версиях Windows Vista и более поздних версий.
Значение для поля Flags обычно устанавливается в HEAP_GROWABLE (2) во всех версиях Windows. Значение для поля ForceFlags обычно устанавливается равным нулю во всех версиях Windows. Однако оба эти значения зависят от версии подсистемы хост-процесса для 32-битного процесса (у 64-битного процесса такой зависимости нет). Значения полей соответствуют указанным, только если версия подсистемы равна 3.51 или выше. Если версия подсистемы 3.10-3.50, тогда флаг HEAP_CREATE_ALIGN_16 (0x10000) также будет установлен в обоих полях. Если версия подсистемы меньше 3.10, файл не будет работать вообще. Это особенно интересно, потому что общепринятым методом является размещение двух и нулевых значений в соответствующих полях, чтобы скрыть присутствие отладчика. Однако, если версия подсистемы не проверена, то это действие может выявить присутствие чего-либо, что пытается скрыть отладчик.
При наличии отладчика в поле Flags обычно устанавливается комбинация этих флагов в Windows NT, Windows 2000 и 32-разрядной Windows XP:
HEAP_GROWABLE (2)
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_SKIP_VALIDATION_CHECKS (0x10000000)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
В 64-разрядных версиях Windows XP и Windows Vista и более поздних версиях поле Flags обычно устанавливается в комбинацию следующих флагов:
HEAP_GROWABLE (2)
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
Когда присутствует отладчик, поле ForceFlags обычно устанавливается в комбинацию этих флагов:
HEAP_TAIL_CHECKING_ENABLED (0x20)
HEAP_FREE_CHECKING_ENABLED (0x40)
HEAP_VALIDATE_PARAMETERS_ENABLED (0x40000000)
Флаг HEAP_TAIL_CHECKING_ENABLED устанавливается в полях кучи, если в поле NtGlobalFlag установлен флаг FLG_HEAP_ENABLE_TAIL_CHECK. Флаг HEAP_FREE_CHECKING_ENABLED устанавливается в полях кучи, если в поле NtGlobalFlag установлен флаг FLG_HEAP_ENABLE_FREE_CHECK. Флаг HEAP_VALIDATE_PARAMETERS_ENABLED (и флаг HEAP_CREATE_ALIGN_16 (0x10000) в Windows NT и Windows 2000) устанавливается в полях кучи, если в поле NtGlobalFlag установлен флаг FLG_HEAP_VALIDATE_PARAMETERS.
Это поведение можно предотвратить в Windows XP и более поздних версиях, в результате чего вместо этого будут использоваться значения по умолчанию, создав переменную среды "_NO_DEBUG_HEAP".
Флаги кучи также могут управляться для каждого отдельного процесса через строковое значение "PageHeapFlags" раздела реестра "HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<filename>".
Расположение кучи можно получить несколькими способами. Одним из способов является использование функции Get32cessHeap() для kernel32. Это эквивалентно использованию этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov eax, fs:[30h] ;Process Environment Block
mov eax, [eax+18h] ;get process heap base
или используя этот 64-битный код для проверки 64-битной Среда Windows:
push 60h
pop rsi
gs:lodsq ;Process Environment Block
mov eax, [rax+30h] ;get process heap base
Как и в случае с Блоком Окружения, для 32-разрядного процесса в 64-разрядных версиях Windows существует отдельная куча для 32-разрядной и 64-разрядной частей. Поля в 64-битной части затрагиваются так же, как и для 32-битной части.
Таким образом, существует еще одна проверка, которая использует этот 32-разрядный код для проверки 64-разрядной среды Windows:
mov eax, fs:[30h] ;Process Environment Block
;64-bit Process Environment Block
;follows 32-bit Process Environment Block
mov eax, [eax+1030h] ;get process heap base
Другой способ заключается в использовании функции Get32cessHeaps() kernel32. Эта функция просто пересылается в функцию ntdll RtlGetProcessHeaps(). Функция возвращает массив куч процесса. Первая куча в списке такая же, как и та, которую возвращает функция kernel32 GetProcessHeap(). Запрос также может быть выполнен с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
push 30h
pop esi
fs:lodsd ;Process Environment Block
;get process heaps list base
mov esi, [esi+eax+5ch]
lodsd
или использовать этот 64-битный код для проверки 64-битной среды Windows:
push 60h
pop rsi
gs:lodsq ;Process Environment Block
;get process heaps list base
mov esi, [rsi*2+rax+20h]
lodsd
или используя этот 32-битный код для проверки 64-битной среды Windows:
mov eax, fs:[30h] ;Process Environment Block
;64-bit Process Environment Block
;follows 32-bit Process Environment Block
mov esi, [eax+10f0h] ;get process heaps list base
lodsd
Таким образом, способ обнаружения присутствия отладчика состоит в проверке специальной комбинации флагов в поле Flags. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы находится (или может быть) в диапазоне 3.10-3.50:
call GetVersion
cmp al, 6
cmc
sbb ebx, ebx
and ebx, 34h
mov eax, fs:[30h] ;Process Environment Block
mov eax, [eax+18h] ;get process heap base
mov eax, [eax+ebx+0ch] ;Flags
;neither HEAP_CREATE_ALIGN_16
;nor HEAP_SKIP_VALIDATION_CHECKS
and eax, 0effeffffh
;HEAP_GROWABLE
;+ HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp eax, 40000062h
je being_debugged
или этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы составляет 3,51 или выше:
call GetVersion
cmp al, 6
cmc
sbb ebx, ebx
and ebx, 34h
mov eax, fs:[30h] ;Process Environment Block
mov eax, [eax+18h] ;get process heap base
mov eax, [eax+ebx+0ch] ;Flags
;not HEAP_SKIP_VALIDATION_CHECKS
bswap eax
and al, 0efh
;HEAP_GROWABLE
;+ HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
;reversed by bswap
cmp eax, 62000040h
je being_debugged
или использовать этот 64-битный код для проверки 64-битной среды Windows:
push 60h
pop rsi
gs:lodsq ;Process Environment Block
mov ebx, [rax+30h] ;get process heap base
call GetVersion
cmp al, 6
sbb rax, rax
and al, 0a4h
;HEAP_GROWABLE
;+ HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp d [rbx+rax+70h], 40000062h ;Flags
je being_debugged
или используя этот 32-битный код для проверки 64-битной среды Windows:
push 30h
pop eax
mov ebx, fs:[eax] ;Process Environment Block
;64-bit Process Environment Block
;follows 32-bit Process Environment Block
mov ah, 10h
mov ebx, [ebx+eax] ;get process heap base
call GetVersion
cmp al, 6
sbb eax, eax
and al, 0a4h
;Flags
;HEAP_GROWABLE
;+ HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp [ebx+eax+70h], 40000062h
je being_debugged
Вызов функции GetVersion() из kernel32 может быть еще более обфусцированн путем простого извлечения значения непосредственно из поля NtMajorVersion в структуре KUSER_SHARED_DATA со смещением 0x7ffe026c для конфигураций в 2 ГБ пространства пользователя. Это значение доступно во всех 32-разрядных и 64-разрядных версиях Windows.
Другой способ обнаружить присутствие отладчика - проверить наличие специальной комбинации флагов в поле ForceFlags. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы находится (или может быть) в диапазоне 3.10-3.50:
call GetVersion
cmp al, 6
cmc
sbb ebx, ebx
and ebx, 34h
mov eax, fs:[30h] ;Process Environment Block
mov eax, [eax+18h] ;get process heap base
mov eax, [eax+ebx+10h] ;ForceFlags
;not HEAP_CREATE_ALIGN_16
btr eax, 10h
;HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp eax, 40000060h
je being_debugged
или использовать этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows, если версия подсистемы составляет 3,51 или выше:
call GetVersion
cmp al, 6
cmc
sbb ebx, ebx
and ebx, 34h
mov eax, fs:[30h] ;Process Environment Block
mov eax, [eax+18h] ;get process heap base
;ForceFlags
;HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp [eax+ebx+10h], 40000060h
je being_debugged
или использовать этот 64-битный код для проверки 64-битной среды Windows:
push 60h
pop rsi
gs:lodsq ;Process Environment Block
mov ebx, [rax+30h] ;get process heap base
call GetVersion
cmp al, 6
sbb rax, rax
and al, 0a4h
;ForceFlags
;HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp d [rbx+rax+74h], 40000060h
je being_debugged
или используя этот 32-битный код для проверки 64-битной среды Windows:
call GetVersion
cmp al, 6
push 30h
pop eax
mov ebx, fs:[eax] ;Process Environment Block
;64-bit Process Environment Block
;follows 32-bit Process Environment Block
mov ah, 10h
mov ebx, [ebx+eax] ;get process heap base
sbb eax, eax
and al, 0a4h
;ForceFlags
;HEAP_TAIL_CHECKING_ENABLED
;+ HEAP_FREE_CHECKING_ENABLED
;+ HEAP_VALIDATE_PARAMETERS_ENABLED
cmp [ebx+eax+74h], 40000060h
je being_debugged
3. Куча
Когда куча инициализируется, проверяются флаги кучи, и в зависимости от того, какие флаги установлены, в среду могут вноситься дополнительные изменения. Если установлен флаг EAP_TAIL_CHECKING_ENABLED, то последовательность 0xABABABAB будет добавлена дважды в 32-разрядной среде Windows (четыре раза в 64-разрядной среде Windows) в конце выделенного блока. Если установлен флаг HEAP_FREE_CHECKING_ENABLED, то последовательность 0xFEEEFEEE (или ее часть) будет добавлена, если для заполнения свободного пространства до следующего блока требуются дополнительные байты. Таким образом, способ обнаружить присутствие отладчика - проверить эти значения. Если указатель кучи известен, то проверку можно выполнить путем непосредственного изучения данных кучи. Однако Windows Vista и более поздние версии используют защиту кучи как на 32-разрядной, так и на 64-разрядной платформах, с введением ключа XOR для кодирования размера блока. Использование этого ключа необязательно, но по умолчанию оно используется. Расположение служебного поля также отличается в Windows NT / 2000/XP и Windows Vista и более поздних версиях. Следовательно, версия Windows должна быть принята во внимание. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
или использовать этот 64-битный код для проверки 64-битной среды Windows:xor ebx, ebx
call GetVersion
cmp al, 6
sbb ebp, ebp
jb l1
;Process Environment Block
mov eax, fs:[ebx+30h]
mov eax, [eax+18h] ;get process heap base
mov ecx, [eax+24h] ;check for protected heap
jecxz l1
mov ecx, [ecx]
test [eax+4ch], ecx
cmovne ebx, [eax+50h] ;conditionally get heap key
l1: mov eax, <heap ptr>
movzx edx, w [eax-8] ;size
xor dx, bx
movzx ecx, b [eax+ebp-1] ;overhead
sub eax, ecx
lea edi, [edx*8+eax]
mov al, 0abh
mov cl, 8
repe scasb
je being_debugged
xor ebx, ebx
call GetVersion
cmp al, 6
sbb rbp, rbp
jb l1
;Process Environment Block
mov rax, gs:[rbx+60h]
mov eax, [rax+30h] ;get process heap base
mov ecx, [rax+40h] ;check for protected heap
jrcxz l1
mov ecx, [rcx+8]
test [rax+7ch], ecx
cmovne ebx, [rax+88h] ;conditionally get heap key
l1: mov eax, <heap ptr>
movzx edx, w [rax-8] ;size
xor dx, bx
add edx, edx
movzx ecx, b [rax+rbp-1] ;overhead
sub eax, ecx
lea edi, [rdx*8+rax]
mov al, 0abh
mov cl, 10h
repe scasb
je being_debugged
Не существует эквивалента для 32-разрядного кода для проверки 64-разрядной среды Windows, поскольку 64-разрядная куча не может быть проанализирована 32-разрядной функцией кучи.
Если указатель неизвестен, его можно получить с помощью функции kernel32 HeapWalk() или функции ntdll RtlWalkHeap() (или даже функции kernel32 GetCommandLine()). Возвращенное значение размера блока декодируется автоматически, поэтому версия Windows больше не имеет значения в этом случае. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov ebx, offset l2
;get a pointer to a heap block
l1: push ebx
mov eax, fs:[30h] ;Process Environment Block
push d [eax+18h] ;save process heap base
call HeapWalk
cmp w [ebx+0ah], 4 ;find allocated block
jne l1
mov edi, [ebx] ;data pointer
add edi, [ebx+4] ;data size
mov al, 0abh
push 8
pop ecx
repe scasb
je being_debugged
...
l2: db 1ch dup (0) ;sizeof(PROCESS_HEAP_ENTRY)
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rbx, offset l2
;get a pointer to a heap block
l1: push rbx
pop rdx
push 60h
pop rsi
gs:lodsq ;Process Environment Block
;get a pointer to process heap base
mov ecx, [rax+30h]
call HeapWalk
cmp w [rbx+0eh], 4 ;find allocated block
jne l1
mov edi, [rbx] ;data pointer
add edi, [rbx+8] ;data size
mov al, 0abh
push 10h
pop rcx
repe scasb
je being_debugged
...
l2: db 28h dup (0) ;sizeof(PROCESS_HEAP_ENTRY)
Не существует эквивалента для 32-разрядного кода для проверки 64-разрядной среды Windows, поскольку 64-разрядная куча не может быть проанализирована 32-разрядной функцией кучи.
4. TLS
Локальное хранилище потоков является одним из самых интересных методов борьбы с отладкой, которые существуют, потому что, несмотря на то, что они известны уже более десяти лет, все еще открываются новые способы их использования (и злоупотребления). Локальное хранилище потока существует для инициализации данных, специфичных для потока, до запуска этого потока. Поскольку каждый процесс содержит хотя бы один поток, это поведение включает в себя возможность инициализировать данные до запуска основного потока. Инициализация может быть выполнена путем указания статического буфера, который копируется в динамически распределенную память, и/или посредством выполнения кода в массиве обратных вызовов для динамической инициализации содержимого памяти. Чаще всего злоупотребляют массивом обратного вызова.
Массив обратных вызовов TLS может быть изменен (более поздние записи могут быть изменены) и/или расширен (новые записи могут быть добавлены) во время выполнения. Вновь добавленные или измененные обратные вызовы будут вызываться с использованием новых адресов. Нет ограничений на количество обратных вызовов, которые могут быть размещены. Расширение может быть сделано с помощью этого кода (идентичного для 32-битной и 64-битной) в 32-битной или 64-битной версии Windows:
l1: mov d [offset cbEnd], offset l2
ret
l2: ...
Обратный вызов в l2 будет вызван, когда обратный вызов вернется в l1.
Адреса обратного вызова локального хранилища потоков могут указывать за пределы образа, например, на вновь загруженные библиотеки DLL. Это можно сделать косвенным путем, загрузив DLL и поместив возвращенный адрес в массив обратных вызовов TLS. Это также можно сделать напрямую, если известен адрес загрузки DLL. Значение imagebase может использоваться в качестве адреса обратного вызова, если DLL структурирована таким образом, чтобы победить DEP, если оно включено, или может быть получен и использован действительный адрес экспорта. Вызов может быть выполнен с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
l1: push offset l2
call LoadLibraryA
mov [offset cbEnd], eax
ret
l2: db "myfile", 0
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
mov rcx, offset l2
call LoadLibraryA
mov [offset cbEnd], rax
ret
l2: db "myfile", 0
В этом случае заголовок "MZ" файла с именем "tls2.dll" будет выполнен, когда обратный вызов вернется на l1. Кроме того, файл может ссылаться на себя, используя этот 32-разрядный код в 32-разрядной или 64-разрядной версии Windows:
l1: push 0
call GetModuleHandleA
mov [offset cbEnd], eax
ret
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
l1: xor ecx, ecx
call GetModuleHandleA
mov [offset cbEnd], rax
ret
В этом случае заголовок "MZ" текущего процесса будет выполнен, когда обратный вызов вернется в l1 . Адреса обратного вызова локального хранилища потока могут содержать RVA импортированных адресов из других библиотек DLL, если таблица адресов импорта изменяется, чтобы указывать на массив обратного вызова. Импорт разрешается до вызова обратных вызовов, поэтому импортированные функции будут вызываться нормально при достижении записи массива обратных вызовов.
Обратные вызовы TLS получают три параметра стека, которые могут быть переданы непосредственно в функции. Первый параметр - это ImageBase хост-процесса. Он может быть использован функцией kernel32 LoadLibrary() или функцией kernel32 WinExec(), например. Параметр ImageBase будет интерпретироваться функциями ядра LoadLibrary() или ядра WinExec() как указатель на имя файла для загрузки или выполнения. Создав файл с именем "MZ [некоторая строка]", где "[некоторая строка]" соответствует содержимому заголовка файла хоста, обратный вызов TLS получит доступ к файлу без какой-либо явной ссылки. Конечно, часть строки "MZ" также может быть заменена вручную во время выполнения, но многие функции полагаются непредсказуемыми.
Обратные вызовы локального хранилища потоков вызываются всякий раз, когда поток создается или уничтожается (если только процесс не вызывает kernel32 DisableThreadLibraryCalls() или функции ntdll LdrDisableThreadCalloutsForDll()). Это включает в себя поток, который создается Windows, когда отладчик подключается к процессу. Поток отладчика является особенным в том смысле, что его точка входа не указывает внутри образа. Вместо этого он указывает внутри kernel32.dll. Таким образом, простой метод обнаружения отладчика заключается в использовании обратного вызова TLS для запроса начального адреса каждого потока, который создается. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
push eax
mov eax, esp
push 0
push 4
push eax
push eax
mov eax, esp
push 0
push 4
push eax
или использовать этот 64-битный код для проверки 64-битной среды Windows:
xor ebp, ebp
enter 20h, 0
push 4
pop r9
push rbp
pop r8
;ThreadQuerySetWin32StartAddress
push 9
pop rdx
push -2 ;GetCurrentThread()
pop rcx
call NtQueryInformationThread
leave
cmp rbp, offset l1
jnb being_debugged
...
l1: <code end>
Поскольку обратные вызовы локального хранилища выполняются до того, как отладчик может получить контроль, обратный вызов может вносить другие изменения, такие как удаление точки останова, которая обычно размещается в точке входа . Патч можно сделать с помощью этого кода (идентичного для 32-битной и 64-битной) в 32-битной или 64-битной версии Windows:
;<val> is byte at l1
mov b [offset l1], <val>
ret
l1: <host entrypoint>
Защита от этой техники очень проста и все более необходима. Это вопрос вставки точки останова в первый байт первого обратного вызова локального хранилища потока, а не в точку входа хоста. Это позволит отладчику получить контроль перед тем, как в процессе может выполняться любой код (конечно, за исключением загруженных DLL). Однако необходимо соблюдать осторожность в отношении адреса обратного вызова, поскольку, как отмечено выше, исходное значение по этому адресу может быть RVA импортируемой функции. Таким образом, адрес не может быть прочитан из файла. Это должно быть прочитано из памяти изображения.
Выполнение обратных вызовов TLS также зависит от платформы. Если исполняемый файл импортируется только из ntdll.dll или kernel32.dll, то обратные вызовы не будут вызываться во время события "on attach" при запуске в Windows XP и более поздних версиях. Когда процесс запускается, функция ntdll LdrInitializeThunk() обрабатывает список InLoadOrderModuleList. Список nLoadOrderModuleList содержит список библиотек DLL для обработки. Значение Flags в ссылочной структуре должно иметь бит LDRP_ENTRY_PROCESSED, очищенный хотя бы в одной DLL, для обратных вызовов TLS, вызываемых при присоединении.
Этот бит всегда устанавливается для ntdll.dll, поэтому при импорте файла из только ntdll.dll не будет выполняться обратный вызов локального хранилища потока при присоединении. В Windows 2000 и более ранних версиях была ошибка сбоя, если файл не импортировался из kernel32.dll, явно (то есть импортируется из kernel32.dll напрямую) или неявно (то есть импортировался из DLL, которая импортируется из kernel32.dll; или DLL, которая импортирует из DLL, которая импортирует из kernel32.dll, независимо от длины цепочки).
Эта ошибка была исправлена в Windows XP, заставляя ntdll.dll явно загружать kernel32.dll перед обработкой таблицы импорта хоста. Когда kernel32.dll загружен, он добавляется в InLoadOrderModuleList. Проблема в том, что это исправление внесло побочный эффект.
Побочный эффект возникает, когда ntdll.dll извлекает адрес экспортированной функции из kernel32.dll через функцию ntdll LdrGetProcedureAddressEx(). Побочный эффект может быть вызван в результате получения любой экспортируемой функции, но он запускается в данном конкретном случае, когда ntdll извлекает адрес одной из следующих функций: BaseProcessInitPostImport() (только для Windows XP и Windows Server 2003), BaseQueryModuleData () (Только для Windows XP и Windows Server 2003, если функция BaseProcessInitPostImport() не существует), BaseThreadInitThunk() (Windows Vista и более поздние версии) или BaseQueryModuleData() (Windows Vista и более поздние версии, если BaseThreadInitThunk() не существует ).
Побочным эффектом является то, что функция ntdll LdrGetProcedureAddressEx() устанавливает флаг LDRP_ENTRY_PROCESSED для записи kernel32.dll в списке InLoadOrderModuleList. В результате при импорте файла, импортируемого только из kernel32.dll, обратные вызовы локального хранилища потоков не выполняются. Это можно считать ошибкой в Windows.
Существует простой обходной путь для этой проблемы: импортировать что-либо из другой библиотеки DLL и при условии, что у библиотеки DLL есть ненулевая точка входа. Затем обратные вызовы TLS будут выполняться при присоединении. Обходной путь работает, потому что значение поля Flags будет иметь бит LDRP_ENTRY_PROCESSED, очищенный для этой DLL.
В Windows Vista и более поздних версиях динамически загружаемые библиотеки DLL также поддерживают локальное хранилище потоков. Оно находится в прямом противоречии с существующей документацией формата Portable Executable, в которой говорится, что "Статически объявленные объекты данных TLS", то есть обратные вызовы TLS, могут использоваться только в статически загружаемых файлах образа. Этот факт делает ненадежным использование статических данных локального хранилища потоков в DLL, если вы не знаете, что DLL или что-либо статически связанное с ней никогда не будет загружаться динамически с помощью функции API LoadLibrary". Кроме того, будут вызываться обратные вызовы TLS независимо от того, что присутствует в таблице импорта. Таким образом, DLL может импортировать из ntdll.dll или kernel32.dll или даже вообще без DLL (в отличие от случая .exe, описанного выше), и обратные вызовы будут вызваны!
5.Anti-Step-Over
Большинство отладчиков поддерживают пошаговое выполнение определенных инструкций, таких как последовательности "call" и "rep". В таких случаях программная точка останова часто помещается в поток команд, и затем процессу разрешается возобновить выполнение. Обычно отладчик снова получает управление, когда достигается программная точка останова. Однако в случае последовательности "rep" отладчик должен проверить, что инструкция, следующая за префиксом rep, действительно является инструкцией, к которой rep применяется по закону. Некоторые отладчики предполагают, что любой префикс rep предшествует строковой инструкции. Это создает уязвимость, когда инструкция, следующая за префиксом rep, является полностью другой инструкцией. В частности, проблема возникает, если эта инструкция удаляет программную точку останова, которая была бы помещена в поток, если команда перешагнула. В этом случае, когда инструкция перешагивает, а программная точка останова удаляется командой, выполнение возобновляется под полным контролем процесса и никогда не возвращается к отладчику. Пример кода выглядит так:
rep
l1: mov b [offset l1], 90h
l2: nop
Если попытка перешагнуть на l1, то выполнение будет возобновлено свободно с l2.
Более общий метод использует строковые инструкции для удаления точки останова. Патч можно сделать с помощью этого 32-битного кода в 32-битной или 64-битной версии Windows:
mov al, 90h
xor ecx, ecx
inc ecx
mov edi, offset l1
rep stosb
l1: nop
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
mov al, 90h
xor ecx, ecx
inc ecx
mov rdi, offset l1
rep stosb
l1: nop
Существуют варианты этой техники, например, использование "rep movs" вместо "rep stos". Флаг направления может использоваться для изменения направления записи в память, так что перезапись может быть пропущена. Патч можно сделать с помощью этого 32-битного кода в 32-битной или 64-битной версии Windows:
mov al, 90h
push 2
pop ecx
mov edi, offset l1
std
rep stosb
nop
l1: nop
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
mov al, 90h
push 2
pop rcx
mov rdi, offset l1
std
rep stosb
nop
l1: nop
Решением этой проблемы является использование аппаратных точек останова во время перехода по строковым инструкциям. Это особенно важно, если учесть, что отладчик не может знать, является ли установленная им точка останова выполненной. Если процесс удаляет точку останова, он может впоследствии восстановить точку останова и затем выполнить точку останова, как обычно. Отладчик увидит ожидаемое исключение точки останова и будет вести себя как обычно. Это может быть выполнено с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
mov al, 90h
l1: xor ecx, ecx
inc ecx
mov edi, offset l3
l2: rep stosb
l3: nop
cmp al, 0cch
l4: mov al, 0cch
jne l1
l5: ...
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
mov al, 90h
l1: xor ecx, ecx
inc ecx
mov rdi, offset l3
l2: rep stosb
l3: nop
cmp al, 0cch
l4: mov al, 0cch
jne l1
l5: ...
В этом примере, перешагнув инструкцию на l2, код достигнет l4, а затем вернется к l1. Это приведет к замене точки останова на l2 на втором проходе и выполнению на l3. Затем отладчик восстановит управление. В то время единственное очевидное отличие будет состоять в том, что регистр AL будет содержать значение 0xCC вместо ожидаемого 0x90. Это позволит достичь уровня l5 за один проход вместо двух. Конечно, возможны гораздо более тонкие варианты, включая выполнение совершенно разных путей кода.
Разновидность техники может использоваться для простого обнаружения присутствия отладчика. Проверка может быть выполнена с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
xor ecx, ecx
inc ecx
mov esi, offset l1
lea edi, [esi + 1]
rep movsb
l1: mov al, 90h
l2: cmp al, 0cch
je being_debugged
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
xor ecx, ecx
inc ecx
mov rsi, offset l1
lea rdi, [esi + 1]
rep movsb
l1: mov al, 90h
l2: cmp al, 0cch
je being_debugged
Этот код обнаружит точку останова, которая находится на l1. Он работает путем копирования значения в l1 через 90h в l1 + 1. Затем значение сравнивается на l2.
6.Аппаратура
A. Аппаратные точки останова
Когда возникает исключение, Windows создает контекстную структуру для передачи обработчику исключения. Структура будет содержать значения общих регистров, селекторов, управляющих регистров и регистров отладки. Если отладчик присутствует и передает исключение отладчику с использованием аппаратных точек останова, то регистры отладки будут содержать значения, которые показывают наличие отладчика. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
xor eax, eax
push offset l1
push d fs:[eax]
mov fs:[eax], esp
int 3 ;force an exception to occur
...
l1: ;execution resumes here when exception occurs
mov eax, [esp+0ch] ;get ContextRecord
mov ecx, [eax+4] ;Dr0
or ecx, [eax+8] ;Dr1
or ecx, [eax+0ch] ;Dr2
or ecx, [eax+10h] ;Dr3
jne being_debugged
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rdx, offset l1
xor ecx, ecx
inc ecx
call AddVectoredExceptionHandler
int 3 ;force an exception to occur
...
l1: ;execution resumes here when exception occurs
mov rax, [rcx+8] ;get ContextRecord
mov rcx, [rax+48h] ;Dr0
or rcx, [rax+50h] ;Dr1
or rcx, [rax+58h] ;Dr2
or rcx, [rax+60h] ;Dr3
jne being_debugged
Значения для регистров отладки также могут быть изменены до возобновления выполнения в 32-разрядных версиях Windows, что может привести к неконтролируемому выполнению, если программная точка останова не установлена в соответствующем месте.
B.Подсчет инструкций
Подсчет команд можно выполнить, зарегистрировав обработчик исключений, а затем установив аппаратные точки останова на определенных адресах. При нажатии на соответствующий адрес будет сгенерировано исключение EXCEPTION_SINGLE_STEP (0x80000004). Это исключение будет передано в обработчик исключений. Обработчик исключений может выбрать настройку указателя инструкции для указания новой инструкции, опционально установить дополнительные аппаратные точки останова на определенных адресах и затем возобновить выполнение. Для установки точек останова необходим доступ к контекстной структуре. Копию контекстной структуры можно получить, вызвав функцию kernel32 GetThreadContext(), которая позволяет при необходимости установить начальные значения для аппаратных точек останова. Впоследствии, когда возникает исключение, обработчик исключений автоматически получает копию структуры контекста. Отладчик будет мешать пошаговому выполнению, что приведет к другому количеству команд по сравнению с отсутствием отладчика. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
xor eax, eax
push offset l5
push d fs:[eax]
mov fs:[eax], esp
int 3 ;force exception to occur
l1: nop
l2: nop
l3: nop
l4: nop
cmp al, 4
jne being_debugged
...
l5: push edi
mov eax, [esp+8] ;ExceptionRecord
mov edi, [esp+10h] ;ContextRecord
push 55h ;local-enable DR0, DR1, DR2, DR3
pop ecx
inc d [ecx*2+edi+0eh] ;Eip
mov eax, [eax] ;ExceptionCode
sub eax, 80000003h ;EXCEPTION_BREAKPOINT
jne l6
mov eax, offset l1
scasd
stosd ;Dr0
inc eax ;l2
stosd ;Dr1
inc eax ;l2
stosd ;Dr2
inc eax ;l4
stosd ;Dr3
;local-enable breakpoints
;for compatibility with old CPUs
mov ch, 1
xchg ecx, eax
scasd
stosd ;Dr7
xor eax, eax
pop edi
ret
l6: dec eax ;EXCEPTION_SINGLE_STEP
jne being_debugged
inc b [ecx*2+edi+6] ;Eax
pop edi
ret
Поскольку в этом методе используется структурированный обработчик исключений, его нельзя использовать в 64-разрядных версиях Windows. Код можно легко переписать, чтобы вместо него использовать Vectored Exception Handler. Это требует создания потока и изменения его контекста, потому что регистры отладки не могут быть назначены внутри обработчика исключений с векторным управлением в 64-разрядных версиях Windows. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версии Windows XP или более поздней версии:
xor ebx, ebx
push eax
push esp
push 4 ;CREATE_SUSPENDED
push ebx
push offset l1
push ebx
push ebx
call CreateThread
mov esi, offset l7
push esi
push eax
xchg ebp, eax
call GetThreadContext
mov eax, offset l2
lea edi, [esi+4]
stosd ;Dr0
inc eax
stosd ;Dr1
inc eax
stosd ;Dr2
inc eax
stosd ;Dr3
scasd
push 55h ;local-enable DR0, DR1, DR2, DR3
pop eax
stosd ;Dr7
push esi
push ebp
call SetThreadContext
push offset l6
push 1
call AddVectoredExceptionHandler
push ebp
call ResumeThread
jmp $
l1: xor eax, eax
l2: nop
l3: nop
l4: nop
l5: nop
cmp al, 4
jne being_debugged
...
l6: mov eax, [esp+4]
mov ecx, [eax] ;ExceptionRecord
;ExceptionCode
cmp [ecx], 80000004h ;EXCEPTION_SINGLE_STEP
jne being_debugged
mov eax, [eax+4] ;ContextRecord
cdq
mov dh, 1
inc b [eax+edx-50h] ;Eax
inc d [eax+edx-48h] ;Eip
or eax, -1 ;EXCEPTION_CONTINUE_EXECUTION
ret
l7: dd 10002h ;CONTEXT_i486+CONTEXT_INTEGER
db 0b0h dup (?)
или этот 64-битный код для проверки 64-битной среды Windows:
push rax
push rsp
push 4 ;CREATE_SUSPENDED
sub esp, 20h
xor r9d, r9d
mov r8, offset l1
xor edx, edx
xor ecx, ecx
call CreateThread
mov ebp, eax
mov rsi, offset l7-30h
push rsi
pop rdx
xchg ecx, eax
call GetThreadContext
mov rax, offset l2
lea rdi, [rsi+48h]
stosq ;Dr0
inc rax
stosq ;Dr1
inc rax
stosq ;Dr2
inc rax
stosq ;Dr3
scasq
push 55h ;local-enable DR0, DR1, DR2, DR3
pop rax
stosd ;Dr7
push rsi
pop rdx
mov ecx, ebp
call SetThreadContext
mov rdx, offset l6
xor ecx, ecx
inc ecx
call AddVectoredExceptionHandler
mov ecx, ebp
call ResumeThread
jmp $
l1: xor eax, eax
l2: nop
l3: nop
l4: nop
l5: nop
cmp al, 4
jne being_debugged
...
l6: mov rax, [rcx] ;ExceptionRecord
;ExceptionCode
cmp d [rax], 80000004h ;EXCEPTION_SINGLE_STEP
jne being_debugged
mov rax, [rcx+8] ;ContextRecord
inc b [rax+78h] ;Eax
inc q [rax+0f8h] ;Eip
or eax, -1 ;EXCEPTION_CONTINUE_EXECUTION
ret
l7: dd 10002h ;CONTEXT_i486+CONTEXT_INTEGER
db 0c4h dup (?)
C.Прерывание #3
Всякий раз, когда возникает исключение программного прерывания, адрес исключения и значение регистра EIP будут указывать на инструкцию после той, которая вызвала исключение. Исключение точки останова рассматривается как особый случай. Когда возникает исключение EXCEPTION_BREAKPOINT (0x80000003), Windows предполагает, что оно было вызвано однобайтовым кодом операции "CC" (инструкция "INT 3"). Windows уменьшает адрес исключения, указывая на предполагаемый код операции «CC», а затем передает исключение обработчику исключений. На значение регистра EIP это не влияет. Таким образом, если используется код операции "CD 03" (длинная инструкция "INT 03"), адрес исключения будет указывать на "03", когда обработчик исключения получает управление.
D. Прерывание 0x2d
Прерывание 0x2D является частным случаем. Когда оно выполняется, Windows использует текущее значение регистра EIP в качестве адреса исключения, а затем оно увеличивается на единицу значения регистра EIP. Однако Windows также проверяет значение в регистре EAX, чтобы определить, как настроить адрес исключения. Если регистр EAX имеет значение 1, 3 или 4 во всех версиях Windows или значение 5 в Windows Vista и более поздних версиях, то Windows увеличивает его на один адрес исключения. Наконец, он выдает исключение EXCEPTION_BREAKPOINT (0x80000003), если присутствует отладчик. Поведение прерывания 0x2D может вызвать проблемы для отладчиков. Проблема заключается в том, что некоторые отладчики могут использовать значение регистра EIP в качестве адреса для возобновления, в то время как другие отладчики могут использовать адрес исключения в качестве адреса для возобновления. Это может привести к пропуску однобайтовой инструкции или выполнению совершенно другой инструкции, поскольку первый байт отсутствует. Эти поведения могут использоваться, чтобы вывести присутствие отладчика. Проверка может быть выполнена с использованием этого кода (идентичного для 32-битной и 64-битной), чтобы проверить 32-битную или 64-битную среду Windows:
xor eax, eax ;set Z flag
int 2dh
inc eax ;debugger might skip
je being_debugged
E. Прерывание 0x41
Прерывание 0x41 может отображать другое поведение, если присутствует отладчик режима ядра или нет. Дескриптор прерывания 0x41 обычно имеет нулевой DPL, что означает, что прерывание не может быть успешно выполнено из кольца зазиты 3. Попытка выполнить это прерывание напрямую приведет к тому, что процессор выдает общую ошибку защиты (прерывание 0x0D), что в итоге приводит к исключению EXCEPTION_ACCESS_VIOLATION (0xC0000005). Однако некоторые отладчики перехватывают прерывание 0x41 и устанавливают его DPL равным 3, чтобы прерывание можно было успешно вызывать из пользовательского режима. Этот факт можно использовать для определения наличия отладчика режима ядра. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
xor eax, eax
push offset l1
push d fs:[eax]
mov fs:[eax], esp
mov al, 4fh
int 41h
jmp being_debugged
l1: ;execution resumes here if no debugger present
...
или использовать этот 64-битный код для проверки 64-битной среды Windows (хотя вряд ли он будет поддерживаться 64-битным отладчиком):
mov rdx, offset l1
xor ecx, ecx
inc ecx
call AddVectoredExceptionHandler
push 4fh
pop rax
int 41h
jmp being_debugged
l1: ;execution resumes here if no debugger present
F.MOV SS
Существует простой трюк для определения одноступенчатых операций, который работал с самых ранних процессоров Intel. Он использовался довольно часто во времена DOS, но он все еще работает во всех версиях Windows. Трюк основан на том факте, что определенные инструкции приводят к отключению всех прерываний при выполнении следующей инструкции. В частности, загрузка регистра SS очищает прерывания, чтобы позволить следующей инструкции загрузить регистр ESP без риска повреждения стека. Однако не требуется, чтобы следующая инструкция загружала что-либо в регистр ESP. Любая инструкция может следовать за загрузкой регистра SS. Если для пошагового выполнения кода используется отладчик, то в образе EFLAGS будет установлен флаг T. Обычно это не видно, поскольку флаг T будет очищен в образе EFLAGS после доставки каждого события отладчика. Однако, если флаги сохраняются в стеке до доставки события отладчика, тогда флаг T станет видимым. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
push ss
pop ss
pushfd
test b [esp+1], 1
jne being_debugged
Интересная ситуация возникает в VirtualPC при работе с Windows 2000, которая заключается в том, что инструкция CPUID ведет себя таким же образом. Неизвестно, почему это происходит.
Пример 64-битного кода отсутствует, поскольку в этой среде не поддерживается селектор SS.
7.API
Отладчик может быть обнаружен, отключен (в результате чего он теряет контроль над отладчиком), используя стандартные функции операционной системы.
A. Функции кучи
BasepFreeActivationContextActivationBlock
BasepFreeAppCompatData
ConvertFiberToThread
DeleteFiber
FindVolumeClose
FindVolumeMountPointClose
HeapFree
SortCloseHandle
Единственное, что объединяет все эти функции, это то, что они вызывают функцию ntdll RtlFreeHeap().
The kernel32
Функции BasepFreeActivationContextActivationBlock() и SortCloseHandle() kernel32 существуют только в Windows 7 и более поздних версиях. Функция kernel32 BasepFreeAppCompatData() существует только в Windows Vista и более поздних версиях. Ядро FindVolumeMountPointClose() вызывает функцию ntdll RtlFreeHeap() только как особый случай (в частности, когда параметр hFindVolumeMountPoint является допустимым указателем на дескриптор, который может быть успешно закрыт)
Однако дело в том, что функция ntdll RtlFreeHeap() содержит функцию, которая предназначена для использования вместе с отладчиком, - вызов функции ntdll DbgPrint(). Проблема заключается в том, что способ реализации функции ntdll DbgPrint() позволяет приложению обнаруживать присутствие отладчика при вызове функции.
Когда вызывается функция dtgPrint() ntdll, она вызывает исключение DBG_PRINTEXCEPTION_C (0x40010006), но исключение обрабатывается особым образом, поэтому зарегистрированный структурированный обработчик исключений не увидит его. Причина в том, что Windows внутренне регистрирует свой собственный обработчик структурированных исключений, который использует исключение, если отладчик этого не делает. Однако в Windows XP и более поздних версиях любой зарегистрированный векторный обработчик исключений будет работать до структурированного обработчика исключений, который регистрирует Windows. Это может считаться ошибкой в Windows. Присутствие отладчика, который использует исключение, теперь может быть выведено из отсутствия исключения. Кроме того, другой обработчик доставляется в обработчик исключений если отладчик присутствует, но не использует исключение, или если отладчик вообще отсутствует. Если отладчик присутствует, но не использует исключение, Windows доставит исключение DBG_PRINTEXCEPTION_C (0x40010006). Если отладчик отсутствует, Windows доставит исключение EXCEPTION_ACCESS_VIOLATION (0xC0000005). Присутствие отладчика теперь может быть определено либо отсутствием исключения, либо значением исключения.
Существует еще один случай, который применяется, среди прочего, к функциям кучи и ресурсов, когда функции могут быть принудительно вызваны прерыванием отладки. Общим для них является проверка флага BeingDebugged в Блоке Окружения Процесса. Присутствие отладчика может быть подделано, чтобы вызвать исключение прерывания 3, и исключение должно быть видимым для отладчика. Таким образом, если исключение отсутствует (потому что отладчик использовал его), то присутствие отладчика раскрывается. Проверка может быть выполнена с использованием этого 32-разрядного кода, чтобы проверить 32-разрядную среду Windows в 32-разрядной версии для 64-разрядных версий Windows:
xor eax, eax
push offset l1
push d fs:[eax]
mov fs:[eax], esp
;Process Environment Block
mov eax, fs:[eax+30h]
inc b [eax+2] ;set BeingDebugged
push offset l2
call HeapDestroy
jmp being_debugged
l1: ;execution resumes here due to exception
...
l2: db 0ch dup (0)
dd 40000000h ;HEAP_VALIDATE_PARAMETERS_ENABLED
db 30h dup (0)
dd 40000000h ;HEAP_VALIDATE_PARAMETERS_ENABLED
db 24h dup (0)
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rdx, offset l1
xor ecx, ecx
inc ecx
call AddVectoredExceptionHandler
push 60h
pop rsi
gs:lodsq ;Process Environment Block
inc b [rax+2] ;set BeingDebugged
mov rcx, offset l2
call HeapDestroy
jmp being_debugged
l1: ;execution resumes here due to exception
...
l2: db 14h dup (0)
dd 40000000h ;HEAP_VALIDATE_PARAMETERS_ENABLED
db 58h dup (0)
dd 40000000h ;HEAP_VALIDATE_PARAMETERS_ENABLED
db 30h dup (0)
Значение флага появляется дважды в каждом случае, потому что оно размещается в обоих возможных местах для поля флага, в зависимости от версии Windows. Это исключает необходимость проверки версии.
Обратите внимание, что в Windows Vista и более поздних версиях поведение изменилось незначительно. Ранее прерывание отладки было вызвано вызовом функции ntdll DbgBreakPoint(). Теперь это вызвано инструкцией прерывания 3, которая сохраняется непосредственно в потоке кода. Результат в обоих случаях одинаков.
Обнаружение также может быть немного расширено. LastErrorValue в блоке среды потока может быть установлен в ноль до вызова функции, либо напрямую, либо путем вызова функции kernel32 SetLastError(). Если исключение не произошло, то при возврате из функции значение в этом поле (также возвращаемое функцией GetLastError() ядра32) будет установлено в ERROR_INVALID_HANDLE (6).
B. Дескрипторы
1.Функция OpenProcess
Иногда утверждается, что функция kernel32 OpenProcess() (или функция ntdll NtOpenProcess()) обнаруживает присутствие отладчика при использовании в процессе "csrss.exe". Это неверно. Хотя это правда, что вызов функции будет успешным в присутствии некоторых отладчиков, это происходит из-за побочного эффекта поведения отладчика (в частности, получения привилегии отладки), а не из-за самого отладчика (это должно быть очевидно поскольку вызов функции не удается при использовании с некоторыми отладчиками). Все, что он показывает, - то, что учетная запись пользователя для процесса является членом группы администраторов и имеет привилегию отладки. Причина в том, что успех или неудача вызова функции ограничена только уровнем привилегий процесса. Если учетная запись пользователя процесса является членом группы администраторов и имеет привилегию отладки, то вызов функции будет успешным; если нет, то нет. Обычному пользователю недостаточно приобрести привилегию отладки, и администратор не может успешно вызвать функцию без нее. Идентификатор процесса csrss.exe можно получить с помощью функции ntdll CsrGetProcessId() в Windows XP и более поздних версиях (другие способы существуют для более ранних версий Windows и показаны позже в контексте поиска процесса "Explorer.exe"). ). Вызов может быть выполнен с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
call CsrGetProcessId
push eax
push 0
push 1f0fffh ;PROCESS_ALL_ACCESS
call OpenProcess
test eax, eax
jne admin_with_debug_priv
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
call CsrGetProcessId
push rax
pop r8
cdq
mov ecx, 1f0fffh ;PROCESS_ALL_ACCESS
call OpenProcess
test eax, eax
jne admin_with_debug_priv
Привилегию отладки можно получить с помощью этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
xor ebx, ebx
push 2 ;SE_PRIVILEGE_ENABLED
push ebx
push ebx
push esp
push offset l1
push ebx
call LookupPrivilegeValueA
push eax
push esp
push 20h ;TOKEN_ADJUST_PRIVILEGES
push -1 ;GetCurrentProcess()
call OpenProcessToken
pop ecx
push eax
mov eax, esp
push ebx
push ebx
push ebx
push eax
push ebx
push ecx
call AdjustTokenPrivileges
...
l1: db "SeDebugPrivilege", 0
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
xor ebx, ebx
push 2 ;SE_PRIVILEGE_ENABLED
push rbx
push rbx
mov r8d, esp
mov rdx, offset l1
xor ecx, ecx
call LookupPrivilegeValueA
push rax
mov r8d, esp
push 20h ;TOKEN_ADJUST_PRIVILEGES
pop rdx
or rcx, -1 ;GetCurrentProcess()
call OpenProcessToken
pop rcx
push rax
mov r8d, esp
push rbx
push rbx
sub esp, 20h
xor r9d, r9d
cdq
call AdjustTokenPrivileges
...
l1: db "SeDebugPrivilege", 0
2.CloseHandle
Один из хорошо известных методов обнаружения отладчика включает функция kernel32 CloseHandle(). Если недопустимый дескриптор передается в функцию ядра 32 CloseHandle() (или непосредственно в функцию ntdll NtClose(), или функцию ядра 32 FindVolumeMountPointClose() в Windows 2000 и более поздних версиях (которая просто вызывает функцию ядра 32 CloseHandle ()), и присутствует отладчик, затем будет вызвано исключение EXCEPTION_INVALID_HANDLE (0xC0000008). Это исключение может быть перехвачено обработчиком исключения, и это указывает на то, что работает отладчик. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
xor eax, eax
push offset being_debugged
push d fs:[eax]
mov fs:[eax], esp
;any illegal value will do
;must be dword-aligned
;on Windows Vista and later
push esp
call CloseHandle
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rdx, offset being_debugged
xor ecx, ecx
inc ecx
call AddVectoredExceptionHandler
;any illegal value will do
;must be dword-aligned
;on Windows Vista and later
mov ecx, esp
call CloseHandle
Однако существует второй случай, который предполагает использование защищенного дескриптора. Если защищенный дескриптор передается в функцию CloseHandle() (или непосредственно в функцию ntdll NtClose()) и присутствует отладчик, то будет вызвано исключение EXCEPTION_HANDLE_NOT_CLOSABLE (0xC0000235). Это исключение может быть перехвачено обработчиком исключения, и это указывает на то, что работает отладчик. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
xor eax, eax
push offset being_debugged
push d fs:[eax]
mov fs:[eax], esp
push eax
push eax
push 3 ;OPEN_EXISTING
push eax
push eax
push eax
push offset l1
call CreateFileA
push 2 ;HANDLE_FLAG_PROTECT_FROM_CLOSE
push -1
push eax
xchg ebx, eax
call SetHandleInformation
push ebx
call CloseHandle
...
l1: db "myfile", 0
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rdx, offset being_debugged
xor ecx, ecx
inc ecx
call AddVectoredExceptionHandler
cdq
push rdx
push rdx
push 3 ;OPEN_EXISTING
sub esp, 20h
xor r9d, r9d
xor r8d, r8d
mov rcx, offset l1
call CreateFileA
mov ebx, eax
push 2 ;HANDLE_FLAG_PROTECT_FROM_CLOSE
pop r8
or rdx, -1
xchg ecx, eax
call SetHandleInformation
mov ecx, ebx
call CloseHandle
...
l1: db "myfile", 0
Попытка победить любой из этих методов проще всего в Windows XP и более поздних версиях, где обработчик исключений FirstHandler Vectored может быть зарегистрирован отладчиком, чтобы скрыть исключение и молча возобновить выполнение. Однако существует проблема прозрачного перехвата функции AddVectoredExceptionHandler() из kernel32, чтобы предотвратить регистрацию другого обработчика в качестве первого обработчика. Недостаточно перехватить попытку зарегистрировать первый обработчик, а затем сделать его последним обработчиком. Причина в том, что это будет обнаружено путем регистрации двух обработчиков и последующего исключения, поскольку обработчики будут вызываться в неправильном порядке. Существует также потенциальная проблема, заключающаяся в том, чтобы перехватить функцию и обнаружить такой измененный запрос, а затем просто снова изменить его. Другой способ чтобы исправить это состоит в том, чтобы дизассемблировать функцию, чтобы найти базовый указатель, который содержит заголовок списка, но это зависит от платформы. Конечно, поскольку функция возвращает указатель на структуру обработчика, список можно просмотреть, зарегистрировав обработчик и затем проанализировав структуру.
Эта ситуация все еще лучше, чем проблема прозрачного перехвата функции ntdll NtClose() в Windows NT и Windows 2000, чтобы зарегистрировать обработчик структурированных исключений, чтобы скрыть исключение.
Существует флаг, который может быть установлен для создания исключительного поведения, даже если нет отладчика. Устанавливая флаг FLG_ENABLE_CLOSE_EXCEPTIONS (0x400000) в значении реестра "HKLM\System\CurrentControlSet\Control\Session Manager\GlobalFlag", а затем перезагружая, функция CloseHandle() и функция ntdll NtClose() всегда будут вызывать исключение, если в функцию передан неверный или защищенный дескриптор. Эффект является общесистемным и поддерживается во всех версиях Windows для Windows NT, как 32-разрядных, так и 64-разрядных.
Есть другой флаг, который приводит к аналогичному поведению для других функций, которые принимают дескрипторы. Устанавливая флаг FLG_APPLICATION_VERIFIER (0x100) в значении реестра "HKLM\System\CurrentControlSet\Control\Session Manager\GlobalFlag" и затем перезагружая, функция ObtoferenceObjectByHandle() ntoskrnl всегда вызывает исключение, если недопустимый дескриптор передается в функцию (такую как функция SetEvent() kernel32), которая вызывает функцию ntoskrnl ObReferenceObjectByHandle(). Эффект также общесистемный.
Обратите внимание, что один из этих флагов в настоящее время задокументирован неправильно, а другой из этих флагов в настоящее время документирован не полностью. Flag "Enable close exception" задокументированокак вызывающее исключения для недопустимых дескрипторов, которые передаются функциям, отличным от функции ntoskrnl NtClose(), но это неверно; Flag "Enable bad handles detection" вызовет исключения для недопустимых дескрипторов, которые передаются в функции, отличные от функции ntoskrnl NtClose(), но это поведение не задокументировано.
3.Функция CreateFile
Немного ненадежный способ обнаружить присутствие отладчика - попытаться открыть исключительно файл текущего процесса. Когда присутствуют некоторые отладчики, это действие всегда будет неудачным. Причина в том, что при запуске процесса для отладки открывается дескриптор файла. Это позволяет отладчику читать отладочную информацию из файла (при условии, что она присутствует). Значение дескриптора сохраняется в структуре, которая заполняется, когда происходит событие CREATE_PROCESS_DEBUG_EVENT. Если этот дескриптор не закрыт отладчиком, файл не может быть открыт для монопольного доступа. Поскольку отладчик не открыл файл, было бы легко забыть закрыть его.
Конечно, если какое-либо другое приложение (например, шестнадцатеричный редактор) проверяет файл, то открытие также завершится ошибкой по той же причине. Вот почему метод считается ненадежным, но в зависимости от намерения такие ложные срабатывания могут оказаться приемлемыми. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
push 104h ;MAX_PATH
mov ebx, offset l1
push ebx
push 0 ;self filename
call GetModuleFileNameA
cdq
push edx
push edx
push 3 ;OPEN_EXISTING
push edx
push edx
inc edx
ror edx, 1
push edx ;GENERIC_READ
push ebx
call CreateFileA
inc eax
je being_debugged
...
l1: db 104h dup (?) ;MAX_PATH
или использовать этот 64-битный код для проверки 64-битной среды Windows (но метод не работает для 64-битных процессов):
mov r8d, 104h ;MAX_PATH
mov rbx, offset l1
push rbx
pop rdx
xor ecx, ecx ;self filename
call GetModuleFileNameA
cdq
push rdx
push rdx
push 3 ;OPEN_EXISTING
sub esp, 20h
xor r9d, r9d
xor r8d, r8d
inc edx
ror edx, 1 ;GENERIC_READ
push rbx
pop rcx
call CreateFileA
inc eax
je being_debugged
...
l1: db 104h dup (?) ;MAX_PATH
Функция kernel32 CreateFile() может также использоваться для обнаружения присутствия драйверов режима ядра, которые могут принадлежать отладчику (или любому другому интересующему инструменту). Инструментам, использующим драйверы режима ядра, также необходим способ связи с этими драйверами. Очень распространенным методом является использование именованных устройств. Таким образом, при попытке открыть такое устройство любой успех указывает на присутствие драйвера. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
xor eax, eax
mov edi, offset l2
l1: push eax
push eax
push 3 ;OPEN_EXISTING
push eax
push eax
push eax
push edi
call CreateFileA
inc eax
jne being_debugged
or ecx, -1
repne scasb
cmp [edi], al
jne l1
...
l2: <array of ASCIIZ strings, zero to end>
Типичный список включает в себя следующие имена:
db "\\.\EXTREM", 0 ;Phant0m
db "\\.\FILEM", 0 ;FileMon
db "\\.\FILEVXG", 0 ;FileMon
db "\\.\ICEEXT", 0 ;SoftICE Extender
db "\\.\NDBGMSG.VXD", 0 ;SoftICE
db "\\.\NTICE", 0 ;SoftICE
db "\\.\REGSYS", 0 ;RegMon
db "\\.\REGVXG", 0 ;RegMon
db "\\.\RING0", 0 ;Olly Advanced
db "\\.\SICE", 0 ;SoftICE
db "\\.\SIWVID", 0 ;SoftICE
db "\\.\TRW", 0 ;TRW
db "\\.\SPCOMMAND", 0 ;Syser
db "\\.\SYSER", 0 ;Syser
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rdi, offset l2
l1: xor edx, edx
push rdx
push rdx
push 3 ;OPEN_EXISTING
sub esp, 20h
xor r9d, r9d
xor r8d, r8d
push rdi
pop rcx
call CreateFileA
inc eax
jne being_debugged
or ecx, -1
repne scasb
cmp [rdi], al
jne l1
...
l2: <array of ASCIIZ strings, zero to end>
Однако в настоящее время нет 64-битного списка из-за нехватки отладчиков режима ядра, которые предлагают сервисы режима пользователя.
Обратите внимание, что имя драйвера "\\.\ NTICE" действительно только для SoftICE до версии 4.0. SoftICE v4.x не создает устройства с таким именем на платформах под управлением Windows NT. Вместо этого имя устройства - "\\.\NTICExxxx"), где «xxxx» - это четыре шестнадцатеричных символа. Источником символов являются 9-й, 7-й, 5-й и 3-й символы из данных в значении реестра "Serial". Это значение появляется в нескольких местах в реестре. Драйвер SoftICE использует значение реестра "HKLM\System\CurrentContrlSet\Services\NTice\Serial". Функция DevIO_ConnectToSoftICE() использует значение реестра "HKLM\Software\NuMega\SoftIce\Serial". Алгоритм, который использует SoftICE, состоит в том, чтобы перевернуть строку, а затем, начиная с третьего символа, взять каждый второй символ для четырех символов. Конечно, для этого есть более простой способ. Имя может быть сконструировано с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядных версиях Windows (SoftICE не работает в 64-разрядных версиях Windows):
xor ebx, ebx
push eax
push esp
push 1 ;KEY_QUERY_VALUE
push ebx
push offset l2
push 80000002h ;HKLM
call RegOpenKeyExA
pop ecx
push 0dh ;sizeof(l3)
push esp
mov esi, offset l3
push esi
push eax ;REG_NONE
push eax
push offset l4
push ecx
call RegQueryValueExA
push 4
pop ecx
mov edi, offset l6
l1: mov al, [ecx*2+esi+1]
stosb
loop l1
push ebx
push ebx
push 3 ;OPEN_EXISTING
push ebx
push ebx
push ebx
push offset l5
call CreateFileA
inc eax
4. Функция LoadLibrary
Функция LoadLibrary() в kernel32 - удивительно простой и эффективный способ обнаружения отладчика. Когда файл загружается в присутствии отладчика, используя функцию LoadLibrary() kernel32 (или любой из ее вариантов - функцию LoadLibraryEx() kernel32 или функцию ntdll LdrLoadDll()), открывается дескриптор файла. Это позволяет отладчику читать отладочную информацию из файла (при условии, что она присутствует). Значение дескриптора сохраняется в структуре, которая заполняется, когда происходит событие LOAD_DLL_DEBUG_EVENT. Если этот дескриптор не закрыт отладчиком (выгрузка DLL не закроет его), файл не может быть открыт для монопольного доступа. Поскольку отладчик не открыл файл, было бы легко забыть закрыть его. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov ebx, offset l1
push ebx
call LoadLibraryA
cdq
push edx
push edx
push 3 ;OPEN_EXISTING
push edx
push edx
inc edx
ror edx, 1 ;GENERIC_READ
push edx
push ebx
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rbx, offset l1
push rbx
pop rcx
call LoadLibraryA
xor edx, edx
push rdx
push rdx
push 3 ;OPEN_EXISTING
sub esp, 20h
xor r9d, r9d
xor r8d, r8d
inc edx
ror edx, 1 ;GENERIC_READ
push rbx
pop rcx
call CreateFileA
inc eax
je being_debugged
...
l1: db "myfile.", 0
Альтернативный метод заключается в вызове другой функции, которая вызывает внутреннюю функцию CreateFile() из kernel32 (или любую ее разновидность) - функцию ntdll NtCreateFile() или функцию ntdll NtOpenFile(). Одним из примеров являются функции обновления ресурсов, такие как функция kernel32 EndUpdateResource(). Причина, по которой работает функция kernel32 EndUpdateResource(), заключается в том, что она в конечном итоге вызывает функцию kernel32 CreateFile() для записи новой таблицы ресурсов. Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov ebx, offset l1
push ebx
call LoadLibraryA
push 0
push ebx
call BeginUpdateResourceA
push 0
push eax
call EndUpdateResourceA
test eax, eax
je being_debugged
...
l1: db "myfile.", 0
или использовать этот 64-битный код для проверки 64-битной среды Windows (но метод не работает для 64-битных процессов):
mov rbx, offset l1
push rbx
pop rcx
call LoadLibraryA
xor edx, edx
push rbx
pop rcx
call BeginUpdateResourceA
cdq
xchg ecx, eax
call EndUpdateResourceA
test eax, eax
je being_debugged
...
l1: db "myfile.", 0
5. Функция ReadFile
Функция kernel32 ReadFile() может использоваться для автоматической модификации потока кода путем считывания содержимого файла в местоположение после вызова функции. Она также может использоваться для удаления программных точек останова, которые отладчик может поместить в поток кода, особенно сразу после вызова функции. Результатом в этом случае будет то, что код выполняется свободно. Вызов может быть выполнен с использованием этого 32-разрядного кода в 32-разрядной или 64-разрядной версии Windows:
push 104h ;MAX_PATH
mov ebx, offset l2
push ebx
push 0 ;self filename
call GetModuleFileNameA
cdq
push edx
push edx
push 3 ;OPEN_EXISTING
push edx
;FILE_SHARE_READ
;because a debugger might prevent
;exclusive access to the running file
inc edx
push edx
ror edx, 1
push edx ;GENERIC_READ
push ebx
call CreateFileA
push 0
push esp
push 1 ;more bytes might be more useful
push offset l1
push eax
call ReadFile
l1: int 3 ;replaced by "M" from the MZ header
...
l2: db 104h dup (?) ;MAX_PATH
или используя этот 64-разрядный код в 64-разрядных версиях Windows:
mov r8d, 104h ;MAX_PATH
mov rbx, offset l2
push rbx
pop rdx
xor ecx, ecx ;self filename
call GetModuleFileNameA
cdq
push rdx
push rdx
push 3 ;OPEN_EXISTING
sub esp, 20h
xor r9d, r9d
;FILE_SHARE_READ
;because a debugger might prevent
;exclusive access to the running file
inc edx
push rdx
pop r8
ror edx, 1 ;GENERIC_READ
push rbx
pop rcx
call CreateFileA
push 0
mov r9d, esp
sub esp, 20h
push 1 ;more bytes might be more useful
pop r8
mov rdx, offset l1
xchg ecx, eax
call ReadFile
l1: int 3 ;replaced by "M" from the MZ header
...
l2: db 104h dup (?) ;MAX_PATH
Один из способов преодолеть эту технику - использовать аппаратные контрольные точки вместо программных контрольных точек при переходе через вызовы функций.
C. Время выполнения
При наличии отладчика, который используется для пошагового выполнения кода, существует значительная задержка между выполнением отдельных инструкций по сравнению с собственным выполнением. Эта задержка может быть измерена с использованием одного из нескольких возможных источников времени. Эти источники включают инструкцию RDPMC (однако эта инструкция требует, чтобы в регистре CR4 был установлен флаг PCE, но это не настройка по умолчанию), инструкцию RDTSC , функцию kernel32 GetLocalTime(), функция GetSystemTime(), QueryPerformanceCounter(), GetTickCount(), функция ntoskrnl KiGetTickCount() (предоставляется через интерфейс прерываний 0x2A в 32-разрядных версиях Windows) и функция winmm timeGetTime(). Однако разрешение функции winmm timeGetTime() является переменным, в зависимости от того, является ли оно внутренним или внешнним по отношению к функции GetTickCount(), что делает очень ненадежным измерение небольших интервалов. Инструкция RDMSR также может использоваться в качестве источника времени, но ее нельзя использовать в пользовательском режиме. Проверка может быть выполнена для инструкции RDPMC с использованием этого кода для проверки 32-разрядной или 64-разрядной среды Windows:
xor ecx, ecx ;read 32-bit counter 0
rdpmc
xchg ebx, eax
rdpmc
sub eax, ebx
cmp eax, 500h
jnbe being_debugged
Проверка может быть выполнена для инструкции RDTSC с использованием этого кода (идентичного для 32-битной и 64-битной) для проверки 32-битной или 64-битной среды Windows:
rdtsc
xchg esi, eax
mov edi, edx
rdtsc
sub eax, esi
sbb edx, edi
jne being_debugged
cmp eax, 500h
jnbe being_debugged
Проверка может быть выполнена для функции GetLocalTime() kernel32, использующей этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov ebx, offset l1
push ebx
call GetLocalTime
mov ebp, offset l2
push ebp
call GetLocalTime
mov esi, offset l3
push esi
push ebx
call SystemTimeToFileTime
mov edi, offset l4
push edi
push ebp
call SystemTimeToFileTime
mov eax, [edi]
sub eax, [esi]
mov edx, [edi+4]
sbb edx, [esi+4]
jne being_debugged
cmp eax, 10h
jnbe being_debugged
...
l1: db 10h dup (?) ;sizeof(SYSTEMTIME)
l2: db 10h dup (?) ;sizeof(SYSTEMTIME)
l3: db 8 dup (?) ;sizeof(FILETIME)
l4: db 8 dup (?) ;sizeof(FILETIME)
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rbx, offset l1
push rbx
pop rcx
call GetLocalTime
mov rbp, offset l2
push rbp
pop rcx
call GetLocalTime
mov rsi, offset l3
push rsi
pop rdx
push rbx
pop rcx
call SystemTimeToFileTime
mov rdi, offset l4
push rdi
pop rdx
push rbp
pop rcx
call SystemTimeToFileTime
mov rax, [rdi]
sub rax, [rsi]
cmp rax, 10h
jnbe being_debugged
...
l1: db 10h dup (?) ;sizeof(SYSTEMTIME)
l2: db 10h dup (?) ;sizeof(SYSTEMTIME)
l3: db 8 dup (?) ;sizeof(FILETIME)
l4: db 8 dup (?) ;sizeof(FILETIME)
Проверка может быть выполнена для функции GetSystemTime() с использованием точно такого же кода, что и для функции GetLocalTime() , за исключением изменения имени функции.
Можно выполнить проверку для функции GetTickCount() kernel32, используя этот код (идентичный для 32-битной и 64-битной), чтобы проверить 32-битную или 64-битную среду Windows:
call GetTickCount
xchg ebx, eax
call GetTickCount
sub eax, ebx
cmp eax, 10h
jnbe being_debugged
Проверка может быть выполнена для функции ntoskrnl KiGetTickCount(), использующей этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядных версиях Windows (прерывание не поддерживается в 64-разрядных версиях Windows):
int 2ah
xchg ebx, eax
int 2ah
sub eax, ebx
cmp eax, 10h
jnbe being_debugged
Проверка может быть выполнена для функции kernel32 QueryPerformanceCounter(), использующей этот 32-разрядный код для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
mov esi, offset l1
push esi
call QueryPerformanceCounter
mov edi, offset l2
push edi
call QueryPerformanceCounter
mov eax, [edi]
sub eax, [esi]
mov edx, [edi+4]
sbb edx, [esi+4]
jne being_debugged
cmp eax, 10h
jnbe being_debugged
...
l1: db 8 dup (?) ;sizeof(LARGE_INTEGER)
l2: db 8 dup (?) ;sizeof(LARGE_INTEGER)
или использовать этот 64-битный код для проверки 64-битной среды Windows:
mov rsi, offset l1
push rsi
pop rcx
call QueryPerformanceCounter
mov rdi, offset l2
push rdi
pop rcx
call QueryPerformanceCounter
mov rax, [rdi]
sub rax, [rsi]
cmp rax, 10h
jnbe being_debugged
...
l1: db 8 dup (?) ;sizeof(LARGE_INTEGER)
l2: db 8 dup (?) ;sizeof(LARGE_INTEGER)
Можно выполнить проверку для функции winmm timeGetTime (), используя этот код (идентичный для 32-разрядного и 64-разрядного), чтобы проверить 32-разрядную или 64-разрядную среду Windows:
call timeGetTime
xchg ebx, eax
call timeGetTime
sub eax, ebx
cmp eax, 10h
jnbe being_debugged
D.Process-level
1.Функция CheckRemoteDebuggerPresent
Функция kernel32 CheckRemoteDebuggerPresent() была введена в Windows XP с пакетом обновления 1 (SP1) для запроса значения, существовавшего со времени Windows NT. Remote в этом смысле означает отдельный процесс на одной машине. Функция устанавливает в 0xffffffff значение, на которое указывает аргумент pbDebuggerPresent, если присутствует отладчик (то есть присоединенный к текущему процессу). Проверка может быть выполнена с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
push eax
push esp
push -1 ;GetCurrentProcess()
call CheckRemoteDebuggerPresent
pop eax
test eax, eax
jne being_debugged
или использовать этот 64-битный код для проверки 64-битной среды Windows:
enter 20h, 0
mov edx, ebp
or rcx, -1 ;GetCurrentProcess()
call CheckRemoteDebuggerPresent
leave
test ebp, ebp
jne being_debugged
2.Родительский процесс
Пользователи обычно запускают приложения, щелкая значок, отображаемый процессом оболочки (Explorer.exe). В результате родительский процесс исполняемого процесса будет Explorer.exe. Конечно, если приложение выполняется из командной строки, то родительский процесс исполняемого процесса будет процессом командной строки. Выполнение приложения путем отладки приведет к тому, что родительский процесс исполняемого процесса станет процессом отладчика.
Выполнение приложений из командной строки может вызвать проблемы для некоторых приложений, поскольку они ожидают, что родительский процесс будет Explorer.exe. Некоторые приложения проверяют имя родительского процесса, ожидая, что оно будет "Explorer.exe". Некоторые приложения сравнивают идентификатор родительского процесса с идентификатором Explorer.exe. Несоответствие в любом случае может привести к тому, что приложение будет думать, что оно отлаживается.
На этом этапе мы сделаем небольшой обход и представим тему, которая логически должна появиться позже. Самый простой способ получить идентификатор процесса Explorer.exe - вызвать функции user32 GetShellWindow() и user32 GetWindowThreadProcessId(). Это оставляет идентификатор процесса и имя родительского процесса текущего процесса, что можно получить, вызвав функцию ntdll NtQueryInformationProcess() с классом ProcessBasicInformation. Вызовы могут быть сделаны с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
call GetShellWindow
push eax
push esp
push eax
call GetWindowThreadProcessId
push 0
push 18h ;sizeof(PROCESS_BASIC_INFORMATION)
mov ebp, offset l1
push ebp
push 0 ;ProcessBasicInformation
push -1 ;GetCurrentProcess()
call NtQueryInformationProcess
pop eax
;InheritedFromUniqueProcessId
cmp [ebp+14h], eax
jne being_debugged
...
;sizeof(PROCESS_BASIC_INFORMATION)
l1: db 18h dup (?)
или использовать этот 64-битный код для проверки 64-битной среды Windows:
call GetShellWindow
enter 20h, 0
mov edx, ebp
xchg ecx, eax
call GetWindowThreadProcessId
leave
push 0
sub esp, 20h
push 30h ;sizeof(PROCESS_BASIC_INFORMATION)
pop r9
mov rbx, offset l1
push rbx
pop r8
cdq ;ProcessBasicInformation
or rcx, -1 ;GetCurrentProcess()
call NtQueryInformationProcess
;InheritedFromUniqueProcessId
cmp [rbx+20h], ebp
jne being_debugged
...
;sizeof(PROCESS_BASIC_INFORMATION)
l1: db 30h dup (?)
Однако этот код имеет серьезную проблему, заключающуюся в том, что в одном сеансе может быть несколько экземпляров Explorer.exe, если значение реестра "HKCU\Software\Microsoft\ Windows\CurrentVersion\Explorer\Advanced\SeparateProcess" (введено в Windows 2000) не равно нулю. Это приводит к запуску отдельной копии Explorer.exe для каждого открытого окна. В результате окно оболочки может не быть родительским процессом текущего процесса, и все же Explorer.exe является именем родительского процесса.
3. Функция CreateToolhelp32Snapshot
Идентификатор процесса Explorer.exe и родительского процесса текущего процесса, а также имя этого родительского процесса можно получить с помощью функции CreateToolhelp32Snapshot() и перечисления функции Process32Next (). Вызов может быть выполнен с использованием этого 32-разрядного кода для проверки 32-разрядной среды Windows в 32-разрядной или 64-разрядной версиях Windows:
Последнее редактирование: