HidHide 驱动分析 - HidHideCLI 篇(四):日志系统与异常处理
一、日志系统的分层架构
HidHideCLI 的日志系统与内核驱动共享相同的 ETW 提供者结构,但在用户态实现了独立的日志后端。日志系统分为三个层次:
1.1 宏定义层(Logging.h)
// 三种追踪级别#defineTRACE_DETAILED(message){TraceEvent(...,&EtwEventTraceDetailed,message,"");...}#defineTRACE_PERFORMANCE(message){TraceEvent(...,&EtwEventTracePerformance,message,"");...}#defineTRACE_ALWAYS(message){TraceEvent(...,&EtwEventTraceAlways,message,"");...}// 日志事件宏(简化参数传递)#defineETW(eventDescriptor)&__FILE__[ProjectDirLength],__LINE__,__FUNCTION__,&EtwEventLog##eventDescriptor// 日志与返回宏#defineLOG_AND_RETURN_NTSTATUS(message,result){LogEvent(ETW(Exception),L"%s reports NT status 0x%08X",message,result);return(result);}ProjectDirLength是一个编译期定义的常量,用于裁剪__FILE__中的绝对路径前缀,使日志输出更简洁。
1.2 核心日志函数(Logging.cpp)
LogEvent:格式化并写入事件日志 + 追踪日志
NTSTATUSLogEvent(LPCSTR fileName,UINT32 lineNumber,LPCSTR functionName,PCEVENT_DESCRIPTOR event,LPCWSTR format,...){va_list args;va_start(args,format);std::vector<WCHAR>messageW(LOGGING_MESSAGE_MAXIMUM_SIZE);std::vswprintf(messageW.data(),messageW.size(),format,args);UpdateEventLog(false,fileName,lineNumber,functionName,event,messageW.data(),"");returnSTATUS_SUCCESS;}UpdateEventLog函数封装了EventWriteTransfer调用,将日志事件同时写入日志提供者和追踪提供者。
TraceEvent:仅写入追踪日志(不写入事件日志),用于高性能场景:
NTSTATUSTraceEvent(LPCSTR fileName,UINT32 lineNumber,LPCSTR functionName,PCEVENT_DESCRIPTOR event,LPCWSTR messageW,LPCSTR messageA){UpdateEventLog(true,fileName,lineNumber,functionName,event,messageW,messageA);returnSTATUS_SUCCESS;}DbgPrintEx 模拟:在用户态模拟内核态DbgPrintEx行为,输出到调试器:
NTSTATUSDbgPrintEx(ULONG componentId,ULONG level,LPCSTR format,...){va_list args;va_start(args,format);std::vector<CHAR>buffer(LOGGING_MESSAGE_MAXIMUM_SIZE);std::vsnprintf(buffer.data(),buffer.size(),format,args);OutputDebugStringA(buffer.data());returnSTATUS_SUCCESS;}1.3 ETW 提供者注册/注销
NTSTATUS WINAPILogRegisterProviders()noexcept{EventRegisterNefarius_Hid_Hide_CLI();EventRegisterNefarius_Drivers_HidHideCLI();LogEvent(ETW(Started),L"%s",_L(BldProductVersion));returnSTATUS_SUCCESS;}两个提供者分别对应"日志"(Logging)和"追踪"(Tracing),方便管理员根据需要启用不同级别的诊断信息。
二、异常处理框架
2.1 异常编码与传递(LogException 类)
LogException是一个辅助类,用于将不同来源的错误码(CONFIGRET、HRESULT、NTSTATUS、WIN32)统一编码为字符串,便于日志记录和异常传递。
编码函数:
staticstd::stringEncodeWIN32(LPCSTR fileName,UINT32 lineNumber,LPCSTR functionName,PCEVENT_DESCRIPTOR event,DWORD result)noexcept{std::ostringstream os;FormatMessageA(...);// 获取错误描述os<<PrefixWIN32<<" 0x"<<std::hex<<std::setw(8)<<result<<" at "<<fileName<<"("<<lineNumber<<") "<<functionName<<": "<<buffer.data();returnos.str();}输出格式示例:Error code 0x00000002 at HID.cpp(123) HidModelInfo: The system cannot find the file specified.
解码与转换:
CONFIGRETLogException::ToCONFIGRET()constnoexcept{autoconstdecoded=DecodeExceptionData();switch(decoded.encoding){caseEncoding::configret:returndecoded.configret;caseEncoding::hresult:returnSUCCEEDED(decoded.hresult)?CR_SUCCESS:CR_FAILURE;// ...}}支持在异常处理中将错误码灵活转换为所需类型。
2.2 异常抛出宏
#defineTHROW_WIN32(result){throwstd::runtime_error(LogException::EncodeWIN32(ETW(Exception),result));}#defineTHROW_WIN32_LAST_ERROR{throwstd::runtime_error(LogException::EncodeWIN32(ETW(Exception),GetLastError()));}#defineTHROW_CONFIGRET(result){throwstd::runtime_error(LogException::EncodeCONFIGRET(ETW(Exception),result));}所有 Windows API 调用失败时,通过宏抛出包含文件名、行号和错误描述的std::runtime_error。
2.3 异常捕获与日志记录宏
#defineLOGEXC_AND_RETURN_WIN32{\LogException(LogException::ExceptionMessage()).Log(ETW(Exception)).ToWIN32();\}在catch块中使用,将异常信息记录到 ETW,然后返回对应的 WIN32 错误码。
2.4 顶层异常处理
MainApplication函数使用三层异常捕获:
DWORDMainApplication()noexcept{try{HidHide::CommandInterpreter(false).Start(HidHide::CommandLineArguments());returnERROR_SUCCESS;}catch(std::exceptionconst&exc){std::wcerr<<exc.what()<<std::endl;LOGEXC_AND_RETURN_WIN32;}catch(...){std::wcerr<<L"Unhandled exception"<<std::endl;LOGEXC_AND_RETURN_WIN32;}}- 第一层:
std::exception捕获所有标准 C++ 异常,包括通过THROW_WIN32抛出的std::runtime_error。 - 第二层:
...捕获所有非标准异常(如结构化异常),作为最后的兜底。 - 错误信息同时输出到
std::wcerr(便于脚本捕获错误输出)和 ETW。
三、配置管理:提交与取消
3.1 配置提交(ApplyConfigurationChanges)
交互模式下,用户退出时调用ApplyConfigurationChanges:
m_FilterDriverProxy.ApplyConfigurationChanges();此方法比对缓存与驱动状态,仅提交有变更的配置。
3.2 取消操作(–cancel)
--cancel命令设置m_Cancel = true,在Start方法的主循环中检查此标志:
if(m_Cancel)return;取消后配置变更不会被提交,所有修改在退出时丢弃。
四、实用工具函数
4.1 字符串表加载
StringTable(UINT resourceId)从资源文件中加载本地化字符串:
std::wstringStringTable(UINT stringTableResourceId){autoconsthInstance=GetModuleHandleW(nullptr);std::vector<WCHAR>buffer(UNICODE_STRING_MAX_CHARS);if(0==LoadStringW(hInstance,stringTableResourceId,buffer.data(),buffer.size())&&ERROR_SUCCESS!=GetLastError())THROW_WIN32_LAST_ERROR;returnbuffer.data();}所有用户可见的文本均通过此函数加载,支持国际化。
4.2 多字符串转换
Windows 注册表REG_MULTI_SZ格式与std::vector<std::wstring>的相互转换:
std::vector<std::wstring>MultiStringToStringList(std::vector<WCHAR>const&multiString){for(size_t index=0,start=0;index<multiString.size();index++){if(0==multiString.at(index)){std::wstringstring(&multiString.at(start),0,index-start);if(!string.empty())result.emplace_back(string);start=index+1;}}returnresult;}注意处理双空终止符:空字符串被跳过(!string.empty()判断),避免将列表终止符误认为有效条目。
4.3 命令行参数提取
CommandLineArguments()从完整的命令行字符串中提取参数部分:
std::wstringCommandLineArguments(){std::wstring commandLine=GetCommandLineW();if(L'"'==commandLine.at(0)){autoindex=commandLine.find(L'"',1);returnTrim(commandLine.erase(0,index+1));}else{autoindex=commandLine.find(L' ');returnTrim(commandLine.erase(0,index));}}先跳过可执行文件路径(可能带引号),返回剩余的命令行参数。
五、CLI 与内核驱动的完整交互链路
- 用户输入命令(如
--app-reg "C:\Tools\mapper.exe") - 命令解释器解析:提取命令名
app-reg和参数"C:\Tools\mapper.exe" - 参数验证:
ValOneFullyQualifiedExecutablePath检查文件存在性和可执行性 - 路径转换:
FileNameToFullImageName将C:\Tools\mapper.exe转换为\Device\HarddiskVolume1\Tools\mapper.exe - 代理调用:
FilterDriverProxy.WhitelistAddEntry(fullImageName)更新内存缓存 - 提交(交互退出时):
ApplyConfigurationChanges调用DeviceIoControl(IOCTL_SET_WHITELIST) - 内核处理:驱动更新注册表和运行时集合,新配置生效
- 反馈输出:命令执行结果输出到 stdout/stderr,成功返回 ERROR_LEVEL 0,失败返回非 0
这一完整链路实现了从用户输入到内核生效的全流程自动化,使得 HidHideCLI 成为自动化部署和高级管理的理想工具。