Время на прочтение12 мин
Количество просмотров47K
Вступление
Все мы, время от времени, используем дебаггер для отладки программ. Отладчик может использоваться с C++, C#, Java и ещё сотней других языков. Он может быть как внешним (WinDbg), так и встроенным в среду разработки (Visual Studio). Но вы хоть раз задавались вопросом, как же работает отладчик?
И вам повезло. В этом цикле статей мы разберёмся от и до, как же работает отладка изнутри. В этой статье рассматривается только написание отладчика под Windows. Без компиляторов, линковщиков и других сложных систем. Таким образом, мы сможем отлаживать только исполняемые файлы, так как мы напишем внешний отладчик. Эта статья потребует от читателя понимание основ многопоточности.
Как отлаживать программу:
- Запустить процесс с флагом DEBUG_ONLY_THIS_PROCESS или DEBUG_PROCESS;
- запустить цикл дебага, который будет отлавливать сообщения и события;
Прежде, чем мы начнём, запомните:
- Дебаггер — это процесс/программа, которая будет отлаживать другой процесс;
- отлаживаемая программа (ОП) – это процесс/программа, которая отлаживается;
- именно отладчик присоединяется к ОП. Также отлачик может подключаться к различным процессам (в разных потоках);
- отлаживать можно лишь те процессы, которые были запущены из под отладчика. Таким образом, CreateProcess и цикл отладчика должны находится в одном потоке;
- когда завершается процесс отладчика, то он также завершает ОП;
- Когда отладчик занят обработкой событий, он замораживает все потоки ОП на время. Об этом позже;
Запуск процесса с флагом отладки
Запускаем процесс с помощью функции CreateProcess и в шестом её параметра (dwCreationFlags) указываем флаг DEBUG_ONLY_THIS_PROCESS. Этот флаг указывает Windows подготовить запускаемый процесс для отладки (отладочные события, старт/завершение процесса, исключения и т.п.). Более подробное объяснение чуть позже. Прошу обратить внимание, что мы будем использовать именно DEBUG_ONLY_THIS_PROCESS. Это значит, что мы хотим отлаживать только тот процесс, который мы запускаем, а не ещё и порождаемые им.
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
CreateProcess ( ProcessNameToDebug, NULL, NULL, NULL, FALSE,
DEBUG_ONLY_THIS_PROCESS, NULL,NULL, &si, &pi );
После этого, вы должны увидеть новый процесс в диспетчере задач, но на самом деле, он ещё не запустился. Вновь созданный процесс пока ещё заморожен. Нет, не угадали, нам надо не вызвать ResumeThread, а написать отладочный цикл.
Отладочный цикл
Отладочный цикл – это сердце отладчика, и строится он вокруг функции WaitForDebugEvent. Она получает два параметра: указатель на структуру DEBUG_EVENT и таймаут (DWORD). В качестве таймаута мы укажем INFINITE. Эта функция содержится в kernel32.dll, поэтому никаких дополнительных библиотек нам линковать не надо.
BOOL WaitForDebugEvent(DEBUG_EVENT* lpDebugEvent, DWORD dwMilliseconds);
Структура DEBUG_EVENT включает в себя много отладочной информации: код события, ID процесса, ID потока и прикладную информацию о событии. Как только WaitForDebugEvent завершится и вернёт нам управление, мы получим сообщение отладчика, а после этого вызовем ContinueDebugEvent для продолжения выполнения кода. Ниже вы можете увидеть минимальный отладочный цикл.
DEBUG_EVENT debug_event = {0};
for(;;)
{
if (!WaitForDebugEvent(&debug_event, INFINITE))
return;
ProcessDebugEvent(&debug_event); // User-defined function, not API
ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
DBG_CONTINUE);
}
Вызывая ContinueDebugEvent, мы просим ОС продолжить выполнение ОП. dwProcessId и dwThreadId указывает нам на процесс и поток. Эти значения мы получили из WaitForDebugEvent. Последний параметр указывает, продолжить выполнение или нет. Этот параметр будет иметь значение только тогда, когда в отладку пришло исключение. Это мы рассмотрим позже. Ну а пока используем просто DBG_CONTINUE (другое возможное значение – это DBG_EXCEPTION_NOT_HANDLED).
Получение событий отладки
Есть девять основных событий отладки, и 20 подсобытий в категории исключений. Рассмотрим это, начиная с самого простого. Ниже приведена структура DEBUG_EVENT:
struct DEBUG_EVENT
{
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
};
Когда WaitForDebugEvent успешно завершается, он заполняет эту структуру. dwDebugEventCode указывает, какое событие отладки к нам пришло. В зависимости от этого кода, один из членов union’a u содержит информацию о событии. Например, если dwDebugEventCode==OUTPUT_DEBUG_STRING_EVENT, то верно заполнится тольк OUTPUT_DEBUG_STRING_INFO.
Обработка OUTPUT_DEBUG_STRING_EVENT
Для вывода текста в output, разработчики обычно пользуются функцией OutputDebugString. В зависимости от языка/фреймворка, который вы используете, вы должны быть знакомы с макросами TRACE/ATLTRACE. Разработчики .NET, возможно, знакомы с System.Diagnostics.Debug.Print/System.Diagnostics.Trace.WriteLine. Но все эти методы вызывают OutputDebugString, если объявлен макрос _DEBUG, и отладчик получает сообщение.
Когда сообщение отладки получено, мы обрабатываем DebugString. Структура OUTPUT_DEBUG_STRING_INFO представлена ниже:
struct OUTPUT_DEBUG_STRING_INFO
{
LPSTR lpDebugStringData; // char*
WORD fUnicode;
WORD nDebugStringLength;
};
Поле nDebugStringLength содержит в себе длину строки, включая завершающий null. Поле fUnicode равно нулю, если строка ANSI, и не равна нулю, если юникод. В этом случае, мы должны считывать nDebugStringLength x2 байт. Внимание! lpDebugStringData содержит указатель на строку с сообщением, но указатель ссылается на данные относительно памяти отлаживаемой программы, а не отладчика.
Чтобы прочитать данные из памяти другого процесса, нам необходимо вызвать ReadProcessMemory и у нас должно на это быть разрешение. Так как мы же и создали процесс для отладки, то проблем с разрешением нет.
case OUTPUT_DEBUG_STRING_EVENT:
{
CStringW strEventMessage; // Force Unicode
OUTPUT_DEBUG_STRING_INFO & DebugString = debug_event.u.DebugString;
WCHAR *msg=new WCHAR[DebugString.nDebugStringLength];
// Don't care if string is ANSI, and we allocate double...
ReadProcessMemory(pi.hProcess, // HANDLE to Debuggee
DebugString.lpDebugStringData, // Target process' valid pointer
msg, // Copy to this address space
DebugString.nDebugStringLength, NULL);
if ( DebugString.fUnicode )
strEventMessage = msg;
else
strEventMessage = (char*)msg; // char* to CStringW (Unicode) conversion.
delete []msg;
// Utilize strEventMessage
}
Что если ОП завершится во время считывания памяти?
Что ж, такого не будет Позвольте вам напомнить, что отладчик замораживает все потоки ОП во время отработки отладочного сообщения. Таким образом, сам себя процесс завершить не сможет, Ни один диспетчер задач (стандартный или нет) так же не сможет завершить процесс. Если попробовать, то в следующем сообщении наш отладчик получит событие EXIT_PROCESS_DEBUG_EVENT.
Обработка CREATE_PROCESS_DEBUG_EVENT
Событие появляется, когда ОП только запускается. Это должно быть первое сообщение, которое получает отладчик. Для этого сообщения, соответствующее поле DEBUG_EVENT будет CreateProcessInfo. Ниже вы можете увидеть саму структуру CREATE_PROCESS_DEBUG_INFO:
struct CREATE_PROCESS_DEBUG_INFO
{
HANDLE hFile; // The handle to the physical file (.EXE)
HANDLE hProcess; // Handle to the process
HANDLE hThread; // Handle to the main/initial thread of process
LPVOID lpBaseOfImage; // base address of the executable image
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress;
LPVOID lpImageName; // Pointer to first byte of image name (in Debuggee)
WORD fUnicode; // If image name is Unicode.
};
Обратите внимание, что hProcess и hThread могут отличаться от тех, которые мы получаем в PROCESS_INFORMATION. ID процесса и потока должны быть теми же. Каждый хэндл, который вы получаете от Windows, отличается от остальных. Для этого есть различные причины.
hFile, так же как и lpImageName, может использоваться для получения имени файла ОП. Правда мы уже знаем имя этого файла, ведь мы его и запустили. Но расположение EXE или DLL нам важно знать, потому что при получении сообщения LOAD_DLL_DEBUG_EVENT, хорошо бы знать имя библиотеки.
Как вы можете прочитать в MSDN, lpImageName никогда не содержит полное имя файла и оно будет содержаться в памяти ОП. Более того, не существует гарантий, что в памяти ОП будет также лежать полное имя файла. А ещё имя файла может быть неполный. Поэтому, мы будет получать имя файла из hFile.
Как получить имя файла из hFile
К сожалению, нам необходимо будет использовать метод, описанный в MSDN, который содержит примерно 10 вызовов функций. Ниже сокращённый вариант:
case CREATE_PROCESS_DEBUG_EVENT:
{
CString strEventMessage =
GetFileNameFromHandle(debug_event.u.CreateProcessInfo.hFile);
// Use strEventMessage, and other members
// of CreateProcessInfo to intimate the user of this event.
}
Вы могли заметить, что я не рассмотрел несколько полей этой структуры. В следующих частях мы рассмотрим это всё досконально.
Обработка LOAD_DLL_DEBUG_EVENT
Это событие похоже на CREATE_PROCESS_DEBUG_EVENT, и как вы уже догадались, это событие вызывается, когда ОС загружает DLL. Это событие возникает каждый раз, когда загружается DLL, явно или неявно. Отладочная информация содержит только время, когда была загруженаDLL, и её виртуальный адрес. Для обработки события, мы используем поле union’a LoadDll. Оно имеет тип LOAD_DLL_DEBUG_INFO
struct LOAD_DLL_DEBUG_INFO
{
HANDLE hFile; // Handle to the DLL physical file.
LPVOID lpBaseOfDll; // The DLL Actual load address in process.
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpImageName; // These two member are same as CREATE_PROCESS_DEBUG_INFO
WORD fUnicode;
};
Для получения имени файла, мы будем использовать функцию GetFileNameFromHandle, такую же, как мы использовали в CREATE_PROCESS_DEBUG_EVENT. Я покажу этот код, когда буду рассказывать про UNLOAD_DLL_DEBUG_EVENT. Событие UNLOAD_DLL_DEBUG_EVENT не содержит полной информации об имени DLL библиотеки.
Обработка CREATE_THREAD_DEBUG_EVENT
Это событие генерируется, когда ОП создаёт новый поток. Практически как CREATE_PROCESS_DEBUG_EVENT, это событие создаётся перед тем, как новый поток будет запущен. Чтобы получить информацию об этом событии, мы используем поле CreateThread. Структура CREATE_THREAD_DEBUG_INFO описана ниже:
struct CREATE_THREAD_DEBUG_INFO
{
// Handle to the newly created thread in debuggee
HANDLE hThread;
LPVOID lpThreadLocalBase;
// pointer to the starting address of the thread
LPTHREAD_START_ROUTINE lpStartAddress;
};
ID потока доступен в DEBUG_EVENT::dwThreadId, поэтому нам легко вывести всю информацию о потоке:
case CREATE_THREAD_DEBUG_EVENT:
{
CString strEventMessage;
strEventMessage.Format(L"Thread 0x%x (Id: %d) created at: 0x%x",
debug_event.u.CreateThread.hThread,
debug_event.dwThreadId,
debug_event.u.CreateThread.lpStartAddress);
// Thread 0xc (Id: 7920) created at: 0x77b15e58
}
lpStartAddress – адрес начала функции потока относительно ОП, а не отладчика; Мы его просто отображаем для законченности. Обратите внимание, что это событие не генерируется, когда начинает работу основной поток ОП, только при создании новых потоков основным.
Обработка EXIT_THREAD_DEBUG_EVENT
Это событие генерируется, как только дочерний поток завершается и возвращает код возврата в систему. Поле dwThreadId в DEBUG_EVENT содержит ID завершающегося потока. Для получения хэндла потока и другой информации из CREATE_THREAD_DEBUG_EVENT, нам необходимо хранить эту информацию в каком-либо массиве. Для получения информации об этом событии, мы используем поле ExitThread, которое имеет тип EXIT_THREAD_DEBUG_INFO:
struct EXIT_THREAD_DEBUG_INFO
{
DWORD dwExitCode; // The thread exit code of DEBUG_EVENT::dwThreadId
};
Ниже код обработчика события:
case EXIT_THREAD_DEBUG_EVENT:
{
CString strEventMessage;
strEventMessage.Format( _T("The thread %d exited with code: %d"),
debug_event.dwThreadId,
debug_event.u.ExitThread.dwExitCode); // The thread 2760 exited with code: 0
}
Обработка UNLOAD_DLL_DEBUG_EVENT
Конечно же событие содержит информацию и выгружаемой DLL из памяти ОП. Но не всё так просто! Оно генерируется только в случае вызова FreeLibrary, а не когда система сама выгружает библиотеку. Для получения информации, используйте UnloadDll (UNLOAD_DLL_DEBUG_INFO):
struct UNLOAD_DLL_DEBUG_INFO
{
LPVOID lpBaseOfDll;
};
Как вы видите, для нас доступен только базовый адрес библиотеки. Именно поэтому я не рассказал вам сразу про код для LOAD_DLL_DEBUG_EVENT. Во время загрузки DLL, мы также получаем lpBaseOfDll. Можно использовать Map для хранения имени библиотеки, помимо её адреса.
Важно заметить, что не все события загрузки библиотеки получат своё событие выгрузки. Тем не менее, мы должны хранить все имена библиотек, так как LOAD_DLL_DEBUG_EVENT не даёт нам информации о том, как библиотека была загружена.
Ниже код для обработки обоих событий:
std::map < LPVOID, CString > DllNameMap;
...
case LOAD_DLL_DEBUG_EVENT:
{
strEventMessage = GetFileNameFromHandle(debug_event.u.LoadDll.hFile);
// Storing the DLL name into map. Map's key is the Base-address
DllNameMap.insert(
std::make_pair( debug_event.u.LoadDll.lpBaseOfDll, strEventMessage) );
strEventMessage.AppendFormat(L" - Loaded at %x", debug_event.u.LoadDll.lpBaseOfDll);
}
break;
...
case UNLOAD_DLL_DEBUG_EVENT:
{
strEventMessage.Format(L"DLL '%s' unloaded.",
DllNameMap[debug_event.u.UnloadDll.lpBaseOfDll] ); // Get DLL name from map.
}
break;
Обработка EXIT_PROCESS_DEBUG_EVENT
Это одно из самых простых событий, и как вы можете догадаться, вызывается тогда, когда процесс ОП завершается. Это событие показывает нам, как завершился процесс: нормально или экстренно (например, через диспетчер задач), или отлаживаемая программа упала. Информацию мы получаем из EXIT_PROCESS_DEBUG_INFO ExitProcess;
struct EXIT_PROCESS_DEBUG_INFO
{
DWORD dwExitCode;
};
Как только мы получим это событие, нам необходимо прервать цикл отладки и завершить поток отладки. Для этого мы можем завести флаг, который будет сигнализировать о завершении отладки.
bool bContinueDebugging=true;
...
case EXIT_PROCESS_DEBUG_EVENT:
{
strEventMessage.Format(L"Process exited with code: 0x%x",
debug_event.u.ExitProcess.dwExitCode);
bContinueDebugging=false;
}
break;
Обработка EXCEPTION_DEBUG_EVENT
Это самая удивительная и сложная вещь во всех событиях отладки. Из MSDN:
Это событие генерируется, когда возникает исключение в отлаживаемом процессе (возможно при делении на ноль, выходе за границы массива, выполнения инструкции int 3 или любого другого исключения, описанного в SEH). Структура DEBUG_EVENT содержит структуру EXCEPTION_DEBUF_INFO. Именно она описывает исключение.
Описание обработки этого события требует отдельной статьи, чтобы рассказать про это полностью (да пусть хоть даже и частично). Поэтому я расскажу пока про один тип исключения.
Поле Exception содержит информацию о только что произошедшем исключении. Ниже можно увидеть описание структуры EXCEPTION_DEBUG_INFO:
struct EXCEPTION_DEBUG_INFO
{
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
};
Поле ExceptionRecord содержит детальную информацию об исключении.
struct EXCEPTION_RECORD
{
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 15
};
Прежде чем мы углубимся в EXCEPTION_RECORD, хотелось бы с вами обсудить EXCEPTION_DEBUG_INFO::dwFirstChance
Когда процесс находится под отладкой, отладчик всегда получает исключение до того, как ОП его получит. Вы должно быть видели запись “First-chance exception at 0x00412882 in SomeModule” пока отлаживали приложение под С++. Это ссылается на First Chance исключения. Такие же исключения могут быть, а могут не быть на second chance исключениях.
Когда ОП кидает исключение, оно трактуется как second chance. ОП может обработать это исключение, а может просто упасть. Эти исключения принадлежат не к C++ исключениям, а к механизму Windows SEH. Я раскрою немного больше в следующей части статьи.
Сначала сообщение об исключении получает отладчик (first chance exception), это помогает ему обработать исключение быстрее ОП. Некоторые библиотеки генерируют исключения first-chance чтобы помочь отладчику делать его работу.
Ещё немного о ContinueDebugEvent
Третий параметр этой функции (dwContinueStatus) важен нам только получения исключения. Для остальных событий этот параметр игнорируется.
После получения исключения, ContinueDebugEvent должен быть вызван с:
- DBG_CONTINUE, если исключение было успешно поймано отладчиком. От отлаживаемой программы больше ничего не надо и она может выполняться нормально.
- DBG_EXCEPTION_NOT_HANDLED, если это исключение не обработано (не может быть обработано) отладчиком. Отладчик может лишь сделать запись о том, что это исключение было.
Обратите внимание, что если вернуть DBG_CONTINUE во время того события отладки, в котором это возвращать нельзя, то точно такое же исключение бросится в отладчике, и такое же событие придёт моментально. Но так как мы только начинаем писать отладчик, давайте будет играть с безопасной рогаткой, а не пистолетом, и будем возвращать EXCEPTION_NOT_HANDLED. Исключение в этой статье составляет int 3 (точка останова), которое мы обсудим позже.
Коды исключений
- EXCEPTION_ACCESS_VIOLATION
- EXCEPTION_ARRAY_BOUNDS_EXCEEDED
- EXCEPTION_BREAKPOINT
- EXCEPTION_DATATYPE_MISALIGNMENT
- EXCEPTION_FLT_DENORMAL_OPERAND
- EXCEPTION_FLT_DIVIDE_BY_ZERO
- EXCEPTION_FLT_INEXACT_RESULT
- EXCEPTION_FLT_INVALID_OPERATION
- EXCEPTION_FLT_OVERFLOW
- EXCEPTION_FLT_STACK_CHECK
- EXCEPTION_FLT_UNDERFLOW
- EXCEPTION_ILLEGAL_INSTRUCTION
- EXCEPTION_IN_PAGE_ERROR
- EXCEPTION_INT_DIVIDE_BY_ZERO
- EXCEPTION_INT_OVERFLOW
- EXCEPTION_INVALID_DISPOSITION
- EXCEPTION_NONCONTINUABLE_EXCEPTION
- EXCEPTION_PRIV_INSTRUCTION
- EXCEPTION_SINGLE_STEP
- EXCEPTION_STACK_OVERFLOW
Успокойтесь, я не собираюсь описывать их все. Только EXCEPTION_BREAKPOINT:
case EXCEPTION_DEBUG_EVENT:
{
EXCEPTION_DEBUG_INFO& exception = debug_event.u.Exception;
switch( exception.ExceptionRecord.ExceptionCode)
{
case STATUS_BREAKPOINT: // Same value as EXCEPTION_BREAKPOINT
strEventMessage= "Break point";
break;
default:
if(exception.dwFirstChance == 1)
{
strEventMessage.Format(L"First chance exception at %x, exception-code: 0x%08x",
exception.ExceptionRecord.ExceptionAddress,
exception.ExceptionRecord.ExceptionCode);
}
dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
}
break;
}
Вам должно быть известно, что такое точка останова. Вне стандартной точки зрения, точку останова можно вызвать с помошью DebugBreak API, или с помощью инструкции ассемблера { int 3 }. В .NET её можно создать с помощью System.Diagnostics.Debugger.Break. Отладчик получит код STATUS_BREAKPOINT (такой же, как EXCEPTION_BREAKPOINT). Отладчик обычно использует это событие для остановки текущего процесса, и может показать исходный код того места, где произошло событие. Но так как у нас отладчик только начинает разрабатываться, то мы будем показывать пользователю только базовую информацию без исходного кода.
Если точка останова будет вызвана в приложении, которое не находится под отладчиком, то оно просто упадёт. Можно использовать следующую конструкцию:
if ( !IsDebuggerPresent() )
AfxMessageBox(L"No debugger is attached currently.");
else
DebugBreak();
В заключении, хотелось бы привести простейшее событие отладки: EXCEPTION_DEBUG_EVENT. Это событие будет приходить постоянно. Отладчики вроде Visual Studio игнорируют его, а WinDbg нет.
Заключение
Используйте любой отладчик для DebugMe.
Вторая часть будет ещё интереснее и она на подходе!
UPD: Часть 2
Что такое отладчик, как им пользоваться и как он реализован, после прочтение первых двух частей, вы знаете. В заключительной части статьи попробуем рассмотреть некоторые методы борьбы с отладчиком, на основе знаний о принципах его работы. Я не буду давать шаблонный набор антиотладочных приемов, благо при желании все это можно найти на просторах интернета, попробую это сделать немного другим способом, на основе некоего абстрактного приложения, у которого буду расширять код защиты от самой простейшей схемы до… пока не надоест
Сразу-же оговорюсь, в противостоянии приложение/отладчик, всегда победит последний
Но, только в том случае, если им будет пользоваться грамотный специалист, а с такими спецами бороться практически бесполезно (ну, если вы конечно не обладаете как минимум такой же квалификацией).
Правда как показывает практика, грамотные спецы не занимаются неинтересными им задачами, оставляя их на откуп начинающим реверсерам, которые еще не прогрызли свой гранит науки и могут спотыкаться на некоторых неочевидных трюках.
Вот что-то такое мы и рассмотрим, только в очень упрощенной форме.
Простейшая ShareWare:
Представим, у нас есть некое ПО, которое мы решили продавать. Для простоты пусть это будет обычное VCL приложение из пустой формы (ну хорошо, пусть не пустое, а с картинкой на всю морду) и мы хотим его продать. Первый же вопрос, которым нужно озаботится — как сделать так, чтобы наша картинка был видна только тем, кто за нее заплатил? Точнее — как разграничить триальных и легальных пользователей?
Самым очевидным решением является ключ. Триальный пользователь его не знает, а легальный, заплативший за него реальными деньгами, может активировать легальную копию приложения и насладиться картинкой.
Ключ так ключ.
Создаем новое VCL приложение, кидаем на форму TImage с картинкой, Visible ему выставляем в False. После чего размещаем на форме два TEdit, первый для имени пользователя и второй для кода активации. Ну и две кнопки — закрыть приложение и активировать.
Ну вот как-то так:
После чего пишем совершенно секретный код активации:
function TForm1.GenerateSerial(const AppUserName: string): string; const MagicSerialMask: int64 = $C5315E6121543992; var I: Integer; SN: int64; RawSN: string; begin SN := 0; Result := ''; for I := 1 to Length(AppUserName) do begin Inc(SN, Word(AppUserName[I])); SN := SN * 123456; end; Sn := SN xor MagicSerialMask; RawSN := IntToHex(SN, 16); for I := 1 to 16 do if ((I - 1) mod 4 = 0) and (I > 1) then Result := Result + '-' + RawSN[I] else Result := Result + RawSN[I]; end; procedure TForm1.btnCheckSerialClick(Sender: TObject); begin if edSerial.Text <> GenerateSerial(edAppUserName.Text) then Application.MessageBox('Неверный код активации', PChar(Application.Title), MB_OK or MB_ICONERROR) else begin Image1.Visible := True; Label1.Visible := False; Label2.Visible := False; Label3.Visible := False; edAppUserName.Visible := False; edSerial.Visible := False; btnCancel.Visible := False; btnCheckSerial.Visible := False; end; end;
Суть кода в следующем:
на основании имени пользователя приложение генерирует некий серийник и сравнивает его с введенным пользователем. Если все нормально, то убираются все элементы управления, отвечающие за активацию и отображается картинка, которую и жаждал узреть пользователь.
Оть такая:
(ну… первое что нашел
После произведенных манипуляций, «это» публикуется на различных шароварных сайтах и даже иногда даются ссылки на программерские форумы в топиках вида: «потестируйте плз защиту».
А как это выглядит со стороны взломщика?
Он берет отладчик (для простоты возьмем тот-же Olly Debug) и видит вот такую картинку:
Кода приложения у него нет, но есть характерная ошибка начинающих «защитников» ПО — вывод диалога о неверном ключе.
Что это дает взломщику?
Он ставит ВР на вызов MessageBoxA и запустив приложение ловит вызов данного сообщения, после чего, нажав на кнопку «ОК» он может вернуться к коду, в котором происходит вызов данной ошибки, где, посмотрев немного выше, сможет определить наличие условного перехода, на основании которого и происходит данный вызов:
На картинке точка принятия решения программой выделена восклицательными знаками.
Все что ему остается сделать, это исправить инструкцию JE на JMP, отключив, таким образом, проверку серийного кода и обеспечив валидный переход на область кода, который должен выполнятся только при активации приложения.
Как-то не понятно, да?
Ну тогда вот вам такая картинка, из отладчика Delphi:
Здесь код более понятен для отладки, и его чтение более удобно из-за размапливания адресов и приведения их в читабельный вид. Например теперь явно видно что перед выходом на адрес 0х475729, по которому происходит принятие решения, происходит получение текста из TEdit-ов и вызов процедуры GenerateSerial.
Взломщик такой информацией не обладает, и как видно на предыдущем изображении, ему придется проанализировать все вызовы, чтобы составить более-менее понятную для анализа картинку. Ну, правда, тут я немного утрирую, в действительности карта приложения строится достаточно просто, при наличии инструментария, но… Но некоторые иногда упорно стараются отлаживать системные модуля дельфи, таки честь им и хвала за упорство
Ну и нюанс, по адресу 0х475729 на скринах расположены две разные инструкции — JZ и JE, это нюансы интерпретации дизассемблеров, они идентичны.
Тут есть один интересный подход, который мне несколько раз озвучивали.
Вот чуть выше я озвучил что поставлю ВР на MessageBoxA, а мне говорят что вызовут MessageBoxW и вызов отловить не получится. Это заявление на твердую четверку с плюсом, ибо да, действительно, если приложение вызовет юникодную API, с бряком будет небольшой промах, но есть нюанс. А давайте-ка развернем весь стек вызова MessageBox.
Смотрите какая интересная картинка получается:
MessageBoxA -> MessageBoxExA -> MessageBoxTimeOutA -> MessageBoxTimeOutW-> SoftModalMessageBox()
Таки да, мы можем поставить ВР на вызове любой из перечисленных функций (обычно достаточно MessageBoxTimeOutW) чтобы отловить необходимый нам вызов, ее кстати так же вызовет и функция MessageBoxW.
Есть правда небольшой нюансик, в Delphi есть и иные способы отображения окна.
Ну например ShowMessage(). Данный метод не вызывает API MessageBox.
Достаточно забавно слушать рассуждения, что данный метод целиком и полностью реализован в виде создания отдельной формы, в которой кнопки размещаются так как им надо и вообще это внутренности самого VCL из которых в отладчике вообще ничего не понятно.
Так-то оно так, кабы данный вызов не упирался в API ShowWindow, с которого по стеку мы так же выйдем на необходимый нам участок кода.
Есть еще вызовы диалогов, но с ними будет точно такая же кухня. Все это детектируется без сильных времязатрат.
Поэтому, делайте первый вывод в свой блокнотик:
Вызов сообщения о неуспешной проверке кода, сразу после данной проверки — есть признак дурного тона.
Вводим контроль целостности приложения:
Ну чтож — вот нас и взломали, причем сделав всего лишь изменение в одном единственном байте приложения. Теперь наша веселая картинка доступна всем абсолютно бесплатно.
Печально, но не критично — будем бороться…
Взлом произошел посредством прямой правки тела приложения.
Значит выросла задача: обеспечить проверку целостности исходного кода.
Звучит грозно, но в действительности практически не выполнимо
Вот что мы можем применить для данной проверки?
Есть много умных слов: навесить цифровую подпись, сверить с образом файла на диске, проверить участок кода с контрольной суммой. Все пустое — в итоге все равно приходим к необходимости каким либо образом получить текущее значение кода приложения в памяти…
Ну хорошо: смотрим цифровую подпись. Она, во первых, платная. Во вторых проверка ее производится путем вызова API функции WinVerifyTrust, которая уязвима к перехвату. В третьих она легко удаляется штатными средствами через ImageRemoveCertificate.
Значит не вариант, что у нас по проверке образа файла на диске?
Тут тоже все печально. Смотрите, наш исполняемый файл пропатчили, мы хотим определить это сравнив с образом на диске и что мы делаем — получаем путь к текущему файлу через тот же ParamStr(0) (допустим) после чего открываем файл по данному пути и начинаем проверку, но…
Но на этапе вызова OpenFile/CreateFile взломщик подменяет путь в соответствующем параметре на путь к оригинальному, не измененному образу и все наши проверки идут лесом.
Есть еще один интересный момент. А ведь ваше приложение может храниться на диске и в не измененном виде. Есть такое понятие как лоадеры. Суть их заключается в том, что они запускают процесс и производят модификацию тела приложения непосредственно в памяти.
Вот например возьмем наш отладчик из прошлой статьи и при помощи него запустим наше приложение с волшебной картинкой, а при достижении точки входа выполним следующий код:
procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer; var ReleaseBreakpoint: Boolean); var JmpOpcode: Byte; begin if ExceptionRecord.ExceptionAddress = Pointer(FCore.DebugProcessData.EntryPoint) then begin JmpOpcode := $EB; FCore.WriteData(Pointer($475729), @JmpOpcode, 1);
Приложение на диске останется неизменным, но вот вместо инструкции JE будет выполнен прямой переход из-за записанной инструкции JMP. Что уже гораздо печальней, т.к. в данном случае первые два варианта проверки целостности гарантированно не сработают.
Остается третий вариант, проверка участков кода непосредственно в теле приложения.
Это достаточно ресурсоемкий по реализации вариант и так же не всегда приводящий к успеху по следующим причинам.
Во первых константы контрольных сумм. Если они хранятся в теле приложения, взломщик их изменит на правильные. (второй вывод в ваш блокнотик — константы CRC блоков кода в приложении, есть дурной тон).
Во вторых, во второй части статьи я рассказывал о МВР — Memory Breakpoint. Это идеальный механизм детектирования проверок целостности кода (если не учитывать еще более грамотный HBP — Hardware BreakPoint).
Работает просто — если есть подозрение на то, что текущий участок кода контролируется механизмом защиты, на него навешивается МВР или НВР с целью определить, где именно расположена сама проверка целостности кода.
Если таковая проверка детектируется — она так же отключается патчем.
Ну вот мы собственно и приплыли к патовой ситуации: абонент — не абонент
Впрочем…
Выкрутиться, конечно можно, но…
Но для начала посмотрим, как вообще реализовать проверку целостности кода приложения.
Если конкретнее, то мы хотим защитить от изменений код который был в самом начале. Для этого нам нужно каким-то образом выяснить его расположение в памяти во время работы приложения.
Метки наше «всё».
На основе меток работает большинство навесных протекторов, стало быть зачем нам придумывать очередной велосипед. Что такое метка — в принципе это столь нелюбимый всеми label, используемый при goto(), о котором свое высококвалифицированное «ФИ» не высказал только самый ленивый.
Впрочем… что нам их мнение? Как я и сказал — метки наше все
Правда есть нюансик, label удобно использовать при контролировании небольшой части кода внутри процедуры (при перекрестном контроле — о нем позже), сейчас-же нас интересует несколько процедур в совокупности.
Для этого label не подойдет, но вполне подойдут пустые процедуры, адрес которых мы сможем получить из кода проверки целостности.
Ну и нужна до кучи сама процедура расчета целостности, а так же (что собственно было одним из озвученных выше нюансов) некая константа, с которой мы будем сверять CRC блока данных.
Ну впрочем хватит разглагольствовать, пишем:
const CheckedCodeValidCheckSum: DWORD = 248268; // << оть тут мы будем хранить контрольную сумму procedure CheckedCodeBegin; begin end; function TForm1.CalcCheckSum(Addr: Pointer; Size: Integer): DWORD; var pCursor: PByte; I: Integer; Dumee: DWORD; begin Result := 0; pCursor := Addr; for I := 0 to Size - 1 do begin if pCursor^ <> 0 then Inc(Result, pCursor^) else Dec(Result); Inc(pCursor); end; end; procedure TForm1.CheckCodeProtect; var CheckedCodeBeginAddr, CheckedCodeEndAddr: Pointer; CurrentCheckSum: DWORD; begin // получаем адрес начала защищенного кода CheckedCodeBeginAddr := @CheckedCodeBegin; // получаем адрес конца защищенного кода CheckedCodeEndAddr := @CheckedCodeEnd; // Считем контрольную сумму и сверяемся с оригиналом CurrentCheckSum := CalcCheckSum(CheckedCodeBeginAddr, Integer(CheckedCodeEndAddr) - Integer(CheckedCodeBeginAddr)); if CurrentCheckSum <> CheckedCodeValidCheckSum then begin MessageBox(Handle, 'Нарушение целостности исполняемого кода.', PChar(Application.Title), MB_ICONERROR); TerminateProcess(GetCurrentProcess, 0); end; end; function TForm1.GenerateSerial(const AppUserName: string): string; const MagicSerialMask: int64 = $C5315E6121543992; var I: Integer; SN: int64; RawSN: string; begin SN := 0; Result := ''; for I := 1 to Length(AppUserName) do begin Inc(SN, Word(AppUserName[I])); SN := SN * 123456; end; Sn := SN xor MagicSerialMask; RawSN := IntToHex(SN, 16); for I := 1 to 16 do if ((I - 1) mod 4 = 0) and (I > 1) then Result := Result + '-' + RawSN[I] else Result := Result + RawSN[I]; end; procedure TForm1.btnCheckSerialClick(Sender: TObject); begin // Проверяем целостность кода CheckCodeProtect; if edSerial.Text <> GenerateSerial(edAppUserName.Text) then ShowMessage('Неверный код активации') else begin Image1.Visible := True; Label1.Visible := False; Label2.Visible := False; Label3.Visible := False; edAppUserName.Visible := False; edSerial.Visible := False; btnCancel.Visible := False; btnCheckSerial.Visible := False; end; end; procedure CheckedCodeEnd; begin end;
Что мы здесь имеем:
Две метки в виде пустых процедур CheckedCodeBegin и CheckedCodeEnd, расчет «контрольной суммы» данных между этими двумя метками, производимая процедурой CheckCodeProtect, ну и сама контрольная сумма, вынесенная за область проверяемого кода и представленная константой CheckedCodeValidCheckSum (на ее значение пока не обращайте внимание).
В принципе вообще ничего сложного, но давайте-ка проанализируем, а что нам это вообще дает?
В действительности много, так как:
1. Этот код детектирует патч тела приложения на диске (ибо при запуске оно будет уже с измененными байтами).
2. Этот код детектирует патч тела приложения лоадером (по вышеописанной схеме).
3. И этот код детектирует… помните картинку с прошлой статьи?
Да-да, это самый что ни на есть Breakpoint, установленный отладчиком. И его данной код тоже идеально детектирует, ведь если помните, то механизм установки ВР заключается в модификации тела приложения.
Вот и третья заметочка в ваш блокнот — детект ВР производится проверкой тела кода.
Правда, к сожалению тут не все так просто, в некоторых случаях данный код не сработает, но не будем торопиться, дойдем и до этого.
Теперь к печальному, как я и говорил, данная проверка легко детектируется. Для примера вот скриншот из под отладчика, где он прерывается сразу в начале проверки:
Синим выделен асмкод процедуры расчета контрольной суммы, отладчик прервался на адресе 0х467069, как раз при первой же попытке чтения защищенной области.
Ну точнее тут я немного сжульничал, если-бы код проверки был вне рамок проверяемой области, то остановка произошла бы как раз на данной инструкции, а так я, естественно, остановился на самой первой «PUSH EBX».
Но это лирика, вопрос в другом, и что теперь делать?
Ну, во первых, все не так страшно. Здесь реализована всего лишь одна единственная проверка целостности кода приложения. Да, она легко детектируется. Да, она так-же легко снимается патчем, но что нам мешает сделать их несколько, перекрестно контролирующих друг друга? Снимут и их? Ну не вопрос, добавим еще, а что нам стоит?
Однажды мне прислали продукт на анализ защиты приложения непосредственно разработчики самой защиты (извините — без названий). Бегло просмотрев код инициализации ВМ я примерно сразу наметил путь её разбора, мне нужно было всего лишь вытащить алгоритм крипта маленьких блоков данных при вызове конкретной API функции. Проблема заключалась в том, что как только я пропатчил единственный байт приложения, сработал механизм проверки контрольной суммы. Естественно я его быстро занопил, но как оказалось занопленный код контролировали уже четыре различных алгоритма. Я начал патчить их и что вы думаете? На каждый патч понимались все новые и новые куски кода, контролирующие целостность кода лавинообразно. В итоге я просто утонул в объеме ручных патчей и пришлось писать автоматическую утилиту/отладчик, что заняло почти неделю работы с учетом всех нюансов. А в конце я уперся в следующий уровень ядра защиты.
Впрочем это уже не важно, важен смысл — при желании возможно реализовать достойную головную боль взломщику, даже на банальной проверке контрольных сумм.
Ну а теперь к реальности.
Для детектирования кода проверки целостности взломщик применил MBP.
А теперь вспоминаем как они работают — правильно через назначение странице атрибута PAGE_GUARD. Значит, зная принципы работы отладчика, мы можем этому воспрепятствовать, достаточно просто снять данный атрибут и отладчик перестанет реагировать на доступ к якобы контролируемой им памяти.
Правда есть нюансик, произвести мы это сможем при помощи вызова VirtualProtect, которая уязвима, ибо отладчик может ее перехватить и запретить её вызов. Но и на это у нас есть болт с обратной резьбой, например можно поступись так, как описано в данной статье: читаем.
Правда сделаем так, вариант со снятием PAGE_GUARD в демоприложении я рассматривать не буду. Но не переживайте, я покажу еще один интересный способ, только для этого нужно рассмотреть еще несколько нюансов, поэтому чуть позже.
Ну и с данного момента считаем, что код контроля целостности приложения написан так, что его не взломать (дабы упростить)…
Детектирование отладчика
Ну вот, теперь мы пришли к тому, что нашу форму с картинкой хотят, причем при помощи отладчика. Конечно же нужно научится его детектировать. Пока что остановимся на функции IsDebuggerPresent, для начала этого достаточно.
Пишем код:
function IsDebuggerPresent: BOOL; stdcall; external kernel32; procedure TForm1.FormCreate(Sender: TObject); begin if IsDebuggerPresent then begin MessageBox(Handle, 'Работа приложения под отладчиком запрещена.', PChar(Application.Title), MB_ICONERROR); TerminateProcess(GetCurrentProcess, 0); end; end;
Все очень просто, если мы под отладчиком, данная функция вернет True.
Будем считать, что код проверки целостности приложения у нас настолько сложен, что пропатчить его нельзя и вызов данной функции у нас помещен в защищенный участок.
Что применит в данном случае взломщик?
Вариантов собственно всего три, с учетом того, что патчить тело приложения нельзя:
1. поставить ВР на вызове данной функции, где подменить результат ее вызова.
2. пропатчить код данной функции, чтобы она всегда возвращала False
3. произвести изменение переменной Peb.BeingDebugged в адресном пространстве отлаживаемого процесса.
С третьим вариантом бороться сложно (можно, но не нужно), а вот первые два мы рассмотрим поподробнее, точнее будем рассматривать второй вариант, т.к. в первом так-же производится патч кода приложения, при установке ВР с опкодом 0хСС.
Для начала добавим вот такой код в отлаживаемом приложении в процедуру FormCreate:
procedure TForm1.FormCreate(Sender: TObject); var P: PCardinal; begin P := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent'); ShowMessage(IntToHex(P^, 8));
Он покажет первые 4 байта функции IsDebuggerPresent.
Вот такой код писать нельзя:
function IsDebuggerPresent: BOOL; stdcall; external kernel32; procedure TForm1.FormCreate(Sender: TObject); var P: PCardinal; begin P := @IsDebuggerPresent; ShowMessage(IntToHex(P^, 8));
Ибо во втором варианте мы используем статическую функцию, и адрес будет указывать не на начало тела функции, а на таблицу импорта, где стоит переходник в виде JMP.
Выполним код и запомним значение.
Под каждой системой оно будет разное, в ХР например это будет тело оригинальной функции, в семерке будет переходник на аналог из kernelbase. У меня получилось значение 9090F3EB, что соответствует следующей картинке:
А теперь возьмем наш отладчик из второй части статьи, и в методе OnBreakPoint произведем патч тела данной функции вот таким кодом:
procedure TTestDebugger.HideDebugger; const PachBuff: array [0..2] of Byte = ( $31, $C0, // xor eax, eax $C3 // ret ); var Addr: Pointer; begin Addr := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent'); FCore.WriteData(Addr, @PachBuff[0], 3); end; procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer; var ReleaseBreakpoint: Boolean); var JmpOpcode: Byte; begin if ExceptionRecord.ExceptionAddress = Pointer(FCore.DebugProcessData.EntryPoint) then begin HideDebugger;
Здесь нюансик, адрес библиотеки kernel32.dll одинаков для всех приложений, поэтому адрес функции IsDebuggerPresent будет одинаков и в отладчике и в отлаживаемом приложении.
Смысл патча заключается в обниливании регистра EAX, через который возвращается результат функции и возврата к вызывающему данную функцию коду.
Запускаем отладчик, он запустит наше приложение и в результате вмешательства в память процесса, код в функции FormCreate отладчика не обнаружит. Правда теперь код, который считывает первые 4 байта данной функции вернет нам не число 9090F3EB, а число 90C3C031, которое соответствует опкодам патча.
Как мы можем определить что тело данной функции пропатчено? В принципе мы может считать первые 4 байта данной функции из файла kernel32.dll расположенного на диске, однако в этом случае, при открытии тела библиотеки нам могут подменить путь на такой-же патченый файл и проверка скажет что все нормально.
Но есть еще один способ, достаточно редко применяемый на практике (мне встречался, если не ошибаюсь, всего 1 раз) и заключается он в следующем.
Раз мы не можем считать правильное значение с диска, мы можем его получить, считав нужные нам 4 байта из памяти какого нибудь другого процесса. Есть конечно небольшой шанс, что данный процесс так-же находится под отладчиком и в нем таким-же образом перехвачена требуемая нам функция, но очень маленький.
В итоге пишем такой код:
function IsDebuggerPresent: BOOL; stdcall; external kernel32; procedure TForm1.CheckIsDebugerPresent; var Snapshot: THandle; ProcessEntry: TProcessEntry32; ProcessHandle: THandle; pIsDebuggerPresent: PDWORD; OriginalBytes: DWORD; lpNumberOfBytesRead: DWORD; begin pIsDebuggerPresent := GetProcAddress(GetModuleHandle(kernel32), 'IsDebuggerPresent'); Snapshot := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if Snapshot <> INVALID_HANDLE_VALUE then try ProcessEntry.dwSize := SizeOf(TProcessEntry32); if Process32First(Snapshot, ProcessEntry) then begin repeat if ProcessEntry.th32ProcessID = GetCurrentProcessId then Continue; ProcessHandle := OpenProcess(PROCESS_ALL_ACCESS, False, ProcessEntry.th32ProcessID); if ProcessHandle <> 0 then try if ReadProcessMemory(ProcessHandle, pIsDebuggerPresent, @OriginalBytes, 4, lpNumberOfBytesRead) then begin if OriginalBytes <> pIsDebuggerPresent^ then begin MessageBox(Handle, 'Функция IsDebuggerPresent перехвачена.', PChar(Application.Title), MB_ICONERROR); TerminateProcess(GetCurrentProcess, 0); end; if IsDebuggerPresent then begin MessageBox(Handle, 'Работа приложения под отладчиком запрещена.', PChar(Application.Title), MB_ICONERROR); TerminateProcess(GetCurrentProcess, 0); end; end; finally CloseHandle(ProcessHandle); end; until not Process32Next(Snapshot, ProcessEntry) end; finally CloseHandle(Snapshot); end; end; procedure TForm1.FormCreate(Sender: TObject); begin CheckIsDebugerPresent; CheckCodeProtect; end;
Здесь я не стал мудрить и воспользовался стандартными возможностями TlHelp32 для получения списка процессов, для примера достаточно.
Соответственно очередная заметка в ваш блокнотик — по возможности всегда проверяйте целостность критических API функций, любым известным вам способом, не обязательно делать так, как я показал.
Да, ну и здесь тоже есть очередной нюансик, под семеркой вызов IsDebuggerPresent из kernel32.dll приведет к вызову этой же функции из kernelbase.dll, где ее так же могут пропатчить, но тут уж думайте сами.
В итоге после всех модификаций данная функция у нас плотно опекается системой контроля целостностии и пропатчить ее в лоб уже достаточно проблематично. Как обойти данный кусок защиты, я покажу позже, а сейчас рассмотрим кое что другое.
Детектирование подключения отладчика к процессу.
Вот смотрите, до этого мы запускали приложение под отладчиком, а что нам стоит запустить его без отладчика, дождаться когда все проверки на наличие отладки пройдут и только после этого подключиться отладчиком к приложению?
Да, в таком варианте весь наш код не сработает, точнее сработает, но частично.
Как вариант, для детектирования такого безобразия, можно например поставить таймер и периодически вызывать процедуру CheckIsDebugerPresent, но де-факто, для детектирования подключения нам это не потребуется. Дело в том что при вызове в отладчике функции DebugActiveProcess, в отлаживаемом приложении всегда вызывается функция DbgUiRemoteBreakin. Зная это мы можем провернуть следующий трюк.
Мы пропатчим сами себя, точнее тело функции DbgUiRemoteBreakin, добавив в ее начало переход на адрес функции TerminateProcess, таким образом, как только произойдет подключение отладчика к процессу, процесс сразу же завершится.
Пишем очередной блок кода:
type TDbgUiRemoteBreakinPath = packed record push0: Word; push: Byte; CurrProc: DWORD; moveax: byte; TerminateProcAddr: DWORD; calleax: Word; end; procedure TForm1.BlockDebugActiveProcess; var pDbgUiRemoteBreakin: Pointer; Path: TDbgUiRemoteBreakinPath; OldProtect: DWORD; begin pDbgUiRemoteBreakin := GetProcAddress(GetModuleHandle('ntdll.dll'), 'DbgUiRemoteBreakin'); if pDbgUiRemoteBreakin = nil then Exit; Path.push0 := $006A; Path.push := $68; Path.CurrProc := $FFFFFFFF; Path.moveax := $B8; Path.TerminateProcAddr := DWORD(GetProcAddress(GetModuleHandle(kernel32), 'TerminateProcess')); Path.calleax := $D0FF; if VirtualProtect(pDbgUiRemoteBreakin, SizeOf(TDbgUiRemoteBreakinPath), PAGE_READWRITE, OldProtect) then try Move(Path, pDbgUiRemoteBreakin^, SizeOf(TDbgUiRemoteBreakinPath)); finally VirtualProtect(pDbgUiRemoteBreakin, SizeOf(TDbgUiRemoteBreakinPath), OldProtect, OldProtect); end; end; procedure TForm1.FormCreate(Sender: TObject); begin BlockDebugActiveProcess; CheckIsDebugerPresent; CheckCodeProtect; end;
В результате такого патча в начале функции DbgUiRemoteBreakin будет размещен следующий код:
То есть грубо на стеке размещаются два параметра необходимые функции TerminateProcess (идут в обратном порядке), это параметр uExitCode равный нулю и параметр hProcess, вместо которого подставляется псевдохэндл DWORD(-1) означающий текущий процесс. После чего регистр EAX инициализируется адресом функции TerminateProcess и происходит ее вызов.
Если попробовать присоединится к процессу при помощи отладчика из второй части статьи, то все что мы сможем увидеть — это приход события CREATE_PROCESS_DEBUG_EVENT, но уже даже в момент прихода данного события мы не сможем ничего сделать с отлаживаемым процессом, например попытка установки ВР будет неуспешна, и т.п.
Для большинства отладчиков этого достаточно.
К сожалению, это не бронебойный вариант, ибо ничто не помешает повторно пропатчить тело нашего приложения, вернув оригинальный код обратно, перед вызовом DebugActiveProcess. (Правда я такого не встречал, но все же…)
Обход Memory Breakpoint
Как я уже и говорил, определить наличие МВР можно через проверку атрибута защиты страницы PAGE_GUARD. Делается это при помощи вызова функции VirtualQuery, а можно просто в лоб переназначить атрибуты вызовом VirtualProtect.
Но есть еще один хитрый способ и называется он ReadProcessMemory. Это та же функция, при помощи которой в отладчике мы читали данные из отлаживаемого процесса. Нюанс ее в следующем, если она попробует считать данные с страницы защищенной флагом PAGE_GUARD блок данных соответствующей странице будет заполнен нулями, причем цимус в том, что при этом не произойдет поднятие события EXCEPTION_GUARD_PAGE в отладчике. Такая вот «тихая проверка региона». Если мы будем ее использовать при проверки целостности кода приложения, в том случае если на него будет установлен МВР данные считаются не верно и в итоге контрольная сумма не сойдется с ожидаемой. Более того, если по адресу, откуда будет читать данная функция выставлен Hardware Breakpoint контролирующий запись, чтение/запись отладчик так же не получит уведомления о его срабатывании.
Поэтому перепишем функцию CalcCheckSum следующим образом:
function TForm1.CalcCheckSum(Addr: Pointer; Size: Integer): DWORD; var pRealData, pCursor: PByte; I: Integer; Dumee: DWORD; begin pRealData := GetMemory(Size); try ReadProcessMemory(GetCurrentProcess, Addr, pRealData, Size, Dumee); Result := 0; pCursor := pRealData; for I := 0 to Size - 1 do begin if pCursor^ <> 0 then Inc(Result, pCursor^) else Dec(Result); Inc(pCursor); end; finally FreeMemory(pRealData); end; end;
Таким образом одной единственной функцией мы защищаемся и от ВР, и от МВР, и даже от НВР.
Как это все обойти?
Ну что же, больше усложнять код защиты приложения я не буду, хватит и этой информации.
На некоторых нюансах я еще остановлюсь в конце статьи, а сейчас попробуем написать лоадер на базе отладчика, учитывая тот момент, что мы условились считать код проверки целостности непробиваемым и значит патчить тело проверки мы не будем.
Пишем каркас. Запуск и остановка у нас будет выглядеть так:
constructor TTestDebugger.Create(const Path: string); begin FCore := TFWDebugerCore.Create; if not FCore.DebugNewProcess(Path, True) then RaiseLastOSError; FCore.OnCreateProcess := OnCreateProcess; FCore.OnLoadDll := OnLoadDll; FCore.OnDebugString := OnDebugString; FCore.OnBreakPoint := OnBreakPoint; FCore.OnHardwareBreakpoint := OnHardwareBreakpoint; FCore.OnUnknownBreakPoint := OnUnknownBreakPoint; FCore.OnUnknownException := OnUnknownException; end; destructor TTestDebugger.Destroy; begin FCore.Free; inherited; end;
Второстепенные обработчики я пропущу, их можно будет посмотреть в исходном коде примера, в принципе там ничего нового, все уже было описано в прошлой части статьи.
Первая наша задача каким то образом необходимо отключить детектирование отладчика приложением. Так как приложение проверяет целостность IsDebuggerPresent, а патчить проверку нельзя (по условию задачи) у нас остается только один вариант — изменить значение параметра Peb.BeingDebugged.
Сделаем это следующим кодом:
procedure TTestDebugger.HideDebugger(hProcess: THandle); var pProcBasicInfo: PROCESS_BASIC_INFORMATION; pPeb: PEB; ReturnLength: DWORD; begin if NtQueryInformationProcess(hProcess, 0, @pProcBasicInfo, SizeOf(PROCESS_BASIC_INFORMATION), @ReturnLength) <> STATUS_SUCCESS then RaiseLastOSError; if not ReadProcessMemory(hProcess, pProcBasicInfo.PebBaseAddress, @pPeb, SizeOf(PEB), ReturnLength) then RaiseLastOSError; pPeb.BeingDebugged := False; if not WriteProcessMemory(hProcess, pProcBasicInfo.PebBaseAddress, @pPeb, SizeOf(PEB), ReturnLength) then RaiseLastOSError; end;
Здесь все просто, получаем адрес блока окружения процесса, изменяем параметр BeingDebugged и пишем все обратно. Таким образом функция IsDebuggerPresent перестает реагировать на отладчик. Декларацию используемых структур можно посмотреть в исходнике демопримера.
Первый этап выполнили, теперь второй — надо как-то заставить приложение не реагировать на неверно введенный код и показывать нам картинку в любом случае.
Делается это достаточно просто.
Вы наверное не раз в отладчике меняли значения переменных (это описывалось в первой части статьи). Вот здесь мы сделаем что-то похожее. Как помните отвечает за отображение картинки инструкция JE, если огрубить то представьте что у нас есть булевая переменная и условие if value then..else, если мы прервемся на таком условии то мы сможем контролировать условия выполнение кода, т.е. указать изменением переменной value что именно должно выполнится: блок then или else.
Оператор JE принимает решение о переходе как раз на основе вот такой вот булевой переменной, правда она представлена в виде флага ZF. Если флаг включен происходит прыжок по новому адресу. Стало быть наша задача заставить приложение прерваться на инструкции JE чтобы мы смогли изменить значение данного флага на необходимое нам.
Сделаем это при помощи установки НВР на адрес инструкции JE, т.к. это единственное, что не умеет контролировать наше защищенное приложение. Как узнать данный адрес я пропущу. В примере в составе архива идет исполняемый файл crackme.exe, я его специально вложил в архив из-за того что при каждой перекомпиляции, да и в зависимости от версии дельфи и прочего, этот адрес будет разным. В скомпилированном экзешнике этот адрес уже вычислен и равен значению 0х467840.
Осталось написать код:
procedure TTestDebugger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer; var ReleaseBreakpoint: Boolean); begin if ExceptionRecord.ExceptionAddress = Pointer(FCore.DebugProcessData.EntryPoint) then begin Writeln; Writeln(Format('!!! --> Process Entry Point found. Address: %p', [Pointer(FCore.DebugProcessData.EntryPoint)])); Writeln; HideDebugger(FCore.DebugProcessData.AttachedProcessHandle); FCore.SetHardwareBreakpoint(ThreadIndex, Pointer($467840), hsByte, hmExecute, 0, 'wait JE'); end else begin Writeln; Writeln(Format('!!! --> BreakPoint at addr 0X%p - "%s"', [ExceptionRecord.ExceptionAddress, FCore.BreakpointItem(BreakPointIndex).Description])); Writeln; end; end;
После чего нужно обработать прерывание на НВР и выставить правильное значение флага:
procedure TTestDebugger.OnHardwareBreakPoint(Sender: TObject; ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: THWBPIndex; var ReleaseBreakpoint: Boolean); var ThreadData: TThreadData; begin Writeln; ThreadData := FCore.GetThreadData(ThreadIndex); Writeln(Format('!!! --> Hardware BreakPoint at addr 0X%p - "%s"', [ExceptionRecord.ExceptionAddress, ThreadData.Breakpoint.Description[BreakPointIndex]])); FCore.SetFlag(ThreadIndex, EFLAGS_ZF, True); Writeln; end;
Ну вот и все, можно запускать на выполнение, вводить любое левое значение и наслаждаться картинкой.
Результат будет примерно таким:
Так оно обычно и бывает, думаешь что написал бронебойную защиту, а потом раз и она обходится на коленке, ну не всегда конечно, но бывает
Детектируем Hardware BreakPoint:
Я намерено не остановился на детектировании НВР в рамках защищаемого приложения, по той причине, что будь там такая проверка, то пришлось бы писать достаточно сложный код обхода. А так вообще конечно желательно проверять и их наличие, закрывая таки образом отладчику возможность нормальной работы.
Детект наличия НВР достаточно прост, реализовать можно как через тот же GetThreadContext и проверкой регистра DR7 (если он не пуст — значит стоит НВР), либо, чтобы нас не перехватили на вызове API функции, мы может получить контекст нити при помощи генерации исключения.
Вот первый вариант
procedure TForm1.CheckHardwareBreakPoint; var Context: TContext; begin Context.ContextFlags := CONTEXT_DEBUG_REGISTERS; GetThreadContext(GetCurrentThread, Context); if Context.Dr7 <> 0 then begin MessageBox(Handle, 'Обнаружен HardwareBreaakPoint.', PChar(Application.Title), MB_ICONERROR); TerminateProcess(GetCurrentProcess, 0); end; end;
И второй вариант, в котором поднимается отладочное исключение и снимается информация о контексте нити в обработчике _except_handler.
type // структура для восстановления TSeh = packed record Esp, Ebp, SafeEip: DWORD; end; var seh: TSeh; function _except_handler(ExceptionRecord: PExceptionRecord; EstablisherFrame: Pointer; Context: PContext; DispatcherContext: Pointer): DWORD; cdecl; const ExceptionContinueExecution = 0; begin if Context^.Dr7 <> 0 then begin MessageBox(0, 'Обнаружен HardwareBreaakPoint.', PChar(Application.Title), MB_ICONERROR); TerminateProcess(GetCurrentProcess, 0); end; // возвращаем регистры на место Context^.Eip := seh.SafeEip; Context^.Esp := seh.Esp; Context^.Ebp := seh.Ebp; // и говорим продолжить выполнение Result := ExceptionContinueExecution; end; procedure TForm1.CheckHardwareBreakPoint2; asm // устанавливаем SEH фрейм push offset _except_handler xor eax, eax push fs:[eax] mov fs:[eax], esp // заполняем данные для восстановления lea eax, seh mov [eax], esp add eax, 4 mov [eax], ebp add eax, 4 lea ecx, @done mov [eax], ecx // генерируем исключение mov eax, [0] @done: // удаляем SEH фрейм xor eax, eax pop fs:[eax] add esp, 4 end;
Кстати интересный момент. Обратите внимание на то, сколько информации приходит в обработчик исключения. Вся эта информация нам не доступна в обработчике except, именно поэтому я так часто называю try..finally..except куцей оберткой над SEH
Резюмируя
Теперь вы знаете несколько способов борьбы с отладчиком, правда теперь вы знаете и методы противодействия им, но на то она и статья, чтобы вы делали выводы.
Исходный код с примерами забрать можно по данной ссылке: http://rouse.drkb.ru/blog/dbg_part3.zip
И на этом можно считать мою задачу выполненной.
Все что я хотел рассказать об отладчике, я рассказал. Изначально правда планировалась всего одна статья, но сами видите какое количество материала в итоге получилось
В дальнейшем я еще буду рассказывать об использовании отладчика, но это будет уже совершенно другая история.
—
© Александр (Rouse_) Багель
Москва, ноябрь 2012
Simple x64 Windows Debugger
Just a simple x64 Windows debugger written in C++
Documentation
Used WinAPI functions
These are the usefull functions provided by the Windows API which are used for making the core functionalities of this debugger.
Processes functions
-
CreateProcessA : Creates a new process. Setting DEBUG_PROCESS as a flag for the dwCreationFlags parameter allows the process to receive all related debug events using the WaitForDebugEvent function.
-
OpenProcess : Opens an existing process. In order to perform debugging, we have to set the dwDesiredAccess parameter to PROCESS_ALL_ACCESS.
Debugging functions
-
DebugActiveProcess : Attach the debugger to an active process.
-
WaitForDebugEvent : Waits for a debugging event to occur in a debugged process. The provided DEBUG_EVENT structure contains a dwDebugEventCode member that can informs us if the event comes from a breakpoint (EXCEPTION_DEBUG_EVENT). If the event is triggered by a breakpoint, then the u member would be an EXCEPTION_DEBUG_INFO structure which can provides us extra informations about the event via its EXCEPTION_RECORD structure member.
-
ContinueDebugEvent : Enables a debugger to continue a thread that previously reported a debugging event. The options to continue the thread that reported the debugging event have to be specified inside the dwContinueStatus parameter.
Threads functions
-
CreateToolhelp32Snapshot : Creates a snapshot of a given process. Setting TH32CS_SNAPTHREAD as a flag for dwFlags will provides all the threads in the snapshot. We will then have to compare each thread’s owner ID to the ID of the debugged process.
-
Thread32First : Retrieves the first thread of a process’ snapshot as a THREADENTRY32 structure.
-
Thread32Next : Loops through the rest of the threads of a process’ snapshot.
-
OpenThread : Opens a thread so we can get its context.
-
GetThreadContext : Retrieves the context of a given thread in which we can find all its registers’ states. Feeding the CONTEXT with CONTEXT_FULL and CONTEXT_DEBUG_REGISTERS grants us access to all of the thread’s registers we need.
-
SetThreadContext : Sets the context of a given thread which allows us to modify its registers’ states.
Memory functions
-
ReadProcessMemory : Reads the memory of a process at a given address.
-
WriteProcessMemory : Writes to the memory of a process at a given address.
Memory pages related functions
-
GetSystemInfo : Provides us a SYSTEM_INFO stucture that contains a dwPageSize member which gives us the correct page size of the system.
-
VirtualQueryEx : Retrieves informations about the memory page of a given address of a process. The MEMORY_BASIC_INFORMATION structure provides us the BaseAddress of the memory page as well as its access Protection (which are defined in the Memory Protection Constants)
-
VirtualProtectEx : Allows us to edit the access protection of a given memory page of a process. We can add a GUARD_PAGE access protection to a memory page in order to trigger memory breakpoint on access to this page.
Address resolving functions
-
GetModuleHandle : Provides a HMODULE handle of a specified loaded module.
-
GetProcAddress : Retrieves the address of an exported function or variable from a given module handle.
Seriously, it will take you longer to read this long introduction, than to code a working debugger in C#.
You may also want to check out:
- Accessing stack traces with MDbgEngine and PADRE
- Performance impact of running under MDbgEngine
Bugs in production
Remember all those reports coming back form clients saying “hey, your program crashed” or “hey, your is site showing these ugly yellow pages at random moments”? So do I. Unfortunately, there isn’t much that can be done to diagnose problems in running software, basically we’re at the mercy of our clients’ reports (which vary in quality, most of the time tending towards useless) or more or less verbose logging. Having those, we can reproduce issues during a debugging session to see what code causes them. If that fails, we can go to the extremes of attaching a debugger in a production environment. Not feeling enough pressure today? Attach to a live site and try to setup breakpoints the exact way needed to only catch the error, and not stop a milion people from doing their daily work. At first try. Logging, on the other hand is very safe, it but will only tell you as much as you predicted that would be needed.
Now what if we could have something in the middle? More than logging, but less than an interactive debugging session?
Windows Error Reporting
In the ancient times, when not all software run as web applications, and especially in the even darker times when programs were being written mostly in native code, you could often see hard crashes that literally made entire applications disappear. Then Microsoft invented Windows Error Reporting, that filled that post-crash emptiness with a nice-looking window that supposedly gathered some data and sent it back to the author. This feature is present in all recent versions of Windows, and is available not only for Microsoft applications, but also for third party programs. Provided that their author gets a signing certificate (authenticode), signs all components of the software and registers on a deeply hidden Microsoft site. If all these conditions are met, Windows will automatically create a so called minidump of the process when it crashes (with important parts of memory, including stack traces of all threads) and ask the user to allow sending it to Microsoft.
Since a minidump contains stack state of all threads at the moment of the crash, it allows you to open a post-mortem debugging session, and feel almost as if you were attached to the original process when the failure occured. WinDbg has supported this since… probably always, but Visual Studio 2010 does so too – although through a slightly hidden feature of just opening (File -> Open -> File…) minidumps, i.e. *.dmp files.
Just having access to precise call stacks used to be a killer feature at the time, but does it provide any value to the .NET developer, used to getting stack traces with every exception?
You guessed it: yes, it does.
With a minidump, you’ll be able to see parameter and variable values for all functions on the call stack.
Crashes in a .NET world
Before we go deeper, we need to clarify one thing. What exactly is a crash? What causes Windows Error Reporting to kick in? It’s unhandled exceptions. Native unhandled exceptions. Normally, the last exception handler registered in a running process is a system function that reports program failure, which in turn causes a default (defined in the registry) system just-in-time debugger to launch – which will be Windows Error Reporting a.k.a Dr. Watson, Visual Studio, or something else depending on your setup. Some programs also register their own unhandled exception handlers to report errors. Firefox does that, Winamp too. Perhaps you’ve already seen their crash windows in action. By registering their own handlers, these programs can create minidumps of their own liking (there are various types to choose from) and send them wherever they want.
What about .NET apps then? Do they crash at all? Of course they do, but unfortunately not in the sense described so far. You can have an unhandled managed exception in your code, but it will always be caught by the runtime – which in turn prints a stack trace (in a console application), shows a pop-up window (in a Windows Forms application) or displays the Yellow Screen of Death (in a web application). No minidump will ever be created automatically for a .NET application.
Creating minidumps manually
A minidump is created by calling MiniDumpWriteDump
from dbghelp.dll
. Mind though, that it needs to be a version from Debugging Tools for Windows, not the Windows system folder. There are various tools that call it, like SysInternals’ ProcDump. MiniDumpWriteDump and ProcDump have been described in an article in the recent edition of MSDN Magazine: Writing a Plug-in for Sysinternals ProcDump v4.0.
One of those tools is also ClrDump, which is available both as a very simple command-line tool and a convenient library. Read this blog post, and you’ll know everything about how to create minidumps from .NET applications: Creating and analyzing minidumps in .NET production applications.
Choosing the right place and time
The last article I linked to has one thing wrong though. It triggers minidump generation from AppDomain.UnhandledException
and Application.ThreadException
. And it would be equally wrong to call it from an error handler in Global.asax
or to register one with an HttpModule, like Elmah does. The reason is that those handlers are called from a generic exception handler at the base of your application. At this point all of the context (call stack, variables) available where the exception was thrown is lost. To really benefit from minidumps, you need to trigger them as deep in your call hierarchy as you can. Also, often you’ll face exceptions that are caught by your own code, but you’ll still want minidumps generated, to better explain why those exceptions occured.
And let’s not forget generating minidumps like this is similar to debugging with printf – you need to modify code and redeploy. We need something smarter. And external to the inspected application.
Writing a managed debugger in C#
The CLR exposes debugging capabilities through COM, so it isn’t impossible for a mere mortal to implement a debugger. But thanks to the CLR Managed Debugger Sample anyone can do it, really. The sample mainly consists of a console tool, mdbg.exe
, that you can use as a poor-man’s replacement for windbg.exe
or cdb.exe
, and – more importantly – a shared debuging engine library, written in C#. The latter is now also available on NuGet (courtesy of yours truly) as Microsoft.Samples.Debugging.MdbgEngine.
Let’s consider this trivial console test application:
namespace TestApplication { class Program { static void Main(string[] args) { A(1, new { property = 2 }); } private static void A(int i, object state) { B(i + 4, state); } private static void B(int i, object state) { try { C(i); } catch (Exception exception) { Console.WriteLine(exception.Message + ": " + i); } } private static void C(int i) { throw new InvalidOperationException("Expected failure"); } } }
Here is an only slighly longer console application that runs a debugger and creates minidumps on each exception (as you can see, also those caught in code):
using Microsoft.Samples.Debugging.CorDebug; using Microsoft.Samples.Debugging.MdbgEngine; namespace DebuggerApplication { class Program { static void Main(string[] args) { var stop = new ManualResetEvent(false); var engine = new MDbgEngine(); var process = engine.CreateProcess("TestApplication.exe", "", DebugModeFlag.Default, null); process.Go(); process.PostDebugEvent += (sender, e) => { if (e.CallbackType == ManagedCallbackType.OnBreakpoint) process.Go(); if (e.CallbackType == ManagedCallbackType.OnException2) { ClrDump.CreateDump(process.CorProcess.Id, @"C:\temp.dmp", (int)MINIDUMP_TYPE.MiniDumpWithFullMemory, 0, IntPtr.Zero); } if (e.CallbackType == ManagedCallbackType.OnProcessExit) stop.Set(); }; stop.WaitOne(); } } }
Go ahead and try it. If you open test.dmp
in Visual Studio, you’ll be able to see not only the call stack at the moment when the exception occurred, not only values of variables that are easy to log (like i
), but you’ll be also able to inspect complex and dynamic structures (like state
).
I hope you’re having a big “holy shit” moment. I definitely did.
Where can we go from here?
The code above “logs” all exceptions. To implement this idea in the real world, we’d need quite a bit more – load symbols (you know where to get those from – SymbolSource), set breakpoints at specific locations, catch only specified exceptions.
Catch me on Twitter if you like the idea. This has potential for a great and very useful open source project.
We also have some very cool ideas how to integrate this with SymbolSource. Imagine setting breakpoints on the website and choosing exceptions to monitor, then seeing automatically collected reports from all your deployments. Call stacks with variable values, failure statistics, minidumps for download and offline analysis.
Interested?
Preface
This article is the sequel to previous article Writing basic Windows Debugger, and it is mandatory for the reader to read and understand the
first part first! Without grasping what is mentioned in first part of this series, you may not understand what is
presented here, nor you would be able to appreciate this stuff!
One thing I should mention, which I did not state in previous part, is that our debugger is only able to debug
Native Code. Thus, an attempt to debug a .NET/managed application would fail. May be, in next
article I would cover debugging the Managed Code also.
I am here to present more of intriguing aspects, in debugging parlance, which you might not know before. This
would include showing the source code and the call stack, setting the breakpoints, stepping into code, attaching
(our) debugger to a process, making it a default debugger and so on. Since this article presents somewhat
advanced concepts in debugging, I removed «basic» word from the title!
The debugging experience would be as per with Visual C++, that includes debugging terms (Step-Into), shortcuts
(F5) etc.
Table of contents:
- Start Debugging from main
- Obtain start-address of the process
- Place breakpoint instruction at start-address
- Handle breakpoint, revert instruction
- Halt debugging, wait for user action
- Continue debugging as per user’s command
CDebuggerCore
— The debugging-interface class- Debugging Actions
- Enumerating source files, and line numbers
- Placing User-Breakpoints
- Stepping through the code (Step-in, step-out etc)
- Conditional Breakpoints
- Debugging a Running Process
- Detach, Terminate or wait?
- Debugging a Crashing Process
- Manually attaching a Debugger
Let’s Debug!
So, what you do when you want to debug your program? Well, mostly we hit F5
to begin debugging our
applications, and the Visual Studio Debugger would halt at the places where we have placed breakpoints (or
conditional breakpoints!). A «Debug Assertion Failed» message box, followed by Retry button also
opens the source code, and halts there. The DebugBreak
call or {int 3}
instruction would
also do the same. There are other approaches for debugging, you know!
Rarely or occasionally, we also start debugging from the beginning by hitting F11 (Step-Into),
and the VS starts from main
/wmain
or WinMain
/wWinMain
(or
_t
prefixed variants of them). Well, that is the logical start address of
your process where debugging begins. I call it logical since it is not the actual start address —
the address also known as Entry Point of the module. For a console application, it is
mainCRTStartup
which calls main
function, and VS Debugger starts at main
.
The entry-point is also applicable for DLLs too. Please have a look at /ENTRY switch for more information.
This turns out that we need to stop program execution at the entry-point location, and
let user (programmer) continue debugging. Yes, I said «stop» the execution at entry-point location — the
process is already started (being debugged), and unless we halt it somewhere the process would complete its
startup. The following call-stack appeared as soon as I hit F11, for an MFC application — which clarifies what I
mentioned.
What we need to do, to stop program execution at Entry-Point?
In a nutshell:
- Obtain the start address of process
- Modify the instruction at that address — replace it with breakpoint instruction, for example.
- Handle the breakpoint event, revert the instruction with original instruction
- Stop execution, show call stack, display registers and source code, if available.
- Continue debugging (as per user request)
And this 5-step task is not easy, boy!
1. Obtain the Entry Point of the Process
Start Address, Entry Point and the logical* entry point (your main
/WinMain
) —
welcome to the complex world. Before I present textual content about these terms, let me give you visual idea about
it. But first thing you should understand: the first instruction at the given address is the point where execution
begins, and debuggers play with that address only.
* [This term is coined by me, and is relevant to this article only!]
Here is how WinMain
looks in Disassembly View in Visual Studio, with annotations for the
gibberish you see:
You can launch the same view for you code by right clicking in code editor, and selecting Go To
Disassembly. The Code Bytes are not shown by default (Green content above), you enable it by Show
Code Bytes context menu.
Relax! You need not to understand the machine language instructions, nor the ASM language! This is just for
illustration. For the example above, 00978F10
is the start-address, 8B FF
is the first
instruction. We just need to make the given instruction (mov
blah blah), a breakpoint
instruction. We know the API called DebugBreak
— but that cannot be used here. {int 3}
is the assembly code for the same, and we need the x86 instruction for the same. The breakpoint x86
instruction is 0xCC(204).
Thus, we just replace 8B
with CC
and we are done! When the program continues further
(within our debugger-loop), it would raise an exception event (EXCEPTION_DEBUG_EVENT
), having the
exception code EXCEPTION_BREAKPOINT
(0x80000003
). We know this is our sin, we handle is
appropriately. If you don’t understand this paragraph, I request you (last time) to read the first part of
this article.
The x86 instructions are not of fixed width — but who cares? We don’t need to see if instruction is of
1, 2, or N bytes. We always replace the first byte. The first byte (of instruction) may be
anything, and not just 8B
! But, we must ensure that as soon as desired breakpoint is
hit, we reverse our sin — i.e. replace the replaced instruction with original code-byte.
For few geeks out there, who know it, and for everyone else who do not know it. The breakpointing is
not the only way to stop program at start-address. There is better alternative for these One-shot/stop-once
breakpoints, which is to be discussed. Secondly, CC
instruction is not the only breakpoint instruction
— but it is sufficient enough for us.
There is more of complexity involved with Start-Address, but to keep grabbing your interest, let me
jump straightaway to C++ code that retrieves the start address. The CREATE_PROCESS_DEBUG_INFO
has
member named lpStartAddress
, that represents the start address. We can read this information while
processing the very first debugging event:
// This is inside Debugger-loop controlled by WaitForDebugEvent, ContinueDebugEvent switch(debug_event.dwDebugEventCode) { case CREATE_PROCESS_DEBUG_EVENT: { LPVOID pStartAddress = (LPVOID)debug_event.u.CreateProcessInfo.lpStartAddress; // Do something with pStartAddress to set BREAKPOINT. ... ...
The type of CREATE_PROCESS_DEBUG_INFO::lpStartAddress
is LPTHREAD_START_ROUTINE
, and I
assume you know what exactly it is (function pointer). But, as I mentioned before, there are complexities around
start address. In short, this address (lpStartAddress
) is relative to where image is actually loaded
in memory. To make all this sound convincing and significant, let me show you the output of dumpbin
utility with /HEADERS
switch:
dumpbin /headers DebugMe.exe ... OPTIONAL HEADER VALUES 10B magic # (PE32) 8.00 linker version A000 size of code F000 size of initialized data 0 size of uninitialized data 11767 entry point (00411767) @ILT+1890(_wWinMainCRTStartup) 1000 base of code
This address (00411767
) is what I received with lpStartAddress
member, while debugging
from our debugger. But when I tried to debug the same executable from Visual Studio, the address at
wWinMainCRTStartup
was somewhat different (@ILT stuff has no relevance for this).
Thus, allow me to postpone discussing intricacies around start address. Let’s just have GetStartAddress()
user defined function, whose code would be disclosed later. This would return the exact address where we
would put the breakpoint instruction!
2. Modify instruction at start-address with a breakpoint instruction
Once we get the start-address, modifying the instruction at that location with breakpoint (CC
) is
quite trivial. We need to do:
- Read one byte from that memory location, store it.
- Write
0xCC
instruction at that location. - Flush the instruction cache.
- Continue debugging.
Two important questions should hit you now:
- How we read, write and flush the instructions?
- When we do the same?
Let me uncover the second query first. We do read, write and flush instructions while processing
CREATE_PROCESS_DEBUG_EVENT
debug event (or optionally at EXCEPTION_BREAKPOINT
event).
When the process is being loaded, we grab the actual start address (I mean the CRT-Main’s address!), we
read the very first instruction at that location of one byte, store it, and place the breakpoint
(0xCC
) at that instruction. Then we let our debugger to ContinueDebugEvent
.
To depict how to do the same, here I present relevant code!
DWORD dwStartAddress = GetStartAddress(m_cProcessInfo.hProcess, m_cProcessInfo.hThread); BYTE cInstruction; DWORD dwReadBytes; // Read the first instruction ReadProcessMemory(m_cProcessInfo.hProcess, (void*)dwStartAddress, &cInstruction, 1, &dwReadBytes); // Save it! m_OriginalInstruction = cInstruction; // Replace it with Breakpoint cInstruction = 0xCC; WriteProcessMemory(m_cProcessInfo.hProcess, (void*)dwStartAddress,&cInstruction, 1, &dwReadBytes); FlushInstructionCache(m_cProcessInfo.hProcess,(void*)dwStartAddress,1);
About the code:
m_cProcessInfo
is member of class (not yet presented), which is nothing butPROCESS_INFORMATION
and is filled-in byCreateProcess
function.- The user defined function
GetStartAddress
returns desired start-address of the process.For a GUI based unicode application, it would return the address of
wWinMainCRTStartup
. - Next we call
ReadProcessMemory
to grab what is at the given start-address. We store it in a membervariable of class.
- The we replace the instruction with
0xCC
, a breakpoint instruction, usingWriteProcessMemory
. - Finally we call
FlushInstructionCache
, so that CPU would read the new instruction, and notany of cached (old) instruction. The CPU may or may not cache the CPU instructions, but you should always
flush it.
Remember that Read
ProcessMemory
requires permission
PROCESS_VM_
READ.
Likewise, WriteProcessMemory
requires permission
PROCESS_VM_READ|PROCESS_VM_OPERATION
— all these permissions are already granted to the debugger, as
soon as Debugging-Flag was given to CreateProcess
. Therefore, we need not to do anything, and
reading/writing would always succeed (at valid locations, of course!).
3. Handling breakpoint instruction, and reverting the original instruction.
As you know that breakpoint (EXCEPTION_BREAKPOINT
) instruction is a kind of exception that
arrives under EXCEPTION_DEBUG_EVENT
debugging event. We handle the exception events through
EXCEPTION_DEBUG_INFO
structure. The following code would help you recollect and understand it:
// Inside debugger-loop switch(debug_event.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT: { EXCEPTION_DEBUG_INFO & Exception = debug_event.u.Exception; // Out of union // Exception.ExceptionCode would be the actual exception code. ...
The OS would always send one breakpoint instruction to the debugger, which is just an indication that process is
being loaded. That is why you can also perform «placing breakpoint instruction at start-address»
on this very first breakpoint exception. This would ensure that after this very first breakpoint, all next
breakpoints are yours!
Irrespective of where you put your breakpoint instruction for the start-address, you still need to
ignore the first breakpoint event. Though, debuggers like WinDbg, would also show you this
breakpoint also. Visual C++ debugger, for instance, on other hand, does not give any indication about this very
first breakpoint, and starts executing at Logical beginning of program
(main
/WinMain
and not CRT-Main).
Therefore, the breakpoint handling code would look like:
// 'Exception' is the same variable declared above switch(Exception.ExceptionRecord.ExceptionCode) { case EXCEPTION_BREAKPOINT: if(m_bBreakpointOnceHit) // Would be set to false, before debugging starts { // Handle the actual breakpoint event } else { // This is first breakpoint event sent by kernel, just ignore it. // Optionally display to the user that first BP was ignored. m_bBreakpointOnceHit = true; } break; ...
You can also use the else-part to place the breakpoint instead of placing it on process-start event.
Either way, the main breakpoint event handling code goes in if-part. We need to handle the breakpoint that
we had placed at start-address.
Now it goes tricky and intriguing — you need to concentrate, read carefully, sit relaxed (adjust your butt on
the chair, if you need to!). If you have not taken a break reading this article, take it now!
In simple terms, the breakpoint event has occurred at the location/address where we placed it. We now just halt
debugging, show call stack (and other useful elements), revert the original instruction and wait for user input to
continue debugging.
At assembly or machine-code level, when any exception occurs and the debugger notified, the faulting instruction
has already executed — though, for breakpoint, it would be just of one byte. The instruction pointer has already
moved ahead by few bytes, depending on instruction size. And the system has given us this exception-event to
handle.
Thus, in addition to writing back the original instruction at the «breakpoint-location», we need to
play with CPU registers also. The registers, that are specific to the process (more specifically, thread), can be
retrieved and modified using two Windows functions: GetThreadContext
and
SetThreadContext
. Both of the functions take a CONTEXT
structure. Strictly speaking, the
members of this structure are dependent on processor-architecture. Since, this article is about x86 architecture,
we would follow the structure definition for the same, and that can be found in winnt.h
header.
This is how we retrieve the thread-context of a given thread:
CONTEXT lcContext; lcContext.ContextFlags = CONTEXT_ALL; GetThreadContext(m_cProcessInfo.hThread, &lcContext);
Okay! Now you retrieved the thread-context, now what?
The EIP register tells you the Instruction Pointer, which is
the next instruction to be executed. This is represented by Eip
member of CONTEXT
structure. As I mentioned already, the EIP has already moved ahead, and we need to move it back. Luckily for us, we
just need to move it by exactly one byte, since BP instruction was of one byte. The following code does the
same:
lcContext.Eip --; // Move back one byte SetThreadContext(m_cProcessInfo.hThread, &lcContext);
The EIP is the location from where the CPU would pick the next instruction and start executing. Yes, we need to
have THREAD_GET_CONTEXT
and THREAD_SET_CONTEXT
access permissions to successfully execute
these function, and we do already have.
For a while, allow me to take a Context-Switch to another frame: Reverting the original instruction! To
write back the original instruction into the running process, where BP instruction was placed, we just need to use
WriteProcessMemory
followed by FlushInstructionCache
. This is how we can do it:
DWORD dwWriteSize; WriteProcessMemory(m_cProcessInfo.hProcess, StartAddress, &m_cOriginalInstruction, 1,&dwWriteSize); FlushInstructionCache(m_cProcessInfo.hProcess,StartAddress, 1);
The original-instruction is reverted this way. We then call ContinueDebugEvent
. The combined code
would look like:
//// In the breakpoint event switch-case: // 1. GetThreadContext, reduce EIP by one, SetThreadContext // 2. Revert the original instruction // 3. Continue debugging
Well, where is the call-stack? Registers? Source code? And when did the program halt? It all continued without
user interaction/intervention!
4. Stop execution, show call stack, display registers and source code, if available.
To display the call stack we need to load the debugging symbols, which would exist in relevant
.PDB files. The set of functions from DbgHelp.DLL
would help us to load symbols,
enumerate the source files, walk through the call stack and so on. All these would be discussed soon.
To render the CPU registers, we just need to display relevant registers from the CONTEXT
structure.
To display the 10
registers as shown by Visual Studio debugger (Debug->Windows-
>Registers, or Alt+5), we can use following code to generate text:
CString strRegisters; strRegisters.Format( L"EAX = %08X\nEBX = %08X\nECX = %08X\n" L"EDX = %08X\nESI = %08X\nEDI = %08X\n" L"EIP = %08X\nESP = %08X\nEBP = %08X\n" L"EFL = %08X", lcContext.Eax, lcContext.Ebx, lcContext.Ecx, lcContext.Edx, lcContext.Esi, lcContext.Edi, lcContext.Eip, lcContext.Esp, lcContext.Ebp, lcContext.EFlags );
And display the generated text to relevant window.
To halt program execution, until user gives appropriate commands (Continue, StepIn, Stop Debugging etc), we do
not call ContinueDebugEvent
. Since the debugger-loop would be in another thread than the GUI thread,
we just ask GUI thread to display relevant information, and let the debugger-thread wait for some
«event» to occur. On user command, we trigger that «event», which would end the wait in
debugger-thread.
Confused? The event in quotes is nothing but a windows event, which is created by
CreateEvent
. To halt execution we just call WaitForSingleObject
(in debugger-thread). To
let debugger-thread to continue from that wait-location, we just call SetEvent
(from GUI-thread). Of
couse, depending on your taste, you can use other synchronization primitive, or some class (CEvent
?)
to do the same. This paragraph only gave the rough idea how we can implement Halt-and-Continue.
With this additional desription, the logical-code to achieve all this would be:
GetThreadContext
, reduce EIP by one, callSetThreadContext
.- Revert the original instruction, using
WriteProcessMemory
,FlushInstructionCache
. - Display the relevant members of
CONTEXT
— Registers View. - With the help of symbol-information functions, locate the source file and line (if possible) and
display the Source Code!
- With stack-walking functions and symbol-info functions, enumerate the Call Stack of debuggee and
redner on UI.
- Since the GUI thread is intimated by now, we wait for use action through «Event«.
- On wait-complete, we perform the action initiated by user (Continue, Step, Stop…)
- Call
ContinueDebugEvent
.
Astonished? Great! I hope you are enjoying the Debugging !
One important point worth mentioning here — the thread which is being debugged may not be primary
thread of debuggee, but the thread that caused this breakpoint event to occur. Till now, we are still processing
the very first BP event that we had created to stop execution at process-startup. But the eight-step process I
listed above would be applicable for all debugging events (from any debuggee’ thread), that can
cause program to halt.
There is slightly more of complexity involved with instruction modification and EIP change. Whilst I would show
the solution to it later, let me just mention the problem. The breakpoints may also be placed by
user; and for the same, we would just replace the instructions with CC
instructions on those addresses
(yes, saving the original instructions). When those breakpoints would arrive, we would just revert the instructions
and perform the 8-step processing that I am currently describing. Fine enough? Well, that would prevent the user-
defined breakpoint events to arrive only once! If we don’t revert original instruction — well, that’s a total
mess!
Anyway, let me continue further!
Ah! Source-Code! I know you are dying to see how this can be retrieved!
Any EXE or DLL image you build may have Debugging Information attached with it in form of a
.PDB file. Few points on this:
- The debugging-information is only available when
/DEBUG
switch is given in linker settings. In VS,you can see this here: Linker->Debugging->Generate Debug Info.
- The
/DEBUG
symbol does not mean that the EXE/DLL would be Debug build. The preprocessor macro_DEBUG
/DEBUG
controls the debug build at compile time. Other linker settings andappropriate library imports define the meaning of debug build at link time.
- This means, a Release build can have debugging-information, and a Debug build may not have
debugging-information (no
/DEBUG
linker switch, but_DEBUG
macro defined). - A file with .PDB extension stores the debugging-information, which is generally
program-name.PDB, but it can be renamed in linker options. This file contains all source code
information stored — functions, classes, datatypes and so on.
- The linker puts small foot print about this .PDB file in image-header of the EXE/DLL generated. Since this
information is put into headers section, it does not impact the performance of generated binary at all.
Only the file size increases by few bytes/KBs.
To gain this debugging information, we need to use Sym* functions hosted inside
D
bgHelp.DLL
. This DLL is most important component for source-code level debugging. It
also hosts functions to walk the call stack, and to retrieve information about image (EXE/DLL). Click here to see all
functions exported by this DLL. The required header is Dbghelp.h, and required library is
DbgHelp.lib
To gain debugging information, we need to initialize Symbol Handler for the given process.
Since, the target process is the debuggee, we initialize with the process-handle of debuggee.To initialize symbol
handler, we need to call SymInitialize function:
BOOL bSuccess = SymInitialize(m_cProcessInfo.hProcess, NULL, false);
The first parameter is the handle of running process for which symbol-information is required. Second parameter
is semi-colon separated paths, which specifies where to look for symbol files (for us, the .PDB
files). The third parameter says if the symbol-handler should load the symbols for all modules automatically or
not. Since we need to show if debugging-information is found or not, we pass false
.
The following text would make some sense:
'Debugger.exe': Loaded 'C:\Windows\SysWOW64\msvcrt.dll', Cannot find or open the PDB file 'Debugger.exe': Loaded 'C:\Windows\SysWOW64\mfc100ud.dll', Symbols loaded.
Visual Studio (2010) could not locate symbols for msvcrt.dll
, thus the error. The DLL,
mfc10ud.dll
has its debugging information available, therefore VS was able to load it. This
essentially means that, for MFC DLL, VS would show symbol information, viz source code, function/class names in
call-stack and so on. To explicitly load the symbol file for respective DLL/EXE we call
SymLoadModule64
/ SymLoadModuleEx
function.
Where should we call these functions for the debuggee? It took me lot of time to locate, since I was trying to
initialize and load symbols before the debugging-loop (i.e. before any debug-event, but after
CreateProcess
). It does not work that way. We need to call it on
CREATE_PROCESS_DEBUG_EVENT
debugging event. Since, we are saying «no» to automatic
symbol loading for the dependent modules, we need to call SymLoadModule
64
/Ex
for the EXE file just loaded. For upcoming LOAD_DLL_DEBUG_EVENT
s, we also need to call this function
for the respective DLL being loaded. Depending on outcome of symbol-loading-for-image, we let the end user know if
debugging-information is available or not.
Sample code, that loads the module when a DLL-Load event is encountered. Function named
GetFileNameFromHandle
is described in previous part (code available).
case LOAD_DLL_DEBUG_EVENT: { CStringA sDLLName; sDLLName = GetFileNameFromHandle(debug_event.u.LoadDll.hFile); DWORD64 dwBase = SymLoadModule64 (m_cProcessInfo.hProcess, NULL, sDLLName, 0, (DWORD64)debug_event.u.LoadDll.lpBaseOfDll, 0); strEventMessage.Format(L"Loaded DLL '%s' at address %x.", sDLLName, debug_event.u.LoadDll.lpBaseOfDll); ...
Of course, similar code goes for process-load event. A caveat: initializing the symbol header and loading the
respective module successfully does not mean source-code information is available! We need to call
SymGetModuleInfo64
to determine if symbols in PDB are available. Here is how we do it:
// Code continues from above IMAGEHLP_MODULE64 module_info; module_info.SizeOfStruct = sizeof(module_info); BOOL bSuccess = SymGetModuleInfo64(m_cProcessInfo.hProcess,dwBase, &module_info); // Check and notify if (bSuccess && module_info.SymType == SymPdb) { strEventMessage += ", Symbols Loaded"; } else { strEventMessage +=", No debugging symbols found."; }
I am very thankful to Jochen Kalmbach for his excellent article on Stack-Walking, which has helped
me in locating source code information and walking the call-stack.
When the symbol type is SymPdb
, we have source-code information available. Remember, a successful
PDB load does not mean source-code is available, the PDB only contains information about source-
code, the source code (.H, .CPP and other files) should be available at given path to view! The PDB contains
symbols names, file names, line number information etc. Stack-walking (without source-code view) is perfectly
possible, with fully qualified function names.
Finally, when breakpoint event arrives we can retrieve the call stack and display it. To do this we need to use
StackWalk64
funtion. The following code is stripped version, which shows the usage of this function.
Please refer to the Stack-Walking article I mentioned above for complete details.
void RetrieveCallstack(HANDLE hThread) { STACKFRAME64 stack={0}; // Initialize 'stack' with some required stuff. StackWalk64(IMAGE_FILE_MACHINE_I386, m_cProcessInfo.hProcess, hThread, &stack, &context, _ProcessMemoryReader, SymFunctionTableAccess64, SymGetModuleBase64, 0); ...
STACKFRAME64
is the data-structure that holds addresses from where call-
stack information is retrieved. It essentially tells that current instruction location in given thread. For x86, as
described by Jochen, we need to initialize following members of this structure before calling
StackWalk64
function:
CONTEXT context; context.ContextFlags = CONTEXT_FULL; GetThreadContext(hThread, &context); // Must be like this stack.AddrPC.Offset = context.Eip; // EIP - Instruction Pointer stack.AddrPC.Mode = AddrModeFlat; stack.AddrFrame.Offset = context.Ebp; // EBP stack.AddrFrame.Mode = AddrModeFlat; stack.AddrStack.Offset = context.Esp; // ESP - Stack Pointer stack.AddrStack.Mode = AddrModeFlat;
In call to StackWalk64
, the first constant specifies the machine type, which is x86. Second
argument is the process (the debuggee). Third is the thread whose call stack we need to retrieve (not necessarily
primary thread). Fourth parameter is in/out parameter, which is most important argument for this function-call.
Fifth is a context structure, having required addresses initialized. Function _ProcessMemoryReader
is
user defined function, which does nothing except calling ReadProcessMemory
. Other two
Sym*
functions are from DbgHelp.DLL. The last parameter is also function-pointer, and we don’t need
that.
Walking the call stack definitely requires a loop till stack-walk finishes. Whilst there are issues
like invalid call-stack, never ending call stack and things like that. I chose to keep it simple: run it till
Return-Address becomes zero, or till StackWalk64
fails. This is how we enumerate the call
stack (symbol-name retrieval not yet shown):
BOOL bSuccess; do { bSuccess = StackWalk64(IMAGE_FILE_MACHINE_I386, ... ,0); if(!bTempBool) break; // Symbol retrieval code goes here. // The contents of 'stack' would help determining symbols. // Which would put information in a vector. }while ( stack.AddrReturn.Offset != 0 );
The symbol has few properties attached to it:
- The module (DLL or EXE name)
- The name of symbol — undecorated or decorated.
- The type of symbol — function, class, parameter, local variable etc.
- The virtual address of symbol.
Stack-walking also includes:
- Source-file
- Line number
- First instruction (machine instruction) on that line.
Though, we don’t need machine-instruction, unless dis-assembling the code, we may need the displacement from
first instruction. At source-code level, this may happen if multiple instructions exist (like multiple function
calls), on the same line. For now, I am omitting it altogether.
Hence, we need the module name, the function call and the line number as a complete Stack-entry.
To retrieve module name, respective to the address on stack, we use
SymGetModuleInfo64
. If you recollect, there was similar function to load the
module info — SymLoadModule
XX, which is required to be called previously for
SymGetModuleInfo64
to work. Following code (which is just after StackWalk64
call inside
the loop), demonstrates how to retrieve module information for given address:
IMAGEHLP_MODULE64 module={0}; module.SizeOfStruct = sizeof(module); SymGetModuleInfo64(m_cProcessInfo.hProcess, (DWORD64)stack.AddrPC.Offset, &module);
The variable module.ModuleName
would contain the name of module, without extension
or path. Member module.LoadedImageName
would refer to full module
name with full path and extension. The member module.LineNumbers
would tell if Line-
number information is available or not (1
=available). There are other useful members too.
Next, we retrieve the function name for this stack-entry. We retrieve this information using
SymGetSymFromAddr64
or SymFromAddr
. Former returns
information via PIMAGEHLP_SYMBOL64
having 6 members in structure, while the latter returns via
SYMBOL_INFO
which returns rich symbol information. Both take four arguments, out of which first 3 are
same, and last argument is pointer to desired structure. Here is an example of first function:
IMAGEHLP_SYMBOL64 *pSymbol; DWORD dwDisplacement; pSymbol = (IMAGEHLP_SYMBOL64*)new BYTE[sizeof(IMAGEHLP_SYMBOL64)+MAX_SYM_NAME]; memset(pSymbol, 0, sizeof(IMAGEHLP_SYMBOL64) + MAX_SYM_NAME); pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL64); // Required pSymbol->MaxNameLength = MAX_SYM_NAME; // Required SymGetSymFromAddr64(m_cProcessInfo.hProcess, stack.AddrPC.Offset, &dwDisplacement, pSymbol); // Retruns true on success
About this wicked code:
- Symbol name (
Name
field of structure) may be of variable length. Thus, we need to put more bufferfor this variable.
MAX_SYM_NAME
is predefined macro having value2000
. - Since DbgHelp.DLL and this structure may have different versions,
SizeOfStruct
must be initializedto represent structure size we use.
- Assignment of
MaxNameLength
is pretty obvious.
For us, the important member of this structure are: Name
, which is a null-terminated string.
Address
, which is virtual address of the symbol (including image’ base-address).
Using SymFromAddr
and initializing SYMBOL_INFO
is very similar, and I prefer using this
new function. Though, as of now, for us, there is no extra information. I would point out usefulness of this
extended structure, as the need arrives.
Finally, to complete the stack-entry, we need to find out the source-file name and appropriate line
number in it. Reminding you, the PDB contains this information, and source-info retrieval — only if correct PDB is
loaded successfully. Also, the PDB contains only information about source-code, not source-code!
To retrieve line-number information, we use SymGetLineFromAddr64
, passing it a
IMAGEHLP_LINE64
structure. This function also takes four arguments, and first three are similar to
functions described above. It requires only SizeOfStruct
to be properly initialized with with
structure size. It goes like this:
IMAGEHLP_LINE64 line; line.SizeOfStruct = sizeof(line); bSuccess = SymGetLineFromAddr64(m_cProcessInfo.hProcess, (DWORD)stack.AddrPC.Offset, &dwDisplacement, &line); if(bSuccess) { // Use line.FileName, and line.LineNumber }
Symbol functions, or any other function from DbgHelp.DLL does not support loading and displaying the source. We
need to do it by ourself. If the line number information, or the original source code is not available, we cannot
show the source-code-view.
As of this writing [19-Dec-2010], I have not decided what to be displayed when source code is not
available to display. We can display set of instructions, but x86 instructions aren’t of fixed size, the
«instruction» per se doesn’t fit. But bunch of machine code instructions (like «55 04 FF 76 78
AE…») having fixed instructions in one line can be displayed. Or we can dis-assemble the instructions
and show them instead. Though, I do have source code of disassembling the x86 machine-code, it doesn’t support
complete instruction set.
By now I have shown you the important steps to stop debugging on start-address. This includes obtaining the
start address, putting breakpoint at that address, handling that breakpoint, reverting original instruction,
obtaining registers, call-stack, source-code and registers, and basic idea on how to display all this
information to UI. I have also elaborated that we need to use Windows event for halting and resuming the suspended
debuggee, as per user request.
5. Halting the Debuggee, and continuing as per user’s command
As explicated somewhere in this article, to suspend the debuggee (i.e. keep it suspended when some stoppable
debug event occurs), we just dont call ContinueDebugEvent
. As per the design of debugging-system,
it also ensures that any other running threads in debuggee, remain suspended.
Here is the abstract view of how halt and continue is supposed to execute (UT=User-interface
thread, DT=Debugger thread):
- [UT] User initiates debugging a process through given UI. This operation would not freeze the
UI.
- [
DT
] Initializes a Windows event object usingCreateEvent
. It is non-signaled. - [
DT
] The debugger starts the debuggee usingCreateProcess
. It enters the debugger-loop.
- [
DT
] It receive the breakpoint event, which asks UI to display information and it halts. - [
DT
] It usesWaitForSingleObject
on that event to keep DT suspended. - [UT] User initiates some debugging actions, like Continue, Stop-debugging or
StepIn.
- [UT] It calls appropriate function to Resume-Debugging with the debugging-action
flag. It need to call
SetEvent
to wake-up that DT. - [
DT
] Wakes up, inspects the user-action and continues the debugging loop appropriately,or terminates debugging if Stop action was given.
Everything will get more clear when I would depict the debugging-interface (a class), that I designed. If your
memory and/or inquisitiveness is working well while reading the article, you should know that I did not cover two
things:
- Actual code to retreive the start address (
GetStartAddress
function!). Reminding you,CREATE_PROCESS_DEBUG_INFO::lpStartAddress
is the start-address, but won’t be always correct. - How to handle user-defined breakpoints, which must execute more than once? The start-address breakpoint I have
discussed so far is just hit-once breakpoint, therefore reverting original instruction does the require
job.
May be you need to take a break, and/or read the un-cached stuff again!
Anyway, since I have covered DbgHelp.DLL and Sym* functions I can present the code that retrieves start-address
of a process. The function name is SymFromName
, which takes the symbol-name and
returs information in SYMBOL_INFO
. The similar old function is SymGetSymFromName64
which
returns information in PIMAGEHLP_SYMBOL64
structure. Following is code that retrieves address of
wWinMainCRTStartup
, using SymFromName
function:
DWORD GetStartAddress( HANDLE hProcess, HANDLE hThread ) { SYMBOL_INFO *pSymbol; pSymbol = (SYMBOL_INFO *)new BYTE[sizeof(SYMBOL_INFO )+MAX_SYM_NAME]; pSymbol->SizeOfStruct= sizeof(SYMBOL_INFO ); pSymbol->MaxNameLen = MAX_SYM_NAME; SymFromName(hProcess,"wWinMainCRTStartup",pSymbol); // Store address, before deleting pointer DWORD dwAddress = pSymbol->Address; delete [](BYTE*)pSymbol; // Valid syntax! return dwAddress; }
Ofcourse, it retrieves address of only wWinMainCRTStartup
, which may or may not be start routine.
Well, there is more to go in this article — like determining if EXE specified is valid, is 32-bit module, is
unmanaged EXE, if it is Unicode/ANSI build and things like that. Keep reading!
User-defined breakpoints? I will describe about the mentioned issue when I would elaborate on creating user-
defined breakpoints.
CDebuggerCore — The debugging-interface class
I wrote an abstract class, having few pure-virtual functions that forms the foundation for debugging. Unlike
previous article, which was tightly integrated to UI and MFC, I made this class to be independent. I have used
native Windows handles, STL and CString
class in this class. Remember, CString
class may
be directly used in non-MFC applications, just by including <atlstr.h>
, no linking with MFC is
required at all. I have placed this include in header-file of this class. If you are still discontent about
CString
usage, please use your favorite class for string handling and change code appropriately.
[Subject to Change] Here is the basic skeleton of CDebuggerCore
class:
class CDebuggerCore { HANDLE m_hDebuggerThread; // Handle to debugger thread HANDLE m_heResumeDebugging; // Set by ResumeDebugging PROCESS_INFORMATION m_cProcessInfo; // Other member not shown for now. public: // Asynchronous call to start debugging // Spawns a thread, that has the debugging-loop, which // calls following virtual functions to notify debugging events. int StartDebugging(const CString& strExeFullPath); // How the user responded to continue debugging, // it may also include stop-debugging. // To be called from UI-thread int ResumeDebugging(EResumeMode); // Don't want to listen anything! Terminate! void StopDebugging(); protected: // Abstract methods virtual void OnDebugOutput(const TDebugOutput&) = 0; virtual void OnDllLoad(const TDllLoadEvent&) = 0; virtual void OnUpdateRegisters(const TRegisters&) = 0; virtual void OnUpdateCallStack(const TCallStack&) = 0; virtual void OnHaltDebugging(EHaltReason) = 0; };
For few readers, reading about this user-defined class may not be appeasing. But this is how it is — to explain
about the debugger’ code I must explain the code I wrote!
Since this class is abstract, it must be inherited and all virtual methods (On*
) must be
implemented. Needless to mention, these virtual function are called by core class, depending on different debug-
events. Not a single virtual function demands return value or argument modification — you may keep the
implementations empty.
Assume you have derived a class named CDebugger
, and implemented all virtuals. You start debugging
by using following code:
// In header, or at some persist-able location CDebugger theDebugger; // The point in code where you ask it to start debugging: theDebugger.StartDebugging("Path to executable");
Which would just initialize required variables to set debugging-state, create the event handle I described
above,and spawn the debugger-thread. It will then return — that means StartDebugging is asynchronous call.
The actual debugging-loop is in DebuggerThread
method (not shown above). It creates the given
executable using CreateProcess
, and then enters debuging-loop controlled by
WaitForDebugEvent
and ContinueDebugEvent
, having switch-cases for the debug events. On
different debug events it calls appropriate On*
method passing appropriate arguments. For instance, if
OUTPUT_DEBUG_STRING_EVENT
arrives, it calls OnDebugOutput
with a string parameter. For
other debugging events, it calls appropriate virtual functions. The derived class is responsible for displaying it
the UI.
Few debugging events that halts the debugging, like breakpoint event, the debugger-loop would first
call appropriate On*
function(s) and then it would call HaltDebugging
with proper
reason-code. This function is private to CDebuggerCore
, and has declration like this:
// Enum enum EHaltReason { // Reason codes, like Breakpoint }; // In CDebuggerCore private: void HaltDebugging(EHaltReason);
The implementation of this method is something like below, which halts the debugger-thread:
void CDebuggerCore::HaltDebugging(EHaltReason eHaltReason) { // Halt the debugging OnHaltDebugging(eHaltReason); // And wait for it, until ResumeDebugging is called, which would set the event WaitForSingleObject(m_heResumeDebugging,INFINITE); }
Since the debugger-loop knows the exact reason of halting, it passes it to HaltDebugging
which delegates it to OnHaltDebugging
. The override, OnHaltDebugging
, is supposed to
intimate the end-user about this debugging event. The UI thread doesnt freeze, but it awaits further user action.
The DT remains suspended.
With proper UI, like menus, shortcuts keys etc, the UI thread then calls ResumeDebugging
function
with the resume-mode (i.e. how user responded to this halt event — Continue, StepIn, Stop-debugging?). The
ResumeDebugging
method which takes an EResumeMode
flag, sets this flag to a member
variable in class (of EResumeType
itself) and then calls SetEvent
to signal the event. It
resumes the debugger-thread (which is in HaltDebugging
).
Now, after HaltDebugging
returns (as a result to user-action), the debugger-loop tests
what action did user perform. For the same, it checks the member variable m_eResumeMode
(of type
EResumeType
), which was set by ResumeDebugging
and continues debugging
appropriately; or it terminates, if flag is to Stop-debugging. Just for illustration, enum EResumeMode is
something like:
// What action was initiated by user to resume enum EResumeMode { Continue, // F5 Stop, // Shift+F5 StepOver, // F10 // More .. };
There is more about CDebuggerCore
class, but I have covered the basic skeleton and data/control flow
that goes around this class. Please see the attached source files and comments in it.
Debugging Actions
In this section, I would elaborate different debugging actions like placing a user breakpoint at some source
code location (i.e. at a line in source file), disabling or deleting the breakpoint, continuing from breakpoint,
stepping though source code line by line and similar debugging actions. As you very well know all these
actions would require proper source files to be displayed to the end user. Therefore is is important to learn how
to enumerate relevant source files, load the files, enumerate lines of source files.
Enumerating source files of Debuggee
Definitely, as you know well, the debuggee — the running process, i.e. the executable files does not contain the
source-file information. It is contained in referenced .PDB file (though other formats also possible, other than
PDB, but I would still talk about .PDB). To enumerate all source files related to the executable we call
SymEnumSourceFiles
function. We would call the function on Process-load, and
optionally on Dll-load events. We should call this function only when SymGetModuleInfo64
succeeds and
the loaded module info says the symbol-type is PDB. To recollect, this is how we test it, followed by
SymEnumSourceFiles
call:
// Applicable for CREATE_PROCESS_DEBUG_EVENT and LOAD_DLL_DEBUG_EVENT // Initialization of 'dwBase' and 'module_info' is not shown here, as it is // already described above. BOOL bSuccess = SymGetModuleInfo64(m_cProcessInfo.hProcess, dwBase, &module_info); if ( bSucess && module_info.SymType == SymPdb) { // Symbols successfully loaded for this module, // and source file information is available. // Call SymEnumSourceFiles to retrieve source file information: SymEnumSourceFiles(m_cProcessInfo.hProcess, dwBase, NULL, EnumSourceFilesProc, this); // arguments 3,4,5 }
The first two arguments are quite understandable: the process and the base-address of module for which source-file
information is required. The third argument specified the filter. The 4th argument is a call back function, that we
must write. The last argument is nothing but a void-pointer parameter that would be passed to our callback function
for each file retrieval.
The signature of call back has to be:
BOOL __stdcall EnumSourceFilesProc( PSOURCEFILE pSourceFile, PVOID UserContext );
It would be an insult to your knowledge, if I tell you that this function name is just placeholder — you can
name it whatever you like! The second parameter is the same void-pointer, also refered as user-context.
This function will be called unless enumeration finishes, or the function itself returns FALSE
. Thus,
we would return TRU
so that all source files can be enumerated.E
The structure SOURCEFILE
, from dbhhelp.h, is declared like:
struct SOURCEFILE { DWORD64 ModBase; // Base address of the module CHAR* FileName; // Full source filename, including path };
Since filename comes from a shared address of DbgHelp.DLL, we must copy the buffer, and save it.
A simple test run has shown me that it enumerates all source files, including .res, .inl,
and manifest files and all predefined header files. To limit source file enumeration, we can use the
filter argument (3rd argument of SymEnumSourceFiles
). For instance, to enumerate only
.CPP
files, we can call it as:
SymEnumSourceFiles(m_cProcessInfo.hProcess,dwBase, "*.CPP", EnumSourceFilesProc, this);
Specifying multiple wild card isn’t supported. We must call this function multiple times to enumerated different
extensions. (I don’t have much time to explore this).
Anyway, using this function, in the callback function, we can store all required source files into a
vector
or CString
. Further to this, we can let user place breakpoints in any of these
source files. As of now, the our debugging-interface is designed such that, the user would be able to
locate these source files only after first break point is hit (i.e. main). In near future, I
would make changes to facilitate placing user breakpoints before actually starting the debugging.
Being a programmer, you are well aware that not everyline of source file is an executable line. Comments, Curly-
braces, blank lines, pre-processor directives are few elements that do not constitute a valid executable line. You
might have also observed that Visual Studio shifts ahead the misplaces breakpoints to next valid statement.
Therefore, it is necessary to enumerate lines that are valid executable lines.
To enumerate valid source-lines we can use SymEnumLines
, which is
very similar to SymSnumSourceFile
function. It requires process handle, base-address of module, a
filter to object file (.OBJ file), a filter to source file; and a call-back function and user-context (void-
pointer). First two arguments are same. We don’t need third argument, that means we ask this function to enumerate
through all object-files. Fourth argument we pass is explicit filename with full path — the sourcefile. Since, we
have collected all source files with previous function, we have all source filenames. Here how it goes:
SymEnumLines(m_cProcessInfo.hProcess, dwBase, NULL, SourceFile, EnumLinesProc,this);
The 4th argument, is nothing but fullpath of a source file name. Assume this code is in a loop, where all
collected file names are being enumerated for valid lines. The sixth argument is user-context, which will be passed
to EnumLinesProc
user-defined function. The declaration of EnumLinesProc
must be as:
BOOL __stdcall EnumLinesProc( PSRCCODEINFO LineInfo, PVOID UserContext );
Other detail remain same for this call back. The SRCCODEINFO
structure is declared
like:
// From DbgHelp.H. Not all members shown here struct SRCCODEINFO { DWORD64 ModBase; DWORD64 ModBase; // base address of module this applies to WCHAR Obj[MAX_PATH + 1]; // the object file within the module WCHAR FileName[MAX_PATH + 1]; // full filename DWORD LineNumber; // line number in file DWORD64 Address; // first instruction of line };
SymEnumLines
calls our callback function for each valid/executable line till the current source file
enumeration is finished. Thus, Obj
and FileName
would be repeated. These two members will
be full-path to .OBJ file and source file respectively. The member LineNumber
is important to us,
which will appear incrementing, and will only refer to valid line numbers. The last member is important for us —
the address of first instruction, which we will use to place breakpoints!
Placing Breakpoints
Once we have collected the source-code information using two of the functions enlightened above, we can now
facilitate end-user to place breakpoints directly from the source file. Till now, I have not shown any interface
(programming or UI) to show the source-file, but that’s not important, it is just a matter of opening a text file
and displaying in some multiline edit/richedit control.
Visual Studio is smart enough to find more of invalid lines than we can do — VS would disable/delete
breakpoints placed at some nonsensical location (like on a pre-processor directive). We would apply simple
mechanism. Since, we have collected all source files and valid source lines, we first check if the line on which
breakpoint is placed is actually a valid line. If it is a valid line, then no issues — we happily make that address
a breakpoint (i.e. replace that instruction with 0x
CC
). If the line on which breakpoint
is placed is invalid, we find the next valid line (this would be trivial since we have the source line info).
Of course, there are cases when a module would be dynamically loaded by the process (via LoadLibrary)
. For those cases, we need to allow any source file to be opened in our debugger, and also allow breakpoints
to be placed anywhere in any file. This is complex, yet important aspect; I would consider writing about it!
I know you want to see some action with «placing breakpoint», especially with user-breakpoints that
must be repeatable! What is a repeatable breakpoint, few of you might be confused. Let me refresh
your memory on breakpoints!
The breakpoint we placed at start-address is hit-once type of breakpoint — that code will not get
executed again, thus we just revert the (machine) code-byte. We don’t care about preserving the breakpoint for
future hit. But with other breakpoints, you must ensure that breakpoint remain preserved. Therefore, after
breakpoint is hit, is it first reverted with original code, EIP reduced and actual (original) code gets executed.
Let me try to explicate it:
- End user puts a breakpoint at line
188
in source fileMyApp.CPP
. Let’s say theaddress is
0x0012345
. - In the debugger, you grab what is at address
0x0012345
, save it on some breakpoints-array. Youalso save this address in some array.
- You set the breakpoint instruction (
0xCC
) at this location. For this, we use same set offunctions:
R
ead
/WriteProcessMemory
andFlushInstructionCache
. - The break-point eventually arrives (
EXCEPTION_BREAKPOINT
). You first check if it is user-breakpoint. For this just search in breakpoints-addresses-array. If it is user-breakpoint, locate the original
code-byte, write it, flush it. Do
EIP--
, andSetThreadContext
. - Continue debug event.
Break-points array? One more thing you don’t understand? Just like you can place multiple breakpoints, the user
of your debugger can also! Ignore this array stuff, if you don’t get it, you will understand it sooner or
later.
In abstract terms, if you understand the 5-step described above, you should know that as soon user-breakpoint is
hit, it won’t hit again. You have reverted the instruction! To handle all this, you need to use sub-exception:
EXCEPTION_SINGLE_STEP
(or STATUS_SINGLE_STEP
, both are absolutely same).
Till now, we have been stopping program execution when break-point is hit (via EXCEPTION_BREAKPOINT
),
and showing call stack, registers etc. But the correct way is to actually handle breakpoint in
EXCEPTION_SINGLE_STEP
. You will understand why when you read following:
In EXCEPTION_BREAKPOINT event,
- Get thread context, save it.
- Reduce
EIP
by one. - Set processor
trap flag
for single stepping. - Set thread context.
- call
WriteProcessMemory
, andFlushInstructionCache
, as usual to revertinstruction.
In STATUS_BREAKPOINT event,
- Render the registers for the context saved in previous step.
- Enumerate call-stack from saved context.
- Halt debugging.
Now what is «trap flag»?
Trap flag, when set, asks the CPU to raise EXCEPTION_SINGLE_STEP
for the instruction it is going to
execute. We set the trap-flag in EFlags
member variable of CONTEXT. It is represented
by 8th bit of EFlags. Just use 0x100
or 256
to set it:
// In EXCEPTION_BREAKPOINT lcContext.ContextFlags = CONTEXT_ALL; GetThreadContext(m_cProcessInfo.hThread, &lcContext); lcContext.Eip--; lcContext.EFlags |= 0x100; // Set trap flag, which raises "single-step" exception SetThreadContext(m_cProcessInfo.hThread,&lcContext);
Remember, we reduce EIP by one, which essentially means we set the flag to current instruction — where the
breakpoint was placed. After above given code, we write and flush the original instruction. Therefore, as a result,
the next immediate exception arrives is EXCEPTION_SINGLE_STEP at the same instruction address. In
single-step processing, we dont reduce EIP, but we re-write breakpoint instruction (if it was actually a user-
defined breakpoint!). This essentially makes user-defined breakpoints repeatable!
As a final important note, EXCEPTION_SINGLE_STEP
is not same as single-
stepping through the source code in your favorite debugger (F10/F11 in VS)! Also, the Trap-Flag is
automatically reset as soon as single-step exception arrives, which prevents this same debug-event to
arrive again. Therefore, you need not to reset this flag. Yes, of course, you can enable it everytime for
assembly/machine-code level debugging!
Stepping through the Code
A good debugger should facilitate following code-stepping actions:
- Step-in to each and every line of at source-code level, this includes stepping-inside
the function calls. (F11 in Visual C++)
- Step-over, which is same as Step-in, but goes not steps-in to function calls. (Same
as F10)
- Step-out, which means coming out of currently executing function to the function which has
called this function. It essentially means popping-out current function from the Call-Stack. (Similar to Shift
+F11)
- Run-to-Cursor. Not exactly a code-stepping action. It means placing an invisible, hit-once
breakpoint to a given location. You can do the same with Ctrl+F10 in Visual C++ debugger, while debugging. (Look at
context menu while debugging).
- Set-next-Statement, which allows the debugger-user to set any line to be executed next. Terms
and conditions apply. In VS, it can be achieved through context menu (while debugging). The shortcut is Ctrl+Shift
+F10.
Surprisingly, all these debugging actions run around the core concepts that we have discussed so for:
- Replacing an instruction with 0xCC.
- Handling break-point exception. Adjusting EIP. Setting trap-flag.
- Handling single-step exception. Halt debugging, wait for user action.
Other than the last debugging action (Set-next-statement), all other actions would require invisible-
breakpoint. The instruction is same: 0xCC
, handling is also same. The only difference is that
debuggers don’t give any visual indication that it is actually a breakpoint. This is nothing but absence of Red-dot
on left of source-code in Visual Studio, or presence in ‘Breakpoints’ window (Alt+F9). Disabled, Invalid or any
other kind of user-specified breakpoints are not invisible-breakpoints — they are regular
breakpoints that user specified (or attempted to).
The start-address breakpoint, I had discussed before, is nothing but an invisible breakpoint. There is no visual
indication from debugger that it is actually a breakpoint. In VS, when you start program with F10 or F11, the
debugger would halt at main-function (like WinMain
), but it is not actually a user-breakpoint
(no red-dot!).
In the debugger code, how we would differentiate between user-breakpoint and invisible-breakpoint? Quite simple.
The breakpoints-array, I discussed in last subsection, is for user-specified breakpoints. When a breakpoint is hit,
we find the exception-address in breakpoints-array. If found it is user-BP, otherwise it is invisible-BP. Though,
unless we provide visual or audio indication for «breakpoint», it doesn’t make a difference.
In VS, the visual clue is the Red-icon on left and audio-clue can be enabled form Sounds
settings on Control Panel. In Sounds applet, goto Microsoft Visual Studio, specify a sound for
Breakpoint Hit entry. Restart Visual Studio. When a breakpoint is hit, the same sound would be
played. With this, I found out that user-breakpoint has priority over invisible breakpoint. Just place a breakpoint
to some line, and before that line press F10. The Step-over executes, but breakpoint-hit sound is also emitted! The
same goes for Run-to-cursor. I bet, you did not know this sound stuff!
Now, it is time to see some action. I am elaborating these debugging actions depending on complexity level
involved, simpler first. Please read them all.
Implementing Run-To-Cursor
For sure, it is nothing but an invisible, one-shot breakpoint instruction. For this, we just place a breakpoint
at given location in source code, and do not put this address in break-points array. When it is
hit, just halt the execution, show «Debugging» / «Debugging-halted» in some UI, wait for user-
action, and continue debugging as per user command.
It should be obvious to you that this action also requires 0xCC instruction to be placed at given address, be
saved, and then later reverted. Moreover, since the user is placing this invisible-breakpoint, we need to ensure
that source-file is valid, is loaded against Symbol-information, the line on which this command was given is valid
executable line. If line isn’t valid executable line, we find next valid line and place this invisible-
breakpoint there. Consequently, it means the Run-to-cursor is adjusted appropriately.
Remember, user-breakpoints takes priority over invisible-breakpoints. Therefore, if a user-BP is hit before Run-
to-cursor (or any other invisible-BP), that IBP would now be invalid (To verify, in VS, just place a BP before the
location of Run-to-cursor). Thus, we need to disable IBPs when any BP is hit. Also, there cannot be more than one
IBP pending to be hit at any time, so it is quite trivial to disable IBP! It essentially means that we need exactly
one variable for storing all stepping actions (including start-address IBP!). Even when multiple threads are
running, only IBP can be pending. Draw some sketches on paper to understand this paragraph, and unless you
understand it, dont proceed further!
Implementing Step-Over
For simplicity, let me cover Step-Over only to current function; that means excluding the case
when current function ends and program-execution goes to caller. I would cover this implicit stepping-out from a
function when I would explicate on Step-Out implementation.
Let’s have sample code for understanding:
int Max(int n1,int n2) { if(n1 > n2) return n1; else return n2; } int main() { //* This is line 12 int a, b; a = 10; //* b = 20; //* Max(20,30); //* //Bogus call printf("Max is %d", Max(a,b)); //* return 0; //* } //*
Here, for function main
, only the lines marked with //*
are valid
executable lines. These are the same lines returned by SymEnumLines
function. Therefore, we would the
collect line information, store the line numbers and associated addresses. Let’s say the line number information
(only for main
) are enumerated as below:
Line Address 12 0x411400 14 0x41141e 15 0x411425 17 0x41142c 19 0x411438 21 0x411460 22 0x411462
You definitely need not to understand how these addresses are formed, it all depends on source code, compiler
and linker settings (optimizations, incremental linking, FPO etc) — Ignore it. All we need is to utilize these
lines and addresses for Step-over implementation. First assume that program execution is now sitting at line 12
(via start-address IBP mechanism). That means EIP is 0x411400
!
Debugger is waiting for user input (debugger-loop is on halted on WaitForSingleObject
). Now,
debugger-user asks to Step-over.
- The UI-threads calls
CDebuggerCore::ResumeDebugging
withEResumeFlag
set toStepOver
. - This tells the debugger thread (having the debugger-loop) to put IBP on next line.
- The debugger-thread locates next executable line and address (
0x41141e
), itplaces an IBP on that location.
- It calls then
ContinueDebugEvent
, which tells the OS to continue running debuggee. - The BP is now hit, it passes through EXCEPTION_BREAKPOINT and reaches at EXCEPTION_SINGLE_STEP. Both these
steps are same, including instruction reversal, EIP reduction etc.
- It again calls HaltDebugging, which in turn, awaits user input.
And this process can go as long as user wishes, or program ends (or, of-course, till another exception occurs!).
One important aspect I did not cover, is to disable IBP on any normal BP occurrence, before this IBP is hit.
This means, if user has placed breakpoint somewhere in function Max
, and the program execution is now
passing (in main
) over this function. It would hit that UBP in Max
, and then user might
hit F5 (Continue). Our buggy debugger, would halt at next line in main
! I will de-bug this
issue soon.
Implementing Step-In
This isn’t easy! This is the most difficult part of writing a debugger!
Whilst it would be similar to Step-Over implementation, we need to dis-assemble the code, and
to understand what next statement means (at assembly/machine-code level). Herein, I am attaching a
disclaimer, that I am not assembly expert! Therefore, the following text should be treated as
idea/concept/hint — refer to Intel’s x86 instruction documentation for complete information about opcodes.
As I had commented before, the x86 instructions are not of fixed length. Each instruction can
be of 1
byte, or more than one bytes (I have seen max of 7
-byte x86
instructions). The BP instruction, 0xCC
, is a one byte instruction. The processor understands
all instructions opcodes, and their lengths, and it executes it as atomic instruction. For example, consider
following simple code:
Max(10,20);
Which is disassembled as:
6A 1E push 1Eh 6A 14 push 14h E8 C2 FD FF FF call 00400E00 83 C4 08 add esp,8
I am not putting assembly jargons here, just about the above’ code. First two x86 instructions are
push
ing arguments to stack. The third line is making a call
to function Max
.
Therefore we need to hook the call
instruction!
Hooking? Not exactly. We need to find the address where call
instruction exists in instruction-set
of debuggee. We can use ReadProcessMemory
for the same. And, on that address we place an IBP — and
rest of the things you already know.
When do we do it? When user says step-in (F11), we start searching for this instruction from current EIP, till
we find call
instruction.
The toughest part is locating the call
instruction. No, this is not as easy as searching for op-
code E8
. Following are the reasons:
E8
is not the only instruction for function-calling, there are few otherstoo.
- The x86 instructions are not of fixed size. As you can see from assembly code above, first two instructions are
of 2 bytes each (
push
), thecall
instruction is of 5 bytes, and last instruction is of 3bytes!
- We need to dis-assemble code instructions properly. Dis-assembling is quite complicated task, and with more new
CPU instruction (SSE2 etc), it becomes more troublesome. For the same, it is mandatory that we know the instruction
size, at least, so that that unknown instruction can be ignored safely.
- Each
call
instruction, includingE8
, is different, and they need appropriateinterpretation.
I do need to do further research and development, on this subject; and only after that I can present a practical
and workable solution. Till then, for completeness, I can outline how Step-In can be implemented.
- [When F11 is hit] From the current instruction location, ignoring the current complete-instruction,
read all instruction bytes till next valid executable line (collected via
SymEnumLines
). - Do partial or full dis-assebmly, as applicable/possible.
- Analyze the assembly, locate one of the
call
instructions. - If found, place IBP at that location. The IBP processing goes as usual.
- If not found, just process as if it was Step-Over debugging-action.
The function, if call was found, would now be called, and call-stack would now be changed. The called
function may be in different source file. In either case, this function would eventually exit and program-control
would go back to caller. And this needs processing of Step-out, which is described below. Along with this, there
may be more than one call
instructions in same executable source-code line. Following is an
example:
printf( "%d, %d", max(10,20), min(min(20,30),40) );
Implementing Step-Out
Stepping-out from a function is the nearly inverse of Step-In debugging action. If you are debugging in
Dis-assembly mode, Step-In (F11) will let you debug at instruction level; and from that point if you do Step-Out,
it would be stepping out from the high-level «function», as if you were performing Step-In in source-code
mode. That’s why I said nearly!
To implement Step-Out debugging action, we need help from our friend function StakWalk64
.
If you remember, stack-walking is achieved with loop; and in that loop we call StackWalk64
multiple
times till Return-address becomes zero:
STACKFRAME64 stack; // Initialize stack-frame do { StackWalk64(..., &stack, ..); // Utilize stack-entry } while(stack.AddrReturn.Offset != 0);
The variable AddrReturn
, of type ADDRESS64
, holds the return address where the program
control would go. Let’s not go into how it is implemented at assembly level, but the return-address is
somehow stored, and program-control gets transferred when a function implicitly or explicitly
return
s to its caller. The return-address is the next statement after the call
statement.
Therefore, for the following pseudo-assembly code:
00401070 call 0x1234000 00401075 mov abc, xyz
which is call
ing the function 0x1234000
, the return-address from function 0x1234000
would be 0040175
(and not 0040170
).
Therefore, you just need to call StackWalk64
only once, note the return-address, and place a IBP
over there and your Step-Out is implemented!
If multiple function calls are involved in same source-code line, they would have multiple call
statements at assembly level, and they all would have respective return-addresses. Thus, it is quite possible for
debugger-user to hit F11, Shift+F10 (step-out) or F10 for same source-code line.
Though, we would be displaying only the source-code, in the debugger, we know where exactly the instruction
(EIP) is — the IBP, single-step exceptions, Debugging-APIs are assisting us for the same. Therefore, we need not to
bother if user hits F11, then returns from that function (implicitly or explicitly), and then hits F11 again for
source-line having more than one function calls (like in printf
example given above).
Let’s consider another case when user hits F10, after returning from first sub-function call (into which he/she
entered via Step-In). Here, the execution, at source-code level, is in midst — and at assembly level, few
call
s are made, and few more instructions (including call
s, if any) are pending for
execution. The user has hit F10! What should happen? Well, if you look back to the implementation of Step-Over, you
would find that Step-Over just looks for next executable line (at source-code level), and places an IBP on
next-instruction! Therefore, we need not to worry about this complicated case, either!
What if a user-BP is hit before function returns from F11/F10 debugging-action? That means, in following
function:
printf("Next prime: %d", NextPrime(1000));
Were NextPrime
is supposed to return next prime number from given number, which, for example calls
IsPrime
function. User has placed a BP somewhere in IsPrime
. Now, when program control
comes over this printf
line, user hits F11, which would Step-Into NextPrime; and then he/she
immediately hit Shift+F11. Where the program control should go: To next line of printf
call, or
To that breakpoint in IsPrime
?
Reminding you! User breakpoints have priority over any Invisible breakpoints. The program control would go to BP
in IsPrime
, and IBP placed at return-address of NextPrime
(which was placed by debugger
for Step-Out debugging-action), would be disabled by debugger!
What if user says Run-To-Cursor, in this complicated situation? Pardon me! Do you need same text to read again!
Analyze yourself!
Implementing Set-Next-Statement
Well, a single threaded implementation just needs to adjust EIP to the address of given line. That means, when
user says Set-next-statement (SNS), we look up the lines information we had collected. Verify if
given/selected line is valid executable line (if not just find the next valid line as we do for BP). Grab the
address of that line, and set EIP to that address using SetThreadContext
(after
GetThreadContext
).
Of course, just like VS Debugger does, we need to check if given line falls within same function, or it is from
different function. If it is from different function, we can ask the confirmation from user. My testings have
revealed that VS debugger, even in case of cross-function SNS action, just adjusts EIP appropriately and nothing
else!
Conditional Breakpoints
Debuggers also provide placing conditional breakpoints, where condition can be combination of following:
-
When a particular expression is true, or false. In this kind of CB, you can use programming
elements like variables, operators etc. Of course, this requires debuggers to have knowledge about variables
(local, class, global), and debugger must be able to parse the given expression (like
!(nMargin+10
> m_nMaxMargin)).
- It can filter out few hits, like only hitting even/odd hit on that breakpoint. Or hit when
hit-count is more than some value, Or is multiple/not-multiple of some value. All in all, hit-count is used as
condition for breakpoint.
- With other constraints like matching/not-matching thread or process-id, or machine type and
things like that. When same function is being called from multiple threads, the debugger-user can notice particular
thread-id, and prevent that thread to trigger this breakpoint. Unfortunately, due to elusive user-interface, this
is not well used, IMO.
If two or more categories are applied to same breakpoint, it depends on the debugger to trigger them with
AND
or OR
applied. That means if I place expression as well as filter
it, the debugger can see if both of these conditions are met, or any of these. Visual Studio debugger
applies AND
.
Now the million-dollar question is: How do we implement conditional-breakpoint feature in our debugger?
The first type of CB implementation (expression) is of distant-future, since it involves knowing the
variables, data types, constants etc; that are in debuggee. The DbgHelp functions are not rich enough to support
querying such rich information. For this we have to use DIA (Debug Interface Access),
which I am not willing to discuss (or learn by myself) at this moment, unless I finish this article. Nevertheless,
the idea described below can be used for Expression CB implementation.
In our debugger, UBPs are stored in some breakpoints-array. Now, when a UBP is applied with a condition, Or a
brand new conditional BP is placed by debugger-user, we can put that UBP into another BP-array that would be
holding conditions. The data-structure or algorithm isn’t important here, only the basic idea. We may use
maps, hash-maps to store breakpoints. Store a breakpoint information in some structure, whose condition
member would be null/empty when CB not set, or things like that.
When a breakpoint is hit, we instantly check if a condition is attached to it, and if it is true. If it is true,
we let user notify that BP is hit, otherwise we silently ContinueDebugEvent
without user knowing
about. Irrespective of BP is hit or not, the BP magic must go around our debugger (0xCC
, break-point
exception, EPP--
, SetThreadContext
, Single-step exception…). That means, if BP is
enabled it would always arrive in the debugger!
When a hit-count CB is applied, just count the hits, and break the debuggee on UI when specified hit-count
condition is hit. Again, programming elements we use inside debugger is not important here.
Similarly, constraints types of CB can be implemented quite easily. We just need to facilitate what are
the set of constraints. I may list few:
- Thread should match; or should not match.
- There should be more than; or less than N number of total threads running.
- Only after particular thread is loaded, not-loaded or unloaded from debuggee.
- Some constraints with the number of processes already running.
- Should hit only if called; or not called, from a specific function.
- Should hit when memory usage exceeds some value.
- Check the delay between two breakpoint hits, and notify only when some timeout condition is met. For example,
hit if there is 5 second or more delay hitting the same BP.
Implementing all these isn’t difficult! We need to use few functions (API), employ some data-structures, code
some smart stuff and the work is done! Therefore, I am not elaborating them here.
Debugging a Running Process
While I still have not covered about multiple threads running in debuggee, I am straightaway jumping on
debugging
a running process. There is no strong reason of delaying the discussion of Multithreaded Debugging, I just
found this subject worth elaborating before MT debugging. The dialogbox Attach to
Process might be known to you, which you can launch either from Tools or
Debug menu (or by Ctrl+Alt+P):
Attach to Process
To debug a running process you need to call DebugActiveProcess function, with a process ID, and then it
enters the debugger-loop controlled by WaitForDebugEvent
. Therefore, you can say that you are just
replacing CreateProcess
with DebugActiveProcess
and rest of debugger-loop part remain
same. There are, however, some differences:
- All threads in the process (debuggee) will be suspended.
- The process is already running, along with its threads. Thus, by design,
lpStartAddress
member ofCREATE_PROCESS_DEBUG_INFO
andCREATE_THREAD_DEBUG_INFO
(not discussed yet) will be null. One event notifying process-start will bedelivered, and CREATE_THREAD_DEBUG event will be sent for each running thread.
- For each DLL that is currently loaded in debuggee, either implicitly or explicitly, the debugger would receive
LOAD_DLL_DEBUG_EVENT.
- The debugger will now receive first-breakpoint instruction, which is always sent to a debugger (as you know!).
This would occur from the first thread of debuggee. The debugger would (should) continue from this point.
- All threads will now be resumed by system (of course, excluding the threads not suspended by debuggee
itself).
There are some constraints:
- The debugger must have appropriate permission to debug a particular process. If debugger is running as
normal/non-elevated process, it cannot debug a process having administrative/elevated token. This can be achieved
by running the debugger as elevated process. The Run As Administrator verb while launching the
debugger can be used. Or to run the debugger always as elevated process (for Vista or higher, on VS2008 and
higher), UAC linker flag can be set to
requireAdministrator
. - Our 32-bit debugger cannot debug a 64-bit process, irrespective of its permission level.
- Since our debugger is native-code debugger, it cannot debug a managed processes. It can however, debug a mixed
mode process. More on this later.
Detach, Terminate or wait ?
For long we have discussed starting a process for debugging, and recently discussed how to debug a running
process. What about detaching this relationship? Is it always required to wait until
debuggee exits, or has to quit due to unhandled exception? Until now, the debugger design was to wait until process
exits by itself. We now cover two important aspects: Terminating the debuggee and
Detaching the debuggee.
Terminating the Debuggee
By design, as mentioned in DebugSetProcessKillOnExit function, the debuggee would terminate if the thread that created the
debuggee (i.e. the one having debugger-loop) exits. But I found this to be false. The debuggee does not end, if
debugger just says «bye-bye» asking the debuggee to do whatever it wants to do, by just
return
ing from the debugger-thread. It enters a Limbo state, doing nothing.
It also denies any other debugger to attach on it for debugging (yes, the OS enforces it, not debuggee). Using
Process Explorer, I found a valid call-stack, which clearly sounds-out that debuggee is waiting for
ContinueDebugEvent
!
May be this is not as per current Operating Systems and MSDN is not modified to reflect this.
Therefore, to terminate the debuggee, we need to use TerminateProcess function. Doing this, when requested by
debugger-user, would ask for EXIT_PROCESS_DEBUG_EVENT
to occur next. This goes true, irrespective
which thread calls TerminateProcess
function. I have found that if we call this function as soon as
debuggee is launched, the system would still process all process-loading, dll-loading debugging events and then
send this event to debugger!
In Visual Studio, we achieve the same by Debug->Stop Debugging or using infamous
Shift+F5 keystroke. For any debugger, implementing stop-debugging would be same irrespective how
debuggee was launched/attached. Therefore, it is also possible to terminate a process which cannot be terminated
using TaskManager (not all, though)!
Detaching the Debuggee
I believe this is quite uncommon task the software engineers perform. The concept is simple: detach the
debugger-debuggee relationship and set debuggee free. This is not terminating the debuggee, nor putting the
debuggee into a limbo state. It is just unbinding the debuggee from debugger. After detaching from debuggee, any
next unhandled debugging event would be presented default crash handler (!!).
In Visual Studio, you can do the same by Debug->Detach All command, which will set
debuggee free! If you are in single stepping mode (i.e. actively debugging a program), and then you detach
from debuggee, it would simply release the debuggee from debugger and would continue from that point on. Try it
out!
Therefore, it is perfectly possible to do following:
- Start any process normally.
- Attach to it, start debugging.
- Perform debugging, even at source code level, if possible.
- Once you have done debugging, or analyzed how process is doing, you can detach from debuggee, and set it
free.
Of course, to perform debugging, you need to place some breakpoints (at source-code, or at assembly level). If
you want to just break wherever process is, use Debug->Break All, which will suspend
all threads and present you call stack of primary thread. Break-All is also applicable for processes you have
started. Break-All is only applicable if you are not actively debugging — since there is
nothing to break.
After long therotical session, here is the function you need: DebugActiveProcessStop, which just takes one parameter: the
Process-ID.
Debugging a Crashing Process
Depending on your Windows OS version and settings, when an application crashes, you may see one of the few
standard «Application has stopped working» dialog box. On Windows Vista and higher you can go to
Action Center and change the settings. You may encounter dialog like:
Or a delayed dialogbox like:
In both of the cases, you have the opportunity to Debug the crashing application. If you
carefully (or partially) reading this article, you can now understand what «Exception
Code» in the first dialog would mean. This is access-violation exception
(EXCEPTION_ACCESS_VIOLATION
) that would arrive under EXCEPTION_DEBUG_EVENT
.
So, what happens when user hits Debug? Well, it launches the default crash
handler, which must have been setup in registry. If it is not setup, you won’t see the Debug button!
If you have installed any program that allows debugging, like WinDbg or Visual Studio, you would see the Debug
button. The Windows system keeps information about crash-handler at following location in registry:
- HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug
If your OS is 64-bit, there would be additional entry at following location for debugging 32-bit crashing
processes:
- HKLM\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\AeDebug
I am not aware of what AE means in the key-name (or you can say I’ve not tried hard). While
there are few Values and Keys under this key; for us, Debugger string-value is of interest. The
Debugger value would have the full path of debugger and following two arguments:
-p
%ld — Specifies the process identifier. The same process-Id we can use inDebugActiveProcess
to start debugging a running process. %ld is a placeholder wherethe system would put the actual process-id while launching the debugger.
-e
%ld — Handle to Windows event object which willbe used to intimate the system that debugger has started debugging. This is not a debugging-event,
but an
EVENT
object. As soon as debugger start debugging (i.e. enters debugger-loop), it calls
SetEvent
to let the system know that debugger has started debugging and can release the control todebugger.
- Any additional parameters the custom debugger takes.
By now you must have seen that Debugger entry is set to:
- «C:\Windows\system32\VSJitDebugger.exe»
-p
%ld-e
%ld
Which is self-explanatory. If you have multiple version of Visual Studio installed, «Just-in-Time
Debugger» dialogbox would be shown. Note that, by this time, the debugger is not attached — it would be
attached only when user confirms. For us, the JIT dialogbox, and how it enumerates installed VS debuggers, is of no
interest — it is relevant to VS and not to Windows OS for Debugging.
I can judge your intelligence, that you already deduced we just need to put the path of our debugger in
Debugger registry entry, along with required -e
and -p
parameters. And
at the debugger level, we need to check these arguments and act appropriately. We must have to call
SetEvent
to the handle provided (of course by typecasting it to HANDLE
), as soon as we
start debugging the crashing process.
On 64-bit OS, if a 64-bit process crashes, the default AeDebug entry will be read. If a 32-bit
process crashes the AeDebug entry under Wow6432Node will be read. A 64-bit debugger can debug
either type of process. A 32-bit debugger can debug only 32-bit process — and attempt to debug 64-bit process
(either via CreateProcess
or DebugActiveProcess
) would simply fail.
On 32-bit OS, there would only be default AeDebug entry which will be used to debug 32-bit
processes. I am not aware of 16-bit application crashes on 32-bit OS!
Therefore, our debugging kingdom is only limited to 32-bit OS, or to Wow6432Node on 64-bit OS.
What if we build our debugger as 64-bit process? Well… boy, that would not work because of CONTEXT
structure — there are architecture differences. The EIP register, for example, is not valid on 64-bit architecture.
Also, the 64-bit architecture is not consistent — x64 and Itanuims for example as different!
Manually Attaching a Debugger
This is not same as either of two approaches:
- Using Attach to Process from your favourite debugger.
- Selecting ‘Debug‘ on a crashing process
In the first approach, the debugger would enumerate all processes that can be debugged. Thus, when a debugger-
user selects a process, it passes the same process-id to DebugActiveProcess
and debugger-loop
starts.
In second approach, the debugger starts debugging a process through -p
process-id, and notifies the system by signaling the received event (via -e
).
The third approach can be used by any user from Task Manager, or Process Explorer. The user would select Debug
from context menu (or whatever user-interface the utility has provided). The process-viewer would look for Debugger
entry in AeDebug key and start the given process for debugging this process. Here, only -
p will be passed, since the selected process is running fine (i.e. no unhandled exception
occurred). The debugger will enter debugger-loop for the process-id received.
If ‘Debugger’ entry is missing, the process-viewer utility will disable or hide the Debug menu. Task
Manager hides it, while Process Explorer does not show the Debug entry in context menu, if this
information is missing in registry.
I have seen that for both 32-bit and 64-bit processes, both TM and PE use the default AeDebug key, and not the
key under Wow6432Node, for attaching the debugger. That means, even if process is 32-bit, still the 64-bit debugger
(by registry-definition) would be invoked.
Note: There can be only one debugger attached to a process, irrespective how the debugger-debuggee relationship is made. The second attempt may not be allowed by utility — if mistakenly allowed, the request to attach a debugger would simply be rejected by system.
…
Article is not complete yet. This is such an intricate subject, which requires time, research,
zillion-time debugging the debugger and what not! Since I wrote (and writing) this much, I could not resist myself
from publishing and sharing this information!
In my TODO list (I need to finish fast!):
— Placing user-breakpoints
— Showing call stack
— Allowing debugging actions (Stepping etc).
— Showing source code
— Multi threaded debugging
— Attaching to existing process (also, ‘Attach Debugger’ from task manager)
— Detaching from being-debugged process
— Making it the Crash-handler debugger.
— Showing dis-assembly when no source-code is available (probably not in this part).
— More!
For sure, a well-documented source code, and a sample Debugger would be attached, soon! Till then, you can
bug me with bug reports!
History
- December 16, 2010 — Initial public release
- December 18, 2010 — Symbol retrieval
- December 19, 2010 — Debuggee halt-resume functionality, CCoreDebugger class.
- December 22, 2010 — Source file enumeration, placing user breakpoint (CPU trap flag)
- December 25, 2010 — Stepping through the source-code
- December 29, 2010 — Step-Out, Set-next-statement and Conditional Breakpoints
- January 9, 2010 — Debugging a running process, default crash handler