【web逆向】优某愿 字体混淆
地址: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 < 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
正好可以对应
接下来我们需要做一个映射表来实现比如 上面的 {"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))