【web逆向】优某愿 字体反混淆

查看 91|回复 10
作者:就往丶   
地址:aHR0cHM6Ly93d3cueW91enkuY24vY29sbGVnZXMvc2NvcmVsaW5lP2NvbGxlZ2VDb2RlPTEwMDAzJm5hbWU9JUU2JUI4JTg1JUU1JThEJThFJUU1JUE0JUE3JUU1JUFEJUE2​
接口分析
接口:eW91enkuZG1zLmRhdGFsaWIuYXBpLmVucm9sbGRhdGEuZW50ZXIuY29sbGVnZS5lbmNyeXB0ZWQudjIuZ2V0​

代码分析
先下一个xhr断点

我们往上面找 可以看到有一个promise.then​异步,一般有这个异步的话,很多函数都是封装起来的。好比如你写代码的话,不会重复写同样的代码,这样会给我们逆向带来一些分析的难度
可以看到这个地方显示了我们接口,我们就可以保证待会异步走的话也是我们对应的接口,我们先在这个位置打上一个断点,刷新页面

在 JavaScript 中,Promise 是一个代表异步操作最终完成(或失败)及其结果的对象。它主要用于处理异步操作,例如网络请求、文件读取、定时器等,以避免传统回调函数带来的“回调地狱”(callback hell)问题,并提供更清晰和结构化的方式来管理异步代码。


断电断住了,我们打印一下n​发现是一个异步返回 我们就需要使用then关键词来获取里面返回的信息
下面是一个promise示例

const promiseA = new Promise((resolve, reject) => {
  resolve(777);
});
// 此时,“promiseA”已经敲定了
promiseA.then((val) => console.log("异步日志记录有值:", val));
console.log("立即记录");
// 按以下顺序产生输出:
// 立即记录
// 异步日志记录有值:777


在控制台输出之后放开断点
/* 自行修改 */
n({
    url: "youzy.dms.datalib.api.enrolldata.*************.encrypted.v2.get",
    data: e
}).then(res => console.log(res))

可以看到n​的数据还是没有解密的,那这个函数应该只是返回请求之后的数据,我们继续往上面找

可以看到这里有一个平坦流

平坦流通常指的是一种将嵌套或复杂的数据结构“扁平化”为简单的、线性的数据流或事件流

            var t = Object(n.a)(regeneratorRuntime.mark((function t(e) {  无视
                var r, n;
                return regeneratorRuntime.wrap((function(t) {   无视
                    for (; ; )
                        switch (t.prev = t.next) {   这里来判断走哪个逻辑的
                        case 0:
                            return t.next = 2,   这里会走到 case2
                            i.api.sdk.dms.datalib.enrolldata.encryptedCollegeV2Get(e);
                        case 2:
                            return r = t.sent,   这里t.send 就相当于获取到上面 case0的 返回 encryptedCollegeV2Get 就是上面这个函数的返回值
                            n = r.result,
                            p(n),
                            t.abrupt("return", r);   这里就是整个函数的返回了
                        case 6:
                        case "end":
                            return t.stop()
                        }
                }
                ), t)
            }

我们在 t.abrupt("return", r)​ 打上断点,可以发现返回的值已经发生了变化,可以推测是上面 p​函数来处理的

进到p函数里面,他应该是把上面的对象传进来一个个处理了, 继续进 o​ 函数里面看

如果 e.courses || e.fractions​ 的结果是假值(意味着 e​ 既没有 courses​ 属性,也没有 fractions​ 属性,或者它们的值都是假值),那么就会调用函数 o​,并将当前数组元素 e​ 作为参数传递给 o​。



遍历对象 t​ 的自身属性。对于每个属性 e​:
  • 获取属性值 r​。
  • 如果 r​ 不是一个对象,不是 "-"​,不是 "—"​,并且是一个 truthy 值,同时属性名 e​ 不是 "year"​、"dataType"​、"course"​、"batch"​ 或 "majorCode"​ 中的任何一个,那么就使用函数 Object(a.a)​ 对 t[e]​ 的值进行处理并更新 t[e]​。
  • 无论之前的条件是否满足,如果 t[e]​ 的值是 "఺"​,则将其替换为 "—"​。



    进入这个最后的a函数里看看

    可以看到这个就是他的解密函数

    在w的位置下一个断点,可以看到被解密了内容

    其实到这里就已经解密完成了,我之前不知道还在一直在别的解密位置,后面发现别的就是字体混淆了,浪费了很多时间
    字体混淆
    可以看到f12中,查看网页的字不是一个正常的文字,应该是应该字体混淆了

    进入他的css文件里面查看

    可以看到在他的样式下面有一个font-face​,但是他对应的是 cntext5​不是我们要的,所以我们重新刷新页面,全局搜索一下

    ​@font-face​ 是一个 CSS at-rule,用于在网页中嵌入自定义字体。它的主要作用是允许开发者在用户的设备上没有安装特定字体的情况下,仍然可以使用这些字体来渲染网页上的文本。


    可以搜索到三四个woff2,我们都可以下载看看里面都是什么内容,但是在scoreline中可以看到,里面有一个动态用js加载字体的代码。那么大概率就是这个yfe2.woff2​字体了


    直接下载这个字体


    可以用这个网站打开 https://www.bejson.com/ui/font/​ woff2文字的格式

    复制这个到网站里面查询一下,发现查询就是对应的


    在从请求接口里面获取到这个加密的数据,enterNum​就可以猜测是录取数

    前面扣出来的加密算法
    function cnDeCryptV2(str) {
        var k = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", l = k.length, b, b0, b1, b2, b3, d = 0, s;
        s = new Array(Math.floor(str.length / 4)),
        b = s.length;
        for (var i = 0; i  0 && (-1 != e.search(/【(.*?)】/) ? w += e.replace("【", "").replace("】", "") : e.length > 0 && (w += "&x" + e + ";"))
        }
        )),
        w
    }
    str = "001H0039001H0032001G001I001C001H002R001I002Z0034001E00380034001H003G002R002U002T002T"
    console.log(cnDeCryptV2(str))
    结果是:&#xcfee​     &#x​这个前缀没啥用
    在网页里面搜索  \ucfee​  正好可以对应

    接下来我们需要做一个映射表来实现比如 上面的 {"ceff":5}​ 就是对应的5 我们要获取全部的字编码和这个是什么字
    我们需要借助fontTools​库和ddddocr​库来实现,运行代码请自行配置好环境
    import os
    from fontTools.ttLib import TTFont
    import freetype
    from PIL import Image, ImageDraw, ImageFont
    import ddddocr  # 引入 ddddocr 进行 OCR 识别
    from loguru import logger
    ocr = ddddocr.DdddOcr(beta=False, show_ad=False)  # 识别
    # 1. 解析 WOFF2 字体文件
    def extract_chars_from_woff2(woff2_path):
        font = TTFont(woff2_path)
        cmap_table = font["cmap"]
        characters = {}
        for cmap in cmap_table.tables:
            if cmap.isUnicode():
                for codepoint, glyph_name in cmap.cmap.items():
                    characters[codepoint] = glyph_name  # 以 Unicode 码点作为 key
        font.close()
        return characters
    # 2. 渲染字符到图片
    def render_char_to_image(font_path, char, output_path, img_size=64):
        font = freetype.Face(font_path)
        font.set_char_size(img_size * 64)
        # 创建白色背景的图片
        img = Image.new("L", (img_size, img_size), 255)
        draw = ImageDraw.Draw(img)
        # 获取字符渲染位置
        bbox = draw.textbbox((0, 0), char, font=ImageFont.truetype(font_path, img_size))
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1]
        # 计算居中位置
        x = (img_size - text_width) // 2
        y = (img_size - text_height) // 2
        # 绘制字符
        draw.text((x, y), char, font=ImageFont.truetype(font_path, img_size), fill=0)
        # 保存图片
        img.save(output_path)
    # 3. ddddocr 识别字符
    def recognize_char_from_image(image_path):
        # ocr = ddddocr.DdddOcr()  # 创建 ddddocr 实例
        with open(image_path, "rb") as f:
            img_bytes = f.read()
        result = ocr.classification(img_bytes)  # 识别单个字符
        return result
    # 4. 处理整个 WOFF2 字体文件
    def process_woff2(woff2_path, output_folder):
        os.makedirs(output_folder, exist_ok=True)  # 确保输出文件夹存在
        char_map = extract_chars_from_woff2(woff2_path)
        for codepoint, glyph_name in char_map.items():
            char = chr(codepoint)
            temp_image_path = f"{output_folder}/{glyph_name}.png"
            # 渲染字符到图片
            render_char_to_image(woff2_path, char, temp_image_path)
            # OCR 识别字符
            recognized_char = recognize_char_from_image(temp_image_path)
            # 确保识别的字符存在,避免空值
            recognized_char = recognized_char if recognized_char else "未知"
            # 以 Unicode_识别文字.jpg 命名
            final_image_path = f"{output_folder}/{codepoint:04X}_{recognized_char}.jpg"
            os.rename(temp_image_path, final_image_path)
            print(f"已保存: {final_image_path}")
    # 生成映射字典
    def get_fontdict(filepath):
        # 示例用法
        font_dict = {}
        file_list =  os.listdir(filepath)
        if file_list:
            for filename in file_list:
                name, _ = os.path.splitext(filename)
                temp = name.split("_")
                font_dict[temp[0]] = temp[1]
        logger.success(font_dict)
    # 运行代码
    ttf_path = "yfe2.ttf"  # 替换为你的 ttf 字体路径 从网络上把woff2转换成ttf格式
    output_folder = "char_images"  # 生成的图片存储文件夹
    process_woff2(ttf_path, output_folder)
    get_fontdict(output_folder)

    识别不是百分百的,需要你手动去检查 下划线前面的就是编码,后面是数字



    解密函数
    import requests
    import json
    import execjs
    import os
    import re
    from loguru import logger
    fontdict = {} //把上面获取到的字典写到这里
    def cnDeCryptV2(str_input):
        k = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        l = len(k)
        d = 0
        s = []
        b = len(str_input) // 4
        for i in range(b):
            b0 = k.find(str_input[d])
            d += 1
            b1 = k.find(str_input[d])
            d += 1
            b2 = k.find(str_input[d])
            d += 1
            b3 = k.find(str_input[d])
            d += 1
            s.append(((b1 + b0 * l) * l + b2) * l + b3)
        b = "".join(chr(x) for x in s)
        w = ""
        parts = b.split("|")
        for t, e in enumerate(parts):
            if t > 0:
                match = re.search(r"【(.*?)】", e)
                if match:
                    w += match.group(1)
                elif len(e) > 0:
                    logger.success(f'{e} => {fontdict[str(e).upper()]}')
                    # print(qifeidict["D0A5"])
                    w += fontdict[str(e).upper()]
        return w
    str_input = "0031001L001D0030001I003B001G0035001J001H001F001L001C001H001H001F003G09HS0LQD09HT003G002R001L001J002P003G002R002S001C002U003G002R002Q002T001K003G09HS001409HT003G002S001C001F001I003G002S001C001G001C003G002S001C001K001D003G002R002Q001H002R003G002R002P001I002R003G002R001L002U001L003G09HS001509HT003G09HS001409HT003G002S001C001H002Q003G002S001C001C001D003G002S001C001C001G003G002S001C001L002P003G09HS001509HT"
    logger.success(cnDeCryptV2(str_input))

    函数, 字体

  • BTCQAQ   

    感谢楼主分享!逆向思路清晰,尤其字体映射和加密算法的分析很实用。补充几点建议:
    1. 加密算法可尝试静态分析JS定位密钥;
    2. 动态字体可监听加载事件直接提取数据;
    3. OCR可结合深度学习优化复杂字符识别;
    4. 建议封装版本管理模块应对算法变更。
    期待更多逆向自动化实践分享!
    高质量软件,谢谢楼主。
    就往丶
    OP
      

    @agooo 你看看这个教程,希望对你有帮助
    请跪迎朕   

    这网站破解不容易,牛逼呀
    请跪迎朕   

    楼主多发些这个站点的解密
    mumuaqa   

    感谢大佬分享
    agooo   


    就往丶 发表于 2025-3-16 15:11
    @agooo 你看看这个教程,希望对你有帮助

    感谢大佬,!!!!
    秋海明月   

    很久没看这方面的内容了,感谢大佬~
    yymmll   

    详细的分享,感谢分析,学习学习
    KaliHt   

    这个栈太多了,太难了
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部