Гайд Beginner's notes

  • Автор темы Автор темы n0entry
  • Дата начала Дата начала
Пользователь
Пользователь
Статус
Оффлайн
Регистрация
12 Дек 2022
Сообщения
128
Реакции
31
Как отладчик реализует трассировку стека?
Введение

Трассировка стека - это процесс восстановления цепочки вызовов функций, приведших к текущей точке выполнения. Это один из ключевых инструментов отладки, особенно при работе с краш-дампами или при анализе поведения программы без доступа к исходному коду

Теоретическая база

Каждый вызов функции в x86 и x64 архитектурах сопровождается сохранением адреса возврата в стек. Это позволяет "пройти" по стеку вызовов, начиная с текущего контекста выполнения, и получить последовательность вызовов вплоть до точки входа в приложение

Как устроен стек вызовов?

На низком уровне каждый стековый кадр обычно содержит:

адрес возврата (return address)
указатель на предыдущий стековый кадр (frame pointer, например, EBP в x86)
локальные переменные и аргументы

Стек растёт вниз, и, двигаясь от одного кадра к другому по цепочке указателей (если она не разрушена оптимизациями компилятора), можно получить полную трассировку вызовов

Роль отладочных символов (PDB)

Для отображения имён функций, номеров строк и переменных по адресам инструкций, отладчику необходимы отладочные символы - информация, которую компилятор сохраняет в отдельном PDB-файле (Program Database)


Эти символы можно загрузить в процессе выполнения при помощи:

DbgHelp API (SymInitialize, SymFromAddr, SymGetLineFromAddr64)
DIASDK от Microsoft

Способы трассировки стека

Существует несколько подходов к получению стека вызовов в Windows:


1746214321758.png


Практическая реализация трассировки с помощью DbgHelp

C++:
Expand Collapse Copy
#include <windows.h>
#include <dbghelp.h>
#include <iostream>
#include <iomanip>
#include <vector>
#include <memory>

#pragma comment(lib, "dbghelp.lib")

bool PrintCallStackBackTrace( )
{
    SymSetOptions( SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES );

    CONTEXT context{ };
    RtlCaptureContext( &context );

    // x86 code:

    STACKFRAME64 stack{ };

    stack.AddrPC.Offset = context.Eip;
    stack.AddrPC.Mode = AddrModeFlat;
    stack.AddrStack.Offset = context.Esp;
    stack.AddrStack.Mode = AddrModeFlat;
    stack.AddrFrame.Offset = context.Ebp;
    stack.AddrFrame.Mode = AddrModeFlat;

    std::vector< uint32_t > backtrace{ };

    std::string path = R"(SRV*D:\symbols*https://msdl.microsoft.com/download/symbols)";

    if ( !SymInitialize( reinterpret_cast< HANDLE >( -1 ), path.c_str( ), true ) )
    {
        printf( "[*] failed to SymInitialize with error: %i\n", GetLastError( ) );
        return false;
    }

    while ( StackWalk64( IMAGE_FILE_MACHINE_I386, reinterpret_cast< HANDLE >( -1 ), reinterpret_cast< HANDLE >( -2 ), &stack, &context, nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr ) )
    {
        backtrace.push_back( stack.AddrPC.Offset );
    }

    for ( const auto& frame : backtrace )
    {
        const int max_length = 256;

        uint64_t buffer[( sizeof( SYMBOL_INFO ) + max_length * sizeof( wchar_t ) + sizeof( uint64_t ) - 1 ) / sizeof( uint64_t )];
        memset( buffer, 0, sizeof( buffer ) );

        uint64_t sym_displacement{ };
        PSYMBOL_INFO symbol = reinterpret_cast< PSYMBOL_INFO >( &buffer[0] );

        symbol->SizeOfStruct = sizeof( SYMBOL_INFO );
        symbol->MaxNameLen = max_length - 1;

        bool has_symbol = SymFromAddr( reinterpret_cast< HANDLE >( -1 ), frame, &sym_displacement, symbol );

        if ( has_symbol )
        {
            printf( "[*] 0x%x | %s\n", frame, symbol->Name );
        }
        else
        {
            printf( "[*] 0x%x\n", frame );
        }
    }

    if ( !SymCleanup( reinterpret_cast< HANDLE >( -1 ) ) )
    {
        printf( "[*] failed to SymCleanup with error: %i\n", GetLastError( ) );
        return false;
    }

    return true;
}

void f( )
{
    PrintCallStackBackTrace( );
}

int main( )
{
    f( );
    SetConsoleTitleW ( L"test" );
    system( "pause" );
    return 0;
}


Пример вывода
С отладочными символами (PDB присутствует):

1746216372366.png



Функции отображаются по именам, в том числе и пользовательская f

Без отладочных символов:

1746216390196.png


Здесь видны только адреса и системные функции. Отсутствие имён означает, что отладчик не может сопоставить адреса с функциями без PDB-файлов. Для решения можно:

указать путь к локальным PDB
использовать сервер символов от Microsoft (SRV*...)
использовать альтернативные средства (DIA SDK)


Как работает StackWalk64?

Функция StackWalk64 абстрагирует работу с различными форматами стека (x86, x64, ARM) и восстанавливает адреса возврата, используя:

таблицы unwind-информации (x64),
цепочку EBP-кадров (x86),
или метаданные от компилятора (если включена оптимизация)

Она использует:

SymFunctionTableAccess64 для поиска unwind-таблиц,
SymGetModuleBase64 для определения базового адреса модуля

сделал небольшую табличку о том, как устроен стек вызовов
123123.png

Заключение


В статье был рассмотрен один из популярных способов трассировки стека с использованием библиотеки DbgHelp. Как видно, без загруженных символов (PDB-файлов) ценность такой трассировки существенно снижается, поскольку большинство адресов не разрешаются в читаемые имена функций. Тем не менее, даже в таком виде она может быть полезной, особенно при диагностике с краш-дампов или в отладочных билдах.

Отдельная благодарность vmexit за помощь с кодом и консультации. Спасибо всем, кто дочитал до конца - удачи в реверсе и отладке
 

Вложения

  • 1746214430432.png
    1746214430432.png
    17.5 KB · Просмотры: 27
  • 1746214454030.png
    1746214454030.png
    19.4 KB · Просмотры: 22
Как отладчик реализует трассировку стека?
Введение

Трассировка стека - это процесс восстановления цепочки вызовов функций, приведших к текущей точке выполнения. Это один из ключевых инструментов отладки, особенно при работе с краш-дампами или при анализе поведения программы без доступа к исходному коду

Теоретическая база

Каждый вызов функции в x86 и x64 архитектурах сопровождается сохранением адреса возврата в стек. Это позволяет "пройти" по стеку вызовов, начиная с текущего контекста выполнения, и получить последовательность вызовов вплоть до точки входа в приложение

Как устроен стек вызовов?

На низком уровне каждый стековый кадр обычно содержит:

адрес возврата (return address)
указатель на предыдущий стековый кадр (frame pointer, например, EBP в x86)
локальные переменные и аргументы

Стек растёт вниз, и, двигаясь от одного кадра к другому по цепочке указателей (если она не разрушена оптимизациями компилятора), можно получить полную трассировку вызовов

Роль отладочных символов (PDB)

Для отображения имён функций, номеров строк и переменных по адресам инструкций, отладчику необходимы отладочные символы - информация, которую компилятор сохраняет в отдельном PDB-файле (Program Database)


Эти символы можно загрузить в процессе выполнения при помощи:

DbgHelp API (SymInitialize, SymFromAddr, SymGetLineFromAddr64)
DIASDK от Microsoft

Способы трассировки стека

Существует несколько подходов к получению стека вызовов в Windows:


Посмотреть вложение 305289

Практическая реализация трассировки с помощью DbgHelp

C++:
Expand Collapse Copy
#include <windows.h>
#include <dbghelp.h>
#include <iostream>
#include <iomanip>
#include <vector>
#include <memory>

#pragma comment(lib, "dbghelp.lib")

bool PrintCallStackBackTrace( )
{
    SymSetOptions( SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES );

    CONTEXT context{ };
    RtlCaptureContext( &context );

    // x86 code:

    STACKFRAME64 stack{ };

    stack.AddrPC.Offset = context.Eip;
    stack.AddrPC.Mode = AddrModeFlat;
    stack.AddrStack.Offset = context.Esp;
    stack.AddrStack.Mode = AddrModeFlat;
    stack.AddrFrame.Offset = context.Ebp;
    stack.AddrFrame.Mode = AddrModeFlat;

    std::vector< uint32_t > backtrace{ };

    std::string path = R"(SRV*D:\symbols*https://msdl.microsoft.com/download/symbols)";

    if ( !SymInitialize( reinterpret_cast< HANDLE >( -1 ), path.c_str( ), true ) )
    {
        printf( "[*] failed to SymInitialize with error: %i\n", GetLastError( ) );
        return false;
    }

    while ( StackWalk64( IMAGE_FILE_MACHINE_I386, reinterpret_cast< HANDLE >( -1 ), reinterpret_cast< HANDLE >( -2 ), &stack, &context, nullptr, SymFunctionTableAccess64, SymGetModuleBase64, nullptr ) )
    {
        backtrace.push_back( stack.AddrPC.Offset );
    }

    for ( const auto& frame : backtrace )
    {
        const int max_length = 256;

        uint64_t buffer[( sizeof( SYMBOL_INFO ) + max_length * sizeof( wchar_t ) + sizeof( uint64_t ) - 1 ) / sizeof( uint64_t )];
        memset( buffer, 0, sizeof( buffer ) );

        uint64_t sym_displacement{ };
        PSYMBOL_INFO symbol = reinterpret_cast< PSYMBOL_INFO >( &buffer[0] );

        symbol->SizeOfStruct = sizeof( SYMBOL_INFO );
        symbol->MaxNameLen = max_length - 1;

        bool has_symbol = SymFromAddr( reinterpret_cast< HANDLE >( -1 ), frame, &sym_displacement, symbol );

        if ( has_symbol )
        {
            printf( "[*] 0x%x | %s\n", frame, symbol->Name );
        }
        else
        {
            printf( "[*] 0x%x\n", frame );
        }
    }

    if ( !SymCleanup( reinterpret_cast< HANDLE >( -1 ) ) )
    {
        printf( "[*] failed to SymCleanup with error: %i\n", GetLastError( ) );
        return false;
    }

    return true;
}

void f( )
{
    PrintCallStackBackTrace( );
}

int main( )
{
    f( );
    SetConsoleTitleW ( L"test" );
    system( "pause" );
    return 0;
}


Пример вывода
С отладочными символами (PDB присутствует):

Посмотреть вложение 305297


Функции отображаются по именам, в том числе и пользовательская f

Без отладочных символов:

Посмотреть вложение 305298

Здесь видны только адреса и системные функции. Отсутствие имён означает, что отладчик не может сопоставить адреса с функциями без PDB-файлов. Для решения можно:

указать путь к локальным PDB
использовать сервер символов от Microsoft (SRV*...)
использовать альтернативные средства (DIA SDK)


Как работает StackWalk64?

Функция StackWalk64 абстрагирует работу с различными форматами стека (x86, x64, ARM) и восстанавливает адреса возврата, используя:

таблицы unwind-информации (x64),
цепочку EBP-кадров (x86),
или метаданные от компилятора (если включена оптимизация)

Она использует:

SymFunctionTableAccess64 для поиска unwind-таблиц,
SymGetModuleBase64 для определения базового адреса модуля

сделал небольшую табличку о том, как устроен стек вызовов
Посмотреть вложение 305295

Заключение


В статье был рассмотрен один из популярных способов трассировки стека с использованием библиотеки DbgHelp. Как видно, без загруженных символов (PDB-файлов) ценность такой трассировки существенно снижается, поскольку большинство адресов не разрешаются в читаемые имена функций. Тем не менее, даже в таком виде она может быть полезной, особенно при диагностике с краш-дампов или в отладочных билдах.

Отдельная благодарность vmexit за помощь с кодом и консультации. Спасибо всем, кто дочитал до конца - удачи в реверсе и отладке
класная тема чувак,

на х64 с активной оптимизацией StackWalk64 часто пидарасит из за разрыва цепочки фреймов, помогает парс UNWIND_INFO через RtlLookupFunctionEntry и RtlVirtualUnwind, можешь гайд дополнить или хз еще рассказав про примеры для асинхронной трассировки из экзепшена или сигнала
 
класная тема чувак,

на х64 с активной оптимизацией StackWalk64 часто пидарасит из за разрыва цепочки фреймов, помогает парс UNWIND_INFO через RtlLookupFunctionEntry и RtlVirtualUnwind, можешь гайд дополнить или хз еще рассказав про примеры для асинхронной трассировки из экзепшена или сигнала
Дополню, как будет время
 
Назад
Сверху Снизу