Captain不是船长

V1

2023/05/18阅读:15主题:橙心

财务说

本文由 简悦 SimpRead 转码, 原文地址 juejin.cn

昨天一个后端朋友给我发了一张截图,问我这个 2023 年河南省大学生国家安全知识竞赛网页[1] f12 内 DOM 节点显示韩文,但是页面显示是中文,复制出来的结果是韩文,问我怎么回事。我也是第一次见这种情况,便研究了起来。本文主要记录我调试和发现答案的流程,涉及两部分,一个是该网页字体解密,一个是 JSON 数据 AES 解密

刚写完这篇文章,刷新就打不开这个网页了,发现竞赛学习时间过了🥲

网页字体解密

注意:以下截图均为 github 链接,不用 vpn 很大可能打不开

定位原因

通过调试发现,这类字体都有. secret 类名,该类名指定使用一个名叫 ddjdt 的 font-family,去掉该 font-family 属性页面正常显示为韩文,因此确定是字体原因

寻找资料 了解原理

在网络上搜索字体加密有很多文档,大部分都是拿猫眼的网页当做例子,但是由于每个网站的加密方式不一样,所以那些教程不一定适用于该网页,但是整体的流程大概明白了。

简单来说字体加密主要是为了反爬虫和防复制,比如猫眼这些网站就是为了防止别人爬数据,对于这个知识竞赛网页主要是为了防止复制搜答案。

对于汉字 来说,他可以有宋体、黑体、微软雅黑等不同的字形展示,但是他们对应的 Unicode 编码是同一个,同理也可以创造一个新的字体库叫 animal,把 对应的字形描绘成老虎,这样浏览器在碰到 的 Unicode 时,就会把它渲染成老虎的样式。字体库内部使用 TTGlyph 来存储字形轮廓信息,它和 SVG 一样都是矢量图形格式。

所以该网页 DOM 内部是韩文,这些韩文的 Unicode 编码对应的字形实际上是微软雅黑或者其他字体库的汉字字形 TTGlyph,导致页面渲染出来是汉字。

字体库 xml 分析验证

打开 css 发现该字体使用的是 2023gjaqzsjs.haedu.cn/fonts/wryh2…[2] 这个字体文件,下载文件,然后用 python 把 ttf 转换成 xml 文件看到字体库内部结构,代码如下

from fontTools.ttLib import TTFont, TTCollection

# 某些字体库是集合,TTFont会报错,需要用TTCollection
font = TTFont("wryh2.ttf")
font.saveXML("wryh2.xml")


解析后的 xml 文件首先看到的是 GlyphOrder,内部有很多 GlyphID

<GlyphOrder>
  <GlyphID id="0" />
  <GlyphID id="1" />
  <GlyphID id="2" />
  ...
</GlyphOrder>


搜索 glyph00002,可以看到对应一个 Unicode 编码 0xb460

<cmap_format_4 platformID="0" platEncID="3" language="0">
  ...
  <map code="0xb45c" /><!-- HANGUL SYLLABLE DULS -->
  <map code="0xb460" /><!-- HANGUL SYLLABLE DUM -->
  ...
</cmap_format_4>


转换 0xb460 打印出来是韩文,也证实了上面的说法

String.fromCharCode('0xb460') // '둠'


同时与 glyph00002 相关的还有这个 TTGlyph,也就是它的字形文件了

<TTGlyph >
  <contour>
    <pt x="1077" y="0" on="1"/>
    <pt x="189" y="0" on="1"/>
    <pt x="189" y="169" on="1"/>
    <pt x="536" y="169" on="1"/>
    <pt x="536" y="1345" on="1"/>
    <pt x="180" y="1242" on="1"/>
    <pt x="180" y="1422" on="1"/>
    <pt x="731" y="1582" on="1"/>
    <pt x="731" y="169" on="1"/>
    <pt x="1077" y="169" on="1"/>
  </contour>
  <instructions/>
</TTGlyph>


操作、预览字体库

在查找的文档中,很多人说可以用 FontCreator 加载字体库,但是它只适用于 windows,并且有试用期,故放弃。问了 chatGPT 后说可以用 BirdFont 和 FontDrop,

  • BirdFont[3]:免费,有 mac 客户端,可以根据 Unicode 搜索
  • FontDrop[4]:免费,网页端,没有搜索功能
  • 百度字体编辑器[5]:免费,网页端,可以根据 Unicode 搜索 (吐个槽,百度好像把这个产品下架了,这个链接还是 github 的)

我使用的是 FontDrop,很方便,把字体文件拖进来就能解析,可以看到字体名称和字体作者等信息 下面是字体展示,鼠标滑过会展示该字形的 Unicode 分析 DOM 结构,获取全部字形的 Unicode

[...document.querySelector('#glyph-list-end').children].forEach(item => {
  const regex = /Unicode:\s+(\w+)/;
  const match = item.children[0].textContent.match(regex);
  
  if (match) {
    const unicode = match[1];
    console.log(unicode); // 输出: BFEE
  } else {
    console.log('未找到匹配的内容');
  }
})


执行结果如下 emmmm。竟然还有未找到匹配的内容,调试后发现该字形的 Unicode 是鼠标滑过字形后动态插入到 DOM 的,鼠标没有滑过的字形就是未找到 简单,给全部字形节点加 mouseover 事件,手动触发,再获取全部 Unicode 就正常了

[...document.querySelector('#glyph-list-end').children].forEach(item => {
  var event = new MouseEvent('mouseover');
  item.dispatchEvent(event);
})


获取 Unicode 和字形的映射,创建密码表

没办法,这里只能手动建立映射了,或者有 OCR 接口的话可以优化一下

var unicodeArr = ["none","BFEE","B460","BA67"...]
var decryptArr = ["","0","1","2"...]
var unicodeMap = {}
unicodeArr.forEach((item, index) => {
  unicodeMap[item] = decryptArr[index]
})
console.log(unicodeMap)


映射结果如下

解密

复制一句密文来测试下效果

var encryptStr = '욜춼헵풱쐋뒈킧젋쉍쯃总쉫욜춼헵풱칌,以(      )为宗旨。'
var decryptSte = encryptStr.split('')
  .map(item => {
    const HEX = item.charCodeAt().toString(16).toUpperCase()
    return unicodeMap[HEX] || item
  })
  .join('')
console.log('encryptStr', encryptStr)
console.log('decryptSte', decryptSte)


解密效果如下 看起来解密效果还可以,一部分字体没有解密出来是因为字体文件的问题,多个 Unicode 对应同一个字体文件 TTGlyph,用上面说的百度字体编辑器可以看到 这个字形文件有两个 Unicode,把这些特殊 Unicode 手动加到之前的密码表就好了

JSON 数据解密

确认接口

这些加密字体很明显是后端接口传过来的,但是这个分页接口返回的竟然是加密后的字符串,真有你的

定位代码

在返回的 js 文件内搜索这个接口地址 2023gjaqzsjs.haedu.cn 和 /json/,找到相关代码,看到返回的加密字符串被 $decrypt 方法解密

搜索 $decrypt 方法发现是用 AES 解密

通过其他的一些变量名我猜用的很可能是 crypto-js 这个库,但是看 crypto-js 源码的 decrypt 函数第三个参数是一个配置对象啊,这里怎么是个字符串,难道是用的其他解密库?我在这里卡了半天,打断点才发现是作者创建了一个 AES 对象,把 crypto-js 的 decrypt 又封装了一下

这里的 o 方法就是把 16 进制转为 10 进制 index,然后获取 t[index], 比如 r["AES"][o("0xa")] 实际就是 r["AES"]["decrypt"]

引入 crypto-js 的 cdn 文件,然后复制相关解密变量到本地测试

结果如下

和知识竞赛题目内的加密字体一样,JSON 解密完成

总结

学习了字体加密解密的一些知识,加深了对字体文件的理解。而且在我以往的印象里反爬虫就是 ip 限制,请求频率限制,没想到还有 JSON 加密和字体加密。但是目前还有个问题:后端怎么生成的加密字体文件和字体 JSON,通过密码表还是加密规则,这个字体文件只加密了一部分常用字

参考资料

[1]

https://2023gjaqzsjs.haedu.cn/gajs/#/questionBank: https://link.juejin.cn?target=https%3A%2F%2F2023gjaqzsjs.haedu.cn%2Fgajs%2F%23%2FquestionBank

[2]

https://2023gjaqzsjs.haedu.cn/fonts/wryh2.ttf: https://link.juejin.cn?target=https%3A%2F%2F2023gjaqzsjs.haedu.cn%2Ffonts%2Fwryh2.ttf

[3]

https://birdfont.org/: https://link.juejin.cn?target=https%3A%2F%2Fbirdfont.org%2F

[4]

https://fontdrop.info/: https://link.juejin.cn?target=https%3A%2F%2Ffontdrop.info%2F

[5]

https://kekee000.github.io/fonteditor/: https://link.juejin.cn?target=https%3A%2F%2Fkekee000.github.io%2Ffonteditor%2F

分类:

人工智能

标签:

深度学习

作者介绍

Captain不是船长
V1