首先我们先了解一下什么是字体加密:

字体加密是一种反爬策略,一般字体加密分为静态加密、动态字体加密,很好区分,我们只需要查看返回的字体文件是否是变换的就行。使得网页源码中的数据与显示内容不同,当我们抓到数据时则会与网页所看到的不一样,但是我们打开控制面板会发现相关加密字体标签的内容和页面显示的内容不一样,列如:

这里我们可以明确看到显示不一样,这是为什么呢?

原因分析:

这是因为在控制台看到的字体为电脑默认的字体,将unicode解码为默认的字符,而我们可以看到这个标签上有一个class类为xuetangx-com-encrypted-font,我们查看这个类的css样式:

我们可以看到font-family为exam-data-decrypt-font,而这个自定义的字体就是关键了,由于这个自定义的unicode和标准的不一样,所以渲染出来的字就会不一样。我们去查找这个字体如何来的可以看到:

这里可以看到请求了一个ttf的字体文件,我们将其下载下来,如果想要详细查看这个字体文件的渲染我们可以使用FontCreator查看:

这里我们可以看到上面的unicode对应下面渲染的字体,这是就有小伙伴提问,主播主播这不就出来了吗,拿到这个字对比unicode进行替换不就好了吗,请注意这个字并非真正的“字”,它只是渲染出来的,你可以理解它为一张图片,它是根据字体文件下,x,y坐标渲染出来的,类似于画出来的一张画,类似canves,所以他不是你输入的string字符串,所以我们要创建一个map来映射真正字体和unicode的关系,从而就能解密了。如何映射这是关键,这是有人会问主播主播一个个手写太难了,有没有什么简单的方法,主播:有的有的。这就要使用非常好用的ocr识别库:ddddocr,首先我们分析完原理,直接pycharm启动,需要安装操作字体依赖库fontTools,ddddocr

开始操作:

pip install fontTools

pip install ddddocr

 安装完后我们打开我们下载好的ttf字体文件并解析为xml:

    # 解析字体文件
    font = TTFont('font.ttf')  # 打开文件
    cmap = font.getBestCmap()
    font.saveXML('font.xml')  # 保存存为xml

我们打开xml:

这里我们能看到TTGlyph标签的name为uni957F,contour下就是字体渲染的坐标,然后浏览器引擎根据x,y坐标进行渲染,查看完成后我们对其手动渲染成img,再使用ddddocr进行识别:

# 拆解ttf文件,保存为单个字体图片:保存至 imgs 文件夹
def font_split_single_img():
    # 解析字体文件
    font = TTFont('font.ttf')  # 打开文件
    cmap = font.getBestCmap()
    font.saveXML('font.xml')  # 保存存为xml
    index = 1
    for n, v in cmap.items():
        d = v
        glyph = font.getGlyphSet()[d]  # 通过字形名称选择某一字形对象
        pen = FreeTypePen(None)  # 实例化Pen子类
        glyph.draw(pen)  # “画”出字形轮廓
        # pen.show()    # 显示
        b = pen.array()
        print(index, '/', len(cmap), '~~~', glyph)
        plt.figure()
        plt.imshow(b)
        plt.axis('off')  # 禁用坐标轴
        os.makedirs('imgs', exist_ok=True)
        plt.savefig('./imgs/{0}.jpg'.format(d))
        # plt.show()    # 显示
        plt.clf()
        plt.cla()
        plt.close()

然后我们可以查看渲染完成后的jpg:

接下来我们使用ocr进行识别:

# 用 ddddocr 识别图片文字,保存至 imgs_copy_word 文件夹
def ocrWords():
    ocr = ddddocr.DdddOcr(beta=False, show_ad=False)  
    word_map = {}
    for parent, dirnames, filenames in os.walk('imgs'):  
        for filename in filenames:
            k = filename.split('.')[0]
            currentPath = os.path.join(parent, filename)
            with open(currentPath, 'rb') as f:
                image = f.read()
            res = ocr.classification(image)
            if len(res) == 0:
                res = '未找到'
            if len(res) > 1:
                res = res[0]
            print(k, 'res:', res)
            os.makedirs('imgs_copy_word', exist_ok=True)
            d = f'{k}__{res}.jpg'
            img = Image.open(currentPath)
            img.save('imgs_copy_word/%s' % d)
            word_map[k] = res

识别结果:

识别可能不准确,可以换其他ocr识别库,最后进行对比,或者人工修改,然后我们创建unicode映射的字符:

# 根据识别后的名称,提取结果,并保存为 .json文件
def readImagName(imagesPath='imgs_copy_word', saveJsonName='ocr_dddd.json'):
    word_map = {}
    for parent, dirnames, filenames in os.walk(imagesPath): 
        for filename in filenames:
            k = filename.split('.')[0]
            res = k.split('__')[1]
            word_map[k.split('__')[0]] = res
    if word_map:
        with open(saveJsonName, 'w', encoding='utf-8') as f:
            f.write(json.dumps(word_map, ensure_ascii=False))

然后我们就可以开始使用这个映射开始解密:

def fontDecrypte(text):
    map = decrypte()
    font = TTFont('font2.ttf')
    cmap = font.getBestCmap()
    words = []
    for itm in text:
        try:
            d = cmap[ord(itm)]
            word = map[d]
            # print(word)
            words.append(word)
        except:
            # d = -1
            word = -1
            words.append(itm.strip())

    return ''.join(words)

最后就能得到解密后的字符串。

动态字体加密:

以为结束了?这只是作者最开始提到的静态解密的方法,还有另外一种动态加密每次返回不同的字体文件,难不成每次都识别?显然这是不可能的。再说别人哪里里有这么多字体,这当中肯定有蹊跷,我们将两次的ttf文件解析后可以发现unicode顺序发生变化,也就是说字形并没有发生变化,而是绘制坐标对应的unicode发生变化,当中字符的x,y坐标这些是没有变化的,这样一来我们又可以开始操作了,我们将我们最开始解码的字体文件和解码后的json映射的进行保存,而这个进行模板对别,至于怎么对比,我们总不能x,y参数挨个对比吧,这显然不显示,那有没有好办法呢?

这是我们要祭出hash大法,我们对字体绘制内容转哈希值,将两份字体文件的绘制坐标转哈希并且和unicode分别创建映射关系,最后我们对比hash,如果hash值相同,自然绘制内容就相同,然后根据两次的映射,再根据第一次的解码内容映射,就能解码了,

# 解析ttf,并将coordinates进行hash转换
def parse_ttf_with_hash(font_path):
    font = TTFont(font_path)
    glyph_data = {}
    for idx, glyph_name in enumerate(font.getGlyphOrder()):
        try:
            array = font["glyf"][glyph_name].coordinates.array
            hash = hashlib.sha256(array).hexdigest()
            glyph_data[glyph_name] = hash
        except:
            pass
    return glyph_data

接下来对hash值进行对比:

def decrypte():
    first = parse_ttf_with_hash('font.ttf')
    second = parse_ttf_with_hash('font2.ttf')
    new = {v: k for k, v in first.items()}
    findList = {}
    for k,v in second.items():
        find = new[v]
        findList[find] = k
    result = {}
    for k, v in findList.items():
        try:
            word = words_map[k]
            result[v] = word
        except:
            pass
    return result

最后根据对比结果得到的map进行解密即可

def fontDecrypte(text):
    map = decrypte()
    font = TTFont('font2.ttf')
    cmap = font.getBestCmap()
    words = []
    for itm in text:
        try:
            d = cmap[ord(itm)]
            word = map[d]
            # print(word)
            words.append(word)
        except:
            word = -1
            words.append(itm.strip())

    return ''.join(words)

结束:

到此动态的字体解密也已经完成了,不过也别高兴太早,如果厂商在字体的绘制里面加上随机数,随机修改一点点,这样就失效了,就必须得换换精准的ocr,为什么推荐ddddocr就是因为可以在原版的基础上自己训练,提升准确度,这时就可以自己训练提升准确度了。

Logo

加入社区!打开量化的大门,首批课程上线啦!

更多推荐