用調試函數跟蹤API

 

在分析研究目標程序的內部調用機制時是非常有幫助的。在這裏所指的API,不僅包括狹義上的Windows系統函數,還包括廣義上第三方(及自身)提供DLL的輸出函數。如果從跟蹤監視的需求來講,跟蹤監視的API就不僅僅包括廣義的API,更希望包括EXE和DLL中的內部未導出的函數。

 
對某一目標程序進行API函數的跟蹤監視分析時,一般來講是沒有源代碼和調試版本,更多的情況是隻有EXE和DLL的發行版。跟蹤的目標就是通過運行目標程序,得到調用API函數的參數和運行結果,而不希望改變目標程序的運行路徑。
 
跟蹤API函數 跟蹤監視方案概覽
 
當我們對某一目標程序進行API函數的跟蹤監視分析時,根據跟蹤監視的目標,基本上有以下幾種途徑實現對API函數的跟蹤監視:

Log記錄分析

如果擁有目標程序的源代碼,就可以在關鍵的API函數的入口點和出口點記錄API的參數和運行結果。在除錯程序中是經常可以看到這種方法的。該方法的缺點就是必須擁有源代碼,每次修改Log時必須重新編譯源代碼。由於該方案和我們要討論的目標不同,在此不作討論。

將監視代碼注入目標程序

該方案的原理是在目標程序運行時,將監視代碼注入到目標程序的進程空間。監視代碼通過前期的準備工作,修改目標程序的運行代碼,使得目標程序調用指定的API函數時,運行指令會跳轉到監視代碼中,這樣,監視代碼就可以記錄下API函數的運行參數,然後,監視代碼在運行用來的API函數代碼。在用來的API函數代碼運行完後,再回到監視代碼中,將來下運行結果,再返回目標程序。其運行原理圖爲:
 


在32位Windows平臺上,各進程空間是獨立的。要在目標程序中運行監視代碼就必須將監視代碼注入到目標進程。一種常用的方法就是將監視代碼編譯成一個DLL,再將該DLL注入到目標進程中。關於將DLL注入目標進程的文章有很多,比如在《Windows核心編程》上就詳細介紹過CreateRemoteThread和SetWindowsHookEx的方法,關於DLL的注入不是本文的重點,在這裏就不進行介紹,請讀者參考其它相關的文章。

利用在目標程序中注入監視代碼實現監視的方案,常見以下幾種方式:

1.在目標函數寫入跳轉指令(jmp),跳轉至監視代碼實現監視

2.和第1種方式相同,在目標函數入口寫入跳轉指令,但在監視代碼中實現監視的機制不同。

3.利用API Hook功能,修改EXE和DLL的導入表(Import Address Table),將監視代碼中函數入口地址寫入導入表中,當EXE或DLL調用其它DLL中API函數時,就可以跳轉到監視代碼中實現跟蹤監視。

這裏介紹的1~3種方案在很多資料上都介紹過,這裏不再重複,他們都有一個最大的缺點:監視某個函數時必須知道函數的原型(即參數個數和調用方式——WINAPI調用還是其它?)。在寫監視代碼時必須確保監視代碼函數的原型和被監視函數的原型一致。希望增加一個監視函數時必須增加一個監視代碼。詳細可以參考Detours和API Hook。

用代理DLL實現API函數的監視

用代理DLL實現API函數的監視就是將原來的DLL改名,用一個新的DLL代替原來的DLL。這個新的DLL的導出函數和用來DLL的導出函數相同,並且導出函數的順序和原來的DLL一樣。新的DLL名字和原來DLL的名字也一樣。在新DLL中的每個函數實現代碼中,就負責記錄運行參數和運行結果,同時調用原來DLL的函數。其運行方式如圖所示:
 


比如:A.exe調用B.DLL,希望監視B.DLL的導出函數時,就將B.DLL改名爲C.DLL,生成一個新的監視模塊B.DLL,其導出函數名和順序和原來B.DLL完全相同,這樣A.EXE調用B.DLL時就進入了監視模塊,實現跟蹤監視的目標。

這裏新的B.DLL就是原來B.DLL的代理了,所有調用B.DLL的函數都會經過新的B.DLL得到監視。如果B.exe也調用了B.DLL,這樣,不止A.exe的調用被監視了,B.exe的調用也被監視了。監視記錄就多了一些無用的數據,對分析就增加了難度。對於這種方式,本文在此也不着更多的介紹。

利用調試函數實現跟蹤監視

跟蹤監視程序作爲調試器對目標進程進行調試,在目標進程的API函數的入口設置斷點。這樣,當目標進程調用被監視的API函數時,目標程序將產生調試中斷,系統將中斷調試信息通知跟蹤監視程序,同時被調試的目標進程掛起。這樣,跟蹤監視程序就可以訪問目標進程的內存,得到API函數的參數。然後通知系統讓掛起的目標進程繼續運行。同樣,在API函數的出口處再設置斷點,就可以得到API函數的處理結果。

通過調試函數實現跟蹤監視的方案實際上最基本的用法就是調試器。在調試器中可以輕鬆得到API函數的輸入輸出參數的,也可以得到變量的值。但利用調試器來作爲跟蹤監視程序的話,就非常的不方便了。產生斷點調試信息時,首先必須自己記錄下API函數的輸入輸出參數,其次讓目標進程繼續運行必須人工進行干預。由於過多的需要人工干預,在作爲跟蹤分析的工具上就無法有太大的作用。作爲一個比較實用的跟蹤監視工具,應該可以自動記錄下輸入輸出參數,同時可以讓目標進程自動進行運行而無需用戶的干預。

那麼,如果我們能自己編寫一個類似調試器的功能,這個調試器只需要實現我們對於跟蹤監視工具的要求,即自動記錄輸入輸出參數,自動讓目標進程繼續運行。就可以達到跟蹤監視工具的目的了。在下一篇文章中我們將對如何用調試函數達到這一要求進行詳細說明。

 用Debug函數實現API函數的跟蹤
本文詳細介紹瞭如何利用調試函數實現API跟蹤的方法,通過這種方法我們可以自己編寫一個類似調試器的功能,實現自動記錄輸入輸出參數,並讓目標進程繼續運行...
 
 如果我們能自己編寫一個類似調試器的功能,這個調試器需要實現我們對於跟蹤監視工具的要求,即自動記錄輸入輸出參數,自動讓目標進程繼續運行。下面我們就來介紹在不知道函數原型的情況下也可以簡單輸出監視結果的方案——用Debug函數實現API函數的監視。
Debug函數實現API函數的監視


大家知道,VC可以用來調試程序,除了調試Debug程序,當然也可以調試Release程序(調試Release程序時爲彙編代碼)。如果知道函數的入口地址,只需在函數入口上設置斷點,當程序調用了設置斷點的函數時,VC就會暫停目標程序的運行,你就可以得到目標程序內存的所有你希望得到的東西了。一般來說,只要你有足夠的耐心和毅力,以及一些彙編知識,對於監視API函數的輸入輸出參數還是可以完成的。

不過,由於VC的調試器會在每次斷點時暫停目標程序的運行,對目標程序的過多的暫停對於監視任務而言實在不能忍受。所以,不會有太多的人真的會用VC的調試器作爲一個良好的API函數監視器的。

如果VC調試器能夠在你設置好斷點後,在運行時自動輸出斷點時的堆棧值(也就是函數的輸入參數),在函數運行結束時也自動輸出堆棧值(也就是函數的輸出參數)和CPU寄存器的值(就是函數返回值),並且不會暫停目標程序。所有一切都是自動的無需我們干預。你會用它來作爲監視器嗎?我會的。

我不知道如何讓VC這樣作(或許VC真的可以這樣,但我不知道。有人知道的話請通知我一聲,謝謝),但我知道顯然VC也是通過調用Windows API函數完成調試器的任務,而且,這些函數顯然可以實現我的要求。我需要作的事情就是自己利用這些API函數,寫一個簡單的調試器,在目標程序斷點發生時自動輸出監視結果並且自動恢復目標程序的運行。

顯然,用VC調試器作爲監視器的話無需知道目標函數的原型就可以得到簡單的輸入輸出參數和函數運行結果,而且,由於監視代碼沒有注入目標程序中,就不會出現監視目標函數和監視代碼的衝突。VC調試器顯然可以跟蹤遞歸函數,也可以跟蹤DLL模塊調用DLL本身的函數,以及EXE內部調用自身的函數。只要你知道目標函數的入口地址,就可以跟蹤了(監視Exe自身的函數可以通過生成Exe模塊時選擇輸出Map文件,就可以參考Map文件得到Exe內部函數的地址)。沒有聽說VC不能調試多線程的,最多是說調試多線程比較麻煩----證明多線程是可以調試的。顯然,VC也可以調試DllMain中的代碼。這些,已經可以證明通過調試函數可以實現我們的目標了。
如何編寫實現我們目標的程序?需要哪些調試函數?


首先,讓目標程序進入被調試狀態:

對於一個已經啓動的進程而言,利用DebugActiveProcess函數就可以捕獲目標進程,將目標進程進入被調試狀態。

BOOL DebugActiveProcess(DWORD dwProcessId);


參數dwProcessId是目標進程的進程ID。如何通過ToolHelp系列函數或Psapi庫函數獲得一個運行程序的進程ID在很多文章中介紹過,這裏就不再重複。對於服務器程序而言,由於沒有權限無法捕獲目標進程,可以通過提升監視程序的權限得到調試權限進行捕獲目標進程(用戶必須擁有調試權限)。

對於啓動一個新的程序而言,通過CreateProcess函數,設置必要的參數就可以將目標程序進入被調試狀態。

BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,  LPSECURITY_ATTRIBUTES
lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID
lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation );


該函數的具體說明請參考MSDN,在這裏我僅介紹我們感興趣的參數。這裏和一般的用法不同,作爲被調試程序dwCreationFlags必須設置爲DEBUG_PROCESS或DEBUG_ONLY_THIS_PROCESS。這樣啓動的目標程序就會進入被調試狀態。這裏說明一下DEBUG_PROCESS和DEBUG_ONLY_THIS_PROCESS。DEBUG_ONLY_THIS_PROCESS就是隻調試目標進程,而DEBUG_PROCESS參數則不僅調試目標進程,而且調試由目標進程啓動的所有子進程。比如:在A.exe中啓動B.exe,如果用DEBUG_ONLY_THIS_PROCESS啓動,監視進程只調試A.exe不會調試B.exe,如果是DEBUG_PROCESS就會調試A.exe和B.exe。爲簡單起見,本文只討論啓動參數爲DEBUG_ONLY_THIS_PROCESS的情況。

使用方法:

STARTUPINFO st = {0};
PROCESS_INFORMATION pro = {0};
st.cb = sizeof(st);
CreateProcess(NULL, pszCmd, NULL, NULL, FALSE,                                        
DEBUG_ONLY_THIS_PROCESS,
NULL, szPath, &st, &pro));
// 關閉句柄---這些句柄在調試程序中不再使用,所以可以關閉
CloseHandle(pro.hThread);
CloseHandle(pro.hProcess);


其次,對進入被調試狀態的程序進行監視:

目標進程進入了被調試狀態,調試程序(這裏調試程序就是我們的監視程序,以後不再說明)就負責對被調試的程序進行調試操作的調度。調試程序通過WaitForDebugEvent函數獲得來自被調試程序的調試消息,調試程序根據得到的調試消息進行處理,被調試進程將暫停操作,直到調試程序通過ContinueDebugEvent函數通知被調試程序繼續運行。

BOOL WaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,  // debug event information
  DWORD dwMilliseconds         // time-out value
);


在參數lpDebugEvent中可以獲得調試消息,需要注意的是該函數必須和讓目標程序進入調試狀態的線程是同一線程。也就是說和通過DebugActiveProcess或CreateProcess調用的線程是一個線程。另外,我又喜歡將dwMilliseconds設置爲-1(無限等待)。所以我通常都會將CreateProcess和WaitForDebugEvent函數在一個新的線程中使用。

typedef 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;
} DEBUG_EVENT, *LPDEBUG_EVENT;

在這個調試消息結構體中,dwDebugEventCode記錄了產生調試中斷的消息代碼。消息代碼的詳細說明可以參考MSDN。其中,我們感興趣的消息代碼爲:

EXCEPTION_DEBUG_EVENT:產生調試例外
CRATE_THREAD_DEBUG_EVENT:新的線程產生
CREATE_PROCESS_DEBUG_EVENT:新的進程產生。注:在DEBUG_ONLY_THIS_PROCESS時只有一次,
在DEBUG_PROCESS時如果該程序啓動了子進程就可能有多次。
EXIT_THREAD_DEBUG_EVENT:一個線程運行中止
EXIT_PROCESS_DEBUG_EVENT:一個進程中止。注:在DEBUG_ONLY_THIS_PROCESS時只有一次,
在DEBUG_PROCESS可能有多次。
LOAD_DLL_DEBUG_EVENT:一個DLL模塊被載入。
UNLOAD_DLL_DEBUG_EVENT:一個DLL模塊被卸載。


在得到目標程序的調試消息後,調試程序根據這些消息代碼進行不同的處理,最後通知被調試程序繼續運行。

BOOL ContinueDebugEvent(
  DWORD dwProcessId,       // process to continue
  DWORD dwThreadId,        // thread to continue
  DWORD dwContinueStatus   // continuation status
);


該函數通知被調試程序繼續運行。

使用例:

DEBUG_EVENT dbe;
BOOL rc;
CreateProcess(NULL, pszCmd, NULL, NULL, FALSE,                                        
DEBUG_ONLY_THIS_PROCESS,
NULL, szPath, &st, &pro));
while(WaitForDebugEvent(&dbe, INFINITE))
{
// 如果是退出消息,調試監視結束
if(dbe. dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
break;
// 進入調試監視處理
rc = OnDebugEvent(&dbe);
if(rc)
ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId , DBG_CONTINUE );
else
ContinueDebugEvent(dbe.dwProcessId , dbe.dwThreadId ,
DBG_ DBG_EXCEPTION_NOT_HANDLED);
}
// 調試消息處理程序
BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)
{
// 我們還沒有對目標進程進行操作,所以,先返回TRUE。
return TRUE;
}


上面這些程序就是一個最簡單的調試程序了。不過,它基本上沒有什麼用途。你還沒有在目標進程中設置斷點,你就不能完成對API函數監視的任務。
 
 Debug函數實現API函數的跟蹤 2
 
對目標進程設置斷點:

我們的目標是監視API函數的輸入輸出,那麼,首先應該知道DLL模塊中提供了哪些API函以及這些API的入口地址。在前面將過,廣義的API還包括未導出的內部函數。如果你有DLL模塊的調試版本和調試連接文件(pdb文件),也可以根據調試信息得到內部函數的信息。

· 得到函數名及函數入口地址

通過程序得到函數的入口地址有很多種方法。對於用VC編譯出來的DLL,如果是Debug版本,可以通過ImageHlp庫函數得到調試信息,分析出函數的入口地址。如果沒有Debug版本,也可以通過分析導出函數表得到函數的入口地址。

1.用Imagehlp庫函數得到Debug版本的函數名和函數入口地址。

可以利用Imagehlp庫函數分析Debug信息,關聯的函數爲SymInitialize、SymEnumerateSymbols和UnDecorateSymbolName。詳細可以參考MSDN中關於這些函數的說明和用法。不過,用Imagehlp只能分析出用VC編譯的程序,對C++Builder編譯的程序不能用這種方法分析。

2.DLL的導出表得到函數導出函數名和函數的入口地址。

在大多數情況下,我們還是希望監視的是Release版本的輸入輸出參數,畢竟Debug版本不是我們最終提供給用戶的產品。Debug和Release的編譯條件不同導致產生的結果不同,在很多BBS中都討論過。所以,我認爲跟蹤監視Release版本更加有實用價值。

通過分析DLL導出表得到導出函數名在MSDN上就有源代碼。關於導出表的說明大家可以參考關於PE結構的文章。

3.通過OLE函數取得COM接口

你也可以通過OLE函數分析DLL提供的接口函數。接口函數不是通過DLL導出表導出的。你可以通過LoadTypeLib函數來分析COM接口,得到COM記錄接口的入口地址,這樣,你就可以監視COM接口的調用了。這是API HOOK沒法實現的。在這裏我不打算分析分析COM接口的方式了。在MSDN上通過搜索LoadTypeLib sample關鍵詞你就可以找到相關的源代碼進行修改實現你的目標。

這裏是通過計算機自動分析目標模塊得到DLL導出函數的方案,作爲我們監視的目的而言,這些工作只是爲了得到一系列的函數名和函數地址而已。函數名只是一個讓我們容易識別函數的名稱而已,該函數入口地址纔是我們真正關心的目標。換句話說,如果你能夠確保某一個地址一定是一個函數(包括內部函數)的入口地址,你就完全可以給這個函數定義自己的名稱,將它加入你的函數管理表中,同樣可以實現監視該函數的輸入輸出參數的功能。這也是實現Exe內部函數的監視功能的原因。如果你有Exe編譯時生成的Map文件(你可以在編譯時選擇生成Map文件),你就可以通過分析Map文件,得到內部函數的入口地址,將內部函數加入到你的函數管理表中。(一個函數的名稱對於監視功能來講究竟是FunA還是FunB並沒有什麼意義,但名稱是FunA還是FunB的名稱對於監視者分析監視結果是有意義的,你完全可以將MessageBox的函數在輸出監視結果是以FunA的名稱輸出,所以在監視一些內部無名稱的函數時,你完全可以定義你自己的名字)。

· 在函數入口地址處設置斷點

設置斷點非常簡單,只要將0xCC(int 3)寫入指定的地址就可以了。這樣程序運行到指定地址時,將產生調試中斷信息通知調試程序。修改指定進程的內存數據可以通過WriteProcessMemory函數來完成。由於一般情況下作爲程序代碼段都被保護起來了,所以還有一個函數也會用到。VirtualProtectEx。在實際情況下,當調試斷點發生時,調試程序還應該將原來的代碼寫回被調試程序。

unsigned char SetBreakPoint(DWORD pAdd, unsigned char code)
{
         unsigned char b;
         BOOL rc;
         DWORD dwRead, dwOldFlg;
// 0x80000000以上的地址爲系統共有區域,不可以修改
if( pAdd >= 0x80000000 || pAdd == 0)
         return code;
// 取得原來的代碼
rc = ReadProcessMemory(_ghDebug, pAdd, &b, sizeof(BYTE), &dwRead);
// 原來的代碼和準備修改的代碼相同,沒有必要再修改
if(rc == 0 || b == code)
             return code;
// 修改頁碼保護屬性
VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), PAGE_READWRITE,
&dwOldFlg);
// 修改目標代碼
WriteProcessMemory(_ghDebug, pAdd, &code, sizeof(unsigned char), &dwRead);
// 恢復頁碼保護屬性
VirtualProtectEx(_ghDebug, pAdd, sizeof(unsigned char), dwOldFlg, &dwOldFlg);
         return b;
}


在設置斷點時你必須將原來的代碼保存起來,這樣在恢復斷點時就可以將代碼還原了。一般用法爲:設置斷點m_code = SetBreakPoint( pFunAdd, 0xCC); 恢復斷點:SetBreakPoint( pFunAdd, m_code); 記住,每個函數入口地址的代碼都可能不同,你應該爲每個斷點地址保存一個原來的代碼,在恢復時就不會發生錯誤了。

好了,現在目標程序中已經設置好了斷點,當目標程序調用設置了斷點的函數時,將產生一個調試中斷信息通知調試程序。我們就要在調試程序中編寫我們的調試中斷程序了。
編寫調試中斷處理程序


被調試程序產生中斷時,將產生一個EXCEPTION_DEBUG_EVENT信息通知調試程序進行處理。同時將填充EXCEPTION_DEBUG_INFO結構。

typedef struct _EXCEPTION_DEBUG_INFO {
  EXCEPTION_RECORD ExceptionRecord;
  DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
typedef struct _EXCEPTION_RECORD {
  DWORD ExceptionCode;
  DWORD ExceptionFlags;
  struct _EXCEPTION_RECORD *ExceptionRecord;
  PVOID ExceptionAddress;
  DWORD NumberParameters;
  ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

在該結構中,我們比較感興趣的是產生中斷的地址ExceptionAddress和產生中斷的信息代碼ExceptionCode。在信息代碼中與我們任務相關的信息代碼爲:

EXCEPTION_BREAKPOINT:斷點中斷信息代碼
EXCEPTION_SINGLE_STEP:單步中斷信息代碼


斷點中斷是由於我們在前面設置斷點0xCC代碼運行時產生的。由於產生中斷後,我們必須將原來的代碼寫回被調試程序中繼續運行。但是,代碼一旦被寫回目標程序,這樣,當目標程序再次調用該函數時將不會產生中斷,我們就只能實現一次監視了。所以,我們必須在將原代碼寫回被調試程序後,應該讓被調試程序已單步的方式運行,再次產生一個單步中斷的調試信息。在單步中斷處理中,我們再次將0xCC代碼寫入函數的入口地址,這樣就可以保證再次調用時產生中斷。

首先,在進行中斷處理前我們必須作些準備工作,管理起線程ID和線程句柄。爲了管理單步中斷處理,我們還必須維護一個基於線程的單步地址的管理,這樣就可以允許被調試程序擁有多線程的功能。--我們不能保證單步運行時不被該進程的其他線程所打斷。

// 我們利用一個map進行管理線程ID和線程句柄之間的關係
// 同時也用一個map管理函數地址和斷點的關係
typedef map<DWORD, HANDLE, less<DWORD> > THREAD_MAP;
typedef map<DWORD, void*, less<DWORD> > THREAD_SINGLESTEP_MAP;
THREAD_MAP _gthreads;
FUN_BREAK_MAP _gFunBreaks;
// 並且假設設置斷點時採用瞭如下方案進行原來代碼的管理
BYTE code = SetBreakPoint(pFunAdd, 0xCC);
if(code != 0xCC)
_gFunBreaks[pFunAdd] = code;
// 調試處理程序
BOOL WINAPI OnDebugEvent(DEBUG_EVENT* pEvent)
{
BOOL rc = TRUE;
switch(pEvent->dwDebugEventCode)
{
case CREATE_PROCESS_DEBUG_EVENT:
// 記錄線程ID和線程句柄的關係
_gthreads[pEvent->dwThreadId] = pEvent->u.CreateProcessInfo.hThread;
break;
case CREATE_THREAD_DEBUG_EVENT:
// 記錄線程ID和線程句柄的關係
_gthreads [pEvent->dwThreadId] = pEvent->u.CreateThread.hThread;
break;
case EXIT_THREAD_DEBUG_EVENT:
// 線程退出時清除線程ID
_gthreads.erase (pEvent->dwThreadId);
break;
case EXCEPTION_DEBUG_EVENT:
// 中斷處理程序
rc = OnDebugException(pEvent);
break;
}
return rc;
}


下面進行中斷處理程序。同樣,我們只考慮我們關心的中斷信息代碼。在發生中斷時,我們通過GetThreadContext(&context)得到中斷線程的上下文信息。此時,context.esp就是函數的返回地址,context.esp+4位置的值就是函數的第一個參數,context.esp+8就是第二個參數,依次類推可以得到你想要的任何參數。需要注意的是因爲參數是在被調試進程中的內容,所以你必須通過ReadProcessMemory函數才能得到:

DWORD buf[4]; // 取4個參數
ReadProcessMemory(_ghDebug, (void*)(context.esp + 4), &buf,  sizeof(buf),
&dwRead);


那麼buf[0]就是第一個參數,buf[1]就是第二個參數。。。注意,在FunA(int a, char* p, OPENFILENAME* pof)函數調用時,buf[0] = a, buf[1] = p這裏buf[1]是p的指針而不是p的內容,如果你希望訪問p的內容,必須同樣通過ReadProcessMemory函數再次取得p的內容。對於結構體指針也必須如此:

// 取得p的內容:
char pBuf[256];
ReadProcessMemory(_ghDebug, (void*)(buf[1]), &pBuf,  sizeof(pBuf), &dwRead);
//取得pof的內容:
OPENFILENAME of
ReadProcessMemory(_ghDebug, (void*)(buf[2]), &of,  sizeof(of), &dwRead);


如果結構體中還有指針,要取得該指針的內容,也必須和取得p的內容一樣的方式讀取被調試程序的內存。總的來說,你必須意識到監視目標程序的所有內容都是對目標進程的內存讀取操作,這些指針都是目標進程的內存地址,而不是調試進程的地址。
 
 
Debug函數實現API函數的跟蹤 3
 
很明顯,當被調試進程在函數入口產生中斷調試信息時,調試程序只能得到函數的輸入參數,而不能得到我們希望的輸出參數及返回值!爲了實現我們的目標,我們必須在函數調用結束時,再次產生中斷,取得函數的輸出參數和返回值。在處理函數入口中斷時,就必須設置好函數的返回地址的斷點。這樣,在函數返回時,就可以得到函數的輸出參數和返回值了。關於這裏的實現說明請參考附錄的源代碼。

你完全可以參照附錄的源代碼寫出你自己的簡單的調試監視程序。當然,有幾個問題因爲比較複雜,我沒有在這裏進行說明。一個就是函數返回斷點的處理,比如TRY、CATCH的處理,就必須重新設計好RETURN_FUN_STACK的結構,考慮一些除錯處理還是可以解決這個問題的。另外一個問題就是函數的入口斷點和返回斷點沒有任何關係。這個問題更好解決,只需重新設計RETURN_FUN,FUN_BREAK_MAP等結構體就可以將它們關聯起來。由於我在這裏只要是分析如何實現中斷調試處理的過程,這些完善程序的工作就由讀者自行跟蹤改造了。
關於Win9X系統


細心的讀者在上面可以發現一個問題,那就是在SetBreakPoint函數中有一個限制,就是函數的入口地址不能大於0x80000000。確實如此,我們知道0x80000000以上的空間是系統共有的空間,我們一般不能修改這些空間的程序,否則將影響系統的工作。在NT環境下,所有的DLL都被加載在0x80000000下,修改0x80000000以下空間的代碼不會對其它進程產生影響。所以在NT下可以用上面的方案監視所有的DLL函數。然而,在Win9X下,kernel32.dll,user32.dll,gdi32.dll等系統DLL都被加載到0x80000000以上的空間,修改這些空間的代碼將破壞系統工作。那麼,在9X下就不能監視這些DLL模塊的函數嗎?

的確,在Win9X平臺下不能利用在函數入口處設置斷點的方法實現監視。我們必須採用另外的方法實現該功能。在前面討論中知道,通過API HOOK修改模塊導入表的方法可以實現將API的入口修改爲自己監視程序的入口,也可以實現監視功能。如果採用API HOOK的方法有限制,即必須知道函數原型,對每一個函數都必須編寫相應的監視代碼,靈活性受到限制。而我們的目標是不管有多少個DLL,不管DLL有多少個導出函數,在不修改我們的程序前提下都可以實現我們的監視功能。所以,API HOOK是不可以完成我們的目標,但我們可以利用修改導入表的方案實現目標。首先,修改導入表,將函數的調用地址指向我們的監視代碼,在監視代碼中,我們無需對函數編程,只是簡單調用jmp XXXX就可以了。然後,設置斷點時,不是設置在函數的入口點,而是設置在我們的監視代碼上。這樣,當我們的模塊調用系統API函數時,就可以實現監視功能了。修改原理如圖:
 


如圖所示,假設我們的監視代碼在目標進程的的0x20000000空間,我們在分析DLL導出表的同時,將導出表函數的地址經過計算,在監視代碼中設置爲jmp xxxx的代碼。這樣我們在修改EXE模塊的導入表時寫入的地址爲監視代碼的地址。當目標程序調用MessageBox函數是,程序將首先跳轉到監視代碼中執行jmp指令到user32.dll的MessageBox入口地址中。經過這樣處理後,我們希望監視MessageBox函數的調用時,只需在監視代碼的0x20000000處設置斷點,就達到了監視的目的。限於篇幅原因,這裏不再討論。
擴展應用


你可以很輕鬆的在此基礎上進行擴展你的監視跟蹤功能。只需要修改一下記錄輸入輸出函數結果的程序,就得到一個新的功能:

1.在記錄輸入輸出參數的地方加入取得當前時刻的功能,就實現了監視函數調用性能的功能。(相當於Numega的TrueTime功能)由於採用了Debug技術,得到的時間將包括調試函數導致產生進程的切換時間。等到的時間只是一個參考價值,但對分析性能而言一般足夠。

2.在記錄輸入輸出參數的地方加入函數調用的計數器,就實現了Numega的TrueCoverage功能。

3.監視malloc, free, realloc函數的輸入輸出值,並進行統計,就實現了簡單的內存泄漏檢查功能。關鍵的是你可以通過Map文件得到Release版本的malloc等函數的地址,實現對Release版的跟蹤。

4.在記錄輸入參數處理中加入StackWalk函數可以實現call stack功能,分析是由哪個函數調用了自己。在jmp方案中也可以實現這個功能,但是你必須確保StackWalk關聯的函數沒有調用被你監視的函數。在Hook API(IAT)的方案中到是不用保證,但得出的調用列表中有可能包含你的監視代碼。

有一點需要注意的是,我們的目標是監視程序的運行路徑,並不是改變參數和修改結果,所以,在jmp和Hook Api(IAT)中可以實現的修改參數和運行路徑的做法在這裏不能實現。

其他:

本文附錄的代碼TestDebug.zip就是實現了一個簡單的調試監視器,自動輸出監視函數的4個輸入參數的地址內容和函數調用返回值。該代碼只是表明通過監視函數可以實現對API的跟蹤,所以沒有實現9X下對系統DLL的監視。

DebugApi.zip是一個利用這個方案編寫的應用程序DebugApiSpy.exe,它實現了這個方案中的最基本的跟蹤監視函數的輸入輸出參數功能,也實現了9X下對系統DLL的監視支持。該程序支持Win9X/NT/W2K/XP上的運用。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章