一:背景 1.講故事 前段時間分析了一個dump,一頓操作之後,我希望用外力來阻止程式內部對某一個com組件的調用,對,就是想藉助外力實現,如果用 windbg 的話,可以說非常輕鬆,但現實情況比較複雜,客戶機沒有windbg,也不想加入任何的手工配置,希望全自動化來處理。 真的很無理哈。。。不過這 ...
一:背景
1.講故事
前段時間分析了一個dump,一頓操作之後,我希望用外力來阻止程式內部對某一個com組件的調用,對,就是想藉助外力實現,如果用 windbg 的話,可以說非常輕鬆,但現實情況比較複雜,客戶機沒有windbg,也不想加入任何的手工配置,希望全自動化來處理。
真的很無理哈。。。不過這種無理要求花點心思還是可以實現的,方法就是用代碼將應用程式變成調試器 來實現自動化阻止,為了簡化操作,我們拿 C# 的 File.WriteAllText
來舉個例子,讓我的調試器來截獲它的 content。
2. 測試案例
為了方便講述,創建一個 WPF 程式,在 button 事件中用 File.WriteAllText
方法來寫日誌,參考代碼如下:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
System.IO.File.WriteAllText("C:\\1.txt", DateTime.Now.ToString());
}
}
代碼非常簡單,點一下按鈕就寫一條時間日誌,接下來分別用 WinDbg 和 自定義調試器 來截獲這個時間。
二:WinDbg 下的實現
1. 實現原理
要想截獲日誌,需要知道這個鏈路的下游方法,比如:kernel32!WriteFile
,msdn 上的定義如下:
BOOL WriteFile(
[in] HANDLE hFile,
[in] LPCVOID lpBuffer,
[in] DWORD nNumberOfBytesToWrite,
[out, optional] LPDWORD lpNumberOfBytesWritten,
[in, out, optional] LPOVERLAPPED lpOverlapped
);
其中 lpBuffer
存放的就是 content 信息, nNumberOfBytesToWrite
存放的是長度,有了這些基礎,就可以通過 bp 下斷點了。
0:007> bp kernel32!WriteFile
0:007> g
Breakpoint 0 hit
eax=0126a4e8 ebx=00000000 ecx=000004a0 edx=76663510 esi=0320eb6c edi=010feaa8
eip=76663510 esp=010fea24 ebp=010fea90 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00200202
KERNEL32!WriteFile:
76663510 ff2558106c76 jmp dword ptr [KERNEL32!_imp__WriteFile (766c1058)] ds:002b:766c1058={KERNELBASE!WriteFile (75ebd760)}
0:000> kb 3
# ChildEBP RetAddr Args to Child
00 010fea90 6a829fef 00000000 010feaa8 00000013 KERNEL32!WriteFile
01 010feab8 6a829f2c 010fead4 00000000 00000013 mscorlib_ni!System.IO.FileStream.WriteFileNative+0x6f
02 010feae0 6a829ec5 00000013 00000000 0320d69c mscorlib_ni!System.IO.FileStream.WriteCore+0x3c
因為 kernel32!WriteFile
用的是 stdcall 協定,所以 lpBuffer
變數在 esp+0x8
的位置, nNumberOfBytesToWrite
變數在 esp+0xc
的位置。
0:000> da poi(esp+8)
0320eb6c "2022/11/24 17:25:39"
0:000> dp esp+0xc L1
010fea30 00000013
0:000> ? poi(esp+0xc)
Evaluate expression: 19 = 00000013
從卦中看,content 和 length 都出來了,非常完美,接下來看下如何自定義實現調試器。
三:自己實現一個調試器
1. 技術原理
要想自定義實現,需要打通這三塊。
- 如何給
kernel32!WriteFile
下 bp 斷點
bp 的原理其實就是 int 3
,簡而言之就是 windbg 會將 kernel32!WriteFile
指令的首位元組修改成機器碼 0xcc
,命中之後又將 0xcc
撤銷掉。這一串邏輯是 windbg 內部自己實現的,接下來我們驗證下,將首位元組直接改成 0xcc
。
0:011> x kernel32!WriteFile
76663510 KERNEL32!WriteFile (_WriteFile@20)
0:011> db 76663510 L1
76663510 ff .
0:011> eb 76663510 cc
0:011> db 76663510 L1
76663510 cc .
從卦中看已修改成功,接下來直接點擊 WPF 窗體的 button 按鈕就會直接命中這裡的 int 3
實現中斷。
到了這一步後,可以在程式中使用 WriteProcessMemory
恢復 WriteFile
原始位元組為 ff
。
- 如何讓 int 3 中斷給程式
剛纔看到的是中斷給WinDbg,那怎麼中斷給程式呢? 其實 Win32 API 中有一個叫 DebugActiveProcess
函數可以讓宿主程式充當調試器,mdsn 中的描述如下:
- 如何讀寫 wpf 的記憶體和寄存器
只要獲取到了 wpf 程式的進程和線程句柄,可以用 WriteProcessMemory
和 ReadProcessMemory
讀寫記憶體,用 GetThreadContext
和 SetThreadContext
讀寫寄存器。
2. 代碼實現
思路和技術都搞清楚後,代碼落地就非常簡單了,參考如下:
// HookDebug.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
#include <iostream>
#include <Windows.h>
LPVOID writefile_addr = NULL;
CREATE_PROCESS_DEBUG_INFO cpdi;
BYTE int3 = 0xCC;
BYTE ff = 0;
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) {
writefile_addr = GetProcAddress(GetModuleHandle(L"kernel32.dll"), "WriteFile");
memcpy(&cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
ReadProcessMemory(cpdi.hProcess, writefile_addr, &ff, sizeof(BYTE), NULL);
WriteProcessMemory(cpdi.hProcess, writefile_addr, &int3, sizeof(BYTE), NULL);
return TRUE;
}
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {
CONTEXT ctx;
PBYTE lpBuffer = NULL;
DWORD lpBufferStart, nNumberOfBytesToWrite;
PEXCEPTION_RECORD pr = &pde->u.Exception.ExceptionRecord;
//int3 斷點
if (pr->ExceptionCode == EXCEPTION_BREAKPOINT && writefile_addr == pr->ExceptionAddress) {
//1. unhook,恢復 writefile 的
WriteProcessMemory(cpdi.hProcess, writefile_addr, &ff, sizeof(BYTE), NULL);
//2. 獲取上下文
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(cpdi.hThread, &ctx);
//3. 獲取 WriteFile 寫入的內容
ReadProcessMemory(cpdi.hProcess, (PVOID)(ctx.Esp + 0x8), &lpBufferStart, sizeof(DWORD), NULL);
ReadProcessMemory(cpdi.hProcess, (PVOID)(ctx.Esp + 0xc), &nNumberOfBytesToWrite, sizeof(DWORD), NULL);
//4. 分配緩衝區
lpBuffer = (PBYTE)calloc(nNumberOfBytesToWrite + 1, sizeof(BYTE));
//5. copy 數據到緩衝區中
ReadProcessMemory(cpdi.hProcess, (LPVOID)lpBufferStart, lpBuffer, nNumberOfBytesToWrite, NULL);
printf("截獲的內容: %s \n", lpBuffer);
//6. 重新修改 eip ,指向 writefile 開頭,寫回到線程上下文中
ctx.Eip = (DWORD)writefile_addr;
SetThreadContext(cpdi.hThread, &ctx);
//7. 繼續執行
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);
//8. 重新 hook
WriteProcessMemory(cpdi.hProcess, writefile_addr, &int3, sizeof(BYTE), NULL);
return TRUE;
}
return FALSE;
}
void loop() {
DEBUG_EVENT de;
while (WaitForDebugEvent(&de, INFINITE))
{
//註入事件
if (de.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) {
OnCreateProcessDebugEvent(&de);
}
//異常事件
if (de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) {
if (OnExceptionDebugEvent(&de)) continue;
}
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
}
}
int main()
{
//程式日誌
DWORD dwPID = 23264;
if (!DebugActiveProcess(dwPID)) {
printf("fail");
return 1;
}
loop();
return 0;
}
代碼中的 dwPID
是 WPF 程式的 PID,指定好之後把程式跑起來,點擊 button 按鈕觀察,截圖如下,非常完美。
三:總結
在無法安裝 windbg 的受限環境下,部署 HookDebug.exe
就是我們的另一種選擇,而且完全自動化攔截,基本實現無人工干預。