基于plug110源码详解OllyDBG插件开发机制与编译调试技巧

📅 发布时间:2026/9/9 2:36:43
基于plug110源码详解OllyDBG插件开发机制与编译调试技巧
简介plug110 是一份面向 OllyDBG 插件开发者的源码示例包尤其适合逆向工程初学者与需要扩展调试器功能的用户。整个 zip 压缩包共 21 个文件体积仅 209KB却包含了 C/C 源文件、make/dsp/dsw/bpr 工程配置、函数导出定义、库文件、头文件以及 hlp/rtf 格式的帮助文档文件类型相当齐全便于在不同编译环境下对照学习。源码围绕书签标记、命令执行、命令行解析等典型插件功能展开完整演示了插件初始化、消息响应、API 调用和卸载流程开发者可以从中理解插件如何注册自身、如何与 OllyDBG 主程序交互进而在这些示例基础上加入自定义命令或扩展功能。针对 Borland C 与 Visual C 两套编译工具链资源还准备了对应的工程配置参考减少环境搭建障碍配套帮助文档则从插件结构到编译步骤提供了循序渐进指引相当于一份入门手册。目前已有 229 人学习这套源码对于想快速掌握 OllyDBG 插件开发框架的人来说是不错的起点范例。 第一次拿到 plug110 的时候我正在为 OllyDBG 的插件开发挠头。网上能搜到的资料大多是零散代码片段很少有把“插件源码”从头讲到尾的。plug110 正好是个极简的例子项目不大核心文件就一个 main.c 加上两个头文件却被很多人当成插件模板来回抄。我对着那几个 ODBG_ 开头的导出函数看了一下午才搞明白哪个是入口、哪个是出口也才意识到OllyDBG 的插件机制本质上就是一套约定好的 DLL 接口读懂了这套约定后面写什么功能都不难。这篇文章就把我基于 plug110 源码积累的理解、编译心得和排错经历完整梳理一遍适合刚接触 OD 插件开发、手里有源码却不知道从哪里下手的读者。1. OllyDBG 插件机制的核心逻辑OD 是怎么把你的 DLL 变成菜单的1.1 插件加载流程DLL 扫描与导出函数探测OllyDBG 1.10 启动时会扫描自身所在目录下的所有 DLL 文件逐个用 LoadLibrary 加载然后通过 GetProcAddress 查找一个叫ODBG_Plugindata的导出函数。找到了就认为这是一个合法的插件继续走后续的初始化流程找不到就直接忽略这个 DLL。所以文件名是什么其实无所谓插件能不能被识别完全取决于你有没有导出那几个约定好的函数。这个设计思路跟浏览器扩展里的 manifest.json 有点像只不过它的“描述文件”不是一个独立配置文件而是硬编码成了 C 导出函数。对开发者来说这意味着插件跟普通 DLL 没有任何本质区别你可以先用任意方式写一个 DLL再通过导出表把它变成 OD 插件。反过来也解释了一个常见现象为什么有些 DLL 放错目录也不会导致 OD 崩溃最多就是没被加载。1.2 两个基础回调ODBG_Plugindata 与 ODBG_PlugininitOD 找到ODBG_Plugindata之后会调用它来获取插件的基本信息。这个函数要往三个缓冲区里填内容短名称、描述文字、版本号然后返回一个整数表示插件所期望的 SDK 接口版本。OD 拿到返回的版本号后会做内部比较如果不匹配就会放弃加载这个插件。紧接着调用的是ODBG_Plugininit它接收主程序版本号、主窗口句柄、以及一个用于声明插件特性的指针。plug110 在这个函数里通常会保存主窗口句柄供后面弹窗、输出日志时使用。这里最容易犯的错误是把返回值当成“成功/失败”的布尔值来写实际上这个返回值承担的是版本协商功能。如果你返回的版本号和 OD 内部的插件 API 版本对不上插件会被静默卸载而 OD 界面上看不到任何提示。1.3 两类菜单函数千万别搞混在阅读 plug110 源码时你会看到两个名字几乎一样的函数一个是ODBG_Pluginmenu注意是小写的 menu负责向 OD 提供菜单定义文本另一个是ODBG_PLUGINMENU全大写是用户点击菜单项后的回调函数。约定上前者是 OD 向你索取菜单内容后者是 OD 告诉你用户点了什么。这个区别非常关键。很多新手把两个函数弄混或者只写了其中一个结果就是 OD 的插件菜单里要么什么都不显示要么显示了菜单但点击毫无反应。我在第一次编译 plug110 时就踩过这个坑当时以为是编译配置的问题来回折腾了快一个小时才发现回调函数名大小写少写了一段。2. plug110 源码走读框架的四个关键函数到底做了什么2.1 先看目录结构文件少但都不可缺plug110 的工程结构通常极其精简一个实现文件 main.c一个 OD 官方插件头文件 plugin.h一个主程序接口头文件 ollydbg.h再加上工程文件。有些版本还会带一个 .def 导出文件用来显式声明需要导出的函数名。别嫌它简陋这套结构本身就是最标准的 OD 插件骨架——你以后写任何插件都可以在这个骨架上做加法。2.2 ODBG_Plugindata给插件做一张身份证这个函数实现起来很直白就是往传入的字符数组里拷贝信息。plug110 中常见的写法是extc int cdecl ODBG_Plugindata(char shortname[32], char descr[128], char version[16]) { strcpy(shortname, plug110); strcpy(descr, OllyDBG 1.10 plugin framework demo); strcpy(version, 1.00); return PLUGIN_VERSION; }PLUGIN_VERSION宏在头文件里定义直接返回它就行。这里有一个值得留意的细节函数签名里的cdecl不是随便加的。OD 头文件里通过宏把导出函数声明为 C 调用约定如果你在重写函数时不小心去掉cdecl编译器默认使用__stdcall函数名修饰规则一变OD 通过 GetProcAddress 查找时又会失败。这种错误很难排查因为编译链接都不报错但插件就是加载不出来。2.3 ODBG_Plugininit保存句柄与版本校验的好地方初始化函数是插件跟 OD 主程序建立联系的第一站。plug110 在这个函数里所做的核心事情有两个保存主窗口句柄设置插件特性。特性指针features是个输出参数你用*features 0清零或按需声明支持的扩展能力都可以。版本方面头文件常量通常是0x0001000A这样的十六进制值实际的版本协商逻辑不是简单“大于就通过”而是 OD 内部做匹配判断。所以我个人的建议是这个函数能不改就不改保持返回PLUGIN_VERSION宏即可除非你确实需要针对不同 OD 版本做差异化处理。2.4 ODBG_Pluginmenu 与 ODBG_PLUGINMENU菜单的建与应接下来是插件最直观的展示层菜单。ODBG_Pluginmenu的参数里有一个origin用来区分当前是哪个窗口在请求菜单内容。PM_MAIN表示主窗口菜单栏PM_DISASM表示反汇编窗口右键菜单plug110 最常用的就是主窗口菜单。你需要往data[4096]缓冲区里写一段特殊格式的菜单定义文本用竖线|分隔不同菜单项。比如extc int cdecl ODBG_Pluginmenu(int origin, char data[4096], void *item) { if (origin PM_MAIN) { strcpy(data, 0 plug110|1 Read EIP|2 About); return 3; } return 0; }菜单项前面的数字是这个菜单项在菜单组内的索引返回值为菜单项总数。当用户实际点击某个菜单项时OD 会调用全大写的回调函数并把被点击菜单项的文本传进来。回调里最常见的就是用strstr或直接比较字符串来分支extc void cdecl ODBG_PLUGINMENU(int origin, char data[4096], void *item) { if (origin ! PM_MAIN) return; if (strstr(data, Read EIP)) { addtolist(0, 0, Current EIP: %08X, plugingetvalue(VAL_EIP)); } else if (strstr(data, About)) { MessageBox(h_main_wnd, plug110 framework, About, MB_OK); } }这段代码展示了插件开发中最常用的两个 OD 原生接口addtolist用来在 OD 日志窗口输出彩色文字plugingetvalue用来读取调试器内部的寄存器、变量等信息。它们不需要你手动 GetProcAddressplugin.h 里已经通过导入库声明好了。2.5 容易被忽略的收尾导出函数plug110 里除了上面四个函数通常还包含ODBG_Pluginclose。这个函数在插件被卸载前调用主要用来释放内存、保存配置、清理临时文件。demo 级别的插件并不强制实现它但养成习惯后你的插件才不会在反复加载卸载之间泄漏句柄。3. 从源码到 DLL编译 plug110 的完整流程与老工具链的坑3.1 为什么还得用老编译器OllyDBG 1.10 是纯 Win32 时代的程序配套的 SDK 头文件里的结构体、宏定义都是按十年前编译器设计的。我用现在的 Visual Studio 编译 x64 目标时遇到过大量类型冲突比如 OD 定义的ulong跟 Windows SDK 的类型定义打架。解决起来不是不行但折腾时间远超预期。如果你跟我一样想在最短时间内跑通环境最稳妥的方案是装一台 32 位 Windows 虚拟机配上 VC6 或 VS2005/VS2008 这一类老工具链。现代 VS 也可以尝试但要新建 Win32 控制台或 DLL 工程并在预处理设置里手动处理一些类型兼容问题只适合喜欢折腾的人。3.2 工程配置的关键几项新建一个 Win32 DLL 空项目把 main.c、plugin.h、ollydbg.h 加进去再引入 SDK 自带的导入库通常是 ollydbg.lib然后确认三件事第一导出方式。如果你手头这份 plug110 源码的函数定义里已经有了extc和cdecl宏并且工程配置了正确的 DEF 文件那么导出表会自动生成。如果没有 DEF 文件建议显式添加一个EXPORTS ODBG_Plugindata ODBG_Plugininit ODBG_Pluginmenu ODBG_PLUGINMENU ODBG_Pluginclose第二字符集设置。工程属性里的“字符集”一定要选“多字节字符集”不要选 Unicode。OD 1.10 内部的字符串处理大量依赖 ANSI 字节流选错字符集后printf 系函数会隐式转换出问题最常见的就是中文菜单乱码甚至直接崩溃。第三运行库链接方式。把运行库从默认的“多线程 DLL”改成“多线程静态库”也就是 /MT 选项。这样编译出的插件 DLL 不依赖目标机器上的 VC 运行时库拿到任何装了 OD 的机器上都能加载不至于因为缺少 MSVCRT 库而失败。3.3 加载验证与三个常见失败原因编译完成后把 plug110.dll 复制到 OllyDBG.exe 同目录启动 OD看菜单栏里有没有插件项。如果没出现优先检查三件事一是导出表。用 CFF Explorer 或 Dependency Walker 打开 DLL确认五个 ODBG_ 函数都在且函数名没有被 C 修饰如果看到一堆?ODBG_Plugindata...说明extern C宏失效了。二是初始化返回值。ODBG_Plugininit 返回版本不匹配时OD 会卸载插件且通常没有任何提示。可以在该函数开头加OutputDebugString输出日志用 DebugView 捕捉加载过程。三是依赖模块。如果 DLL 依赖的运行时、第三方库缺失LoadLibrary 会失败。这也是我强烈推荐静态链接 CRT 的原因实测中至少一半的“插件加载不出来”问题都源于动态 CRT 依赖。4. 调试插件不只是看日志OD 自己调试自己的两种玩法4.1 日志先行OutputDebugString 与 OD 日志窗口插件代码跑在 OD 进程内部printf 根本看不到输出。我早期调试插件经常感觉瞎猜后来养成一个习惯凡是进入回调函数第一步先写一行日志。输出方式有两种一种是 Windows 调试输出OutputDebugString(plug110: ODBG_PLUGINMENU called\n);然后用 DebugView 捕捉好处是不影响 OD 运行即使插件崩溃日志也已经留下来了。另一种是直接调用writelog写进 OD 的日志窗口好处是直观插件加载成功、菜单触发都能在 OD 界面上看到不用切到外部工具。4.2 双开 OD让插件断在断点上单靠日志只能确认执行流想看变量、看内存、看调用栈就得靠调试器来调试调试器。具体操作是开一个 OD 实例 A再开另一个实例 BB 里加载了 plug110。然后在 A 中采用附加进程的方式挂到 B 的进程上A 的模块窗口里就能看到 plug110.dll接着在ODBG_PLUGINMENU函数入口下断点。切回 B点击插件菜单A 的断点就会触发你可以像调试普通程序一样单步跟踪。这个技巧听起来有点绕但实际效果极好。第一次跑通的时候我才真正理解了 OD 回调函数的参数含义每一步菜单点击后 data 里到底传了什么看一眼寄存器就清楚了。如果你懒得用 A 去附加也可以在插件代码里临时塞一行内嵌断点__asm int 3;点击菜单时 B 会自己停下来再用 A 附加 B一样能切进调试状态。记得调试完删掉这行不然插件每次触发都会断。4.3 一个让我排查了很久的坑菜单文本比较失败有段时间我给插件加了中文菜单菜单已经能正常显示但点击后分支就是不进入。用双开调试后发现data参数里传进来的文本是 OD 内部处理后的内容跟我在菜单定义字符串里写的原文存在细微差异。中文在 OD 1.10 的 ANSI 环境下经过一次字节转换后strstr匹配不到反而是常态。我的最终方案是菜单显示文本照常用中文但在回车符或隐藏位置附加一个固定英文标识分支判断全部基于英文标识彻底避开编码转换问题。这个思路分享给在插件里用中文菜单的朋友可以少踩一次坑。5. 从 menu demo 到真实功能基于 plug110 的扩展思路5.1 先梳理 OD 给插件开放的能力范围plug110 之所以适合当起点是因为它把菜单链路完整跑通了。有了这条链路下一步就是熟悉 OD 暴露给插件的接口地图。日常开发中我高频使用的大概就这几组plugingetvalue读取寄存器、当前指令地址、调试状态等是最重要的信息入口。pluginreadmemory和pluginwritememory读写被调试进程的内存绕过 DEBUG 权限的很多复杂操作都靠它完成。addtolist和writelog向 OD 界面输出文字是插件和用户交互的简单通道。plugincmd让插件向 OD 命令框发送命令等于把 OD 已有的指令能力直接接入插件逻辑写批量操作时特别好用。5.2 一个可落地的练手目标读取 EIP 并输出到日志不要一上来就想写复杂的分析工具先把最小闭环跑通。我给自己的第一个正规功能是点一下插件菜单输出当前 EIP 所在的模块名和偏移。核心代码就是 2.4 节里的那段addtolist再加一个pluginreadmemory读取 EIP 处的几个字节并打印成十六进制。整个功能半小时内能写完但它能让你完整经历“获取调试状态 - 读取进程内存 - 输出结果”这条最常用的插件开发链路后续写内存补丁、自动化检测脚本都是这个模式的变体。5.3 从 1.10 到 x64dbgplug110 带来的框架复利OllyDBG 1.10 已经是很老的软件现在很多新样本和调试场景我都转到 x64dbg 上了。但 plug110 这段学习过程仍然值得投入因为 x64dbg 的插件 SDK 在设计思路上跟 OD 如出一辙同样是通过导出函数建立插件入口同样有菜单构建与回调分离的机制。当你理解了一个调试器插件系统的运行逻辑换平台时只需要把函数名和结构体定义对照新 SDK 重写一遍核心心智模型完全一致。最后分享一个我自己对插件开发的小习惯永远保持一个最小可编译、可加载、可点击的工程副本。plug110 就是这样一个副本。每次想实验新 API先在这个干净框架里跑通再搬进正式项目能避免很多来源不明的问题。至少到目前为止我所有基于 OD 与风格类似调试器的扩展功能都是从这份小小的源码里长出来的。本文还有配套的精品资源点击获取