淺析如何截獲C#程序產(chǎn)生的日志
一:背景
1.講故事
前段時(shí)間分析了一個(gè)dump,一頓操作之后,我希望用外力來(lái)阻止程序內(nèi)部對(duì)某一個(gè)com組件的調(diào)用,對(duì),就是想借助外力實(shí)現(xiàn),如果用 windbg 的話,可以說(shuō)非常輕松,但現(xiàn)實(shí)情況比較復(fù)雜,客戶機(jī)沒(méi)有windbg,也不想加入任何的手工配置,希望全自動(dòng)化來(lái)處理。
真的很無(wú)理哈。。。不過(guò)這種無(wú)理要求花點(diǎn)心思還是可以實(shí)現(xiàn)的,方法就是用代碼將應(yīng)用程序變成調(diào)試器 來(lái)實(shí)現(xiàn)自動(dòng)化阻止,為了簡(jiǎn)化操作,我們拿 C# 的 File.WriteAllText 來(lái)舉個(gè)例子,讓我的調(diào)試器來(lái)截獲它的 content。
2. 測(cè)試案例
為了方便講述,創(chuàng)建一個(gè) WPF 程序,在 button 事件中用 File.WriteAllText 方法來(lái)寫(xiě)日志,參考代碼如下:
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());
}
}
代碼非常簡(jiǎn)單,點(diǎn)一下按鈕就寫(xiě)一條時(shí)間日志,接下來(lái)分別用 WinDbg 和 自定義調(diào)試器 來(lái)截獲這個(gè)時(shí)間。
二:WinDbg 下的實(shí)現(xiàn)
1. 實(shí)現(xiàn)原理
要想截獲日志,需要知道這個(gè)鏈路的下游方法,比如: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 存放的是長(zhǎng)度,有了這些基礎(chǔ),就可以通過(guò) bp 下斷點(diǎn)了。
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
因?yàn)?nbsp;kernel32!WriteFile 用的是 stdcall 協(xié)定,所以 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 都出來(lái)了,非常完美,接下來(lái)看下如何自定義實(shí)現(xiàn)調(diào)試器。
三:自己實(shí)現(xiàn)一個(gè)調(diào)試器
1. 技術(shù)原理
要想自定義實(shí)現(xiàn),需要打通這三塊。
如何給 kernel32!WriteFile 下 bp 斷點(diǎn)
bp 的原理其實(shí)就是 int 3 ,簡(jiǎn)而言之就是 windbg 會(huì)將 kernel32!WriteFile 指令的首字節(jié)修改成機(jī)器碼 0xcc,命中之后又將 0xcc 撤銷(xiāo)掉。這一串邏輯是 windbg 內(nèi)部自己實(shí)現(xiàn)的,接下來(lái)我們驗(yàn)證下,將首字節(jié)直接改成 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 .

從卦中看已修改成功,接下來(lái)直接點(diǎn)擊 WPF 窗體的 button 按鈕就會(huì)直接命中這里的 int 3 實(shí)現(xiàn)中斷。

到了這一步后,可以在程序中使用 WriteProcessMemory 恢復(fù) WriteFile 原始字節(jié)為 ff。
如何讓 int 3 中斷給程序
剛才看到的是中斷給WinDbg,那怎么中斷給程序呢? 其實(shí) Win32 API 中有一個(gè)叫 DebugActiveProcess 函數(shù)可以讓宿主程序充當(dāng)調(diào)試器,mdsn 中的描述如下:

如何讀寫(xiě) wpf 的內(nèi)存和寄存器
只要獲取到了 wpf 程序的進(jìn)程和線程句柄,可以用 WriteProcessMemory 和 ReadProcessMemory 讀寫(xiě)內(nèi)存,用 GetThreadContext 和 SetThreadContext 讀寫(xiě)寄存器。
2. 代碼實(shí)現(xiàn)
思路和技術(shù)都搞清楚后,代碼落地就非常簡(jiǎn)單了,參考如下:
// 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 斷點(diǎn)
if (pr->ExceptionCode == EXCEPTION_BREAKPOINT && writefile_addr == pr->ExceptionAddress) {
//1. unhook,恢復(fù) writefile 的
WriteProcessMemory(cpdi.hProcess, writefile_addr, &ff, sizeof(BYTE), NULL);
//2. 獲取上下文
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(cpdi.hThread, &ctx);
//3. 獲取 WriteFile 寫(xiě)入的內(nèi)容
ReadProcessMemory(cpdi.hProcess, (PVOID)(ctx.Esp + 0x8), &lpBufferStart, sizeof(DWORD), NULL);
ReadProcessMemory(cpdi.hProcess, (PVOID)(ctx.Esp + 0xc), &nNumberOfBytesToWrite, sizeof(DWORD), NULL);
//4. 分配緩沖區(qū)
lpBuffer = (PBYTE)calloc(nNumberOfBytesToWrite + 1, sizeof(BYTE));
//5. copy 數(shù)據(jù)到緩沖區(qū)中
ReadProcessMemory(cpdi.hProcess, (LPVOID)lpBufferStart, lpBuffer, nNumberOfBytesToWrite, NULL);
printf("截獲的內(nèi)容: %s \n", lpBuffer);
//6. 重新修改 eip ,指向 writefile 開(kāi)頭,寫(xiě)回到線程上下文中
ctx.Eip = (DWORD)writefile_addr;
SetThreadContext(cpdi.hThread, &ctx);
//7. 繼續(xù)執(zhí)行
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,指定好之后把程序跑起來(lái),點(diǎn)擊 button 按鈕觀察,截圖如下,非常完美。

三:總結(jié)
在無(wú)法安裝 windbg 的受限環(huán)境下,部署 HookDebug.exe 就是我們的另一種選擇,而且完全自動(dòng)化攔截,基本實(shí)現(xiàn)無(wú)人工干預(yù)。
到此這篇關(guān)于淺析如何截獲C#程序產(chǎn)生的日志的文章就介紹到這了,更多相關(guān)C#截獲程序產(chǎn)生日志內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實(shí)現(xiàn)一個(gè)Word保護(hù)性模板文件
這篇文章主要為大家詳細(xì)介紹了C#如何實(shí)現(xiàn)一個(gè)Word保護(hù)性模板文件,文中的示例代碼講解詳細(xì),具有一定的借鑒價(jià)值,需要的小伙伴可以參考一下2024-01-01
基于C#動(dòng)手實(shí)現(xiàn)網(wǎng)絡(luò)服務(wù)器Web Server
這篇文章主要為大家詳細(xì)介紹了基于C#動(dòng)手實(shí)現(xiàn)網(wǎng)絡(luò)服務(wù)器Web Server,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-10-10
基于動(dòng)態(tài)修改App.Config與web.Config的使用詳解
本篇文章是對(duì)動(dòng)態(tài)修改App.Config與web.Config的使用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
C#中的三種定時(shí)計(jì)時(shí)器Timer用法介紹
這篇文章介紹了C#中的三種定時(shí)計(jì)時(shí)器Timer的用法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03
C#中參數(shù)個(gè)數(shù)可變的方法實(shí)例分析
這篇文章主要介紹了C#中參數(shù)個(gè)數(shù)可變的方法,以一個(gè)簡(jiǎn)單實(shí)例分析了C#中參數(shù)個(gè)數(shù)可變的方法,主要是使用params關(guān)鍵字來(lái)實(shí)現(xiàn)的,是C#編程中比較實(shí)用的技巧,需要的朋友可以參考下2014-11-11
C# HTTP認(rèn)證方式詳解與代碼實(shí)現(xiàn)
在C#中,HTTP認(rèn)證是客戶端與服務(wù)器之間進(jìn)行身份驗(yàn)證的一種機(jī)制,常見(jiàn)的HTTP認(rèn)證方式包括:Basic認(rèn)證、Digest認(rèn)證、OAuth、Bearer Token等,下面我們將從工作原理、優(yōu)缺點(diǎn)對(duì)比、代碼實(shí)現(xiàn)、案例實(shí)戰(zhàn)四個(gè)方面詳細(xì)介紹這些認(rèn)證方式,需要的朋友可以參考下2025-03-03
C#實(shí)現(xiàn)自定義動(dòng)畫(huà)鼠標(biāo)的示例詳解
這篇文章主要為大家詳細(xì)介紹了如何利用C#實(shí)現(xiàn)自定義動(dòng)畫(huà)鼠標(biāo)效果,文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
Unity向量按照某一點(diǎn)進(jìn)行旋轉(zhuǎn)
這篇文章主要為大家詳細(xì)介紹了Unity向量按照某一點(diǎn)進(jìn)行旋轉(zhuǎn),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-01-01

