聊一聊如何截获 C# 程序产生的日志
一:背景
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
就是我们的另一种选择,而且完全自动化拦截,基本实现无人工干预。
聊一聊如何截获 C# 程序产生的日志的更多相关文章
- log4net--帮助程序员将日志信息输出到各种目标(控制台、文件、数据库等)的工具
1. log4net库是Apache log4j框架在Microsoft .NET平台的实现,是一个帮助程序员将日志信息输出到各种目标(控制台.文件.数据库等)的工具. 2. Log4net的结构如下 ...
- android 程序崩溃crash日志的捕捉
android 程序崩溃crash日志的捕捉 之前在项目开发过程中,一直会遇到程序崩溃了,但是测试組的哥哥们又没及时的导出日志.... 后来在诳群的时候听别人说起,腾讯有那么一个叫bugly的东西 将 ...
- C#控制台程序使用Log4net日志组件
1.Log4net一般都不陌生,但是在配置上不同类型的项目又不相同的地方比如C#控制台程序和C# MVCWeb项目,拿控制台项目为例 项目源码在文章底部 2.首先创建一个控制台程序,引入Log4n ...
- java笔记--重定向输出流实现程序输出到日志
重定向输出流实现程序输出到日志 --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3877248.html "谢谢-- 利用Sy ...
- Spark应用程序运行的日志存在哪里(转)
原文链接:Spark应用程序运行的日志存在哪里 在很多情况下,我们需要查看driver和executors在运行Spark应用程序时候产生的日志,这些日志对于我们调试和查找问题是很重要的. Spark ...
- mr程序无法输出日志进行调试的解决方法
mr程序无法输出日志进行调试的解决方法 @(Hadoop) yarn开启日志输出设置 在yarn-site.xml文件中添加如下配置: <property> <name>yar ...
- 使用kibana可视化报表实时监控你的应用程序,从日志中找出问题,解决问题
先结果导向,来看我在kibana dashborad中制作的几张监控图. 一:先睹为快 dashboard1:监控几个维度的日志,这么点日志量是因为把无用的清理掉了,而且只接入了部分应用. <1 ...
- 使用 Linux 自带的 logrotate 程序来控制日志文件尺寸
1. 编写配置文件,内容如下(以 Amadeus 系统为例): 编写配置文件,放在 /etc/logrotate.d/xxxx 下,其中 xxxx 是自己取的名字,无需后缀.例如 Amadeus 系统 ...
- 【Windows】查看Windows上运行程序的异常日志
任何在windows系统上运行的程序,只要发生异常导致程序异常终止,windows都会在日志中详细记录这个异常.可以在计算机管理中查看,如图:也可以在操作中心查看,如图:
- [Prism框架实用分享]如何在Prism应用程序中使用日志
前言 在Prism中有关日志的命名空间: Microsoft.Practices.Prism.Logging 在Prism中,你可以使用Prism自带的Logger(比如TextLogger等),也可 ...
随机推荐
- Linux做bond4
一.编辑bond网络配置 vim /etc/sysconfig/network-scripts/ifcfg-bond4 DEVICE=bond4 NAME=bond4 TYPE=Bond ONBOOT ...
- 华南理工大学 Python第6章课后测验-2
1.(单选)以下关于语句 a = [1,2,3,(4,5)]的说法中,正确的个数有( )个.(1)a是元组类型 (2)a是列表类型 (3)a有5个元素 (4)a有4个元素(5)a[1] ...
- PHP获取当前周一、周末时间等(持续更新)
获取周一和周日的日期 $week = date('w') == 0 ? 7 : date('w'); $Sunday = strtotime('today -' . ($week - 1) . 'da ...
- Logstash:运用 fingerprint 过滤器处理重复的文档
文章转载自:https://blog.csdn.net/UbuntuTouch/article/details/106639848 背景:Elasticsearch 索引 在介绍重复数据删除解决方案之 ...
- 重要参考步骤---ProxySQL实现读写分离
MySQL配置主从同步文章地址:https://www.cnblogs.com/sanduzxcvbnm/p/16295369.html ProxySQL实现读写分离与读负载均衡参考文档:https: ...
- KubeOperator安装好后默认会占用80端口,替换成其他端口
使用KubeOperator安装好k8s后,然后修改如下的配置文件,最后重启应用即可 重启:koctl restart
- 在 Linux 中找出内存消耗最大的进程
1 使用 ps 命令在 Linux 中查找内存消耗最大的进程 ps 命令用于报告当前进程的快照.ps 命令的意思是"进程状态".这是一个标准的 Linux 应用程序,用于查找有关在 ...
- MySQL学习(1)---MySQL概述
什么是数据库 概述 数据库(Database)是长期存储在计算机内有组织.大量.共享的数据集合.它可以供各种用户共享,具有最小冗余度和较高的数据独立性.数据库管理系统DBMS(Database Man ...
- 2022.9.17 Java第二次课总结
以下是本节课后的问题 首先是关于静态变量 在类中,使用 static 修饰符修饰的属性(成员变量)称为静态变量,也可以称为类变量,常量称为静态常量,方法称为静态方法或类方法,它们统称为静态成员,归整个 ...
- IDEA中直接将 SpringBoot项目打包成 Docker镜像时 pom.xml的配置
<plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactI ...