当前位置: 首页 > article >正文

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

地址: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))

结果是:&#xcfee&#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


http://www.kler.cn/a/588262.html

相关文章:

  • 修改 Macbook 终端窗口的显示信息
  • Leetcode做题记录----3
  • [特殊字符]Windows 11 安装 Git 图文教程(含详细配置说明)
  • IMX6ULL学习整理篇——Linux驱动开发的基础2 老框架的一次实战:LED驱动
  • 五大基础算法——递归算法
  • LangChain教程 - Agent -之 REACT_DOCSTORE
  • 1.备战SISAP 2025挑战:调研2024挑战
  • Java语言的WebSocket
  • 施磊老师c++(七)
  • 游戏引擎学习第163天
  • 如何在 GitHub 上修改他人的分支
  • Java 大视界 -- Java 大数据在智慧交通自动驾驶仿真与测试数据处理中的应用(136)
  • 【MCU】芯片复位与软件复位 在生产工装上的应用
  • 苹果app上架app store 之苹果开发者账户在mac电脑上如何使用钥匙串访问-发行-APP发布证书ios_distribution.cer-优雅草卓伊凡
  • yungouos微信扫码登录demo示例(支持个人免费)
  • 使用 OpenSSL 生成的 RSA 私钥文件(如`prikey.pem`)可以用于加密和解密数据
  • 【Cadence软件技巧集萃】从Capture到Allergo——分布演示从原理符号导出到网络表
  • OrioleDB: 新一代PostgreSQL存储引擎
  • 增量数据同步怎么做
  • HTTPS 证书相关