原理
在看雪看到一篇文章:[原创]逆向调用QQ截图NT与WeChatOCR-软件逆向-看雪-安全社区|安全招聘|kanxue.com。里面说了怎么调用微信和QQ本地的OCR模型,还有很详细的分析过程。
我稍微看了下文章,大概流程是使用mmmojo.dll这个dll来与WeChatOCR.exe做通信的,也是用它来启动和关闭WeChatOCR.exe进程的。所以关键只需要知道这个dll里的导出函数怎么使用,就能自己实现调用OCR。并且可以脱离微信,不需要启动微信就能调用。
既然这样,那完全可以使用Python加载mmmojo.dll来启动WeChatOCR.exe并和它通信进行OCR。代码怎么实现的就不多说了,感兴趣的可以看我github的Python源码。我就说下有意思的一个技巧和一个踩坑的地方。ctypes调用dll,可以看我的另一篇文章:【Python基础库】ctypes
技巧
OCR识别成功完成后会调用你给定的回调函数,并将结果作为参数传给回调函数。比如其中一个回调函数的原型是static void OCRRemoteOnConnect(bool is_connected, void* user_data);
。第一个参数是当前连接状态,而第二个参数就是重点
第二个参数是你给定的一个指针,它可以通过SetMMMojoEnvironmentCallbacks
这个导出函数来设置,然后你就可以在回调函数里访问到这个指针。这个有什么用呢?就以上面github里的C++代码为例,代码里是将它设置为类的this指针,然后在建立连接后调用OCRRemoteOnConnect
回调函数,通过这个this指针改变类变量m_connect_con_var,然后你才能调用DoOCRTask
,如果m_connect_con_var变量没有被设置,说明没有连接成功,就一直等待。
搜了一下,在Python里也可以实现这样一个操作,把这个值设置成Python类对象,然后就可以在回调函数访问这个类对象。原理大概像这篇文章:python - Back-casting a ctypes.py_object in a callback - Stack Overflow。先使用ctypes.py_object
将对象转化为一个PyObject指针传给c层,然后在回调函数里再通过ctypes.cast(context, py_object).value
得到这个对象,在这个项目里的代码如下:
# 将self转为c指针设置成user_data
SetMMMojoEnvironmentCallbacks(m_mmmojo_env_ptr, 0, py_object(self))
# 在回调函数里使用它
def OCRRemoteOnConnect(is_connected:c_bool, user_data:py_object):
print(f"OCRRemoteOnConnect 回调函数被调用, 参数, is_connected: {is_connected}")
if user_data:
manager_obj:OcrManager = cast(user_data, py_object).value
manager_obj.SetConnectState(True)
修改不可变字符串
说个题外知识点,上面的内容可以知道,Python对象其实就是py_object指针,而Python中的字符串也是一个指针。众所周知,Python中的字符串是不可变的,如果你给字符串变量赋值,会得到一个新的对象。
但是c语言的指针其实是可以改变的,也就是说Python字符串其实是可以变的,只是Python没有提供接口改变它,而是新创建了一个字符串。那么我是不是可以通过ctypes来修改它的值,搜了一下确实可以,可以看这篇文章:ctypes.memmove() in-place with an str argument, without converting to bytes
import ctypes
import sys
s = "Hello World"
print(s, id(s))
# 获取它的内存地址,计算方式不清楚怎么得来的
s_mem = (ctypes.c_byte * len(s)).from_address(id(s) + sys.getsizeof(s) - len(s) - 1)
# 给这个地址赋值
ctypes.memmove(s_mem, b"Howdy", 5)
print(s, id(s))
可以发现s的id并没有改变,但是值变了。要想知道内存地址的计算方式是怎么来的,得去看Python的源代码了。不过通过搜索可以得到id(s)
返回的就是s这个对象的PyObject结构体指针的地址,那么sys.getsizeof(s) - len(s) - 1
就是在计算字符串相对于这个结构体的偏移。用x64dbg查看这个地址,也能看到下面的字符串
当然,这个知识点其实没啥应用场景。既然Python官方都不推荐你修改字符串,也没什么场景需要这样去修改字符串,就当个知识点了解一下就行了
踩坑
调用dll时的参数不能直接用c_wchar_p
,需要先赋值给一个变量,不然会被垃圾回收机制给回收了。而且错误很难定位,不会报错,程序直接终止
# 错误代码,c_wchar_p(m_exe_path)会在这行代码结束就被回收
SetMMMojoEnvironmentInitParams(m_mmmojo_env_ptr, 2, c_wchar_p(m_exe_path))
# 正确代码
c_m_exe_path = c_wchar_p(m_exe_path)
SetMMMojoEnvironmentInitParams(m_mmmojo_env_ptr, 2, c_m_exe_path)
另外还要注意的它的生命周期,和使用的时间。有些使用比较久的,你还得定义成全局变量或者赋值给self.
如何使用
安装
我已经发布到了pypi上,可以使用pip安装:pip install wechat-ocr
如果使用的是国内源,可能还没有更新,可以使用pip install wechat-ocr -i https://pypi.org/simple
来使用官方源安装
使用
import os
import time
from wechat_ocr.ocr_manager import OcrManager, OCR_MAX_TASK_ID
# WeChatOCR.exe路径
wechat_ocr_dir = "C:\\Users\\Administrator\\AppData\\Roaming\\Tencent\\WeChat\\XPlugin\\Plugins\\WeChatOCR\\7057\\extracted\\WeChatOCR.exe"
# mmmojo.dll所在路径
wechat_dir = "D:\\InstallSoftware\\WeChat\\[3.9.7.29]"
def ocr_result_callback(img_path:str, result_json:str):
result_file = os.path.basename(img_path) + ".json"
print(f"识别成功,img_path: {img_path}, result_file: {result_file}")
with open(result_file, 'w', encoding='utf-8') as f:
f.write(result_json)
def main():
ocr_manager = OcrManager(wechat_dir)
# 设置WeChatOcr目录
ocr_manager.SetExePath(wechat_ocr_dir)
# 设置微信所在路径
ocr_manager.SetUsrLibDir(wechat_dir)
# 设置ocr识别结果的回调函数
ocr_manager.SetOcrResultCallback(ocr_result_callback)
# 启动ocr服务
ocr_manager.StartWeChatOCR()
# 开始识别图片
ocr_manager.DoOCRTask(r"img\1.png")
ocr_manager.DoOCRTask(r"img\2.png")
ocr_manager.DoOCRTask(r"img\3.png")
time.sleep(2)
# 等待所有任务完成
while ocr_manager.m_task_id.qsize() != OCR_MAX_TASK_ID:
pass
ocr_manager.KillWeChatOCR()
if __name__ == "__main__":
main()
运行结果:
源码仓库
https://github.com/kanadeblisst00/wechat_ocr
温馨提示
不要用于商业用途,自己当个工具用用肯定是没什么事的