_Great_
03.12.2006, 12:50
Article: Анализ стека
Author: Great
Date: 12.08.2006/3.12.2006
Theme: Coding/Reversing
Lang.: C/C++
Base: -
Note: Статья рассчитана знающих язык С++, программирующих на Win32 API и знающих основы вызова функций в ассемблере.
I. Кручу-верчу, раскрутить хочу
Вероятно, ты не раз видел во всяких полезных утилитах вроде отладчика или просмотрщика процессов одну полезную функцию - раскрутка стека. Если нет - поясню, что это такое. При вызове функции командой CALL в стеке сохраняется адрес возврата, который потом снимается оттуда командой RET. Проанализировав стек потока, можно узнать, каким путем и через какие вызовы выполнение добралось до текущей точки. Такая полезная возможность есть, например, в Process Explorer'е от небезызвестного Марка Руссиновича или, например, во встроенном отладчике в MS Visual Studio. Обычно, формат выводимой информации таков:
имя_модуля!имя_функции + смещение [аргументы]
Насчет аргументов. По идее, в стек запихиваются и переданные в функцию аргументы, поэтому можно при анализе стека еще и показывать аргументы. Правда, нигде не сохраняется информация об их размере, потому что вызываемая программа обычно знает размер своих аргументов. Но так как большая часть всех аргументов имеет размер 32 бита или 4 байта (это, например, int, long, enum, bool, все указатели и еще некоторые типы), это не большая проблема.)
Попробуем и мы реализовать такую штуку, тем более, что это не сложно.
II. Инструменты
Для работы с именами функций и модулями мы будем использовать библиотеку DBGHELP.DLL, входящую в базовый комплект поставки детища дяди Билла. (По странной причине я не обнаружил в MS VC++ 6.0 к ней библиотеки импорта, хотя хидер там был - чудеса :). Пришлось спереть либу из masm32.) В ней для нас полезны следующие функции:
SymInitialize: Подготовка процесса к работе с символами
BOOL SymInitialize(
HANDLE hProcess, // хендл процесса
PSTR UserSearchPath, // путь поиска. Ставим ноль в надежде, что все окажется в одном каталоге
BOOL fInvadeProcess // указан ли верный хендл процесса. Если FALSE, то вместо hProcess можно передать любое другое число, идентифицирующее процесс, например, его ID. Мы честно передаем хендл и ставим TRUE
);
Функция возвращает булево значение, означающее успешность выполнения.
SymGetSymFromAddr: Возвращает имя функции и ее стартовый адрес по адресу какого-то, принадлежащего ей, байта.
BOOL SymGetSymFromAddr(
HANDLE hProcess, // хенлд процесса
DWORD Address, // адрес для исследования
PDWORD Displacement, // сюда запишут смещение адреса от стартового
PIMAGEHLP_SYMBOL Symbol // сюда запишут инфу о функции, в т.ч. ее адрес и имя
);
Формат структуры IMAGEHLP_SYMBOL таков:
typedef struct _IMAGEHLP_SYMBOL {
DWORD SizeOfStruct; // размер структуры
DWORD Address; // адрес
DWORD Size; // размер
DWORD Flags; // зарезервировано
DWORD MaxNameLength; // макс. длина имени
CHAR Name[1]; // имя
} IMAGEHLP_SYMBOL, *PIMAGEHLP_SYMBOL;
Т.к. длина имени может варьироваться, то выделять память под него должна вызывающая программа. Проще выделить память для всей структуры разом:
BYTE lpMemory[256];
IMAGEHLP_SYMBOL* sym = (IMAGEHLP_SYMBOL*)lpMemory;
Функция возвращает булево значение, означающее успешность выполнения.
SymGetModuleBase: возвратить базовый адрес загрузки модуля по адресу какого-то его байта
DWORD SymGetModuleBase(
HANDLE hProcess, // хендл процесса
DWORD dwAddr // адрес байта
);
Для инициализации DBGHELP.DLL понадобятся SymSetOptions().
DWORD SymSetOptions(
DWORD SymOptions
);
Опции могут быть следующими:
SYMOPT_CASE_INSENSITIVE Поиск имен ведется без учета регистра символов
SYMOPT_UNDNAME Все имена представлены в обычном виде (без постфиксов @N, характерных для соглашения вызова stdcall)
SYMOPT_DEFERRED_LOADS Имена не загружаются, пока не возникнет ссылка на них. Это наиболее быстрый способ использования имен.
SYMOPT_NO_CPP Во всех именах C++, содержащих '::', будет произведена замена на '__'
SYMOPT_LOAD_LINES Загрузить информацию о номерах строк
Так же нам понадобится стандартная API GetModuleFileName()
III. Кодинг
Первое, что мы напишем, будет функция получения имени функции и ее стартового адреса - оболочка для SymGetSymFromAddr.
Сначала надо проинициализировать процесс. Мы вызовем SymSetOptions и укажем DBGHELP.DLL производить отложенную загрузку и раздекорировать имена.
Далее мы выделим память для структуры IMAGEHLP_SYMBOL и вызовем SymGetSymFromAddr.
Итак,
/* GetProcBaseAddressAndName()
*
* Описание
* Функция возвращает стартовый адрес функции и ее имя по переданному адресу (адрес может и не указывать на
* начало функции).
*
* Параметры
* hProcess хендл процесса
* dwAddress адрес
* lpdwBase адрес переменной, куда будет записан стартовый адрес
* lpszProcName строка, куда будет записано имя функции
* nSize длина этой строки
*
* Возвращаемое значение
* TRUE, если поиск удался, в противном случае - FALSE
*/
BOOL GetProcBaseAddressAndName(HANDLE hProcess, DWORD dwAddress, LPDWORD lpdwBase, LPDWORD lpdwOffset, LPTSTR lpszProcName, DWORD nSize)
{
// Иницаализация процесса
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
if (!SymInitialize(hProcess, NULL, TRUE))
return 0;
// Ищем функцию
BYTE buffer[256];
PIMAGEHLP_SYMBOL pSymbol = (PIMAGEHLP_SYMBOL)buffer;
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL);
pSymbol->MaxNameLength = sizeof(buffer) - sizeof(IMAGEHLP_SYMBOL) + 1;
if (!SymGetSymFromAddr(hProcess, dwAddress, lpdwOffset, pSymbol))
return 0;
strncpy(lpszProcName, pSymbol->Name, nSize);
*lpdwBase = pSymbol->Address;
return 1;
}
Далее напишем функцию, которая нам по заданному адресу возвратит строку вида "модуль!функция+смещение", где "модуль" - имя модуля, "функция" - найденная функция, "смещение" - смещение заданного адреса относительно ее базового адреса. Если адрес не найдем, возвратим "модуль!адрес", если не найдем модуль - возвратим "unknown!адрес".
/* GetModuleAndFunctionNameByAddress()
*
* Описание
* Функция возвращает строку вида "имя_модуля!имя_функции + смещение" по переданному адресу. К примеру, если передать
* ExitProcess+0x1a, функция вернет строку "kernel32!ExitProcess + 0x001a". Если функция не может найти адрес,
* строка будет вида "имя_модуля!адрес", а если не может найти модуль - имя модуля будет "unknown"
*
* Параметры
* hProcess хендл процесса
* dwAddress адрес
* lpszString строка
* nSize длина строки
*
* Возвращаемое значение
* TRUE, если поиск удался, в противном случае - FALSE
*/
BOOL GetModuleAndFunctionNameByAddress(HANDLE hProcess, DWORD dwAddress, LPTSTR lpszString, DWORD nSize)
{
DWORD base,offset;
char name[1024], ret[10240];
if(!GetProcBaseAddressAndName(hProcess, dwAddress, &base, &offset, name, sizeof(name)))
{
if(GetLastError()==ERROR_MOD_NOT_FOUND || GetLastError()==ERROR_INVALID_ADDRESS)
{
if(!dwAddress)
{
wsprintf(ret, "unknown!0x%08x", dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
DWORD module = SymGetModuleBase(hProcess, dwAddress);
if(!module)
{
wsprintf(ret, "unknown!0x%08x", dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
IMAGEHLP_MODULE modinfo = {sizeof(modinfo)};
if(!SymGetModuleInfo(hProcess, module, &modinfo))
{
wsprintf(ret, "unknown!0x%08x", dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
wsprintf(ret, "%s!0x%08x", modinfo.ModuleName, dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
return 0;
}
DWORD module = SymGetModuleBase(hProcess, dwAddress);
IMAGEHLP_MODULE modinfo = {sizeof(modinfo)};
SymGetModuleInfo(hProcess, module, &modinfo);
wsprintf(ret, "%s!%s + 0x%04x", modinfo.ModuleName, name, offset);
strncpy(lpszString, ret, nSize);
return 1;
}
Теперь все готово, для того, чтобы проанализировать стек потока и посмотреть, какие функции были вызваны и каким путем выполнение программы пришло в текущую точку. Например, мы хотим проанализировать последние 20 адресов в стеке. Мы прочитаем 20 чисел по адресу EBP текущего потока и проанализируем их.
/* StackUnwind()
*
* Описание
* Раскрутка локального стека по адресу из EBP. Отображает окно с информацией
*
* Параметры
* nSize глубина раскрутки. Столько адресов, начиная с вершины стека, будет проанализировано
* bSkipUnknownModules флаг - пропускать "бесхозные" адреса (имена модулей которых получить не удалось)
*
* Возвращаемое значение
* нет
*/
void StackUnwind(DWORD nSize=10, BOOL bSkipUnknownModules = TRUE)
{
DWORD *__ebp;
_asm mov __ebp, ebp; // сохраним значение EBP
char buffer[10240] = ""; // сюда запишем всю информацию для вывода через MessageBox
wsprintf(buffer+lstrlen(buffer),"Unwinding stack by EBP=0x%08x\n", __ebp);
char buf[1024];
for(DWORD i=0;i<nSize;i++)
{
// получаем имя функции в виде "модуль!функция+смещение"
GetModuleAndFunctionNameByAddress(GetCurrentProces s(), (DWORD)__ebp[i], buf, 1024);
// имя модуля определить не удалось => это не адрес возврата, а, скорее всего аргумент функции.
if(!strncmp(buf, "unknown", 7) && bSkipUnknownModules)
continue;
wsprintf(buffer+lstrlen(buffer), "0x%08x %s\n", __ebp[i], buf);
if(!strcmp(buf, "kernel32!RegisterWaitForInputIdle + 0x0049"))
{
wsprintf(buffer+lstrlen(buffer), "Found kernel32!BaseProcessStart (shown as 'kernel32!RegisterWaitForInputIdle + 0x0049'), stopping\n");
break;
}
}
MessageBox(0, buffer, "Stack unwind information", MB_ICONINFORMATION);
}
В цикле видно странный if, в котором проверяется совпадение имени функции с kernel32!RegisterWaitForInputIdle + 0x0049. Зачем это нужно, мы узнаем чуть позже.
...
Author: Great
Date: 12.08.2006/3.12.2006
Theme: Coding/Reversing
Lang.: C/C++
Base: -
Note: Статья рассчитана знающих язык С++, программирующих на Win32 API и знающих основы вызова функций в ассемблере.
I. Кручу-верчу, раскрутить хочу
Вероятно, ты не раз видел во всяких полезных утилитах вроде отладчика или просмотрщика процессов одну полезную функцию - раскрутка стека. Если нет - поясню, что это такое. При вызове функции командой CALL в стеке сохраняется адрес возврата, который потом снимается оттуда командой RET. Проанализировав стек потока, можно узнать, каким путем и через какие вызовы выполнение добралось до текущей точки. Такая полезная возможность есть, например, в Process Explorer'е от небезызвестного Марка Руссиновича или, например, во встроенном отладчике в MS Visual Studio. Обычно, формат выводимой информации таков:
имя_модуля!имя_функции + смещение [аргументы]
Насчет аргументов. По идее, в стек запихиваются и переданные в функцию аргументы, поэтому можно при анализе стека еще и показывать аргументы. Правда, нигде не сохраняется информация об их размере, потому что вызываемая программа обычно знает размер своих аргументов. Но так как большая часть всех аргументов имеет размер 32 бита или 4 байта (это, например, int, long, enum, bool, все указатели и еще некоторые типы), это не большая проблема.)
Попробуем и мы реализовать такую штуку, тем более, что это не сложно.
II. Инструменты
Для работы с именами функций и модулями мы будем использовать библиотеку DBGHELP.DLL, входящую в базовый комплект поставки детища дяди Билла. (По странной причине я не обнаружил в MS VC++ 6.0 к ней библиотеки импорта, хотя хидер там был - чудеса :). Пришлось спереть либу из masm32.) В ней для нас полезны следующие функции:
SymInitialize: Подготовка процесса к работе с символами
BOOL SymInitialize(
HANDLE hProcess, // хендл процесса
PSTR UserSearchPath, // путь поиска. Ставим ноль в надежде, что все окажется в одном каталоге
BOOL fInvadeProcess // указан ли верный хендл процесса. Если FALSE, то вместо hProcess можно передать любое другое число, идентифицирующее процесс, например, его ID. Мы честно передаем хендл и ставим TRUE
);
Функция возвращает булево значение, означающее успешность выполнения.
SymGetSymFromAddr: Возвращает имя функции и ее стартовый адрес по адресу какого-то, принадлежащего ей, байта.
BOOL SymGetSymFromAddr(
HANDLE hProcess, // хенлд процесса
DWORD Address, // адрес для исследования
PDWORD Displacement, // сюда запишут смещение адреса от стартового
PIMAGEHLP_SYMBOL Symbol // сюда запишут инфу о функции, в т.ч. ее адрес и имя
);
Формат структуры IMAGEHLP_SYMBOL таков:
typedef struct _IMAGEHLP_SYMBOL {
DWORD SizeOfStruct; // размер структуры
DWORD Address; // адрес
DWORD Size; // размер
DWORD Flags; // зарезервировано
DWORD MaxNameLength; // макс. длина имени
CHAR Name[1]; // имя
} IMAGEHLP_SYMBOL, *PIMAGEHLP_SYMBOL;
Т.к. длина имени может варьироваться, то выделять память под него должна вызывающая программа. Проще выделить память для всей структуры разом:
BYTE lpMemory[256];
IMAGEHLP_SYMBOL* sym = (IMAGEHLP_SYMBOL*)lpMemory;
Функция возвращает булево значение, означающее успешность выполнения.
SymGetModuleBase: возвратить базовый адрес загрузки модуля по адресу какого-то его байта
DWORD SymGetModuleBase(
HANDLE hProcess, // хендл процесса
DWORD dwAddr // адрес байта
);
Для инициализации DBGHELP.DLL понадобятся SymSetOptions().
DWORD SymSetOptions(
DWORD SymOptions
);
Опции могут быть следующими:
SYMOPT_CASE_INSENSITIVE Поиск имен ведется без учета регистра символов
SYMOPT_UNDNAME Все имена представлены в обычном виде (без постфиксов @N, характерных для соглашения вызова stdcall)
SYMOPT_DEFERRED_LOADS Имена не загружаются, пока не возникнет ссылка на них. Это наиболее быстрый способ использования имен.
SYMOPT_NO_CPP Во всех именах C++, содержащих '::', будет произведена замена на '__'
SYMOPT_LOAD_LINES Загрузить информацию о номерах строк
Так же нам понадобится стандартная API GetModuleFileName()
III. Кодинг
Первое, что мы напишем, будет функция получения имени функции и ее стартового адреса - оболочка для SymGetSymFromAddr.
Сначала надо проинициализировать процесс. Мы вызовем SymSetOptions и укажем DBGHELP.DLL производить отложенную загрузку и раздекорировать имена.
Далее мы выделим память для структуры IMAGEHLP_SYMBOL и вызовем SymGetSymFromAddr.
Итак,
/* GetProcBaseAddressAndName()
*
* Описание
* Функция возвращает стартовый адрес функции и ее имя по переданному адресу (адрес может и не указывать на
* начало функции).
*
* Параметры
* hProcess хендл процесса
* dwAddress адрес
* lpdwBase адрес переменной, куда будет записан стартовый адрес
* lpszProcName строка, куда будет записано имя функции
* nSize длина этой строки
*
* Возвращаемое значение
* TRUE, если поиск удался, в противном случае - FALSE
*/
BOOL GetProcBaseAddressAndName(HANDLE hProcess, DWORD dwAddress, LPDWORD lpdwBase, LPDWORD lpdwOffset, LPTSTR lpszProcName, DWORD nSize)
{
// Иницаализация процесса
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
if (!SymInitialize(hProcess, NULL, TRUE))
return 0;
// Ищем функцию
BYTE buffer[256];
PIMAGEHLP_SYMBOL pSymbol = (PIMAGEHLP_SYMBOL)buffer;
pSymbol->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL);
pSymbol->MaxNameLength = sizeof(buffer) - sizeof(IMAGEHLP_SYMBOL) + 1;
if (!SymGetSymFromAddr(hProcess, dwAddress, lpdwOffset, pSymbol))
return 0;
strncpy(lpszProcName, pSymbol->Name, nSize);
*lpdwBase = pSymbol->Address;
return 1;
}
Далее напишем функцию, которая нам по заданному адресу возвратит строку вида "модуль!функция+смещение", где "модуль" - имя модуля, "функция" - найденная функция, "смещение" - смещение заданного адреса относительно ее базового адреса. Если адрес не найдем, возвратим "модуль!адрес", если не найдем модуль - возвратим "unknown!адрес".
/* GetModuleAndFunctionNameByAddress()
*
* Описание
* Функция возвращает строку вида "имя_модуля!имя_функции + смещение" по переданному адресу. К примеру, если передать
* ExitProcess+0x1a, функция вернет строку "kernel32!ExitProcess + 0x001a". Если функция не может найти адрес,
* строка будет вида "имя_модуля!адрес", а если не может найти модуль - имя модуля будет "unknown"
*
* Параметры
* hProcess хендл процесса
* dwAddress адрес
* lpszString строка
* nSize длина строки
*
* Возвращаемое значение
* TRUE, если поиск удался, в противном случае - FALSE
*/
BOOL GetModuleAndFunctionNameByAddress(HANDLE hProcess, DWORD dwAddress, LPTSTR lpszString, DWORD nSize)
{
DWORD base,offset;
char name[1024], ret[10240];
if(!GetProcBaseAddressAndName(hProcess, dwAddress, &base, &offset, name, sizeof(name)))
{
if(GetLastError()==ERROR_MOD_NOT_FOUND || GetLastError()==ERROR_INVALID_ADDRESS)
{
if(!dwAddress)
{
wsprintf(ret, "unknown!0x%08x", dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
DWORD module = SymGetModuleBase(hProcess, dwAddress);
if(!module)
{
wsprintf(ret, "unknown!0x%08x", dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
IMAGEHLP_MODULE modinfo = {sizeof(modinfo)};
if(!SymGetModuleInfo(hProcess, module, &modinfo))
{
wsprintf(ret, "unknown!0x%08x", dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
wsprintf(ret, "%s!0x%08x", modinfo.ModuleName, dwAddress);
strncpy(lpszString, ret, nSize);
return 1;
}
return 0;
}
DWORD module = SymGetModuleBase(hProcess, dwAddress);
IMAGEHLP_MODULE modinfo = {sizeof(modinfo)};
SymGetModuleInfo(hProcess, module, &modinfo);
wsprintf(ret, "%s!%s + 0x%04x", modinfo.ModuleName, name, offset);
strncpy(lpszString, ret, nSize);
return 1;
}
Теперь все готово, для того, чтобы проанализировать стек потока и посмотреть, какие функции были вызваны и каким путем выполнение программы пришло в текущую точку. Например, мы хотим проанализировать последние 20 адресов в стеке. Мы прочитаем 20 чисел по адресу EBP текущего потока и проанализируем их.
/* StackUnwind()
*
* Описание
* Раскрутка локального стека по адресу из EBP. Отображает окно с информацией
*
* Параметры
* nSize глубина раскрутки. Столько адресов, начиная с вершины стека, будет проанализировано
* bSkipUnknownModules флаг - пропускать "бесхозные" адреса (имена модулей которых получить не удалось)
*
* Возвращаемое значение
* нет
*/
void StackUnwind(DWORD nSize=10, BOOL bSkipUnknownModules = TRUE)
{
DWORD *__ebp;
_asm mov __ebp, ebp; // сохраним значение EBP
char buffer[10240] = ""; // сюда запишем всю информацию для вывода через MessageBox
wsprintf(buffer+lstrlen(buffer),"Unwinding stack by EBP=0x%08x\n", __ebp);
char buf[1024];
for(DWORD i=0;i<nSize;i++)
{
// получаем имя функции в виде "модуль!функция+смещение"
GetModuleAndFunctionNameByAddress(GetCurrentProces s(), (DWORD)__ebp[i], buf, 1024);
// имя модуля определить не удалось => это не адрес возврата, а, скорее всего аргумент функции.
if(!strncmp(buf, "unknown", 7) && bSkipUnknownModules)
continue;
wsprintf(buffer+lstrlen(buffer), "0x%08x %s\n", __ebp[i], buf);
if(!strcmp(buf, "kernel32!RegisterWaitForInputIdle + 0x0049"))
{
wsprintf(buffer+lstrlen(buffer), "Found kernel32!BaseProcessStart (shown as 'kernel32!RegisterWaitForInputIdle + 0x0049'), stopping\n");
break;
}
}
MessageBox(0, buffer, "Stack unwind information", MB_ICONINFORMATION);
}
В цикле видно странный if, в котором проверяется совпадение имени функции с kernel32!RegisterWaitForInputIdle + 0x0049. Зачем это нужно, мы узнаем чуть позже.
...