把 ocr.dll 拿下:从逆向到 Python 调用验证码识别
文中涉及的ocr.dll和ocr.py都在
https://github.com/BINBIN02/captcha_ocr
把 ocr.dll 拿下:从逆向到 Python 调用验证码识别
记录从零开始逆向 ocr.dll,把一个没有源码的 ocr.dll 逆了个遍,搞清楚导出和调用约定,然后用 Python 写了个 ocr.py 做验证码识别。过程里有试错、有踩坑,也有几个关键的取证点,方便直接套用。
要解决什么问题
- 没有源码,只有一个
ocr.dll,目标是拿到验证码文本。 - 现实难点:导出可能只有序号、调用约定不一致、入口支持多种输入形态(字节流/文件路径/位图句柄),还要保证初始化与释放不出坑。
环境准备
- 操作系统:Windows(10)
- 工具:IDA(或你顺手的反汇编工具)、Python 3.10+(实际逆向及脚本环境是 3.13.9)
- 文件:
ocr.py(脚本)、ocr.dll(库)、captcha.png(样例图)
逆向入口与方法
- 先看导出表:有没有名字(比如
Recognize、GetText、init、un),没有就按序号试。 - 调用约定不猜测,直接都试:
WINFUNCTYPE和CFUNCTYPE两套都准备好。 - 输入全都准备:内存字节、文件路径(ANSI/Unicode)、
HBITMAP三条路,谁能通用谁。 - 如果库里有
init/un这种初始化/清理,我在调用前后补上,避免状态脏掉。
关键取证点
1) 导出到底有什么
- 解析了导出表(脚本里有一个轻量级解析器),最后得到三个关键导出:
init (ordinal 1)、ocr (ordinal 2)、un (ordinal 3)。
2) 识别候选入口,哪个入口能用来识别
- 有命名就按常见名(
Recognize、GetText等)试;没有就用序号。我的样本就是 2 号入口ocr能跑。
3) 调用约定和参数怎么对上
- 不纠结,两个约定都装上;参数准备了四套:
- 内存:
(bytes*, len, char* out, out_len)/void*变体; - 文件(ANSI/Unicode):
(path, out, out_len); - 位图:
(HBITMAP, out, out_len); - 特殊打包:把地址和长度塞进一个 64 位参数,函数返回指针。
- 内存:
4) 初始化和清理
- 入口前调
init,结束后调un,这是我踩过的坑:不调有时候会莫名返回空或者句柄泄漏。
5) 位图句柄方案
- 有些入口只认位图句柄,用 GDI+ 把图片转成
HBITMAP再传进去。用完一定记得DeleteObject,并GdiplusShutdown。
最终总结的几种可用调
为了最大覆盖真实 DLL 的入口签名,脚本实现了以下几类调用:
A) 内存缓冲(最常见)
(uint8_t* in, unsigned len, char* out, unsigned out_len)或者void*版;有些不带out_len也能跑。- 输出要么自己准备
out,要么函数直接给返回指针(见下面的打包式)。
B) 文件路径(ANSI/Unicode)
- ANSI 和 Unicode 都试过,签名基本是
(path, out, out_len)。
C) 位图句柄(GDI+)
(HBITMAP bmp, char* out, unsigned out_len),有的少一个长度参数也行。
D) 64 位打包参数
- 有些入口把地址和长度塞进一个 64 位参数,两种打包都准备了:
val1 = (addr & 0xFFFFFFFF) | (len << 32)val2 = (len & 0xFFFFFFFF) | (addr << 32)
- 函数一般返回一个指针,字符串以
\0结尾(也可能是宽字符串)。
脚本里都做了什么
- GDI+ 的启停、位图转换、句柄销毁都封装好了,避免资源泄漏。
- 导出解析自己写了一套轻量级的,够用也够快。
- 调用约定和参数签名做了穷举器,命中就用,不行就换下一套。
- 识别流程里加入了可选的
init/un调用,降低“偶尔空结果”的概率。 - 命令行支持
--dll、--no-mem、--bmp、--ordinal,便于快速切换策略。
最小可用示例
1. 基本用法
1 | |
- 成功会直接打印识别结果并退出码为
0;失败一般是5。
踩过的坑(以及怎么绕过去)
- 运行无输出直接退:大多是位数不匹配或者 DLL 依赖没装好,先确认 Python 和 DLL 都是 x86 或 x64。
- 识别为空:加上初始化/释放(
init/un),再试不同签名(比如改成文件路径或位图)。 - GDI 泄漏:
HBITMAP用完一定DeleteObject,否则跑久了会莫名失败。 - 工作目录不对:相对路径加载失败,直接给绝对路径或者在脚本目录跑。
安全性与习惯用法
- 不在日志里留下敏感内容(验证码明文等)。
- 网络图片先做基本校验,别把超大的东西直接塞进内存。
- 库返回的指针和句柄用完就释放,别拖到进程结束。
最后
- 这套策略的核心就是:不猜,直接试;把常见签名全覆盖,再把初始化/释放补齐,基本都能跑出来。
- 后续可以把最终确定的函数原型固定下来,减少现场试错;再加一个纯 Python OCR 做兜底,稳定性会更好。
如果你手里的 DLL 导出名/序号不一样,或者调用约定比较怪,直接改 ocr_recognize 的候选和顺序,按你的样本来就行。
把 ocr.dll 拿下:从逆向到 Python 调用验证码识别
https://www.305871230.xyz/posts/1129563706/