ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

windows 驱动实例分析系列: wintun驱动分析-example篇(上)

windows 驱动实例分析系列: wintun驱动分析-example篇(上) Wintun Example 示例程序深度解析这部分作为演示如何使用wintun驱动进行二次开发的例子分为两部分。一、模块概述example文件夹是 Wintun 项目提供的一个完整的、可直接运行的示例程序用于演示如何使用wintun.dll的 API 来创建虚拟网络适配器、配置 IP 地址、收发数据包ICMP 回显请求/响应。它充当了一个最小化 VPN 客户端原型展示了从初始化到清理的完整生命周期。这个示例不仅是一个教学工具也是 Wintun 开发者进行集成测试的基准程序。其代码风格严谨、错误处理周全完全可以作为实际项目开发的参考模板。二、核心代码逐段分析2.1 函数指针与动态加载InitializeWintunstaticWINTUN_CREATE_ADAPTER_FUNC*WintunCreateAdapter;// ... 其余 13 个函数指针staticHMODULEInitializeWintun(void){HMODULE WintunLoadLibraryExW(Lwintun.dll,NULL,LOAD_LIBRARY_SEARCH_APPLICATION_DIR|LOAD_LIBRARY_SEARCH_SYSTEM32);if(!Wintun)returnNULL;#defineX(Name)((*(FARPROC*)NameGetProcAddress(Wintun,#Name))NULL)if(X(WintunCreateAdapter)||X(...)||/* ... */)#undefX{/* 错误处理并释放 */}returnWintun;}设计要点使用LoadLibraryEx并指定LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32确保优先从应用目录和系统目录加载wintun.dll防止 DLL 劫持。通过宏X批量获取所有导出函数的地址若任一失败则回滚并报错。返回的HMODULE在程序结束时释放确保资源正确清理。这种做法体现了延迟绑定的思想不静态链接wintun.lib而是运行时动态加载使得示例程序可以在未安装 Wintun 的系统上优雅地报错退出也方便替换不同版本的 DLL 进行测试。2.2 日志系统ConsoleLogger Log 辅助staticvoidCALLBACKConsoleLogger(WINTUN_LOGGER_LEVEL Level,DWORD64 Timestamp,constWCHAR*LogLine){SYSTEMTIME SystemTime;FileTimeToSystemTime((FILETIME*)Timestamp,SystemTime);WCHAR LevelMarker(LevelWINTUN_LOG_INFO)?L:(LevelWINTUN_LOG_WARN)?L-:L!;fwprintf(stderr,L%04u-%02u-%02u %02u:%02u:%02u.%04u [%c] %s\n,...);}亮点实现了 Wintun 要求的WINTUN_LOGGER_CALLBACK回调原型通过WintunSetLogger注册使 Wintun 内部日志也能输出到控制台。时间戳100 ns 间隔自 1601-01-01被正确转换为本地时间便于阅读。用、-、!分别代表 Info、Warn、Error 级别直观简洁。此外程序还提供了LogError、LogLastError、Log三个辅助函数LogError利用FormatMessage将 Win32 错误码转换为可读的字符串并自动添加前缀。LogLastError封装GetLastError()LogError简化调用。Log变参函数格式化输出到日志回调。这组日志工具使得程序在任何错误发生时都能输出详尽的诊断信息极大提升了调试效率。2.3 控制台信号处理优雅退出staticHANDLE QuitEvent;staticvolatileBOOL HaveQuit;staticBOOL WINAPICtrlHandler(DWORD CtrlType){switch(CtrlType){caseCTRL_C_EVENT:caseCTRL_BREAK_EVENT:caseCTRL_CLOSE_EVENT:caseCTRL_LOGOFF_EVENT:caseCTRL_SHUTDOWN_EVENT:Log(WINTUN_LOG_INFO,LCleaning up and shutting down...);HaveQuitTRUE;SetEvent(QuitEvent);returnTRUE;}returnFALSE;}机制HaveQuit是volatile全局标志供工作线程轮询。QuitEvent是一个手动重置事件用于唤醒阻塞在WaitForSingleObject上的线程。当用户按下 CtrlC 或关闭控制台时CtrlHandler被调用设置标志并触发事件所有线程收到信号后自行退出。这保证了示例程序在退出前能正确调用WintunEndSession和WintunCloseAdapter避免驱动资源泄漏。2.4 数据包处理逻辑PrintPacket MakeICMP2.4.1 收包解析PrintPacketstaticvoidPrintPacket(constBYTE*Packet,DWORD PacketSize){if(PacketSize20){/* 错误 */return;}BYTE IpVersionPacket[0]4,Proto;WCHAR Src[46],Dst[46];if(IpVersion4){RtlIpv4AddressToStringW((structin_addr*)Packet[12],Src);RtlIpv4AddressToStringW((structin_addr*)Packet[16],Dst);ProtoPacket[9];Packet20;PacketSize-20;}elseif(IpVersion6){// 检查最小长度 40 字节RtlIpv6AddressToStringW((structin6_addr*)Packet[8],Src);RtlIpv6AddressToStringW((structin6_addr*)Packet[24],Dst);ProtoPacket[6];Packet40;PacketSize-40;}else{Log(WINTUN_LOG_INFO,LReceived packet that was not IP);return;}// 如果协议是 ICMP (1) 且是回显应答 (type0)特殊标记if(Proto1PacketSize8Packet[0]0)Log(WINTUN_LOG_INFO,LReceived IPv%d ICMP echo reply from %s to %s,IpVersion,Src,Dst);elseLog(WINTUN_LOG_INFO,LReceived IPv%d proto 0x%x packet from %s to %s,IpVersion,Proto,Src,Dst);}解析要点通过 IPv4 首部第一个字节的高 4 位判断版本区分 IPv4 和 IPv6。使用RtlIpv4AddressToStringW/RtlIpv6AddressToStringW将二进制地址转换为字符串需链接ip2string.h和ntdll.lib。对于 ICMP 回显应答Echo Replytype0专门输出“ICMP echo reply”提示便于观察 ping 响应。2.4.2 发包构造MakeICMPstaticvoidMakeICMP(BYTE Packet[28]){memset(Packet,0,28);Packet[0]0x45;// IPv4, 首部长度 5*(USHORT*)Packet[2]htons(28);// 总长度 28Packet[8]255;// TTLPacket[9]1;// 协议 ICMP*(ULONG*)Packet[12]htonl((1024)|(616)|(78)|(80));// 源 10.6.7.8*(ULONG*)Packet[16]htonl((1024)|(616)|(78)|(70));// 目的 10.6.7.7*(USHORT*)Packet[10]IPChecksum(Packet,20);// IPv4 首部校验和Packet[20]8;// ICMP 类型: Echo Request*(USHORT*)Packet[22]IPChecksum(Packet[20],8);// ICMP 校验和Log(WINTUN_LOG_INFO,LSending IPv4 ICMP echo request to 10.6.7.8 from 10.6.7.7);}注意构造的是ICMP Echo Requestping 请求源地址为10.6.7.8目的地址为10.6.7.7。但示例程序在后续配置中将适配器的 IP 设为了10.6.7.7见后文这意味着它将 ping自身——实际上数据包会由操作系统路由回本机然后被ReceivePackets捕获并打印为“ICMP echo reply”不对因为源地址是 10.6.7.8而适配器是 10.6.7.7所以实际上是从 10.6.7.8 发往 10.6.7.7但源地址 10.6.7.8 并未分配给任何接口因此这个包会进入网络栈并被路由由于目标 10.6.7.7 是本机系统会响应 ICMP Echo Reply但源地址仍为 10.6.7.7目标为 10.6.7.8。所以最终接收到的包应该是 Echo ReplyPrintPacket 会识别出 type0输出“ICMP echo reply”。这个设计巧妙模拟了 ping 环回测试。IPv4 校验和计算函数IPChecksum实现了标准的一进制补码求和支持任意长度。
返回列表