Windows平台逆向工程实战:从字符串解密到DLL劫持实现消息处理拦截 1. 项目概述一次针对特定版本客户端的探索最近在整理一些旧项目资料时翻到了一个挺有意思的实战记录是关于一个特定版本4.0客户端消息防撤回功能的逆向分析与实现。这并非鼓励任何干扰他人正常通信或侵犯隐私的行为而是纯粹从一个技术研究者的角度复盘一次完整的逆向工程过程。这个过程涉及了从静态分析、字符串解密到动态调试、DLL劫持与注入的完整链路对于想深入理解Windows平台应用逆向、PE文件结构以及运行时拦截技术的朋友来说是个不错的综合案例。我们探讨的所有技术细节都仅限于本地、已授权的环境下的学习与研究旨在剖析其实现机制与防御思路请务必遵守相关法律法规与服务条款。这个项目最初源于一个简单的需求在本地测试环境中观察和理解特定通信软件的消息处理流程。我们选择了历史上一个较为经典的版本作为分析对象因其模块结构相对清晰防护措施与现代版本相比不那么复杂更适合作为学习逆向工程入门到进阶的过渡案例。整个实战将带你走一遍完整的逆向流程如何定位关键代码、如何处理经过简单混淆的字符串、如何分析关键函数调用并最终通过编写一个自定义的DLL动态链接库来“劫持”原有的消息处理流程实现我们的观察目标。你会发现逆向工程远不止是使用工具更是一场与开发者逻辑的思维博弈。2. 逆向工程的核心思路与前期准备逆向工程说白了就是“黑盒测试”的终极形态。我们面对的是一个编译后的、没有源代码的可执行程序比如WeChat.exe我们的目标是通过各种手段推断出它内部的部分运行逻辑并尝试进行可控的修改或观察。对于“防撤回”这个场景我们的核心思路是拦截并修改处理“消息撤回”指令的相关函数。2.1 目标分析与工具选型首先我们需要明确目标。一个消息的“撤回”在客户端看来至少包含两个动作接收撤回指令客户端从服务器收到一条特殊的命令告知“某条消息已被发送者撤回”。执行本地更新客户端根据这条指令在本地聊天窗口中移除或标记该条消息并可能更新UI。我们的切入点就是拦截并取消第二个动作让客户端“假装”没收到或没处理这条撤回指令。为此我们需要找到执行这个“移除或标记消息”功能的函数。工欲善其事必先利其器。Windows平台下的逆向一套顺手的工具链至关重要静态分析工具IDA Pro反汇编界的“瑞士军刀”用于将二进制代码反汇编成可读的汇编指令并进行深入分析。其强大的图形视图和交叉引用功能是理解程序结构的核心。GhidraNSA开源的反汇编工具自带反编译功能可以将汇编代码转换成更易读的伪C代码对于快速理解函数逻辑帮助巨大。它是IDA的一个优秀免费替代品。动态分析工具x64dbg / OllyDbg强大的动态调试器。我们可以在程序运行时下断点、单步执行、查看和修改内存与寄存器值是验证静态分析猜想、跟踪数据流的必备工具。x64dbg对64位程序支持更好是现代逆向的首选。Process Monitor来自Sysinternals套件可以监控程序对文件、注册表、网络和进程的操作帮助我们定位程序读取了哪些配置文件、加载了哪些DLL等行为。辅助工具PEiD / Exeinfo PE用于查壳检测程序是否被加密或压缩。如果目标程序加了壳我们需要先脱壳才能进行有效分析。Strings快速提取二进制文件中所有可读字符串的小工具是寻找线索的第一步。010 Editor十六进制编辑器用于直接查看和修改二进制文件。对于这个项目我们主要使用IDA Pro或Ghidra进行静态分析用x64dbg进行动态调试验证用Process Monitor辅助观察。2.2 定位关键代码从字符串与行为入手面对一个庞大的可执行文件几十MB直接看汇编代码无异于大海捞针。我们需要一些“地标”来导航。最常用的地标就是字符串和导入函数。字符串搜索我们可以猜测程序中必然存在一些与“撤回”相关的提示文本比如“撤回了一条消息”、“Recall”等。使用IDA的字符串视图ShiftF12或直接用Strings工具扫描WeChat.exe搜索相关中英文关键词。这是一个非常有效的突破口。在4.0版本中我们可能搜索到类似“recall”或“msgtype”这样的字符串。行为监控先不进行逆向直接运行程序并操作一次“撤回”功能。同时用Process Monitor过滤WeChat.exe的进程操作观察在点击撤回按钮的瞬间程序访问了哪些独有的文件、注册表键或网络地址。这能为我们提供额外的线索。API监控消息的显示最终会调用Windows的UI API例如SetWindowTextW,CreateWindowExW等。我们可以在x64dbg中对这些函数下断点然后触发撤回看调用栈来自哪个模块的哪个地址从而反向定位到我们关心的代码区域。注意现代软件常会对字符串进行加密或混淆直接搜索明文可能一无所获。这就需要我们进行“字符串解密”分析这也是本实战的第一个技术难点。3. 核心细节解析字符串解密与函数定位在静态分析中我们很快发现一个现象直接搜索“recall”、“撤回”等关键词结果寥寥无几甚至没有。这暗示程序中的字符串可能经过了简单的处理比如异或加密、Base64编码或简单的位移。3.1 识别字符串解密函数我们转而搜索一些更基础的、可能与字符串处理相关的函数调用比如malloc,free,strlen,memcpy等。在IDA的导入表中找到这些函数然后查看它们的交叉引用Xrefs看看哪些代码调用了它们。通常字符串解密函数会有一个固定的模式输入一个加密的字节数组可能存储在程序的.rdata只读数据段和一个密钥可能是硬编码的数字或另一个字符串。处理一个循环对加密数组的每个字节进行运算如与密钥进行异或、加减固定值等。输出在堆heap上分配一块新内存将解密后的字符串存入并返回指针。我们在IDA中寻找具有这种特征的函数。例如看到一个函数sub_XXXXXX它接收两个参数一个指针一个长度内部有一个循环循环体内有xor异或操作并且调用了malloc和memcpy。这很可能就是我们要找的字符串解密函数。实操心得在Ghidra中由于有反编译功能这个模式会更直观。你会看到一个函数其C伪代码类似于char * __cdecl decrypt_string(int encrypted_data, int length) { char *output; int i; output (char *)malloc(length 1); for (i 0; i length; i i 1) { output[i] *(byte *)(encrypted_data i) ^ 0x55; // 假设密钥是0x55 } output[length] \0; return output; }3.2 动态调试验证与密钥获取找到疑似函数后我们需要用x64dbg动态验证。在目标程序启动后在疑似解密函数的入口处下断点Breakpoint。然后在程序中触发一些必然涉及字符串显示的操作比如打开一个聊天窗口。当断点命中时观察函数的参数通常在RCX/RDX寄存器或栈上。在内存窗口中查看参数指针指向的数据你会看到一堆“乱码”。单步执行F7/F8通过这个函数观察返回值通常放在RAX寄存器。再次查看RAX指向的内存如果出现了可读的字符串如聊天对象昵称、菜单项文字等那么恭喜你找到了正确的解密函数并且知道了它的解密算法比如 XOR 0x55。接下来我们需要找到所有调用这个解密函数的地方。在IDA中对这个函数按CtrlX查看交叉引用列表。每一个调用点都对应着一个加密字符串的使用处。我们需要在这些调用点附近寻找与我们目标功能相关的逻辑。3.3 定位消息处理函数通过字符串解密我们可能找到了诸如“msgtype”、“sysmsg”等关键字符串。围绕这些字符串的引用代码很可能就是消息解析和处理的核心逻辑。我们可以在x64dbg中对解密后的“recall”或“sysmsg”字符串的内存地址下内存访问断点。当程序读取这个字符串进行比较时断点就会触发。此时的调用栈Call Stack会清晰地展示出是从哪个函数、哪一行代码发起的这次读取。向上回溯几层调用我们就能定位到处理“撤回类型消息”的高层函数我们暂且称它为ProcessRecallMessage。这个函数内部很可能包含一个判断if (msgType MSGTYPE_RECALL) { ... }。在{ ... }的代码块里就会调用真正执行“移除消息”操作的函数我们称之为RemoveMessageFromUI。我们的终极目标就是让这个RemoveMessageFromUI函数失效。4. 实操过程DLL劫持与代码注入找到了关键函数我们有两种思路进行修改1. 直接修改WeChat.exe的二进制文件打补丁2. 运行时注入代码进行拦截。前者不够灵活且容易被版本更新或完整性校验破坏。我们选择更优雅、更常用的后者——DLL劫持。4.1 DLL劫持原理Windows程序在启动时会按照一定的顺序搜索并加载其依赖的DLL动态链接库。这个搜索顺序可以通过SetDllDirectory等API改变但默认情况下在搜索系统目录如System32之前会优先搜索应用程序自身的目录。DLL劫持就是利用这个规则我们制作一个与系统DLL同名的伪造DLL例如劫持一个WeChat本身就会加载的、不那么核心的第三方库放在WeChat.exe的同级目录下。当WeChat启动时就会加载我们的DLL。在我们的DLL中我们实现并导出目标程序需要的所有函数。对于我们不关心的函数直接转发给原始的系统DLL对于我们想拦截的函数则实现自己的逻辑。4.2 目标DLL选择与转发器制作首先我们需要选择一个合适的劫持目标。使用Process Monitor监控WeChat启动过程过滤Load Image操作查看它加载了哪些非系统核心的DLL。一些常见的图形、音频、网络库的旧版本是较好的目标例如d3d9.dll,winhttp.dll,msvcp*.dll等。我们需要选择一个WeChat会加载且其函数我们比较容易处理的DLL。假设我们选择fake_lib.dll这是一个假设名实际需根据分析确定。我们不会从头实现这个DLL的所有功能那太复杂了。标准做法是制作一个“转发器DLL”。获取导出函数列表使用dumpbin /exports C:\Windows\System32\real_lib.dll命令获取原始DLL的所有导出函数。创建DLL项目在Visual Studio中创建一个新的DLL项目。编写模块定义文件.def这是关键。在.def文件中我们需要为原始DLL的每一个导出函数创建一个转发器。例如LIBRARY fake_lib EXPORTS OriginalFunc1 real_lib.OriginalFunc1 OriginalFunc2 real_lib.OriginalFunc2 ...这行代码的意思是当我们的fake_lib.dll被调用OriginalFunc1时直接转发给系统目录下的real_lib.dll中的OriginalFunc1去执行。这样我们的DLL就像一个透明的代理。实现自定义入口点DLL有一个入口函数DllMain。当DLL被加载到进程空间时DllMain会被调用。我们需要在这里做文章#include windows.h #include stdio.h // 假设我们通过逆向分析得到了目标函数RemoveMessageFromUI的地址是0x12345678在WeChat.exe模块内 #define TARGET_FUNCTION_ADDR 0x12345678 // 定义一个函数指针类型用于调用原函数 typedef void (__stdcall *REMOVE_MSG_FUNC)(int msgId); BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { if (ul_reason_for_call DLL_PROCESS_ATTACH) { // DLL被加载时执行我们的代码 // 1. 获取WeChat.exe的模块基址我们的DLL和它在同一进程空间 HMODULE hWeChat GetModuleHandleA(WeChat.exe); if (!hWeChat) { OutputDebugStringA([MyDll] Failed to get WeChat module handle.); return TRUE; } // 2. 计算目标函数的绝对地址基址 偏移量 // 注意0x12345678这个偏移量是我们在IDA中看到的相对于WeChat.exe模块基址的偏移RVA。 REMOVE_MSG_FUNC pOriginalRemoveMsg (REMOVE_MSG_FUNC)((BYTE*)hWeChat TARGET_FUNCTION_ADDR); // 3. 修改目标函数的内存保护属性使其可写 DWORD oldProtect; if (!VirtualProtect(pOriginalRemoveMsg, 5, PAGE_EXECUTE_READWRITE, oldProtect)) { OutputDebugStringA([MyDll] VirtualProtect failed.); return TRUE; } // 4. 进行Inline Hook用一条跳转指令jmp覆盖原函数的前几个字节 // 跳转到我们自定义的函数MyRemoveMessageFromUI // x86机器码E9 [相对偏移地址] BYTE jmpCode[5] { 0xE9, 0x00, 0x00, 0x00, 0x00 }; DWORD relativeAddr (DWORD)MyRemoveMessageFromUI - (DWORD)pOriginalRemoveMsg - 5; memcpy(jmpCode[1], relativeAddr, 4); memcpy(pOriginalRemoveMsg, jmpCode, 5); // 5. 恢复内存保护 VirtualProtect(pOriginalRemoveMsg, 5, oldProtect, oldProtect); OutputDebugStringA([MyDll] Hook installed successfully.); } return TRUE; } // 这是我们自定义的“移除消息”函数 void __stdcall MyRemoveMessageFromUI(int msgId) { // 什么都不做或者只打印日志让撤回操作失效 char log[256]; sprintf_s(log, sizeof(log), [MyDll] Recall attempt blocked for msgId: %d, msgId); OutputDebugStringA(log); // 注意我们完全跳过了原函数的执行。 // 如果需要执行原函数后再做处理可以在这里调用保存的原函数前5字节的指令然后再跳回原函数5的位置。 }4.3 编译与部署将我们的DLL项目编译生成fake_lib.dll。将原始的real_lib.dll从WeChat目录下重命名如改为real_lib.dll.bak然后将我们编译好的fake_lib.dll复制并重命名为real_lib.dll放入WeChat.exe的同级目录。启动WeChat如果一切顺利我们的DLL会被加载DllMain中的钩子Hook会被安装。当撤回事件触发程序调用RemoveMessageFromUI时就会跳转到我们的MyRemoveMessageFromUI函数我们在这里简单地记录日志并返回原函数就不会执行从而实现防撤回的效果。5. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到各种问题。下面是一些常见坑点及解决方案问题现象可能原因排查与解决思路DLL加载失败程序报错或闪退1. DLL依赖项缺失如VC运行时库。2. .def文件转发格式错误或函数名/序号不对。3. DLL的位数32/64位与主程序不匹配。1. 使用Dependency Walker或Visual Studio的dumpbin /dependents检查你的DLL依赖确保运行环境有对应库。可以静态链接运行时库/MT。2. 仔细核对.def文件中的函数名和序号必须与原始DLL完全一致。使用dumpbin /exports反复确认。3. 确保你的DLL编译平台x86/x64与目标程序一致。Hook安装成功但防撤回无效1. 函数地址计算错误。2. Hook的时机不对目标函数可能已经被调用过了。3. 找错了函数RemoveMessageFromUI并非真正执行移除操作的函数。1. 在DllMain中打印计算出的函数地址然后在调试器中如x64dbg验证该地址是否确实是目标函数的开头。2. 尝试在DllMain中创建线程在线程中Sleep一小段时间后再安装Hook确保目标模块完全初始化。3. 回头用x64dbg动态调试在撤回发生时下断点重新确认调用栈和关键函数。程序崩溃特别是执行原函数时1. Inline Hook破坏了原函数的栈平衡或寄存器状态。2. 自定义函数MyRemoveMessageFromUI的调用约定__stdcall,__cdecl等与原函数不一致。3. 跳转回来时地址计算错误。1. 这是最复杂的情况。如果需要在自定义函数里调用原函数必须确保完美地保存和恢复上下文。通常使用成熟的Hook库如Microsoft Detours、minhook会更安全。2. 在IDA中仔细查看目标函数的反汇编开头和结尾确定其调用约定。__stdcall是Windows API常用约定。3. 如果采用“跳过去再跳回来”的方式务必精确计算跳转地址。建议初学者先实现“完全拦截”不调用原函数更稳定。防撤回生效但其他功能异常我们的转发DLL没有正确处理所有导出函数或者我们的Hook影响了其他调用链。检查.def文件是否包含了所有必要的导出函数转发。确保我们的Hook代码具有高度的针对性只修改我们确定的目标函数字节不影响其他代码区。字符串解密函数找不到或算法复杂字符串加密方式可能不是简单的XOR可能是更复杂的自定义算法或标准加密算法如AES、TEA。1. 在动态调试时关注解密函数的输入和输出尝试识别常见的加密模式。2. 可以尝试将解密函数代码片段提取出来用Python或C写一个小程序进行模拟反复测试。3. 如果算法过于复杂可以转变思路不一定要完全解密只要能识别出加密后的“撤回指令”特征码即可直接在网络层或消息流中匹配特征码。最后的忠告逆向工程是一项对耐心、细心和逻辑思维能力要求极高的活动。这个实战案例涵盖了从静态分析到动态修改的完整链条但每一步都充满了变数。真正的挑战往往在于调试和排错。务必在虚拟机或完全隔离的测试环境中进行操作并始终保持对技术的敬畏和对法律的遵守。技术是用来创造和保护的而不是破坏。希望这个详细的指南能为你打开一扇窗看到软件内部精妙的运行逻辑。