在有了栈和函数调用的认识之后,可以动手模拟和栈相关的调试技术。
1.栈基础
以如下代码为例子(关闭缓冲区检查和优化):
#include
#include
#include
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
通过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
#include
#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
#include
#include
#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(lpParameter);
//wprintf(L"RegThreadProc pRegValues address is %d\n", pRegData->pRegValues);
while(!bRet && dwIndexdwRegValuesCount)
{
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 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
#include
#include
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
#include
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代码发现由于调用约定的不匹配导致的这个问题。