
前言
学一下utf8等编码方式。
相信大家都接触过乱码文本,就是锟斤拷之类的,这个就是由编码问题导致的。而编码问题的本质,就是用不同的方式读同一段字节,有些可能两个两个字节读,有些可能四个四个字节读。
在CTF里,遇到过UTF8 Overlong Encoding 绕过,也遇到过由于高字节丢弃产生的绕过,但由于对缺乏对编码的知识,所以不是很理解,因此现在来学习一下。
UTF8
UTF-8 是一种可变长度的 Unicode 编码方式,使用 1~4 个字节 表示一个字符,兼容 ASCII,并广泛用于互联网和文件存储。它的设计目标是节省空间(英文字符仅需 1 字节),同时支持所有 Unicode 字符(包括中文、emoji 等)。
UTF-8 的编码规则
UTF-8 的编码方式根据 Unicode 码点(Code Point)的范围,采用不同长度的字节序列:
Unicode 码点范围(十六进制) | 码点位数 | UTF-8 字节序列格式(二进制) | 字节数 | 示例 |
---|---|---|---|---|
U+0000 ~ U+007F |
7 bits | 0xxxxxxx |
1 | 'A' (0x41) |
U+0080 ~ U+07FF |
11 bits | 110xxxxx 10xxxxxx |
2 | 'é' (0xC3A9) |
U+0800 ~ U+FFFF |
16 bits | 1110xxxx 10xxxxxx 10xxxxxx |
3 | '你' (0xE4BDA0) |
U+10000 ~ U+10FFFF |
21 bits | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
4 | '𠀀' (0xF0A08080) |
编码步骤
- 确定码点范围 → 选择对应的 UTF-8 字节格式。
- 将 Unicode 码点转换为二进制,填充到 UTF-8 的模板中。
- 转换为十六进制字节,得到最终的 UTF-8 编码。
示例:'你'
的 UTF-8 编码
1. 获取 Unicode 码点
1 | ord('你') # 20320(十进制) → 十六进制 `U+4F60` |
2. 确定 UTF-8 编码格式
U+4F60
属于 U+0800 ~ U+FFFF
范围 → 3 字节,格式:
1 | 1110xxxx 10xxxxxx 10xxxxxx |
3. 将 U+4F60
转换为二进制
1 | 4F60 (十六进制) → 0100 1111 0110 0000 (二进制) |
4. 填充 UTF-8 模板
1 | 码点二进制:0100 111101 100000 |
5. 转换为十六进制
1 | 11100100 → 0xE4 |
最终 UTF-8 编码:\xE4\xBD\xA0
(3 字节)
Python 验证
1 | '你'.encode('utf-8') # b'\xe4\xbd\xa0' |
UTF-8 的特点
- 兼容 ASCII:
U+0000~U+007F
的字符(如'A'
)编码与 ASCII 完全相同(1 字节)。 - 无字节序问题:UTF-8 的字节顺序固定,无需 BOM(但某些编辑器可能添加
EF BB BF
作为标记)。 - 空间高效:
- 英文:1 字节(ASCII 兼容)。
- 欧洲字符:2 字节(如
'é'
)。 - 中文/日文:3 字节(如
'你'
)。 - 生僻字/emoji:4 字节(如
'𠀀'
)。
UTF16
UTF-16 是一种定长或变长的 Unicode 编码方式,使用 2 或 4 字节 表示一个字符。它的核心特点是:
- 基本多文种平面(BMP)字符(
U+0000
~U+FFFF
)用 2 字节 直接存储。 - 辅助平面字符(
U+10000
~U+10FFFF
)用 4 字节(代理对,Surrogate Pair)存储。
UTF-16 的编码规则
1. 基本多文种平面(BMP,2 字节)
范围:
U+0000
~U+FFFF
(不包括代理区U+D800
~U+DFFF
)。直接存储:码点数值直接转为 2 字节。
- 示例:
'A'
(U+0041
)→0x0041
(大端:00 41
,小端:41 00
)'你'
(U+4F60
)→0x4F60
(大端:4F 60
,小端:60 4F
)
辅助平面这个略看一下就行。
2. 辅助平面(4 字节,代理对)
- 范围:
U+10000
~U+10FFFF
(如 emoji'𠀀'
U+20000
)。 - 代理对计算:
- 码点减去
0x10000
,得到 20 位中间值。 - 高 10 位 +
0xD800
→ 高位代理(High Surrogate)。 - 低 10 位 +
0xDC00
→ 低位代理(Low Surrogate)。
- 码点减去
- 示例:
1 | # 计算 '𠀀'(U+20000)的 UTF-16 代理对 |
字节序(大端 vs 小端)
UTF-16 的 2 字节或 4 字节序列需要明确字节序:
大端(BE):高位字节在前(如
0x4F60
→4F 60
)。小端(LE):低位字节在前(如
0x4F60
→60 4F
)。BOM(字节顺序标记):
0xFEFF
(大端 BOM,存储为FE FF
)。0xFFFE
(小端 BOM,存储为FF FE
)。
Python 示例
这里遇到过一个坑,python如果只指定utf16,输出是带BOM的,当时不知道,一直想不明白为什么会输出四个字节。
1 | # 默认 UTF-16(带 BOM,小端) |
UTF-16 的特点
- 定长与变长混合:
- BMP 字符:2 字节。
- 辅助平面字符:4 字节(代理对)。
- 字节序敏感:需明确大端或小端(或依赖 BOM)。
- 适用场景:
- Windows 系统内部(如
wchar_t
)。 - Java/C# 的字符串内存表示。
- Windows 系统内部(如
UTF-8 vs. UTF-16
特性 | UTF-8 | UTF-16 |
---|---|---|
最小字节数 | 1(ASCII) | 2(基本字符) |
最大字节数 | 4 | 4(代理对) |
字节序 | 无(单字节存储) | 需考虑大端/小端(BOM) |
适用场景 | 互联网、文本文件、数据库 | 操作系统内部(如 Windows/Java) |
总结
所有字符的Unicode码点是唯一的,可以通过hex(ord(str))的方式得到16进制格式的码点。根据得到的码点,套用不同的编码方式,就可以得到不同编码下的字符。
高位丢失情况
先看一个例子
1 | package solution; |
输出结果:
1 | 字符转十进制整数(Unicode码点的整数形式):20320 |
这里很好地展示了高位丢失的情况。因为byte只有八个字节,而char有16个字节(char 在Java中本质上是一个16位的Unicode字符)。
这个性质有时候可以用来绕过一些过滤。比如D^3CTF25的d3jtar那题。
UTF8 Overlong Encoding
根据前面学习过的UTF8编码,我们知道,’A’对应的码点是41,转换成二进制就是0010 0001,然后根据0xxxxxxx的方式补位,得到的utf8编码就是0010 0001,转换成16进制就是 41。但是,我们可以在码点前面补零,将A的码点变成0000 0010 0001,然后根据110xxxxx 10xxxxxx
补成两个字节。即11000000 10100001
转成16进制就是C0A1
这样就得到了’A’的UTF8 Overlong Encoding形式。
如果没有对UTF8编码进行校验的话,就可以尝试通过这种方法绕过,比如Java的原生反序列化。