JS逆向实战:动态字体解析

大家好,今天分享一个动态字体反爬实战,本题目为猿人学第 7 题(match7)。

动态字体

image-20250413174615222

先看请求,返回了一个 base64 字符串,猜测可能是字体文件,还有一些像是某种编码的字符串。

image-20250413174753260

查看具体的数据,发现数据是乱码,确认使用了自定义的字体。

先将字体文件保存下载,然后使用字体解析工具来解析一下,看看是不是真的字体文件:

image-20250413175751533

可以正常解析,并且每个字体都带有标识,刚才返回的数据可能会对应到每个标识,通过标识来匹配每个数字。

字体解析

image-20250413193021868

由于是动态字体,每次的字体文件可能都会不一样,所以要把每次请求的字体文件都保存下来,并且使用日志断点打印一下每个数字对应的字符串都是什么,方便后面调试解析逻辑。

刚才使用网页解析了字体文件,但是爬虫肯定不能每次都是用网页来解析字体文件,Python 中可以解析字体文件的库是 fontTools,使用它可以将字体文件解析成 xml 文件,然后从 xml 文件中可以获取每个数字和 id 的对应关系。

image-20250413192724694

image-20250413192735163

到这里解析的思路就有了,现在还剩下一个问题,那就是每个标识和具体数字的对应关系还没有。image-20250413203827978

先记住现在的标识和数字的对应关系,重新下载一个字体文件,查看标识和数字是不是能对得上。

image-20250413203956882

发现标识也是随机变化的,和数字完全对不上。

image-20250413204546447

image-20250413204558169

继续分析生成的 xml 文件,发现 contour 标签中的 on 值每个数字都是一样的,contour 意思是轮廓,应该是数字的轮廓,也就是说每个数字的外形是不会变化的,所以可以根据这个轮廓和标识的对应关系定位到每个数字。

image-20250413205213126

简单处理一下,最终形成一个字典。但是目前还不知道具体每个字符串对应的数字,这个对应关系是需要提前人工提取出来放到代码中的,例如 1001101111 对应的就是数字 1,但是前面的 unic796 标识每次都会变化,1001101111 这个字符串不会变化。由此可以在代码中先找到 unicxxx 和字符串 1001101111 的对应关系,然后由于提前知道了 1001101111 是数字 1,也就知道了 unicxxx 对应的是 1 了。

image-20250413205653096

将上面的结果再次查询定义好的字典,现在变成了这样,notdef 这个标签由于没有意义,找不到也正常。

现在来写代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import base64

import requests
import urllib3
from fontTools.ttLib import TTFont
from lxml import etree

urllib3.disable_warnings()
result = 0
result_name = ''
headers = {
'content-length': '0',
'pragma': 'no-cache',
'cache-control': 'no-cache',
'sec-ch-ua-platform': '"macOS"',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
'sec-ch-ua': '"Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"',
'dnt': '1',
'sec-ch-ua-mobile': '?0',
'accept': '*/*',
'origin': 'https://match.xxxxxx.cn',
'sec-fetch-site': 'same-origin',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
'referer': 'https://match.xxxxxx.cn/match/3',
'accept-encoding': 'gzip, deflate, br, zstd',
'accept-language': 'zh-CN,zh;q=0.9',
'cookie': 'sessionid=',
'priority': 'u=0, i',
}
number_dict = {
'1001101111': '1',
'1110101001001010110101010100101011111': '5',
'100110101001010101011110101000': '2',
'101010101101010001010101101010101010010010010101001000010': '8',
'111111111111111': '4',
'1111111': '7',
'10101010100001010111010101101010010101000': '6',
'10010101001110101011010101010101000100100': '9',
'10101100101000111100010101011010100101010100': '3',
'10100100100101010010010010': '0',
}
nodes_dict = {}
name = ['极镀ギ紬荕', '爷灬霸气傀儡', '梦战苍穹', '傲世哥', 'мaη肆風聲', '一刀メ隔世', '横刀メ绝杀', 'Q不死你R死你',
'魔帝殤邪', '封刀不再战', '倾城孤狼', '戎马江湖', '狂得像风', '影之哀伤', '謸氕づ独尊', '傲视狂杀', '追风之梦',
'枭雄在世', '傲视之巅', '黑夜刺客', '占你心为王', '爷来取你狗命', '御风踏血', '凫矢暮城', '孤影メ残刀',
'野区霸王', '噬血啸月', '风逝无迹', '帅的睡不着', '血色杀戮者', '冷视天下', '帅出新高度', '風狆瑬蒗',
'灵魂禁锢', 'ヤ地狱篮枫ゞ', '溅血メ破天', '剑尊メ杀戮', '塞外う飛龍', '哥‘K纯帅', '逆風祈雨', '恣意踏江山',
'望断、天涯路', '地獄惡灵', '疯狂メ孽杀', '寂月灭影', '骚年霸称帝王', '狂杀メ无赦', '死灵的哀伤', '撩妹界扛把子',
'霸刀☆藐视天下', '潇洒又能打', '狂卩龙灬巅丷峰', '羁旅天涯.', '南宫沐风', '风恋绝尘', '剑下孤魂', '一蓑烟雨',
'领域★倾战', '威龙丶断魂神狙', '辉煌战绩', '屎来运赚', '伱、Bu够档次', '九音引魂箫', '骨子里的傲气',
'霸海断长空', '没枪也很狂', '死魂★之灵']


def parse_glyph_ids(xml_string):
root = etree.fromstring(xml_string)
ttg = root.xpath("//TTGlyph")
for glyph in ttg:
name = glyph.get('name')
pt_nodes = glyph.xpath('.//contour/pt')
on_sequence = ''.join(pt.get('on') for pt in pt_nodes)
nodes_dict[name[-4:]] = number_dict.get(on_sequence)


for page in range(2, 6):
print(page)
url = f"https://match.xxxx.cn/api/match/7?page={page}"
response = requests.get(url, headers=headers, verify=False)
with open('font.ttf', 'wb') as f:
f.write(base64.b64decode(response.json()['woff']))
font = TTFont(file='font.ttf')
font.saveXML('font.xml')
with open('font.xml', 'rb') as f:
parse_glyph_ids(f.read())
# print(nodes_dict)
for index, a in enumerate(response.json()['data'],1):
value = a['value']
num = ''.join([nodes_dict[b[-4:]] for b in value.split(' ') if b])
print(value,num)
if int(num) > result:
result = int(num)
result_name = name[index + (page - 1) * 10]
print(result_name)
print(result)
print(result_name)

由于答案要求是战力最高的人名,而且人名又是通过一个大的数组算出来的,将数组抠出来直接通过索引来计算,然后判断最大值并且更新名字,结束。

总结

这道题比之前做过的题目稍微难一点,之前的题目可以根据 id 和标识直接得出数字,不需要去获取 contour 标签,而且字体文件不会变化。其实只要静下心来,仔细分析一下题目中的 xml 文件,还是可以找到规律的,找到规律后,写代码就简单多了,把思路用代码实现了就好。

本文章首发于个人博客 LLLibra146’s blog

本文作者:LLLibra146

更多文章请关注公众号 (LLLibra146):LLLibra146

版权声明:本博客所有文章除特别声明外,均采用 © BY-NC-ND 许可协议。非商用转载请注明出处!严禁商业转载!

本文链接
https://blog.d77.xyz/archives/2f608365.html