抓包分析
首先点击下一章,发起一个请求,本篇文章主要讲字体反爬,查询参数是什么就不过多说了,某大厂比较热门的一个参数。该参数逆向的话,总的来说还是一个vmp,找到函数调用的地方插桩,看日志,然后配合条件断点,耐心细心,最后一定可以弄出来的。
1.png (24.08 KB, 下载次数: 3)
下载附件
2024-11-30 12:36 上传
我们看响应,一大堆乱码,有很大的概率是字体反爬。
2.png (81.59 KB, 下载次数: 3)
下载附件
2024-11-30 12:36 上传
进一步验证一下,有字体文件。
3.png (26.46 KB, 下载次数: 3)
下载附件
2024-11-30 12:36 上传
有CSS样式,字体反爬没错了。
4.png (23.8 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
因为不常遇到字体反爬,所以这方面的经验相对较少。
我简单说下我的处理方式,请各位路过的大佬指导指导小弟。
首先观察字体反爬的类型,雪碧图?样式偏移?还是其他的...
其次看是不是动态字体,就是数据接口会不会连带有和字体相关的请求
最后就是字体解析,文件类型的话直接拿映射表,雪碧图和样式偏移就找对应的偏移量...
逆向分析
根据前面分析的流程,我们知道dc027189e0ba4cd大概率就是字体文件。
5.png (21.96 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
我们可以直接双击下载字体文件,然后找一个网站进行解析。
字体文件解析网址:aHR0cHM6Ly9rZWtlZTAwMC5naXRodWIuaW8vZm9udGVkaXRvci8=。
很不给面子,说错误的ttf文件(网上说可能woff2是较新的格式,然后网站不支持解析这种类型的字体文件)。
6.png (14.62 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
那我们就模拟请求下载字体文件,经过了一番尝试,可以下载otf类型的文件并完成解析,我是直接把文件后缀改成了otf,可能服务器并没有校验文件的类型,如果有大佬试出了其他类型的,或者有其他方法的,也可以分享分享。
7.png (53.42 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
python代码:
import requests
headers = {
"accept": "application/font-otf",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7",
"cache-control": "no-cache",
"origin": "脱敏信息",
"pragma": "no-cache",
"priority": "u=0",
"referer": "脱敏信息",
"sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "font",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
url = "脱敏信息/dc027189e0ba4cd.otf"
response = requests.get(url, headers=headers)
with open('test.otf', 'wb') as f:
print(response.content)
f.write(response.content)
然后我们回到网站,再分析分析响应的内容,
我们随便选一个文字,就“人”这个字吧,响应中是不可见字符,仔细观察网页上的,和正常的文字也会有点区别。
8.png (65.16 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
虽然这是不可见字符,但我们可以得到它的unicode码点,且这个码点大概率是在字体文件有对应关系的。
可以在下图这复制,然后去控制台拿到这个不可见字符的unicode码点,' '.charCodeAt()得到58562。
9.png (20.77 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
然后我们回到字体解析网站找“人”这个字,发现确实对应了gid58562,gid是标识字形的唯一标识符。
10.png (25.14 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
按照往常的经验,标准的字体文件每个字的唯一标识符gid会对应自己的unicode编码,
我们可以看看“人”这个字的unicode编码是多少,发现是20154,对不上,那这字体文件应该就是自定义的,也就是说我们不能通过字体文件中的唯一标识符知道是什么字。
11.png (6.08 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
那我们该怎么处理才能拿到文字和唯一标识符gid的对应关系呢,我瞎摸索出了两种比较鸡肋的方法,都是基于svg转图片然后进行文字识别的。
第一种
半自动化方案,有点鸡肋,运行程序后需要手动加载字体文件,然后控制台回车就可以。
这种方案只适合非动态字体,只是减少工作量。
import time
import cairosvg
from DrissionPage import ChromiumPage
from lxml import etree
import os
class FontSpider:
# 图片文件夹路径
if not os.path.exists('./test'):
os.mkdir('./test')
@staticmethod
def svg_to_image(html_code):
tree = etree.HTML(html_code)
svg_lst = tree.xpath('//*[@class="glyf-list"]/div/svg')
unicode_lst = tree.xpath('//*[@class="glyf-list"]/div/div[last()]/text()')
for svg, unicode in zip(svg_lst, unicode_lst):
if unicode.startswith('.'):
continue
svg.set('width', '1000')
svg.set('height', '1000')
svg_str = etree.tostring(svg, encoding='unicode', pretty_print=True)
print(svg_str)
# 将SVG字符串转换为PNG文件
cairosvg.svg2png(bytestring=svg_str, write_to=f'./test/{unicode.replace("gid", "")}.png', background_color='white')
@staticmethod
def get_single_char_image():
driver = ChromiumPage()
driver.get("https://kekee000.github.io/fonteditor/")
input('加载字体文件完毕后回车>>>')
FontSpider.svg_to_image(driver.html)
for i in range(3):
driver.ele('xpath://*[@id="glyf-list-pager"]/button[3]').click()
time.sleep(1)
FontSpider.svg_to_image(driver.html)
driver.quit()
if __name__ == '__main__':
FontSpider.get_single_char_image()
第二种
这种方案直接将字体文件解析为svg然后转为png图片。
from fontTools import ttLib
from fontTools.pens.svgPathPen import SVGPathPen
import pathlib
import cairosvg
class FontConverter:
def __init__(self, font_path):
self.font = ttLib.TTFont(font_path)
self.units_per_em = self.font['head'].unitsPerEm
def get_glyph_names(self):
return self.font.getGlyphOrder()
def get_cmap(self):
return self.font.getBestCmap()
def glyph_to_svg(self, glyph_name):
# 获取字形对象
glyph_set = self.font.getGlyphSet()
glyph = glyph_set[glyph_name]
# 创建SVG路径笔
pen = SVGPathPen(glyph_set)
# 绘制字形
glyph.draw(pen)
# 获取路径数据
path_data = pen.getCommands()
# 获取边界框
bbox = None
if hasattr(glyph, 'xMin'):
bbox = {
'xMin': glyph.xMin,
'yMin': glyph.yMin,
'xMax': glyph.xMax,
'yMax': glyph.yMax
}
return {
'path': path_data,
'bbox': bbox
}
def create_svg(self, glyph_name, width=1000, height=1000):
glyph_data = self.glyph_to_svg(glyph_name)
# 计算变换参数
scale = 0.9 # 缩放因子
baseline = self.units_per_em * 0.8 # 基线位置
svg = f'''
'''
return svg
def batch_convert(self, image_folder):
output_path = pathlib.Path(image_folder)
output_path.mkdir(parents=True, exist_ok=True)
# 获取字符映射
cmap = self.get_cmap()
# 转换每个字形
for unicode_value, glyph_name in cmap.items():
try:
# 创建SVG
svg = self.create_svg(glyph_name)
# 将SVG字符串转换为PNG文件
cairosvg.svg2png(
bytestring=svg,
write_to=f'{image_folder}/{unicode_value}.png',
background_color='white',
output_height=64,
output_width=64
)
except Exception as e:
print(f"转换字形 {glyph_name} (U+{unicode_value:04X}) 时出错: {e}")
def main(fp, image_folder):
# 使用示例
converter = FontConverter(fp)
# 获取所有字形名称
glyph_names = converter.get_glyph_names()
print(f"字体包含 {len(glyph_names)} 个字形")
# 批量转换
converter.batch_convert(image_folder)
if __name__ == '__main__':
font_path = './font.otf'
image_folder = 'test'
main(font_path, image_folder)
文件名都是每个字的唯一标识符gid。
然后我们需要进行文字识别来构造一个映射,键为gid,值就是哪一个字。
识别用的开源库ddddocr,我们将识别结果保存为一个json文件方便后续使用。
import os
import ddddocr
from pathlib import Path
import json
from PIL import Image
import io
import matplotlib.pyplot as plt
ocr = ddddocr.DdddOcr(show_ad=False, beta=True, use_gpu=True)
# 图片保存的文件夹路径
image_folder = 'test'
dir_path = Path(image_folder)
files = list(dir_path.glob('*.png'))
with open('mapping.json', 'w', encoding='utf-8') as mf:
data = {}
for file in files:
file_name = file.name
[unicode, _] = file_name.split('.')
with open(os.path.join(image_folder, file_name), 'rb') as f:
img_bytes = f.read()
result = ocr.classification(img_bytes)
if len(result) > 1:
# 从字节数据创建图片
img = Image.open(io.BytesIO(img_bytes))
plt.imshow(img)
plt.axis('off') # 隐藏坐标轴
plt.show()
result = input('请输入正确的结果>>>')
data[unicode] = result
else:
data[unicode] = result
mf.write(json.dumps(data))
上面这个方案可能还需要手动处理一下,因为模型识别不是百分百的准确率,我们暂且认为识别出一个字的就是正确的,两个字的就需要手动处理一下。
12.png (110.47 KB, 下载次数: 0)
下载附件
2024-11-30 12:36 上传
最后我们模拟请求一下,并从json文件中取对应的映射来替换不可见字符。
import json
import requests
import re
with open('mapping.json', 'r') as f:
mapping = json.load(f)
def get_data():
headers = {
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,pt;q=0.7",
"cache-control": "no-cache",
"ismobile": "0",
"pragma": "no-cache",
"priority": "u=1, i",
"referer": "脱敏信息",
"sec-ch-ua": "\"Google Chrome\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
}
cookies = {}
url = "脱敏信息"
params = {}
response = requests.get(url, headers=headers, cookies=cookies, params=params)
print(response.text)
content: str = response.json()['data']['chapterData']['content']
content_list = re.findall(r'(.*?)
', content)
for c in content_list:
target_content = []
for char in c:
if '\\u' in repr(char):
hex_str = repr(char).replace('\\u', '')
target_char = mapping[str(int(hex_str.strip('\''), 16))]
target_content.append(target_char)
else:
target_content.append(char.strip('\''))
print(''.join(target_content))
get_data()
13.png (92.45 KB, 下载次数: 0)
下载附件
2024-11-30 12:37 上传
成功!!!
解析字体的第二种方案,加上一个好的文字识别模型,其实能够做到不用手动干预了,奈何本菜鸡不会训练模型。