網絡安全編程:編寫密碼顯示程序
本文使用調試API針對CrackMe來編寫一個顯示密碼的程序。
在編寫關于CrackMe的密碼顯示程序以前需要準備兩項工作,第一項工作是知道要在什么地方合理地下斷點,第二項工作是從哪里能讀取到密碼。帶著這兩個問題重新來思考一下。在這里的程序中,要對兩個字符串進行比較,而比較的函數是strcmp(),該函數有兩個參數,分別是輸入的密碼和真正的密碼。也就是說,在調用strcmp()函數的位置下斷點,通過查看它的參數是可以獲取到正確的密碼的。在調用strcmp()函數的位置設置INT3斷點,也就是將0xCC機器碼寫入這個地址。用OD看一下調用strcmp()函數的地址,如圖1所示。
圖1 調用strcmp()函數的地址
從圖1中可以看出,調用strcmp()函數的地址為00401E9E。有了這個地址,只要找到該函數的兩個參數,就可以找到輸入的錯誤的密碼及正確的密碼。從圖1中可以看出,正確的密碼的起始地址保存在EDX中,錯誤的密碼的起始地址保存在ECX中。只要在00401E9E地址處下斷點,并通過線程環境讀取EDX和ECX寄存器值就可以得到兩個密碼的起始地址。
進行準備的工作已經做好了,下面來寫一個控制臺的程序。先定義兩個常量,一個是用來設置斷點的地址,另一個是INT3指令的機器碼。定義如下:
- // 需要設置 INT3 斷點的位置
- #define BP_VA 0x00401E9E
- // INT3 的機器碼
- const BYTE bInt3 = '\xCC';
把CrackMe的文件路徑及文件名當參數傳遞給顯示密碼的程序。顯示的程序首先要以調試的方式創建CrackMe,代碼如下:
- // 啟動信息
- STARTUPINFO si = { 0 };
- si.cb = sizeof(STARTUPINFO);
- GetStartupInfo(&si);
- // 進程信息
- PROCESS_INFORMATION pi = { 0 };
- // 創建被調試進程
- BOOL bRet = CreateProcess(pszFileName,
- NULL,NULL,NULL,FALSE,
- DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS,
- NULL,NULL,&si,&pi);
- if ( bRet == FALSE )
- {
- printf("CreateProcess Error \r\n");
- return -1;
- }
然后進入調試循環,要處理兩個調試事件,一個是CREATE_PROCESS_DEBUG_EVENT,另一個是EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT。處理CREATE_PROCESS_DEBUG_EVENT的代碼如下:
- // 創建進程時的調試事件
- case CREATE_PROCESS_DEBUG_EVENT:
- {
- // 讀取欲設置 INT3 斷點處的機器碼
- // 方便后面恢復
- ReadProcessMemory(pi.hProcess,(LPVOID)BP_VA,
- (LPVOID)&bOldByte,sizeof(BYTE),&dwReadWriteNum);
- // 將 INT3 的機器碼 0xCC 寫入斷點處
- WriteProcessMemory(pi.hProcess,(LPVOID)BP_VA,
- (LPVOID)&bInt3,sizeof(BYTE),&dwReadWriteNum);
- break;
- }
在CREATE_PROCESS_DEBUG_EVENT中對調用strcmp()函數的地址處設置INT3斷點,再將0xCC寫入這里時要把原來的機器碼讀取出來。讀取原機器碼使用ReadProcess Memory(),寫入INT3的機器碼使用WriteProcessMemory()。讀取原機器碼的作用是當寫入的0xCC產生中斷以后,需要將原機器碼寫回,以便程序可以正確繼續運行。
再來看一下EXCEPTION_DEBUG_EVENT下的EXCEPTION_BREAKPOINT是如何進行處理的,代碼如下:
- // 產生異常時的調試事件
- case EXCEPTION_DEBUG_EVENT:
- {
- // 判斷異常類型
- switch ( de.u.Exception.ExceptionRecord.ExceptionCode )
- {
- // INT3 類型的異常
- case EXCEPTION_BREAKPOINT:
- {
- // 獲取線程環境
- context.ContextFlags = CONTEXT_FULL;
- GetThreadContext(pi.hThread, &context);
- // 判斷是否斷在設置的斷點位置處
- if ( (BP_VA + 1) == context.Eip )
- {
- // 讀取正確的密碼
- ReadProcessMemory(pi.hProcess,(LPVOID)context.Edx,
- (LPVOID)pszPassword,MAXBYTE,&dwReadWriteNum);
- // 讀取錯誤密碼
- ReadProcessMemory(pi.hProcess,(LPVOID)context.Ecx,
- (LPVOID)pszErrorPass,MAXBYTE,&dwReadWriteNum);
- printf("你輸入的密碼是: %s \r\n", pszErrorPass);
- printf("正確的密碼是: %s \r\n", pszPassword);
- //指令執行了 INT3 而被中斷
- // INT3 的機器指令長度為 1 字節
- // 因此需要將 EIP 減一來修正 EIP
- // EIP 是指令指針寄存器
- // 其中保存著下條要執行指令的地址
- context.Eip --;
- // 修正原來該地址的機器碼
- WriteProcessMemory(pi.hProcess,(LPVOID)BP_VA,
- (LPVOID)&bOldByte,sizeof(BYTE),&dwReadWriteNum);
- // 設置當前的線程環境
- SetThreadContext(pi.hThread, &context);
- }
- break;
- }
- }
- }
對于調試事件的處理,應該放到調試循環中。上面的代碼給出的是對調試事件的處理,再來看一下調試循環的大體代碼:
- while ( TRUE )
- {
- // 獲取調試事件
- WaitForDebugEvent(&de, INFINITE);
- // 判斷事件類型
- switch ( de.dwDebugEventCode )
- {
- // 創建進程時的調試事件
- case CREATE_PROCESS_DEBUG_EVENT:
- {
- break;
- }
- // 產生異常時的調試事件
- case EXCEPTION_DEBUG_EVENT:
- {
- // 判斷異常類型
- switch ( de.u.Exception.ExceptionRecord.ExceptionCode )
- {
- // INT3 類型的異常
- case EXCEPTION_BREAKPOINT:
- {
- }
- break;
- }
- }
- }
- ContinueDebugEvent(de.dwProcessId,de.dwThreadId,DBG_CONTINUE);
- }
只要把調試事件的處理方法放入調試循環中,程序就完整了。接下來編譯連接一下,然后把CrackMe直接拖放到這個密碼顯示程序上。程序會啟動CrackMe進程,并等待用戶的輸入。輸入賬號及密碼后,單擊“確定”按鈕,程序會顯示出正確的密碼和用戶輸入的密碼,如圖2所示。
圖2 顯示正確密碼
根據圖2顯示的結果進行驗證,可見獲取的密碼是正確的。程序到此結束,大家可以把該程序改成通過附加調試進程來顯示密碼,以鞏固所學的知識。