2023年河南省大学生国家安全知识竞赛网页技术解密

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

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

网页字体解密

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

定位原因

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

image

寻找资料 了解原理

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

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

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

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

字体库xml分析验证

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

1
2
3
4
5
from fontTools.ttLib import TTFont, TTCollection

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

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

1
2
3
4
5
6
<GlyphOrder>
<GlyphID id="0" name="glyph00000"/>
<GlyphID id="1" name="glyph00001"/>
<GlyphID id="2" name="glyph00002"/>
...
</GlyphOrder>

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

1
2
3
4
5
6
<cmap_format_4 platformID="0" platEncID="3" language="0">
...
<map code="0xb45c" name="glyph00157"/><!-- HANGUL SYLLABLE DULS -->
<map code="0xb460" name="glyph00002"/><!-- HANGUL SYLLABLE DUM -->
...
</cmap_format_4>

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<TTGlyph name="glyph00002" xMin="0" yMin="0" xMax="1077" yMax="1582">
<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:免费,有mac客户端,可以根据Unicode搜索
  • FontDrop:免费,网页端,没有搜索功能
  • 百度字体编辑器:免费,网页端,可以根据Unicode搜索(吐个槽,百度好像把这个产品下架了,这个链接还是github的)

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

1
2
3
4
5
6
7
8
9
10
11
[...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('未找到匹配的内容');
}
})

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

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

image

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

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

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

映射结果如下
image

解密

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

1
2
3
4
5
6
7
8
9
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)

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

JSON数据解密

确认接口

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

定位代码

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

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

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

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

image

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

结果如下
image

image

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

总结

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


2023年河南省大学生国家安全知识竞赛网页技术解密
https://xuemeijing.github.io/2023/05/17/河南大学生国家安全知识竞赛网页解密/
作者
Xue Meijing
发布于
2023年5月17日
许可协议