前言
学一下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的原生反序列化。