猿人学web端爬虫攻防大赛赛题解析_第七题:动态字体,随风漂移

一、前言

上一道字体反扒的css加密题还是第四题,不过那道题主要还只是做了个数字的坐标偏移,逻辑分析起来不算难,这道动态字体的题算是真正的字体反扒题,做了一下感觉挺长见识的,对于了解同类型字体加密有不小帮助,闲言少叙,开搞!

二、分析过程

题目的要求是采集胜点列的数据,找出胜点最高的召唤师,那加密的数据肯定就是胜点数据了,核心应该是解析出胜点值,其次就是将这个值与对应的召唤师匹配,最后比较五十个玩家的胜点,找出胜点最高那一个。

这里仔细观察一下,可以发现胜点数字的字形都不是很规则,看着就像是图形渲染的,复制粘贴一下就会发现,这个值果然是图片符号。
在这里插入图片描述

打开开发者模式看一下请求的结果,不难发现这里的data应该是加密后的数值,而woff则是加密字体文件,而且这个字符串是base64加密后的。
在这里插入图片描述
往前查看一下请求调用栈,在网页源码这找到了对字体进行处理的部分代码:
在这里插入图片描述字体文件加载是通过这两行处理的,也就是通过这里加载得到字体的ttf文件,在页面渲染的过程中对data内的value值进行渲染从而得到最后的显示结果。

 ttf = data.woff;
 $('.font').text('').append('<style type="text/css">@font-face { font-family:"fonteditor";src: url(data:font/truetype;charset=utf-8;base64,' + ttf + '); }</style>');

从源代码这里看不出字体的加密字符和数值的对应关系,于是把woff文件逆编码后保存为ttf格式的字体文件:

b64_code="AAEAAAAKAIAAAwAgT1MvMgPtafAAAAEoAAAAYGNtYXBBft4uAAABpAAAAYpnbHlmPD0Wg.....省略部分字符"
with open('D:\\yuan_font.ttf', 'wb') as f:
    f.write(base64.b64decode(b64_code))

将保存的ttf字体文件在在线字体编辑器里打开,可以发现这里还原出了每个加密编码对应的数字字符:
在这里插入图片描述
这里验证了一下这个字体加密编码跟对应的数值关系,发现每次请求,返回的value值都是不一样的,以第一页为例,对于第一个胜点3236,每次value值都在变化,所以想靠固定编码关系来直接破解是不可能了:

第一次请求:
0: {value: "&#xf134 &#xc261 &#xf134 &#xa613 "}
第二次请求:
0: {value: "&#xf524 &#xa934 &#xf524 &#xc825 "}
第三次请求:
0: {value: "&#xf289 &#xa471 &#xf289 &#xa265 "}

只好进一步查看字体文件的详细信息,利用python的fontTools这个字体库的函数将ttf字体转换为xml格式:

font = TTFont('D:\\yuan_font.ttf')
font.saveXML('D:\\yuan_font.xml')

打开xml文件,这里的codename对应的就是0-9这十个数字的加密编码:
在这里插入图片描述
底下的TTGlyph内,contour标签中就是每个数字字形的坐标,全部绘制出来就构成了网页最后渲染出的字体效果,那么既然编码对应的是具体的字形坐标,无论每次返回的数字字符编码是多少,那每个字形的坐标总不会变吧。在这里插入图片描述

好家伙,我还是天真了,打开了两份文件对比了一下,发现即使是相同的数字,其字形竟然也不同,坐标值x,y会有会有小范围的浮动,这就导致即使同样的数字每次渲染出来都会有所不同,就比如底下这个2,虽然看着都一样,但是在轮廓上还是有明显差异:
在这里插入图片描述

如此看来,轻松简单的方法是不可能存在了,现在可能的解法有两种,一是虽然每个数字每次字形都有差异,但差异毕竟有限,可以尝试对同一个数字字形的两组坐标值进行对比,如果差别不超过某个阈值,那么就可以认为是同一个字;二是直接把字形文件的坐标绘制出来,生成字体图片,之后再ocr识别数字。

三、代码实现

3.1、方法一:字体映射关系识别数字

为了找到坐标跟对应字符间的映射关系,这里选了数字4的坐标来分析。可以发现,虽然每次返回的坐标值都不一样,但是同一个字体总的坐标对数量是一致的,而且pt内的on值是完全一致的,那么就可以尝试根据两个字体文件内所有的on值是否完全相等来判断是不是同一个数字。
在这里插入图片描述在这里插入图片描述

先随便找一个字体文件,手动分析出字符编码与数字的对应关系:
在这里插入图片描述在这里插入图片描述

以这个关系作为其他字符编码相比较的参考基准,即如果下一个字体文件中某一个数字对应的所有on值都跟标准对照表里的on值相等,那就确定是同一个数字

    #选定一个基准参照,确定flag和数字的关系
    font = TTFont("F:\\temp\yuan_font_1.ttf")
    gs = font.getGlyphSet()
    glyphNames = font.getGlyphNames()

    #这个数字顺序跟xml里的TTGlyph name顺序是一致的
    num_list = [4,6,9,0,2,8,1,5,3,7]
    map_dict={}

    for i,name in enumerate(glyphNames[1:]):
        g = gs[name]
        flag = list(g._glyph.flags)#读取每个坐标对应的on值
        # coord=g._glyph.coordinates#获取每个字符的坐标序列

        #map_dict字典里的键对应的数字,值则是on值构成的列表
        map_dict[num_list[i]]=flag

    print('标准对应关系为:',map_dict)


    #待解析文件
    font = TTFont("F:\\temp\yuan_font_2.ttf")
    gs = font.getGlyphSet()
    glyphNames = font.getGlyphNames()
    list2=[]
    for i,name in enumerate(glyphNames[1:]):
        g = gs[name]
        flag = list(g._glyph.flags)#读取每个坐标对应的on值

        for key,value in map_dict.items():
            if value==flag:
                list2.append((name,key))

    print('解析后的数字对应关系:')
    for m in list2:
        print(m)
        pass        

运行一下,输出结果为:

标准对应关系为: {4: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 6: [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0], 9: [1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0], 0: [1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0], 2: [1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0], 8: [1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0], 1: [1, 0, 0, 1, 1, 0, 1, 1, 1, 1], 5: [1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1], 3: [1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0], 7: [1, 1, 1, 1, 1, 1, 1]}

解析后的数字对应关系:
('unia168', 1)
('unia254', 8)
('unia362', 4)
('unib967', 7)
('unie634', 3)
('unie671', 2)
('unie813', 6)
('unif175', 5)
('unif416', 0)
('unif845', 9)

这是被解析的字体文件,和程序输出结果进行比较,可以发现编码与数字对应关系是一致的
在这里插入图片描述
最难啃的部分已经解决,此后完整代码如下:

def parseByFlag(fontfile):
    #选定一个基准参照,确定flag和数字的关系
    font = TTFont("F:\\temp\yuan_font_1.ttf")
    gs = font.getGlyphSet()
    glyphNames = font.getGlyphNames()

    #这个数字顺序跟xml里的TTGlyph name顺序是一致的
    num_list = [4,6,9,0,2,8,1,5,3,7]
    map_dict={}

    for i,name in enumerate(glyphNames[1:]):
        g = gs[name]
        flag = list(g._glyph.flags)#读取每个坐标对应的on值
        # coord=g._glyph.coordinates#获取每个字符的坐标序列

        #字典里的键对应的数字,值则是on值构成的列表
        map_dict[num_list[i]]=flag

    # print('标准对应关系为:',map_dict)



    font = TTFont(BytesIO(fontfile))#待解析字体文件
    gs = font.getGlyphSet()
    glyphNames = font.getGlyphNames()
    list2=[]
    for i,name in enumerate(glyphNames[1:]):
        g = gs[name]
        flag = list(g._glyph.flags)#读取每个坐标对应的on值

        for key,value in map_dict.items():
            if value==flag:
                list2.append((name,key))

    # print('解析后的数字对应关系:')
    # for m in list2:
    #     print(m)

    return list2

def get_parseResult():
    headers = {
        'Proxy-Connection': 'keep-alive',
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'User-Agent': 'yuanrenxue.project',
        'X-Requested-With': 'XMLHttpRequest',
        'Referer': 'http://match.yuanrenxue.com/match/7',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }

    name = ['极镀ギ紬荕', '爷灬霸气傀儡', '梦战苍穹', '傲世哥', 'мaη肆風聲', '一刀メ隔世', '横刀メ绝杀', 'Q不死你R死你', '魔帝殤邪', '封刀不再战', '倾城孤狼', '戎马江湖',
            '狂得像风', '影之哀伤', '謸氕づ独尊', '傲视狂杀', '追风之梦', '枭雄在世', '傲视之巅', '黑夜刺客', '占你心为王', '爷来取你狗命', '御风踏血', '凫矢暮城', '孤影メ残刀',
            '野区霸王', '噬血啸月', '风逝无迹', '帅的睡不着', '血色杀戮者', '冷视天下', '帅出新高度', '風狆瑬蒗', '灵魂禁锢', 'ヤ地狱篮枫ゞ', '溅血メ破天', '剑尊メ杀戮',
            '塞外う飛龍', '哥‘K纯帅', '逆風祈雨', '恣意踏江山', '望断、天涯路', '地獄惡灵', '疯狂メ孽杀', '寂月灭影', '骚年霸称帝王', '狂杀メ无赦', '死灵的哀伤', '撩妹界扛把子',
            '霸刀☆藐视天下', '潇洒又能打', '狂卩龙灬巅丷峰', '羁旅天涯.', '南宫沐风', '风恋绝尘', '剑下孤魂', '一蓑烟雨', '领域★倾战', '威龙丶断魂神狙', '辉煌战绩', '屎来运赚',
            '伱、Bu够档次', '九音引魂箫', '骨子里的傲气', '霸海断长空', '没枪也很狂', '死魂★之灵']

    player_dict = {}

    for i in range(1,6):
        map_dict={}
        page=i
        params = (
            ('page', str(page)),
        )

        response = requests.get('http://match.yuanrenxue.com/api/match/7', headers=headers, params=params)
        data=response.json()
        woff=data['woff']
        valuelist = data['data']

        fontfile = base64.b64decode(woff.encode())
        match_result=parseByFlag(fontfile)

        match_result=[(l[0].replace('uni','&#x'),l[1]) for l in match_result]
        map_dict={}
        for m in match_result:
            map_dict[m[0]]=str(m[1])

        for n, v in enumerate(valuelist):
            # 解析得到胜点值
            win_point = v['value'].split(' ')
            del win_point[-1]
            real_win_point = [map_dict[num] for num in win_point]
            str_ = ''
            real_win_point = str_.join(real_win_point)
            print(real_win_point)

            # 将玩家与胜点值关联
            player = name[(n + 1) + (page - 1) * 10]

            player_dict[player] = int(real_win_point)

        for key, value in player_dict.items():
            print(key, value)


get_parseResult()

最终输出结果如下:

爷灬霸气傀儡 3236
梦战苍穹 5041
傲世哥 3958
мaη肆風聲 8550
一刀メ隔世 7037
横刀メ绝杀 8898
Q不死你R死你 2190
魔帝殤邪 8400
封刀不再战 4500
倾城孤狼 7478
戎马江湖 2342
狂得像风 1926
影之哀伤 5826
謸氕づ独尊 2827
傲视狂杀 369
追风之梦 4384
枭雄在世 2934
傲视之巅 5468
黑夜刺客 9107
占你心为王 2132
爷来取你狗命 5553
御风踏血 687
凫矢暮城 5688
孤影メ残刀 6179
野区霸王 7722
噬血啸月 35
风逝无迹 6301
帅的睡不着 9221
血色杀戮者 6534
冷视天下 9711
帅出新高度 6995
風狆瑬蒗 3705
灵魂禁锢 5413
ヤ地狱篮枫ゞ 2333
溅血メ破天 5660
剑尊メ杀戮 7142
塞外う飛龍 8826
哥‘K纯帅 9291
逆風祈雨 5778
恣意踏江山 2920
望断、天涯路 5983
地獄惡灵 9015
疯狂メ孽杀 1533
寂月灭影 4337
骚年霸称帝王 746
狂杀メ无赦 4349
死灵的哀伤 4229
撩妹界扛把子 4928
霸刀☆藐视天下 2830
潇洒又能打 1206

这里需要注意的是,因为这道题只有十个数字,字符类型不多,所以寻找字体编码与字形映射关系时可以用on值来做关联,如果是有较多字符的情况下,是有可能出现不同字符的on值完全相同的情况,这样就只能按坐标的xy值浮动偏差值来进行比较了。

3.2、方法二:ocr识别数字

以ocr的思路来实现字体解密的核心思路如下:

  1. 先请求数据页api,得到woff文件和加密字体的value数据;
  2. 对woff文件进行编译,转换为TTFont对象;
  3. 通过reportlab库来绘制数字字符图形;
  4. 将绘制的字形结果保存为图片,并用muggle_ocr库来做ocr识别;
  5. 得到所有加密编码对应的数字;
  6. 将数字拼接后得到胜点,并与玩家名字关联
import requests
import base64
from fontTools.ttLib import TTFont
from fontTools.pens.basePen import BasePen
from reportlab.graphics.shapes import Path
from reportlab.lib import colors
from reportlab.graphics import renderPM
from reportlab.graphics.shapes import Group, Drawing, scale
import muggle_ocr
from io import BytesIO

#设置字形绘制笔刷
class ReportLabPen(BasePen):
    """A pen for drawing onto a reportlab.graphics.shapes.Path object."""

    def __init__(self, glyphSet, path=None):
        BasePen.__init__(self, glyphSet)
        if path is None:
            path = Path()
        self.path = path

    def _moveTo(self, p):
        (x, y) = p
        self.path.moveTo(x, y)

    def _lineTo(self, p):
        (x, y) = p
        self.path.lineTo(x, y)

    def _curveToOne(self, p1, p2, p3):
        (x1, y1) = p1
        (x2, y2) = p2
        (x3, y3) = p3
        self.path.curveTo(x1, y1, x2, y2, x3, y3)

    def _closePath(self):
        self.path.closePath()

#绘制字形并保存为图片
def ttfToImageNum(fontfile, imagePath, fmt="png"):
    # 由于TTFont接收一个文件类型
    # BytesIO(bin_data) 把二进制数据当作文件来操作
    font = TTFont(BytesIO(fontfile))
    gs = font.getGlyphSet()
    glyphNames = font.getGlyphNames()
    map_dict={}
    for i in glyphNames:
        if i[0] == '.':  # skip'.notdef','.null'
            continue

        g = gs[i]
        pen = ReportLabPen(gs, Path(fillColor=colors.black, strokeWidth=1))
        g.draw(pen)
        #设置图片高度和宽度
        w = 1024
        h = 1024
        g = Group(pen.path)
        g.translate(0, 0)
        d = Drawing(w, h)
        d.add(g)
        imageFile = imagePath + "/" + i + ".png"
        renderPM.drawToFile(d, imageFile, fmt)

        #图片数字ocr识别
        img_bytes=open(imageFile,'rb').read()
        sdk = muggle_ocr.SDK(model_type=muggle_ocr.ModelType.OCR)
        text_number = sdk.predict(image_bytes=img_bytes)

        #修正识别错误
        if text_number=='O':
            text_number=0
        if text_number=='G':
            text_number=6
        if text_number == 'B':
            text_number = 8

        i=i.replace('uni','&#x')
        map_dict[i]=str(text_number)
    # print(map_dict)
    return map_dict

if __name__=="__main__":


    headers = {
        'Proxy-Connection': 'keep-alive',
        'Accept': 'application/json, text/javascript, */*; q=0.01',
        'User-Agent': 'yuanrenxue.project',
        'X-Requested-With': 'XMLHttpRequest',
        'Referer': 'http://match.yuanrenxue.com/match/7',
        'Accept-Language': 'zh-CN,zh;q=0.9',
    }
    name = ['极镀ギ紬荕', '爷灬霸气傀儡', '梦战苍穹', '傲世哥', 'мaη肆風聲', '一刀メ隔世', '横刀メ绝杀', 'Q不死你R死你', '魔帝殤邪', '封刀不再战', '倾城孤狼', '戎马江湖',
            '狂得像风', '影之哀伤', '謸氕づ独尊', '傲视狂杀', '追风之梦', '枭雄在世', '傲视之巅', '黑夜刺客', '占你心为王', '爷来取你狗命', '御风踏血', '凫矢暮城', '孤影メ残刀',
            '野区霸王', '噬血啸月', '风逝无迹', '帅的睡不着', '血色杀戮者', '冷视天下', '帅出新高度', '風狆瑬蒗', '灵魂禁锢', 'ヤ地狱篮枫ゞ', '溅血メ破天', '剑尊メ杀戮',
            '塞外う飛龍', '哥‘K纯帅', '逆風祈雨', '恣意踏江山', '望断、天涯路', '地獄惡灵', '疯狂メ孽杀', '寂月灭影', '骚年霸称帝王', '狂杀メ无赦', '死灵的哀伤', '撩妹界扛把子',
            '霸刀☆藐视天下', '潇洒又能打', '狂卩龙灬巅丷峰', '羁旅天涯.', '南宫沐风', '风恋绝尘', '剑下孤魂', '一蓑烟雨', '领域★倾战', '威龙丶断魂神狙', '辉煌战绩', '屎来运赚',
            '伱、Bu够档次', '九音引魂箫', '骨子里的傲气', '霸海断长空', '没枪也很狂', '死魂★之灵']
    player_dict={}
    for i in range(1,6):
        page=i
        params = (
            ('page', str(page)),
        )

        response = requests.get('http://match.yuanrenxue.com/api/match/7', headers=headers, params=params)
        data=response.json()
        woff=data['woff']
        valuelist = data['data']

        #解析加密后字体与数字的映射关系
        map_dict=ttfToImageNum(fontfile=base64.b64decode(woff.encode()), imagePath="D:\\")


        for n,v in enumerate(valuelist):
            #解析得到胜点值
            win_point=v['value'].split(' ')
            del win_point[-1]
            real_win_point=[map_dict[num] for num in win_point]
            str_=''
            real_win_point=str_.join(real_win_point)
            print(real_win_point)

            #将玩家与胜点值关联
            player=name[(n+1) + (page - 1) * 10]

            player_dict[player]=int(real_win_point)


    for key,value in player_dict.items():
        print(key,value)

生成图片过程中,得到的图片样式类似这样:
在这里插入图片描述
ocr过程如下,需要注意的是由于生成的数字本身形状不是很标准,所以识别过程中会出现类似把0识别成字母O,8识别成字母B这种情况,需要手动做一下替换。此外ocr识别的过程还是稍有些慢,如果字符数比较多久很好时了,所以不适用于生产环境,也许有更好的识别库日后可以试试。
在这里插入图片描述
最终输出的结果如下,通过跟原始网页对比可以看出玩家名字与胜点值都一一对应上了,且并无错误。
在这里插入图片描述

四、参考文献

  1. 破解字体加密解决思路
  2. 听说你爬回来的都是乱码?三个案例教你破解字体加密
  3. Python converts ttf to png
  4. Python爬虫:网页字体加密与解密实践
  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值