吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3239|回复: 53
收起左侧

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

  [复制链接]
就往丶 发表于 2025-3-16 15:10

地址:aHR0cHM6Ly93d3cueW91enkuY24vY29sbGVnZXMvc2NvcmVsaW5lP2NvbGxlZ2VDb2RlPTEwMDAzJm5hbWU9JUU2JUI4JTg1JUU1JThEJThFJUU1JUE0JUE3JUU1JUFEJUE2

接口分析

接口:eW91enkuZG1zLmRhdGFsaWIuYXBpLmVucm9sbGRhdGEuZW50ZXIuY29sbGVnZS5lbmNyeXB0ZWQudjIuZ2V0

image

代码分析

先下一个xhr断点

image

我们往上面找 可以看到有一个promise.then​异步,一般有这个异步的话,很多函数都是封装起来的。好比如你写代码的话,不会重复写同样的代码,这样会给我们逆向带来一些分析的难度

可以看到这个地方显示了我们接口,我们就可以保证待会异步走的话也是我们对应的接口,我们先在这个位置打上一个断点,刷新页面

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

image

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

下面是一个promise示例

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

// 按以下顺序产生输出:
// 立即记录
// 异步日志记录有值:777

image

在控制台输出之后放开断点

/* 自行修改 */
n({
    url: "youzy.dms.datalib.api.enrolldata.*************.encrypted.v2.get",
    data: e
}).then(res => console.log(res))

image

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

image

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

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

            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)
            }

image

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

image

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

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

image

遍历对象 t​ 的自身属性。对于每个属性 e​:

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

image

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

image

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

image

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

image

其实到这里就已经解密完成了,我之前不知道还在一直在别的解密位置,后面发现别的就是字体混淆了,浪费了很多时间

字体混淆

可以看到f12中,查看网页的字不是一个正常的文字,应该是应该字体混淆了

image

进入他的css文件里面查看

image

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

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

image

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

image

image

直接下载这个字体

image

image

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

image

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

image

image

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

image

前面扣出来的加密算法

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 < b; i++)
        b0 = k.indexOf(str.charAt(d)),
        d++,
        b1 = k.indexOf(str.charAt(d)),
        d++,
        b2 = k.indexOf(str.charAt(d)),
        d++,
        b3 = k.indexOf(str.charAt(d)),
        d++,
        s[i] = ((b1 + b0 * l) * l + b2) * l + b3;
    b = eval("String.fromCharCode(" + s.join(",") + ")");
    var w = "";
    return b.split("|").forEach((function(e, t) {
        t > 0 && (-1 != e.search(/【(.*?)】/) ? w += e.replace("【", "").replace("】", "") : e.length > 0 && (w += "&#x" + e + ";"))
    }
    )),
    w
}

str = "001H0039001H0032001G001I001C001H002R001I002Z0034001E00380034001H003G002R002U002T002T"

console.log(cnDeCryptV2(str))

结果是:​     &#x​这个前缀没啥用

在网页里面搜索  \ucfee​  正好可以对应

image

接下来我们需要做一个映射表来实现比如 上面的 {"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)

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

image

image

解密函数

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))

image

免费评分

参与人数 15吾爱币 +14 热心值 +15 收起 理由
笙若 + 1 + 1 谢谢@Thanks!
wangxiaoqiqiqi + 1 + 1 我很赞同!
wzh103103 + 1 我很赞同!
梦旅意中人 + 1 + 1 我很赞同!
丶峰宇 + 1 + 1 谢谢@Thanks!
louchen94 + 1 + 1 我很赞同!
jokin1999 + 2 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
liuxuming3303 + 1 + 1 谢谢@Thanks!
BTCQAQ + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
coderyl + 1 我很赞同!
Junlanok + 1 + 1 我很赞同!
xhr2025 + 1 + 1 我很赞同!
ioyr5995 + 1 + 1 我很赞同!
FitContent + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

BTCQAQ 发表于 2025-3-17 21:52
本帖最后由 BTCQAQ 于 2025-3-17 21:53 编辑

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

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
就往丶 + 1 + 1 我很赞同!

查看全部评分

 楼主| 就往丶 发表于 2025-3-16 15:11
请跪迎朕 发表于 2025-3-16 16:05
请跪迎朕 发表于 2025-3-16 16:05
楼主多发些这个站点的解密
mumuaqa 发表于 2025-3-16 16:10
感谢大佬分享
agooo 发表于 2025-3-16 16:28
就往丶 发表于 2025-3-16 15:11
@agooo 你看看这个教程,希望对你有帮助

感谢大佬,!!!!
秋海明月 发表于 2025-3-16 16:41
很久没看这方面的内容了,感谢大佬~
yymmll 发表于 2025-3-16 16:47
详细的分享,感谢分析,学习学习
KaliHt 发表于 2025-3-16 17:49
这个栈太多了,太难了
yb66vs 发表于 2025-3-16 17:51
是个好软件,,牛
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2025-5-23 05:48

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表