基于plug110源码详解OllyDBG插件开发机制与编译调试技巧
2026/9/9 2:36:42 网站建设 项目流程

简介: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_Plugininit

OD 找到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用来读取调试器内部的寄存器、变量等信息。它们不需要你手动 GetProcAddress,plugin.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 源码的函数定义里已经有了extccdecl宏,并且工程配置了正确的 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,再开另一个实例 B,B 里加载了 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:读取寄存器、当前指令地址、调试状态等,是最重要的信息入口。
  • pluginreadmemorypluginwritememory:读写被调试进程的内存,绕过 DEBUG 权限的很多复杂操作都靠它完成。
  • addtolistwritelog:向 OD 界面输出文字,是插件和用户交互的简单通道。
  • plugincmd:让插件向 OD 命令框发送命令,等于把 OD 已有的指令能力直接接入插件逻辑,写批量操作时特别好用。

5.2 一个可落地的练手目标:读取 EIP 并输出到日志

不要一上来就想写复杂的分析工具,先把最小闭环跑通。我给自己的第一个正规功能是:点一下插件菜单,输出当前 EIP 所在的模块名和偏移。核心代码就是 2.4 节里的那段addtolist,再加一个pluginreadmemory读取 EIP 处的几个字节,并打印成十六进制。整个功能半小时内能写完,但它能让你完整经历“获取调试状态 -> 读取进程内存 -> 输出结果”这条最常用的插件开发链路,后续写内存补丁、自动化检测脚本都是这个模式的变体。

5.3 从 1.10 到 x64dbg:plug110 带来的框架复利

OllyDBG 1.10 已经是很老的软件,现在很多新样本和调试场景我都转到 x64dbg 上了。但 plug110 这段学习过程仍然值得投入,因为 x64dbg 的插件 SDK 在设计思路上跟 OD 如出一辙,同样是通过导出函数建立插件入口,同样有菜单构建与回调分离的机制。当你理解了一个调试器插件系统的运行逻辑,换平台时只需要把函数名和结构体定义对照新 SDK 重写一遍,核心心智模型完全一致。

最后分享一个我自己对插件开发的小习惯:永远保持一个最小可编译、可加载、可点击的工程副本。plug110 就是这样一个副本。每次想实验新 API,先在这个干净框架里跑通,再搬进正式项目,能避免很多来源不明的问题。至少到目前为止,我所有基于 OD 与风格类似调试器的扩展功能,都是从这份小小的源码里长出来的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询