首页
社区
课程
招聘
[原创] 某截图软件 ocr 功能逆向
发表于: 2025-5-11 21:59 4352

[原创] 某截图软件 ocr 功能逆向

2025-5-11 21:59
4352

某一天,好奇用了许久的截图软件 ocr 功能是如何实现的,于是打开 ida 走上了两天的不归之路()
本文仅供参考,学习逆向

首先使用 Process Monitor 或者 火绒剑 等监视工具查看该软件文件相关的操作,我们可以看到在截图时这个软件在读取以下文件
图片描述
但是通过互联网搜索我们可以知道这几个文件是关于 qrcode 也就是二维码识别有关的,不要被带偏
图片描述
而当我们点击识别图像,也就是 ocr 操作的时候,他会读取以下内容
图片描述
很明显这是一个 sqlite 数据库文件,我们看看里面有什么
图片描述
里面有个 OcrResult 的字段引起了我的关注,至此信息收集就完毕了

拖入 ida,搜索 OcrResult 字符串,就两个结果,并且很容易猜出来上面一个是有关 sqlite 创建的
图片描述
查看第二个字符串的引用,可以猜测这里是 sqlite 的写入
图片描述
看该函数的上层,上面 sub_7FF60B4606D0 很明显是一个类的创建,进去看看
图片描述
看看发现了什么,这正是我们要找的 ocr 功能相关的类
图片描述
通过分析该类的 vftable 我们大概可以猜出各个函数的名称
图片描述
其中有个函数长这样,有很多图片相关的操作引起了我的注意
图片描述
也就是上图中的 ??_7OcrRunnable@@6B@_02 dq offset ocrFunc? (这个函数我自己重命名了)

然后就是一个一个对函数进行猜测然后重命名,其中里面有一个函数似乎读取了模型,但是我们并搜不到该文件,答案是这个是一个 qt 的资源类型,qt 会将他打包在程序中,需要用到的时候从程序中读取出来,所以我们直接在 readMemFileAll 之后断点就能截取到数据啦
图片描述
通过询问 ai ,我们可以知道这个文件可能是 ncnn 的格式文件,所以我们直接猜测该程序使用 ncnn 作为框架,进行 ocr 的识别
det_param
通过分析,大概的流程长这样,然后还有一些对于图像的操作,找模型识别出来的轮廓、找出文本区域
图片描述
接着,程序会加载第二个 ai 模型来识别文本,而前一个模型是用来找出哪些区域需要识别文本的,其中 ocr_text_dict 为字典
图片描述
ai 模型调用部分和上面一样,最后会输出一堆 float 的数组
图片描述
最后输出识别的文本比较麻烦,它主要长这样,通过分析,我们能够猜测和跟踪得出源代码差不多长这样

识别结果
图片描述

调试效果图(当然 cv 操作不一定是对的,是自己写的)


改了下最后的代码,第二次 resize 的定值忘记改了,目前还是在想怎么匹配第一个模型找的区域对应的文本

改完代码,能用(应该),然后上传模型附件

添加效果图

#include <algorithm>
#include <array>
#include <cassert>
#include <print>
#include <string>
#include <vector>
 
#include "Utils.h"
 
#include <opencv2/opencv.hpp>
#include "net.h"
 
 
#define DetectParamPath "../data/det.param"
#define DetectModelPath "../data/det.bin"
#define RecognizeParamPath "../data/rec.param"
#define RecognizeModelPath "../data/rec.bin"
#define CharDictPath "../data/ocr_text_dict.txt"
 
#define IsDebug 1
 
namespace Impl {
    /**
     *
     * @param cols
     * @param rows
     * @param outCols
     * @param outRows
     * @param maxSideConstraint 最大边长?
     * @param alignment 对齐大小
     * @return
     */
    uint64_t calcTargetSize(uint32_t cols, uint32_t rows, uint32_t *outCols, uint32_t *outRows, uint32_t maxSideConstraint, uint32_t alignment) {
        unsigned int *v7;
 
        if (cols < maxSideConstraint && rows < maxSideConstraint) {
            maxSideConstraint = cols;
            if (cols < rows)
                maxSideConstraint = rows;
            if (maxSideConstraint % alignment)
                maxSideConstraint += alignment - maxSideConstraint % alignment;
        }
        if (rows <= cols) {
            *outCols = maxSideConstraint;
            v7       = outRows;
            *outRows = maxSideConstraint * rows / cols;
        } else {
            v7       = outRows;
            *outRows = maxSideConstraint;
            *outCols = maxSideConstraint * cols / rows;
        }
        unsigned int v8 = *outCols % alignment;
        if (v8)
            *outCols = alignment + *outCols - v8;
        unsigned int v10    = *v7 % alignment;
        uint64_t     result = *v7 / alignment;
        if (v10)
            *v7 = alignment + *v7 - v10;
        return result;
    }
 
    std::vector<cv::Mat> getEachRegion(const cv::Mat &image, const cv::Mat &binaryMask) {
        cv::Mat newBinary = binaryMask.clone();
        cv::resize(newBinary, newBinary, cv::Size(image.cols, image.rows));
 
        std::vector<std::vector<cv::Point> > contours;
        std::vector<cv::Vec4i>               hierarchy;
        cv::findContours(newBinary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
 
#if IsDebug
        // 调试用
        cv::cvtColor(newBinary, newBinary, cv::COLOR_GRAY2BGR);
         
        cv::Mat              debugImageBinary = newBinary.clone();
        cv::Mat              debugImage       = image.clone();
#endif
 
 
        // 存储所有裁剪出的区域
        std::vector<cv::Mat> cropped_images;
 
        // 获取原始图像的尺寸,用于边界检查
        constexpr int padding_x = 10;
        constexpr int padding_y = 7;
        constexpr int min_area  = 4;
 
        std::println("find contours: {}", contours.size());
        for (size_t i = 0; i < contours.size(); ++i) {
            // 计算当前轮廓的原始边界框
            cv::Rect original_box = cv::boundingRect(contours[i]);
 
            // 过滤掉非常小的区域
            if (original_box.area() < min_area) {
                std::println("area too small: {}", original_box.area());
                continue;
            }
 
            // 计算扩展后的边界框
            cv::Rect expanded_box;
            expanded_box.x      = std::ranges::max(0, original_box.x - padding_x);
            expanded_box.y      = std::ranges::max(0, original_box.y - padding_y);
            expanded_box.width  = std::ranges::min(image.cols - expanded_box.x, original_box.width + padding_x * 2);
            expanded_box.height = std::ranges::min(image.rows - expanded_box.y, original_box.height + padding_y * 2);
 
            // 检查扩展后的框是否有效 (宽度和高度必须大于0)
            if (expanded_box.width <= 0 || expanded_box.height <= 0) {
                std::println("Skipping contour {} because expanded box is invalid.", i);
                continue;
            }
 
#if IsDebug
            // 调试线条
            cv::rectangle(debugImage, expanded_box, cv::Scalar(0, 255, 0, 255), 1);       // 绿色框// 绿色框
            cv::rectangle(debugImageBinary, expanded_box, cv::Scalar(0, 255, 0, 255), 1); // 绿色框
#endif
 
            // 使用扩展后的边界框裁剪原始图像
            cv::Mat cropped_region = image(expanded_box).clone();
 
            // 将裁剪出的图像添加到列表中
            cropped_images.emplace_back(cropped_region);
        }
 
#if IsDebug
        cv::imwrite("../output/debugImage.png", debugImage);
        cv::imwrite("../output/debugImageBinary.png", debugImageBinary);
#endif
 
        return cropped_images;
    }
}
 
class OCR {
private:
    ncnn::Net                detectNet_;
    ncnn::Net                recognizeNet_;
    std::vector<std::string> charDict_;
 
public:
    OCR() {
        detectNet_.load_param(DetectParamPath);
        detectNet_.load_model(DetectModelPath);
        recognizeNet_.load_param(RecognizeParamPath);
        recognizeNet_.load_model(RecognizeModelPath);
        charDict_ = Utils::getCharDict(CharDictPath);
    }
 
    // 检测文本区域,QtImage 的图像是 32bit (0xffRRGGBB)
    std::vector<cv::Mat> findRegions(const cv::Mat &image) {
        assert(image.type() == CV_8UC4 && "image type must be CV_8UC4(BGRA)");
 
        // 计算目标大小
        unsigned int calcCols, calcRows;
        Impl::calcTargetSize(image.cols, image.rows, &calcCols, &calcRows, 0x3C0u, 0x20u);
        ncnn::Mat out;
        ncnn::Mat in = ncnn::Mat::from_pixels_resize(
            image.data,
            ncnn::Mat::PIXEL_RGBA2RGB,
            image.cols,
            image.rows,
            // 如果不使用计算值则容易糊在一起
            static_cast<int>(calcCols),
            static_cast<int>(calcRows)
        );
 
        // 归一化
        uint32_t mean_vals[3]; // { 0.485f, 0.456f , 0.406f }
        uint32_t norm_vals[3]; // { 1 / 0.229f / 255.f, 1 / 0.224f / 255.f, 1 / 0.225f / 255.f <-这个对不上}
 
        mean_vals[0] = 0x3EF851EC;
        mean_vals[1] = 0x3EE978D5;
        mean_vals[2] = 0x3ECFDF3B;
        norm_vals[0] = 0x3C8C4936;
        norm_vals[1] = 0x3C8F6AD8;
        norm_vals[2] = 0x3C8EC7AB;
        in.substract_mean_normalize(reinterpret_cast<float *>(mean_vals), reinterpret_cast<float *>(norm_vals));
 
        // 推理
        ncnn::Extractor ex = detectNet_.create_extractor();
        ex.input("x", in);
        ex.extract("sigmoid_0.tmp_0", out, 0);
 
        // 归一化
        uint32_t norms2[3];
        norms2[0] = 0x437F0000;
        norms2[1] = 0x437F0000;
        norms2[2] = 0x437F0000;
        out.substract_mean_normalize(nullptr, reinterpret_cast<float *>(norms2));
 
        // 转换为灰度图
        cv::Mat binaryMask(out.h, out.w, CV_8U);
        out.to_pixels(binaryMask.data, ncnn::Mat::PIXEL_GRAY);
 
        // 二值化
        // xmm 00000000000000004053200000000000
        // xmm 0000000000000000406FE00000000000
        cv::threshold(binaryMask, binaryMask, 76.5, 255.0, cv::THRESH_BINARY);
        assert(binaryMask.type() == CV_8UC1 && "mask type must be CV_8UC1");
        return Impl::getEachRegion(image, binaryMask);
    }
 
    // 识别文本(输入 BGRA 图像,也就是 QtImage)
    std::vector<std::pair<float, std::string> > recognizeText(const cv::Mat &image) {
        assert(image.type() == CV_8UC4 && "image type must be CV_8UC4(BGRA)");
 
        ncnn::Mat out2;
        ncnn::Mat in2 = ncnn::Mat::from_pixels_resize(
            image.data,
            ncnn::Mat::PIXEL_BGRA2BGR,
            image.cols,
            image.rows,
            static_cast<int>(static_cast<float>(image.cols) / static_cast<float>(image.rows) * 48.0),
            0x30
        );
        constexpr uint32_t mean_vals2[3] { 0x42FF0000, 0x42FF0000, 0x42FF0000 };
        constexpr uint32_t norm_vals2[3] { 0x3C008081, 0x3C008081, 0x3C008081 };
        in2.substract_mean_normalize(reinterpret_cast<const float *>(mean_vals2), reinterpret_cast<const float *>(norm_vals2));
 
        ncnn::Extractor extractor = recognizeNet_.create_extractor();
        extractor.input("x", in2);
        extractor.extract("softmax_11.tmp_0", out2, 0); // 0x24A
 
 
        std::vector<std::pair<float, std::string> > results;
 
        // 逐行扫描
        int64_t lastIndex = 0;
        for (int hIndex = 0; hIndex < out2.h; ++hIndex) {
            const auto l1 = out2.row(hIndex);
            const auto l2 = out2.row(hIndex + 1);
 
            const auto maxElementPtr = std::ranges::max_element(l1, l2);
            const auto index         = maxElementPtr - l1;
            if (index > 0 && (hIndex <= 0 || index != lastIndex)) {
                results.emplace_back(*maxElementPtr, charDict_[index - 1]);
            }
            lastIndex = index;
        }
 
        return results;
    }
};
 
int main() {
    auto orginal_image = cv::imread("../test1.png", cv::IMREAD_UNCHANGED);
    cv::cvtColor(orginal_image, orginal_image, cv::COLOR_BGR2BGRA);
 
    auto ocr = OCR();
    for (const auto &element: ocr.findRegions(orginal_image)) {
        std::string t;
        for (auto &[score, text]: ocr.recognizeText(element)) {
            // std::println("Text: {}, Score: {}", text, score);
            t += text;
        }
        std::println("Text: {}", t);
    }
 
    return 0;
}
#include <algorithm>
#include <array>
#include <cassert>
#include <print>
#include <string>
#include <vector>
 
#include "Utils.h"
 
#include <opencv2/opencv.hpp>
#include "net.h"
 
 
#define DetectParamPath "../data/det.param"
#define DetectModelPath "../data/det.bin"
#define RecognizeParamPath "../data/rec.param"
#define RecognizeModelPath "../data/rec.bin"
#define CharDictPath "../data/ocr_text_dict.txt"
 
#define IsDebug 1
 
namespace Impl {
    /**
     *
     * @param cols
     * @param rows
     * @param outCols
     * @param outRows
     * @param maxSideConstraint 最大边长?
     * @param alignment 对齐大小
     * @return
     */
    uint64_t calcTargetSize(uint32_t cols, uint32_t rows, uint32_t *outCols, uint32_t *outRows, uint32_t maxSideConstraint, uint32_t alignment) {
        unsigned int *v7;
 
        if (cols < maxSideConstraint && rows < maxSideConstraint) {
            maxSideConstraint = cols;
            if (cols < rows)
                maxSideConstraint = rows;
            if (maxSideConstraint % alignment)
                maxSideConstraint += alignment - maxSideConstraint % alignment;
        }
        if (rows <= cols) {
            *outCols = maxSideConstraint;
            v7       = outRows;
            *outRows = maxSideConstraint * rows / cols;
        } else {
            v7       = outRows;
            *outRows = maxSideConstraint;
            *outCols = maxSideConstraint * cols / rows;
        }
        unsigned int v8 = *outCols % alignment;
        if (v8)
            *outCols = alignment + *outCols - v8;
        unsigned int v10    = *v7 % alignment;
        uint64_t     result = *v7 / alignment;
        if (v10)
            *v7 = alignment + *v7 - v10;
        return result;
    }
 
    std::vector<cv::Mat> getEachRegion(const cv::Mat &image, const cv::Mat &binaryMask) {
        cv::Mat newBinary = binaryMask.clone();
        cv::resize(newBinary, newBinary, cv::Size(image.cols, image.rows));
 
        std::vector<std::vector<cv::Point> > contours;
        std::vector<cv::Vec4i>               hierarchy;
        cv::findContours(newBinary, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
 
#if IsDebug
        // 调试用
        cv::cvtColor(newBinary, newBinary, cv::COLOR_GRAY2BGR);
         
        cv::Mat              debugImageBinary = newBinary.clone();
        cv::Mat              debugImage       = image.clone();
#endif
 
 
        // 存储所有裁剪出的区域
        std::vector<cv::Mat> cropped_images;
 
        // 获取原始图像的尺寸,用于边界检查
        constexpr int padding_x = 10;
        constexpr int padding_y = 7;
        constexpr int min_area  = 4;
 
        std::println("find contours: {}", contours.size());
        for (size_t i = 0; i < contours.size(); ++i) {
            // 计算当前轮廓的原始边界框
            cv::Rect original_box = cv::boundingRect(contours[i]);
 
            // 过滤掉非常小的区域
            if (original_box.area() < min_area) {
                std::println("area too small: {}", original_box.area());
                continue;
            }
 
            // 计算扩展后的边界框
            cv::Rect expanded_box;
            expanded_box.x      = std::ranges::max(0, original_box.x - padding_x);
            expanded_box.y      = std::ranges::max(0, original_box.y - padding_y);
            expanded_box.width  = std::ranges::min(image.cols - expanded_box.x, original_box.width + padding_x * 2);
            expanded_box.height = std::ranges::min(image.rows - expanded_box.y, original_box.height + padding_y * 2);
 
            // 检查扩展后的框是否有效 (宽度和高度必须大于0)
            if (expanded_box.width <= 0 || expanded_box.height <= 0) {
                std::println("Skipping contour {} because expanded box is invalid.", i);
                continue;
            }
 
#if IsDebug
            // 调试线条
            cv::rectangle(debugImage, expanded_box, cv::Scalar(0, 255, 0, 255), 1);       // 绿色框// 绿色框
            cv::rectangle(debugImageBinary, expanded_box, cv::Scalar(0, 255, 0, 255), 1); // 绿色框
#endif
 
            // 使用扩展后的边界框裁剪原始图像
            cv::Mat cropped_region = image(expanded_box).clone();
 
            // 将裁剪出的图像添加到列表中
            cropped_images.emplace_back(cropped_region);
        }
 
#if IsDebug
        cv::imwrite("../output/debugImage.png", debugImage);

[培训]科锐逆向工程师培训第53期2025年7月8日开班!

最后于 2025-5-15 10:39 被mb_jepgtozh编辑 ,原因:
上传的附件:
收藏
免费 7
支持
分享
最新回复 (10)
雪    币: 1
活跃值: (518)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
这是哪一家的
2025-5-13 17:13
0
雪    币: 530
活跃值: (258)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
hipwang 这是哪一家的

pixpin

2025-5-13 18:01
1
雪    币: 2779
活跃值: (4483)
能力值: ( LV6,RANK:81 )
在线值:
发帖
回帖
粉丝
4
我转成 python 调用识别不出来了
2025-5-14 15:12
0
雪    币: 3675
活跃值: (3294)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
会员还有表格识别功能,这个能搞么
2025-5-14 16:50
0
雪    币: 530
活跃值: (258)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
KingSelyF 我转成 python 调用识别不出来了[em_051]
嗯...你可以把识别的文本区域写出来看看一直不一致?
2025-5-14 18:28
0
雪    币: 530
活跃值: (258)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
7
MANKVIS 会员还有表格识别功能,这个能搞么
暂时还没看
2025-5-14 18:45
0
雪    币: 2779
活跃值: (4483)
能力值: ( LV6,RANK:81 )
在线值:
发帖
回帖
粉丝
8
mb_jepgtozh 嗯...你可以把识别的文本区域写出来看看一直不一致?
也可能是我的代码问题,二值化后出来的图就是全黑的了
2025-5-15 10:30
0
雪    币: 5293
活跃值: (3909)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
百度飞蒋OCR挺好用的,用那个就行,还免费的
2025-5-15 13:46
0
雪    币: 225
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
学习
2025-5-30 19:24
0
雪    币: 0
活跃值: (96)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
能做成功独立的工具就更好了~ 这样大家直接下载就能永久绿色使用了, 
2025-6-4 09:23
0
游客
登录 | 注册 方可回帖
返回