从学习通复制文字乱码看前端版权保护 写在前面 起因是在学习通答题的时候突然发现复制出来的内容是乱码,具体示例如下
1 2 通过修改HTTP headers 中的哪个键值可以伪造来源网址 嶲嶱修改HTTP headers 中的哪嶮嶭值嶰以嶯造来嶬网址
分析过程 通过测试,发现仅 章节检测
中存在文字复制乱码,其他答题页面不存在,我们直接通过 devtools
定位到字体文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @font-face { font-family :'font-cxsecret' ; src :url ('data:application/font-ttf;charset=utf-8;base64,AAEAAAAMAIAAAwBAQkFTRRuOGNgAAFWcAAA...' ) format ('truetype' ); }.font-cxsecret ,.font-cxsecret p ,.font-cxsecret div ,.font-cxsecret i ,.font-cxsecret em ,.font-cxsecret b ,.font-cxsecret strong ,.font-cxsecret a ,.font-cxsecret font ,.font-cxsecret span ,.font-cxsecret pre,.font-cxsecret code { font-family : 'font-cxsecret' !important ; }
将字体文件存储后,我们发现貌似仅仅是字形名称进行了更改
1 2 3 4 >>> f"uni{ord ('下' ):X} " 'uni4E0B' >>> chr (int ('uni5DD5' [3 :], 16 ))'巕'
根据代码所示, 下
字的编码应为 uni4E0B
,而从学习通下载的字体中 下
字的编码为 uni5DD5
,实际文字应为 巕
。我这里使用的字体查看器是 FontLab
,你也可以使用 在线字体编辑器
我们通过获取到的学习通字体信息,检索并下载原版 思源黑体 字体文件
1 SourceHanSansCN-Normal · Regular · Version 1.000;PS 1;hotconv 1.0.78;makeotf.lib2.5.61930
然后通过 TTFont
库分别获取 学习通字体
和 思源黑体
的字形数据
1 2 3 4 5 from fontTools.ttLib import TTFont font = TTFont('Source Han Sans CN Normal.ttf' ) font.saveXML('学习通.xml' )
将获取到的字形数据进行对比,我们发现除了 name
不同外,字形数据是一致的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 # 学习通字体<TTGlyph name ="uni5DD5" xMin ="59" yMin ="-72" xMax ="942" yMax ="762" > <contour > <pt x ="515" y ="695" on ="1" /> <pt x ="515" y ="517" on ="1" /> ... <pt x ="942" y ="762" on ="1" /> <pt x ="942" y ="695" on ="1" /> </contour > <instructions /> </TTGlyph > # 原版思源黑体<TTGlyph name ="uni4E0B" xMin ="59" yMin ="-72" xMax ="942" yMax ="762" > <contour > <pt x ="515" y ="695" on ="1" /> <pt x ="515" y ="517" on ="1" /> ... <pt x ="942" y ="762" on ="1" /> <pt x ="942" y ="695" on ="1" /> </contour > <instructions /> </TTGlyph >
字体解密 那么到此我们就已经理顺了学习通字体的加密思路,简单来说就是以下几个步骤:
更改字形名称 :学习通将原版思源黑体的字形名称进行了更改,例如,下
字的编码从 uni4E0B
变为 uni5DD5
。这种更改使得直接查看字体文件时,无法直接对应到正确的字符。保持字形数据不变 :尽管字形名称发生了变化,但是字形数据并没有改变。这意味着,如果我们能够找到字形名称的映射关系,就可以正确地解析出字符。因此,要解密学习通的字体,我们需要做的就是找到字形名称的映射关系。这可以通过比较学习通字体和原版思源黑体的字形数据来实现。
但是经过一系列测试,发现有些坐井观天了,pt
里面的数据只有 横竖
相同,涉及到 撇捺
的字体,学习通对其进行了一些简单位移
具体如下图所示,左侧为原版字体,右侧为学习通字体
所以我们只能尝试忽略曲线,看看能不能达到预期效果
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 import jsonimport hashlibfrom fontTools.ttLib import TTFontfrom fontTools.pens.basePen import BasePenclass GlyphPen (BasePen ): def __init__ (self, glyphSet ): BasePen.__init__(self , glyphSet) self .points = [] def _moveTo (self, p ): self .points.append(("move" , p)) def _lineTo (self, p ): self .points.append(("line" , p)) def _curveToOne (self, p1, p2, p3 ): pass def get_points (self ): return self .points file_path = "学习通.ttf" font = TTFont(file_path) glyph_set = font.getGlyphSet() result = {}for name in glyph_set.keys(): pen = GlyphPen(glyph_set) glyph = glyph_set[name] glyph.draw(pen) points_str = json.dumps(pen.get_points()) md5 = hashlib.md5(points_str.encode()).hexdigest() result[name] = md5 print (json.dumps(result, indent=4 ))
此时,比对后发现又出现问题,比对仅仅成功了四分之一,譬如 以
、文
和 方
这类整个字体中撇捺占大部分情况下会匹配失败。
后来,又发现 xMin
、yMin
、xMax
和 yMax
貌似是唯一的,我们直接尝试获取这四个值
1 2 3 4 5 6 7 8 9 10 11 12 13 for name in glyph_set.keys(): if name.startswith("uni" ): glyph = glyph_set[name] pen = BoundsPen(glyph_set) glyph.draw(pen) bounds = pen.bounds try : md5_input = f"xMin={bounds[0 ]} , yMin={bounds[1 ]} , xMax={bounds[2 ]} , yMax={bounds[3 ]} " md5 = hashlib.md5(md5_input.encode('utf-8' )).hexdigest() result[name] = md5 except : pass
匹配结果相对比获取 字形轮廓
成功率提升较大,但也仅仅停留在 90%
左右 ,部分字体无法比对出正确字体。
原本以为找到映射关系即可,但不知道是学习通有意为之,还是前面找到的原版字体不对。
至此,整个分析过程以失败结束,虽然结局并不完美,但终究还是收获了一些前端版权保护的思路。
写在后面 或许,两种方法结合又或者通过 selenium
元素截图能达到更好的效果。
另外,附比对时用的代码一份
1 2 3 4 5 6 7 8 9 10 11 12 def compare_json (file1, file2 ): with open (file1, 'r' ) as f: data1 = json.load(f) with open (file2, 'r' ) as f: data2 = json.load(f) reverse_data1 = {v: k for k, v in data1.items()} for k, v in data2.items(): if v in reverse_data1: try : print (f"{chr (int (k[3 :], 16 )), k} : {chr (int (reverse_data1[v][3 :], 16 )), reverse_data1[v]} " ) except : print (f"{k} : {reverse_data1[v]} " )
最后,通过检索 Github 项目,发现 TellMeYourWish/chaoxing_solution_of_font_confusion 项目中提供了字体映射表,但目前并不知两个 pkl
文件从何而来。