关注重要事件 發表於 2022-11-25 08:26:00

聊一聊如何截获 C# 程序产生的日志

<h2 id="一背景">一:背景</h2>
<h3 id="1讲故事">1.讲故事</h3>
<p>前段时间分析了一个dump,一顿操作之后,我希望用<strong>外力</strong>来阻止程序内部对某一个com组件的调用,对,就是想借助外力实现,如果用 windbg 的话,可以说非常轻松,但现实情况比较复杂,客户机没有windbg,也不想加入任何的手工配置,希望全自动化来处理。</p>
<p>真的很无理哈。。。不过这种无理要求花点心思还是可以实现的,方法就是用代码将<strong>应用程序</strong>变成<strong>调试器</strong> 来实现自动化阻止,为了简化操作,我们拿 C# 的 <code>File.WriteAllText</code> 来举个例子,让我的调试器来截获它的 content。</p>
<h3 id="2-测试案例">2. 测试案例</h3>
<p>为了方便讲述,创建一个 WPF 程序,在 button 事件中用 <code>File.WriteAllText</code> 方法来写日志,参考代码如下:</p>
<pre><code class="language-C#">
    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());
      }
    }

</code></pre>
<p>代码非常简单,点一下按钮就写一条时间日志,接下来分别用 WinDbg 和 自定义调试器 来截获这个时间。</p>
<h2 id="二windbg-下的实现">二:WinDbg 下的实现</h2>
<h3 id="1-实现原理">1. 实现原理</h3>
<p>要想截获日志,需要知道这个链路的下游方法,比如:<code>kernel32!WriteFile</code>,msdn 上的定义如下:</p>
<pre><code class="language-C#">
BOOL WriteFile(
                HANDLE       hFile,
                LPCVOID      lpBuffer,
                DWORD      nNumberOfBytesToWrite,
   LPDWORD      lpNumberOfBytesWritten,
LPOVERLAPPED lpOverlapped
);

</code></pre>
<p>其中 <code>lpBuffer</code> 存放的就是 content 信息, <code>nNumberOfBytesToWrite</code> 存放的是长度,有了这些基础,就可以通过 bp 下断点了。</p>
<pre><code class="language-C#">
0:007&gt; bp kernel32!WriteFile

0:007&gt; 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=0023ss=002bds=002bes=002bfs=0053gs=002b             efl=00200202
KERNEL32!WriteFile:
76663510 ff2558106c76    jmp   dword ptr ds:002b:766c1058={KERNELBASE!WriteFile (75ebd760)}

0:000&gt; 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

</code></pre>
<p>因为 <code>kernel32!WriteFile</code> 用的是 stdcall 协定,所以 <code>lpBuffer</code> 变量在 <code>esp+0x8</code> 的位置, <code>nNumberOfBytesToWrite</code> 变量在 <code>esp+0xc</code> 的位置。</p>
<pre><code class="language-C#">
0:000&gt; da poi(esp+8)
0320eb6c"2022/11/24 17:25:39"

0:000&gt; dp esp+0xc L1
010fea3000000013

0:000&gt; ? poi(esp+0xc)
Evaluate expression: 19 = 00000013

</code></pre>
<p>从卦中看,content 和 length 都出来了,非常完美,接下来看下如何自定义实现调试器。</p>
<h2 id="三自己实现一个调试器">三:自己实现一个调试器</h2>
<h3 id="1-技术原理">1. 技术原理</h3>
<p>要想自定义实现,需要打通这三块。</p>
<ol>
<li>如何给 <code>kernel32!WriteFile</code> 下 bp 断点</li>
</ol>
<p>bp 的原理其实就是 <code>int 3</code> ,简而言之就是 windbg 会将 <code>kernel32!WriteFile</code> 指令的首字节修改成机器码 <code>0xcc</code>,命中之后又将 <code>0xcc</code> 撤销掉。这一串逻辑是 windbg 内部自己实现的,接下来我们验证下,将首字节直接改成 <code>0xcc</code> 。</p>
<pre><code class="language-C#">
0:011&gt; x kernel32!WriteFile
76663510          KERNEL32!WriteFile (_WriteFile@20)
0:011&gt; db 76663510 L1
76663510ff                                             .
0:011&gt; eb 76663510 cc
0:011&gt; db 76663510 L1
76663510cc                                             .

</code></pre>
<p><img src="https://img2022.cnblogs.com/blog/214741/202211/214741-20221125082426483-1364929198.png" alt="" loading="lazy"></p>
<p>从卦中看已修改成功,接下来直接点击 WPF 窗体的 button 按钮就会直接命中这里的 <code>int 3</code> 实现中断。</p>
<p><img src="https://img2022.cnblogs.com/blog/214741/202211/214741-20221125082426856-1369317142.png" alt="" loading="lazy"></p>
<p>到了这一步后,可以在程序中使用 <code>WriteProcessMemory</code> 恢复 <code>WriteFile</code> 原始字节为 <code>ff</code>。</p>
<ol start="2">
<li>如何让 int 3 中断给程序</li>
</ol>
<p>刚才看到的是中断给WinDbg,那怎么中断给程序呢? 其实 Win32 API 中有一个叫 <code>DebugActiveProcess</code> 函数可以让宿主程序充当调试器,mdsn 中的描述如下:</p>
<p><img src="https://img2022.cnblogs.com/blog/214741/202211/214741-20221125082426486-187347712.png" alt="" loading="lazy"></p>
<ol start="3">
<li>如何读写 wpf 的内存和寄存器</li>
</ol>
<p>只要获取到了 wpf 程序的进程和线程句柄,可以用 <code>WriteProcessMemory</code> 和 <code>ReadProcessMemory</code> 读写内存,用 <code>GetThreadContext</code> 和 <code>SetThreadContext</code> 读写寄存器。</p>
<h3 id="2-代码实现">2. 代码实现</h3>
<p>思路和技术都搞清楚后,代码落地就非常简单了,参考如下:</p>
<pre><code class="language-C++">
// HookDebug.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include &lt;iostream&gt;
#include &lt;Windows.h&gt;

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(&amp;cpdi, &amp;pde-&gt;u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));

        ReadProcessMemory(cpdi.hProcess, writefile_addr, &amp;ff, sizeof(BYTE), NULL);
        WriteProcessMemory(cpdi.hProcess, writefile_addr, &amp;int3, sizeof(BYTE), NULL);

        return TRUE;
}

BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) {

        CONTEXT ctx;
        PBYTE lpBuffer = NULL;
        DWORD lpBufferStart, nNumberOfBytesToWrite;

        PEXCEPTION_RECORD pr = &amp;pde-&gt;u.Exception.ExceptionRecord;

        //int3 断点
        if (pr-&gt;ExceptionCode == EXCEPTION_BREAKPOINT &amp;&amp; writefile_addr == pr-&gt;ExceptionAddress) {

                //1. unhook,恢复 writefile 的
                WriteProcessMemory(cpdi.hProcess, writefile_addr, &amp;ff, sizeof(BYTE), NULL);

                //2. 获取上下文
                ctx.ContextFlags = CONTEXT_ALL;
                GetThreadContext(cpdi.hThread, &amp;ctx);

                //3. 获取 WriteFile 写入的内容
                ReadProcessMemory(cpdi.hProcess, (PVOID)(ctx.Esp + 0x8), &amp;lpBufferStart, sizeof(DWORD), NULL);
                ReadProcessMemory(cpdi.hProcess, (PVOID)(ctx.Esp + 0xc), &amp;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, &amp;ctx);

                //7. 继续执行
                ContinueDebugEvent(pde-&gt;dwProcessId, pde-&gt;dwThreadId, DBG_CONTINUE);

                Sleep(0);

                //8. 重新 hook
                WriteProcessMemory(cpdi.hProcess, writefile_addr, &amp;int3, sizeof(BYTE), NULL);

                return TRUE;
        }

        return FALSE;
}

void loop() {

        DEBUG_EVENT de;

        while (WaitForDebugEvent(&amp;de, INFINITE))
        {
                //注入事件
                if (de.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) {
                        OnCreateProcessDebugEvent(&amp;de);
                }

                //异常事件
                if (de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) {
                        if (OnExceptionDebugEvent(&amp;de)) continue;
                }

                ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
        }
}

int main()
{
        //程序日志
        DWORD dwPID = 23264;

        if (!DebugActiveProcess(dwPID)) {
                printf("fail");
                return 1;
        }

        loop();

        return 0;
}

</code></pre>
<p>代码中的 <code>dwPID</code> 是 WPF 程序的 PID,指定好之后把程序跑起来,点击 button 按钮观察,截图如下,非常完美。</p>
<p><img src="https://img2022.cnblogs.com/blog/214741/202211/214741-20221125082426490-1468239201.png" alt="" loading="lazy"></p>
<h2 id="三总结">三:总结</h2>
<p>在无法安装 windbg 的受限环境下,部署 <code>HookDebug.exe</code> 就是我们的另一种选择,而且完全自动化拦截,基本实现无人工干预。</p><br><br>
来源:https://www.cnblogs.com/huangxincheng/p/16924078.html
頁: [1]
查看完整版本: 聊一聊如何截获 C# 程序产生的日志