地址:aHR0cHM6Ly93d3cueW91enkuY24vY29sbGVnZXMvc2NvcmVsaW5lP2NvbGxlZ2VDb2RlPTEwMDAzJm5hbWU9JUU2JUI4JTg1JUU1JThEJThFJUU1JUE0JUE3JUU1JUFEJUE2
接口分析
接口:eW91enkuZG1zLmRhdGFsaWIuYXBpLmVucm9sbGRhdGEuZW50ZXIuY29sbGVnZS5lbmNyeXB0ZWQudjIuZ2V0

代码分析
先下一个xhr断点

我们往上面找 可以看到有一个promise.then
异步,一般有这个异步的话,很多函数都是封装起来的。好比如你写代码的话,不会重复写同样的代码,这样会给我们逆向带来一些分析的难度
可以看到这个地方显示了我们接口,我们就可以保证待会异步走的话也是我们对应的接口,我们先在这个位置打上一个断点,刷新页面
在 JavaScript 中,Promise 是一个代表异步操作最终完成(或失败)及其结果的对象。它主要用于处理异步操作,例如网络请求、文件读取、定时器等,以避免传统回调函数带来的“回调地狱”(callback hell)问题,并提供更清晰和结构化的方式来管理异步代码。

断电断住了,我们打印一下n
发现是一个异步返回 我们就需要使用then关键词来获取里面返回的信息
下面是一个promise示例
const promiseA = new Promise((resolve, reject) => {
resolve(777);
});
promiseA.then((val) => console.log("异步日志记录有值:", val));
console.log("立即记录");

在控制台输出之后放开断点
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
from loguru import logger
ocr = ddddocr.DdddOcr(beta=False, show_ad=False)
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
font.close()
return characters
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)
def recognize_char_from_image(image_path):
with open(image_path, "rb") as f:
img_bytes = f.read()
result = ocr.classification(img_bytes)
return result
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)
recognized_char = recognize_char_from_image(temp_image_path)
recognized_char = recognized_char if recognized_char else "未知"
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"
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()]}')
w += fontdict[str(e).upper()]
return w
str_input = "0031001L001D0030001I003B001G0035001J001H001F001L001C001H001H001F003G09HS0LQD09HT003G002R001L001J002P003G002R002S001C002U003G002R002Q002T001K003G09HS001409HT003G002S001C001F001I003G002S001C001G001C003G002S001C001K001D003G002R002Q001H002R003G002R002P001I002R003G002R001L002U001L003G09HS001509HT003G09HS001409HT003G002S001C001H002Q003G002S001C001C001D003G002S001C001C001G003G002S001C001L002P003G09HS001509HT"
logger.success(cnDeCryptV2(str_input))
