Использование отладочных регистров процессора Intel X86 в прикладной программе Windows NT — Архив WASM.RU

Все статьи

Использование отладочных регистров процессора Intel X86 в прикладной программе Windows NT — Архив WASM.RU

  1. Введение.
  2. Описание отладочных регистров.
  3. Векторная обработка исключений в Windows NT.
  4. Установка точек останова из прикладной программы.
  5. Пример использования на практике.
  6. Заключение
  7. Литература

Введение.

Наверное, сейчас ни для кого не секрет, какой огромный выигрыш даёт использование отладочных регистров процессора для исследования механизма защиты программ. Яркий тому пример отладчик SoftIce, который использует эти регистры для установки аппаратных точек останова в программе (команды типа ‘BPM’). Но как использовать их в прикладной программе, где прямое обращение к ним вызывает общую ошибку защиты?

Неужели нет способов, кроме, как написания собственных драйверов ядра, которое само по себе является достаточно трудоёмкой задачей, отпугивающей от их применения? Оказывается, есть, и достаточно простой. Об этом и пойдёт речь в этой статье.

Описание отладочных регистров.

Процессор Intel X86 содержит в своём составе восемь 32 – разрядных отладочных регистров, из которых используется только шесть, это DR0 – DR3, DR6 и DR7, обращение к которым производится с помощью привилегированной команды ассемблера MOV. Команда выполняется, либо в реальном режиме работы процессора, либо в защищищённом, на нулевом кольце защиты. Регистры позволяют установить четыре аппаратных точки останова одновременно. Ниже приводится детальное описание каждого из используемых регистров.

DR7 – регистр управления отладкой, назначение битов такое:

Биты 31 - 30: размер точки останова 3,
	00 - 1 байт,
	01 - 2 байта,
	10 - не используется,
	11 - 4 байта.
Биты 29 - 28: тип точки останова 3,
	00 - при выполнении команды,
	01 - при записи в ячейку памяти,
	10 - при обращении к порту ввода - вывода,
	11 - при чтении или записи в ячейку памяти.
Биты 27 - 26: размер точки останова 2.
Биты 25 - 24: тип точки останова 2.
Биты 23 - 22: размер точки останова 1.
Биты 21 - 20: тип точки останова 1.
Биты 19 - 18: размер точки останова 0.
Биты 17 - 16: тип точки останова 0.
Биты 15 - 14: не используются.
Бит 13 -  обращение к любому из отладочных регистров вызывает 
          генерацию процессором отладочного исключения #DB, 
		  сбрасывается аппаратно на входе в обработчик исключения.
Биты 12 - 10: не используются.
Бит 9 -   разрешает использование глобальных точек останова.
Бит 8 -   разрешает использование локальных точек останова.
Бит 7 -   включение глобальной точки останова 3.
Бит 6 -   включение локальной точки останова 3.
Бит 5 -   включение глобальной точки останова 2.
Бит 4 -   включение локальной точки останова 2.
Бит 3 -   включение глобальной точки останова 1.
Бит 2 -   включение локальной точки останова 1.
Бит 1 -   включение глобальной точки останова 0.
Бит 0 -  включение локальной точки останова 0.

Различие глобальных и локальных точек останова в том, что биты локальных точек, сбрасываются при переключении задачи, а глобальных остаются неизменными.

DR6 – регистр состояния отладки, содержит информацию о причине отладочного останова для обработчика исключения #DB, биты такие:

Биты 31 - 16:  не используются.
Бит 15 - переключение на задачу с установленным 
         отладочным битом в TSS.
Бит 14 - выполнена одна команда при установленном флаге TF в 
         регистре EFLAGS (пошаговый режим отладки, флаг TF 
		 сбрасывается аппаратно на входе  в обработчик исключения).
Бит 13 - произошло обращение к отладочному регистру, при 
         установленном бите 13 в регистре DR7.
Биты 12 - 4: не используются.
Бит 3 - произошёл останов в точке 3.
Бит 2 - произошёл останов в точке 2.
Бит 1 - произошёл останов в точке 1.
Бит 0 - произошёл останов в точке 0.

Перед выходом из обработчика исключения этот регистр необходимо сбросить в нуль.

Регистры DR0 – DR3 содержат линейные адреса точек останова 0 – 3, соответственно. Напомню, что линейный адрес в защищённом режиме состоит из базового адреса дескриптора памяти, селектор которого содержится в сегментном регистре кода, данных или стека, и смещения внутри этого сегмента, но т.к. в Windows NT базовый адрес равен нулю, то линейный адрес и смещение одинаковы.

При срабатывании любой из точек останова, процессором генерируется исключение #DB.


Векторная обработка исключений в Windows NT.

Итак, из описания отладочных регистров, следует, что для их использования, нам необходимо каким – то образом обработать отладочное исключение #DB. Первое, что приходит на ум, это перехватить вектор прерывания, в котором обрабатывается это исключение, но сделать это из прикладной программы, без написания драйвера ядра,  невозможно. Второе – это написать собственный отладчик, занятие тоже довольно утомительное.

К счастью, начиная с Windows 2000, в SDK включена векторная обработка исключений. Ниже приводится детальное описание всех функций и структур поддержки.

PVOID AddVectoredExceptionHandler (

  ULONG FirstHandler,

  PVECTORED_EXCEPTION_HANDLER VectoredHandler );

ULONG RemoveVectoredExceptionHandler (

  PVOID VectoredHandlerHandle );

Первая – устанавливает, а вторая – удаляет обработчик, прототип которого:

LONG WINAPI VectoredHandler (

  PEXCEPTION_POINTERS ExceptionInfo );

Функция должна возвратить одно из двух значений:

  • EXCEPTION_CONTINUE_EXECUTION, или – 1, если обработка исключения закончена.
  • EXCEPTION_CONTINUE_SEARCH, или 0, для поиска и вызова другого обработчика.

FirstHandlerочерёдность вызова обработчика, если не 0, то первым.

VectoredHandler указатель на обработчик.

VectoredHandlerHandle – указатель на обработчик, возвращаемый при его установке.

Функцию RemoveVectoredExceptionHandler нужно, по завершении программы, вызывать обязательно.

ExceptionInfoуказатель на структуру EXCEPTION_POINTERS, которая определена, как:

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT ContextRecord;

} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

ExceptionRecord – указатель на структуру EXCEPTION_RECORD:

typedef struct _EXCEPTION_RECORD {
  DWORD ExceptionCode;
  DWORD ExceptionFlags;
  struct _EXCEPTION_RECORD* NestedExceptionRecord;
  PVOID ExceptionAddress;
  DWORD NumberParameters;
  ULONG_PTR ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS];

} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

ExceptionCode - коды причины вызова исключения, из которых, нас будет интересовать EXCEPTION_SINGLE_STEP или 0x80000004, соответствующий вектору прерывания 1, который и вызывается процессором при возникновении исключения #DB "Отладочное прерывание" и представляет собой ловушку (trap):

  • При пошаговой трассировке, если установлен флаг TF (бит 8) в регистре EFLAGS;
  • При переключении на задачу с установленным отладочным флагом в TSS;
  • При срабатывании точки останова по доступу к данным, определённой в отладочных регистрах;

и ошибку (fault):

  • При срабатывании точки останова по выполнению команды, адрес которой определён в отладочных регистрах;

Здесь оговорюсь, для ясности, что при возникновении исключения типа ошибки (fault), адрес возврата указывает на команду вызвавшую исключение, а ловушки (trap) – на команду, следующую за ней. В этой связи есть один неприятный момент, а именно, невозможность продолжения программы с адреса останова по выполнению команды. Это вызвано тем, что разрешение  на такое продолжение, без повторной генерации исключения, даёт флаг RF (бит 16) регистра EFLAGS, который устанавливается аппаратно, при выполнении команды ассемблера IRETD, а затем сбрасывается, при успешном выполнении любой команды. Поэтому нужно, либо пропустить команду, на которой произошёл останов, изменив регистр EIP, либо выполнить её в пошаговом режиме, установив флаг TF в регистре EFLAGS, либо просто отключить эту точку останова.

ExceptionFlags – флаг продолжения программы, если 0, программа может быть продолжена и, если EXCEPTION_NONCONTINUABLE_EXCEPTION или 0xC0000025, программа должна завершиться.

NestedExceptionRecord – указатель на структуру, содержащую дополнительную информацию, при возникновении вложенного исключения.

ExceptionAddress – адрес возврата из исключения.

NumberParameters – количество элементов в массиве ExceptionInformation.

ExceptionInformation – массив дополнительных параметров, передаваемых при генерации исключения функцией RaiseException.

ContextRecord – указатель на структуру CONTEXT, которая определена в файле WinNT.h, и содержит значения всех регистров процессора, включая отладочные. Ниже приводится структура для процессоров Intel X86, в том же самом виде, за исключением комментариев. Названия членов структуры, говорят сами за себя, поэтому описывать их здесь не буду.

typedef struct _CONTEXT {

    //
    // Набор флагов, в зависимости от которых, может быть считана
    // или записана конкретная секция структуры.
    //
    DWORD ContextFlags;

    //
    // Эта секция может быть считана или записана, если ContextFlags
    // содержит флаг CONTEXT_DEBUG_REGISTERS.
    // Помните, что CONTEXT_DEBUG_REGISTERS не включен в CONTEXT_FULL.
    // 
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;

    //
    // Эта секция может быть считана или записана, если ContextFlags
    // содержит флаг CONTEXT_FLOATING_POINT.
    //
    FLOATING_SAVE_AREA FloatSave;

    //
    // Эта секция может быть считана или записана, если ContextFlags
    // содержит флаг CONTEXT_SEGMENTS.
    //
    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;

    //
    // Эта секция может быть считана или записана, если ContextFlags
    // содержит флаг CONTEXT_INTEGER.
    //
    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;

    //
    // Эта секция может быть считана или записана, если ContextFlags
    // содержит флаг CONTEXT_CONTROL.
    //
    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;
    DWORD   EFlags;
    DWORD   Esp;
    DWORD   SegSs;

    //
    // Эта секция может быть считана или записана, если ContextFlags
    // содержит флаг CONTEXT_EXTENDED_REGISTERS.
    // Содержание и формат зависят от конкретного процессора.
    //
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

Все возможные значения флагов также определены в файле WinNT.h.


Установка точек останова из прикладной программы.

Для установки точек останова, будем использовать следующие функции API Windows:

BOOL GetThreadContext(

  HANDLE hThread,

  LPCONTEXT lpContext );

 

BOOL SetThreadContext(

  HANDLE hThread,

  const CONTEXT* lpContext );

hThread дескриптор потока.

lpContext указатель на структуру CONTEXT, описанную выше.

В документации на эти функции сказано, что перед их использованием, нужно остановить поток, вызвав функцию SuspendThread, а впоследствии запустить, вызвав ResumeThread.  Это не вызывает затруднений, если наш код исполняется в другом потоке, но тогда возникают проблемы с синхронизацией, к тому же его легко обнаружить и уничтожить. Здесь приходит на помощь, то обстоятельство, что контекст потока можно изменять частями, установив определённый флаг в ContextFlags. Практика показывает, что изменение только отладочных регистров из основного потока приложения вполне допускается без его останова, и не приводит ни к каким непредсказуемым результатам. Поэтому так и поступим дальше.


Пример использования на практике.

Прежде, чем перейти к разбору примера, необходимо решить, каким образом наш код будет подключен к прикладной программе. Есть много разных способов, но оптимальным, с моей точки зрения, является оформление его в виде DLL. Для подключения её,  достаточно написать небольшой patch на ассемблере, что – то типа:

	org EntryPoint						; точка входа 
start:								; программы
	push name							; имя dll                                 
	push start							; адрес возврата
	mov  dword ptr[start+6], offset ExitProcess	; выход по ошибке	
	jmp  LoadLibraryA						; загрузка dll	
name:
	db   'mydll', 0

Естественно, кодовый сегмент программы, должен быть доступен для чтения, записи. При успешной загрузке DLL, как известно, будет вызвана функция DllMain, в теле которой, будет восстановлен исходный код программы, установлен обработчик исключений и точки останова. Также там можно восстановить исходный вид файла программы и т.д. и т.п. Далее программа, как ни в чём не бывало, запускается с нашей DLL. В случае невозможности загрузки DLL, программа завершается, вызовом ExitProcess. Но, достаточно теории, перейдём к практике, для этого напишем простенький “crackme, в виде консольной программы:

#include "stdafx.h"
#include 

TCHAR registered[] =
"Спасибо за покупку нашей программы!\n\
Правом пользования обладает: Rom Lameroff.";

TCHAR unregistered[] =
"Вы используете незарегистрированную \
копию программы.\nДальше так продолжаться \
не может!\nВы должны купить её за 1000 у.е.";


BOOL Registration()
{
	return FALSE;
}

int _tmain( int argc, _TCHAR* argv[] )
{

	if( Registration() )
	{
		MessageBox( NULL, registered, "Registered copy", MB_OK );
	}
	else
	{
		MessageBox( NULL, unregistered, "Unregistered copy", MB_OK );
	}
	return 0;
}

Программу компилируем с ключом  /SECTION:.text,ERW  и без оптимизации. Запускаем её под SoftIce, запоминаем адрес точки входа, у меня он равен 40106F, а также 32 байта кода с этой точки. Вводим код нашей “заплатки”, используя команду ‘A’, и также запоминаем его. Восстанавливаем исходный код, ставим точку останова на MessageBoxA, жмём F5, F12. У меня экран выглядит так:

По адресу 401013 мы видим вызов функции регистрации, туда и будем устанавливать точку останова по выполнеию команды, пропустим сам вызов, увеличив EIP на пять байтов, столько занимает эта команда, и установим регистр EAX в единицу, т.е. сымитируем вызов функции регистрации так, как - будто она возвратила TRUE. Осталось претворить это в жизнь, написав библиотеку. Вот она:

#include "stdafx.h"

#define  _WIN32_WINNT 0x500
#include 

#define	ENTRYPOINT	0x40106F
#define	BREAKPOINT	0x401013

BYTE original[] = {
	0x6A, 0x18, 0x68, 0x10, 0x51, 0x40, 0x00, 0xE8,
	0x0D, 0x0D, 0x00, 0x00, 0xBF, 0x94, 0x00, 0x00,
	0x00, 0x8B, 0xC7, 0xE8, 0x59, 0x0E, 0x00, 0x00,
	0x89, 0x65, 0xE8, 0x8B, 0xF4, 0x89, 0x3E, 0x56
};

BOOL SetBreakPoint( DWORD dwAddress ) 
{
	CONTEXT ct;
      HANDLE hThread = GetCurrentThread();

	ct.ContextFlags = CONTEXT_DEBUG_REGISTERS;
	if( !GetThreadContext( hThread, &ct ) )
		return FALSE;

	ct.Dr0 = dwAddress;
	ct.Dr6 = 0;
	ct.Dr7 = ( ct.Dr7 & 0xFFF0FFFF ) | 0x101;

	return SetThreadContext( hThread, &ct );
}

LONG WINAPI MyVectoredHandler( PEXCEPTION_POINTERS ExceptionInfo )
{
	if( ExceptionInfo->ContextRecord->Dr6 & 1 )
	{	
		ExceptionInfo->ContextRecord->Eip += 5;
		ExceptionInfo->ContextRecord->Eax = TRUE;
		ExceptionInfo->ContextRecord->Dr6 = 0;
		return EXCEPTION_CONTINUE_EXECUTION;
	}
	return EXCEPTION_CONTINUE_SEARCH;
}

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved )
{
	static PVOID pv;

	DisableThreadLibraryCalls( (HMODULE)hModule );

	switch ( ul_reason_for_call )
	{
	case DLL_PROCESS_ATTACH:

		CopyMemory( (LPVOID)ENTRYPOINT, original, sizeof(original) );
		pv = AddVectoredExceptionHandler( 1, MyVectoredHandler );
		SetBreakPoint( BREAKPOINT );
		break;

	case DLL_PROCESS_DETACH:

		RemoveVectoredExceptionHandler( pv );
		break;
	}
      return TRUE;
}

Компилируем. В любом HEX – редакторе устанавливаем “заплатку” в файл программы и копируем библиотеку в её папку. Запускаем программу, результат говорит сам за себя.


Заключение.

В заключение,  можно сказать, что данная “технология” может с успехом применяться для программ из категории “неломаемых”, чей код может модифицироваться или подвергаться проверке во время выполнения и даёт большой выигрыш во времени, благодаря схожести с трассировкой SoftIce. Из – за реализации на языке высокого уровня, процесс можно даже автоматизировать, написав один раз универсальные функции или, может быть, саму библиотеку, всё ограничивается только фантазией программиста. Конечно нельзя сказать, что защиты от этого нет, но то, что код выполняется раньше кода прикладной программы, даёт огромное преимущество. Вообщем “ищите и обрящите”. С точки зрения законности ничего положительного сказать не могу, но много ли у нас найдётся компьютеров оснащённых полностью лицензированным программным обеспечением, наверное, единицы. Но, тем не менее, прошу данную статью, ни в коем случае, не считать пропагандой нарушения авторских прав, а просто даю пищу для размышления, как потенциальным “взломщикам”, так и прикладным программистам. То, что опубликовано, перестаёт быть тайной и, надеюсь, будет полезно и тем, и другим. По всем вопросам, связанным с данным материалом, пишите мне сюда.

Литература.

  1. Зубков С.В. “Assembler. Для DOS, Windows и Unix.” – М.: ДМК, 1999. – 640 с.
  2. MSDN Library - Visual Studio .NET 2003.
  3. IA – 32 Intel® Architecture Software Developer’s Manual.

2002-2013 (c) wasm.ru