棧的調試

在有了棧和函數調用的認識之後,可以動手模擬和棧相關的調試技術。

1.棧基礎

以如下代碼爲例子(關閉緩衝區檢查和優化):

#include <windows.h>
#include <stdio.h>
#include <conio.h>

DWORD WINAPI ThreadProcedure(LPVOID lpParameter);
VOID ProcA();
VOID Sum(int* numArray, int iCount, int* sum);

void __cdecl wmain ()
{
    HANDLE hThread = NULL ;

    wprintf(L"Starting new thread...");

    hThread = CreateThread(NULL, 0, ThreadProcedure, NULL, 0, NULL);
    if(hThread!=NULL)
    {
        wprintf(L"Successfully created thread\n");
        WaitForSingleObject(hThread, INFINITE);
        CloseHandle(hThread);
    }
}

DWORD WINAPI ThreadProcedure(LPVOID lpParameter)
{
    ProcA();    
    wprintf(L"Press any key to exit thread\n");
    _getch();
    return 0;
}

VOID ProcA()
{
    int iCount = 3;
    int iNums[] = {1,2,3};
    int iSum = 0;

    Sum(iNums, iCount, &iSum);
    wprintf(L"Sum is: %d\n", iSum);
}

VOID Sum(int* numArray, int iCount, int* sum)
{
    for(int i=0; i<iCount;i++)
    {
        *sum+=numArray[i];
    }
}
通過WinDbg啓動,查找並反編譯ThreadProcedure函數:


這裏有別於書上的例子,第一個語句爲push rbp但是作用是一樣的,在x64中rbp替換ebp的功能,低32位爲x86上的ebp內容,前兩個語句建立新的棧幀。同樣查看ProcA的函數也同樣建立新的棧幀,並且執行sub esp,14h移動棧頂指針爲局部變量分配空間(14h = 20 = 4(iCount) + 3*4(iNums) + 4(iSum))。


2. 棧溢出

準備棧溢出代碼,同樣關閉緩衝區檢查和優化生存exe文件:

#include <windows.h>
#include <stdio.h>

#define MAX_CONN_LEN    30

VOID HelperFunction(WCHAR* pszConnectionString);

void __cdecl wmain (int argc, WCHAR* args[])
{
    if (argc==2)
    {
        HelperFunction(args[1]);
        wprintf (L"Connection to %s established\n",args[1]);
    }
    else
    {
        wprintf (L"Please specify connection string on the command line\n");
    }
}

VOID HelperFunction(WCHAR* pszConnectionString)
{
    WCHAR pszCopy[MAX_CONN_LEN];

    wcscpy(pszCopy, pszConnectionString);
    //
    // ...
    // Establish connection
    // ...
    //
}
通過WinDbg啓動並輸入足夠長的參數使發生棧溢出:

在調試器中執行程序直至程序奔潰,使用kb命令查看調用棧,並查看eip寄存器所指向的下一條指令的內容,出現解析問題內存無法訪問,判定調用棧錯誤(俗稱跑飛了)


重新啓動程序,在HelperFunction的ret指令之前加入斷點並查看變量pszCopy的內容發現函數wcscpy確實完全賦值了字符串的內容:

再單步執行到ret指令,檢查esp發現esp指向的內容並沒有返回調用函數的下一條指令地址,故判斷在函數中棧被破壞了:

檢查代碼並修復問題。對於這種簡單的數組越界訪問問題,通過工具靜態分析代碼可以提早發現。

3.異步操作與棧頂指針

與上邊一樣準備好自己的代碼(與書上不同這裏做了一些修改,不然生成出來的程序顯得很正常):

/*++
Copyright (c) Advanced Windows Debugging (ISBN 0321374460) from Addison-Wesley Professional.  All rights reserved.

    THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
    KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
    IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
    PURPOSE.

--*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>

#define MAX_VALUE_NAME              256 
#define MAX_REG_VALUE_COUNT    2
#define MAX_LEN                             256 
#define ENUM_TIME_LEN                 3000

class CRegValue
{
public: 
    CRegValue() : m_pwszName(NULL), m_dwValue(0) {};
    ~CRegValue()
    {
        if(m_pwszName)
        {
            delete[] m_pwszName;
            m_pwszName=NULL;
        }
    }
    const WCHAR* GetName() const { return m_pwszName; }
    DWORD GetValue() const { return m_dwValue; }
    
    VOID SetProperties(WCHAR* pwszName, DWORD dwValue)
    {
        m_pwszName=pwszName;
        m_dwValue = dwValue;
    }

private:
    WCHAR* m_pwszName;
    DWORD m_dwValue ;
} ;


typedef struct 
{
    CRegValue* pRegValues;
    DWORD dwRegValuesCount;
    HKEY hKey;
} CRegEnumData;

BOOL RegEnum(WCHAR* pwszPath, DWORD dwTimeout);
HANDLE RegEnumAsync(CRegEnumData* pRegData);
DWORD WINAPI RegThreadProc(LPVOID lpParameter);
VOID DisplayError(WCHAR* pwszPath, DWORD dwType, DWORD dwTimeout, BOOL bFullError);


int __cdecl wmain (int argc, WCHAR* args[])
{
    WCHAR wszRegPath[MAX_LEN] ;
    int iTimeout=0;
    BOOL bEnd = FALSE;
    
    while(!bEnd)
    {
        wprintf(L"Enter registry key path (\"quit\" to quit): ");
        wscanf(L"%s", wszRegPath, MAX_LEN);

        if(!_wcsicmp(wszRegPath, L"quit"))
        {
            bEnd=TRUE;
            continue;
        }

        wprintf(L"Enter timeout for enumeration: ");
        wscanf(L"%d", &iTimeout);
        
        if(iTimeout==0)
        {
            wprintf(L"Invalid timeout specified...\n");
            bEnd=TRUE;
        }
        else
        {
            //
            // Enumerate 
            //
            if ( RegEnum(wszRegPath, iTimeout) == FALSE )
            {
                DisplayError(wszRegPath, REG_DWORD, iTimeout, TRUE);
            }

        }
    }
    return 0;
}


BOOL RegEnum(WCHAR* pwszPath, DWORD dwTimeout)
{
    CRegValue regValues[MAX_REG_VALUE_COUNT];
	//wprintf(L"RegEnum regValues address is %d\n", regValues);
    CRegEnumData* pData=NULL;
    BOOL bRet=FALSE;
    HANDLE hWaitHandle = NULL;
    DWORD dwRet=0;
    HKEY hKey=NULL;
    LONG lRes=0;

    if(!pwszPath)
    {
        return FALSE;
    }

    lRes=RegOpenKeyEx(HKEY_CURRENT_USER, pwszPath, 0, KEY_QUERY_VALUE, &hKey);
    if(lRes==ERROR_SUCCESS)
    {
        pData=new CRegEnumData;
        if(pData)
        {
            pData->pRegValues=regValues;
            pData->dwRegValuesCount=MAX_REG_VALUE_COUNT;
            pData->hKey=hKey;

            hWaitHandle=RegEnumAsync(pData);
            if(hWaitHandle!=NULL)
            {
                dwRet=WaitForSingleObject(hWaitHandle, dwTimeout);
                if(dwRet==WAIT_TIMEOUT)
                {
                    wprintf(L"Timeout occured...\n");
                }
                else
                {
                    for(int i=0; i<MAX_REG_VALUE_COUNT; i++)
                    {
                        if(regValues[i].GetName()!=NULL)
                        {
                            wprintf(L"Value %d Name: %s\n", i+1, regValues[i].GetName());
                            wprintf(L"Value %d Data: %d\n", i+1, regValues[i].GetValue());
                            bRet=TRUE;
                        }
                        else
                        {
                            break;
                        }
                    }
                }
                CloseHandle(hWaitHandle);
            }
            else
            {
                wprintf(L"Async registry enumeration failed...");
            }
        }
    }

    return bRet;
}

HANDLE RegEnumAsync(CRegEnumData* pRegData)
{
    return CreateThread(NULL, 0, RegThreadProc, pRegData, 0, NULL);
}

DWORD WINAPI RegThreadProc(LPVOID lpParameter)
{
    DWORD dwIndex=0;
    BOOL bRet=FALSE;

    Sleep(ENUM_TIME_LEN);

    CRegEnumData* pRegData=reinterpret_cast<CRegEnumData*>(lpParameter);
	//wprintf(L"RegThreadProc pRegValues address is %d\n", pRegData->pRegValues);
    while(!bRet && dwIndex<pRegData->dwRegValuesCount)
    {
        DWORD dwType=0;
        LONG lRes=0;
        DWORD dwData=0; // Only get DWORD values
        DWORD dwDataSize=sizeof(DWORD);
        DWORD dwNameLen=MAX_VALUE_NAME;
        WCHAR* pwszValueName=new WCHAR[MAX_VALUE_NAME];
        if(pwszValueName)
        {
            lRes=RegEnumValue(pRegData->hKey, dwIndex, pwszValueName, &dwNameLen, NULL, &dwType, (LPBYTE)&dwData, &dwDataSize);
            if(lRes==ERROR_SUCCESS && dwType==REG_DWORD)
            {
                pRegData->pRegValues[dwIndex].SetProperties(pwszValueName, dwData);
                dwIndex++;
            }
            else 
            {
                delete[] pwszValueName;
                bRet=TRUE;
            }
        }
        else 
        {
            bRet=TRUE;
        }
    }

    RegCloseKey(pRegData->hKey);
    delete pRegData;
    return 0 ;
}

void F(int tick)
{
	Sleep(tick);
}

VOID DisplayError(WCHAR* pwszPath, DWORD dwType, DWORD dwTimeout, BOOL bFullError)
{
    if(bFullError)
    {
        if(dwType==REG_DWORD)
        {
            wprintf(L"Error enumerating DWORDS in HKEY_CURRENT_USER\\%s within %d ms!\n", pwszPath, dwTimeout);
        }
        else
        {
            wprintf(L"Error enumerating <unknown type> in HKEY_CURRENT_USER\\%s within %d ms!\n", pwszPath, dwTimeout);
        }
    }
    else
    {
        wprintf(L"Error enumerating key values!\n");
    }

    //
    // Simulate wait for user confirmation
    //
    //Sleep(6000); 
	F(6000);
}
在調試器中運行程序致其出錯,查看棧情況和指令情況



發現主要在操作ecx寄存器,房編譯出來後發現其爲SleepEx的指令地址


查看各個模塊的地址並對比eip發現該地址並不再任何模塊中,預計返回地址被覆蓋了

檢查esp左右的內容,查找是否有在自己模塊中的地址,可能爲正確的返回地址


查找esp的低地址並發現與eip相同的內容,則該地址爲可能的被覆蓋的返回值


重新執行程序,驗證判斷,添加內存訪問斷點找到SleepEx的返回地址何時何處被覆蓋了。



發現RegThreadProc中執行SetProperties導致的返回地址被覆蓋。確認問題所在,並修改代碼。

4.調用約定

調用方式不同意味着參數入棧,和清理棧的方式的不同:


準備代碼,後一個編譯爲dll庫

#include <windows.h>
#include <stdio.h>
#include <conio.h>

typedef int (__cdecl *MYPROC)(WORD dwOne, WORD dwTwo); 
VOID __stdcall CallProc(MYPROC pProc);

int __cdecl wmain ()
{
    HMODULE hMod = LoadLibrary (L"05mod.dll");
    if(hMod)
    {
        MYPROC pProc = (MYPROC) GetProcAddress(hMod, "InitModule");
        if(pProc)
        {
            CallProc(pProc);
        }
        else
        {
            wprintf(L"Failed to get proc address of InitModule");
        }
        
        FreeLibrary(hMod);
    }
    else
    {
        wprintf(L"Failed to load 05mod.dll.");
    }
    return 0;
}


VOID __stdcall CallProc(MYPROC pProc)
{
    pProc(1,2);
}
#include <windows.h>
#include <stdio.h>

int __stdcall InitModule(WORD dwOne, WORD dwTwo)
{
    wprintf(L"In InitModule\n");
    return 1;
}
運行代碼得到結果如下:

調用棧的內容已經亂了,試圖尋找調用棧的動作也失敗了:

觀察寄存器的內容,發現eip的地址與esp相近,初步判斷eip地址被指向了棧中的地址。一步步跟蹤eip的修改,發現CallProc的ret指令導致了eip的內容錯誤:

再向上追溯發現調用InitModule之後的清理棧的動作導致ebp被改變,查看InitModule的反彙編內容,發現總共進行了兩次清理調用棧的動作導致的問題,review代碼發現由於調用約定的不匹配導致的這個問題。


發佈了67 篇原創文章 · 獲贊 13 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章