-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
猫眼网站上几乎所有重要的数据(数字),都是通过加载字体集,然后按unicode去映射展示。因为每次页面重新加载,字符集就会同时刷新,所以无法通过事先记录数字映射表的方式来进行解析。
解决方案
我们可以用截图、图像识别的方式去识别数字。但是这里,我们使用更加“地道”的方式进行hack,也就是通过分析woff文件找到映射表的方式进行解析数字。
WOFF介绍
WOFF(Web Open Font Format),一种网页所采用的字体格式标准。WOFF本质上是包含了基于SFNT的字体(如TrueType、OpenType或其他开放字体格式),且这些字体均经过WOFF的编码工具压缩,以便嵌入网页中。这个字体格式使用zlib压缩,文件大小一般比TTF小40%。
WOFF文件格式
WOFF Header
类型 | 字段 | 说明 |
---|---|---|
uint32 | signature | 固定值,0x774f4646 'wOFF' |
uint32 | flavor | 源字体文件"sfnt版本", 0x00010000代表TrueType, 0x4f54544f代表了CFF |
uint16 | length | 字体表(font tables)的个数 |
uint16 | reserved | 保留字段,必须为0 |
uint32 | totalSfntSize | 未压缩的字体数据大小 |
uint16 | majorVersion | major version of the WOFF font |
uint16 | minorVersion | minorversion of the WOFF font |
uint16 | metaOffset | metadata的偏移值,从WOFF文件头开始计算。如果没有,置为0。 |
uint32 | metaLength | metadata的大小(压缩后)。如果没有metadata,置为0。 |
uint32 | metaOrigLength | metadata的大小(未压缩)。如果没有metadata,置为0。 |
uint32 | privOffset | 私有数据的偏移,同 metaOffset |
uint32 | privLength | 私有数据的大小,同metaLength |
table
WOFF文件内容紧跟在header后面的就是font table了。Header里的length定义了font table的个数。每个font table含20个字节,分别由5个4字节字段组成:
类型 | 字段 | 说明 |
---|---|---|
uint32 | tag | table标识 |
uint32 | offset | 地址偏移量,后面会通过这个字段来获取字体压缩数据 |
uint32 | compLength | 字体文件大小,配合上面上的offset可以获取到压缩数据 |
uint32 | origLength | 源文件大小,通过对比可以判断是否zlib压缩过 |
uint32 | origChecksum | checksum |
步骤
- 先读取woff文件
- 按照woff的header进行分析
- 获取到font table的压缩内容
- 解压缩font table
- 分析压缩后的内容,获取包含uni的content,就可以获取字符集的映射表
go实战
语言只是个工具,这里有Go来演示。先定义WOFF的header和font table:
type WoffTable struct {
tag uint32
offset uint32
compLength uint32
origLength uint32
origChecksum uint32
compContent []byte
origContent []byte
}
type Woff struct {
signature uint32
flavor uint32
length uint32
numTables uint16
reserved uint16
totalSfntSize uint32
majorVersion uint16
minorVersion uint16
metaOffset uint32 // 可选项
metaLength uint32 // 可选项
metaOrigLength uint32 // 可选项
privOffset uint32 // 可选项
privLength uint32 // 可选项
tables []*WoffTable
}
然后将WOFF文件解析成对象:
func (woff *Woff) DecodeFromBytes(data []byte) error {
woff.signature = binary.BigEndian.Uint32(data[0:4])
woff.flavor = binary.BigEndian.Uint32(data[4:8])
woff.length = binary.BigEndian.Uint32(data[8:12])
woff.numTables = binary.BigEndian.Uint16(data[12:14])
woff.reserved = binary.BigEndian.Uint16(data[14:16])
woff.totalSfntSize = binary.BigEndian.Uint32(data[16:20])
woff.majorVersion = binary.BigEndian.Uint16(data[20:22])
woff.minorVersion = binary.BigEndian.Uint16(data[22:24])
woff.metaOffset = binary.BigEndian.Uint32(data[24:28])
woff.metaLength = binary.BigEndian.Uint32(data[28:32])
woff.metaOrigLength = binary.BigEndian.Uint32(data[32:36])
woff.privOffset = binary.BigEndian.Uint32(data[36:40])
woff.privLength = binary.BigEndian.Uint32(data[40:44])
for i := 0; i < int(woff.numTables); i++ {
sign := 44 + i * 20
t := &WoffTable{
tag: binary.BigEndian.Uint32(data[sign:sign + 4]),
offset: binary.BigEndian.Uint32(data[sign + 4:sign + 8]),
compLength: binary.BigEndian.Uint32(data[sign + 8:sign + 12]),
origLength: binary.BigEndian.Uint32(data[sign + 12:sign + 16]),
origChecksum: binary.BigEndian.Uint32(data[sign + 16:sign + 20]),
}
t.compContent = data[t.offset:t.offset + t.compLength + 1]
if t.compLength == t.origLength {
t.origContent = t.compContent
} else {
w, err := zlib.NewReader(bytes.NewReader(t.compContent))
if err != nil {
log.Error(err)
}
o, err := ioutil.ReadAll(w)
if err != nil {
log.Error(err)
}
t.origContent = o
}
fmt.Println(hex.Dump(t.origContent)) //这里打印出压缩后的字符集供大家查阅
woff.tables = append(woff.tables, t)
}
return nil
}
为了方便大家学习和查看,随便贴一张猫眼WOFF文件的Hex图,大家可以根据上面提到的结构对号入座,看看是否能手工找到length和table的位置:
最后,我们查看打印出来的结果,其中一个content里包含了字符表,分别对应了0-9的顺序: