编码方式学习
1diot9 Lv3

前言

学一下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)

编码步骤

  1. 确定码点范围 → 选择对应的 UTF-8 字节格式。
  2. 将 Unicode 码点转换为二进制,填充到 UTF-8 的模板中。
  3. 转换为十六进制字节,得到最终的 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
2
3
4
码点二进制:0100 111101 100000
填充模板:1110xxxx 10xxxxxx 10xxxxxx
结果:
1110**0100** 10**111101** 10**100000**

5. 转换为十六进制

1
2
3
11100100 → 0xE4
10111101 → 0xBD
10100000 → 0xA0

最终 UTF-8 编码:\xE4\xBD\xA0(3 字节)

Python 验证

1
'你'.encode('utf-8')  # b'\xe4\xbd\xa0'

UTF-8 的特点

  1. 兼容 ASCIIU+0000~U+007F 的字符(如 'A')编码与 ASCII 完全相同(1 字节)。
  2. 无字节序问题:UTF-8 的字节顺序固定,无需 BOM(但某些编辑器可能添加 EF BB BF 作为标记)。
  3. 空间高效
    • 英文: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)。
  • 代理对计算
    1. 码点减去 0x10000,得到 20 位中间值。
    2. 高 10 位 + 0xD800高位代理(High Surrogate)
    3. 低 10 位 + 0xDC00低位代理(Low Surrogate)
    • 示例:
1
2
3
4
5
6
# 计算 '𠀀'(U+20000)的 UTF-16 代理对
code_point = 0x20000
temp = code_point - 0x10000 # 0x10000
high_surrogate = (temp >> 10) + 0xD800 # 0xD840
low_surrogate = (temp & 0x3FF) + 0xDC00 # 0xDC00
# 最终代理对:0xD840 0xDC00

字节序(大端 vs 小端)

UTF-16 的 2 字节或 4 字节序列需要明确字节序:

  • 大端(BE):高位字节在前(如 0x4F604F 60)。

  • 小端(LE):低位字节在前(如 0x4F6060 4F)。

  • BOM(字节顺序标记)

    • 0xFEFF(大端 BOM,存储为 FE FF)。
    • 0xFFFE(小端 BOM,存储为 FF FE)。

Python 示例

这里遇到过一个坑,python如果只指定utf16,输出是带BOM的,当时不知道,一直想不明白为什么会输出四个字节。

1
2
3
4
5
6
7
8
9
10
11
# 默认 UTF-16(带 BOM,小端)
print('你'.encode('utf-16')) # b'\xff\xfe\x60\x4f'(BOM + 小端)

# 显式指定大端(无 BOM)
print('你'.encode('utf-16be')) # b'\x4f\x60'

# 显式指定小端(无 BOM)
print('你'.encode('utf-16le')) # b'\x60\x4f'

# 辅助平面字符 '𠀀'(U+20000)
print('𠀀'.encode('utf-16be')) # b'\xd8\x40\xdc\x00'(代理对)

UTF-16 的特点

  1. 定长与变长混合
    • BMP 字符:2 字节。
    • 辅助平面字符:4 字节(代理对)。
  1. 字节序敏感:需明确大端或小端(或依赖 BOM)。
  2. 适用场景
    • Windows 系统内部(如 wchar_t)。
    • Java/C# 的字符串内存表示。

UTF-8 vs. UTF-16

特性 UTF-8 UTF-16
最小字节数 1(ASCII) 2(基本字符)
最大字节数 4 4(代理对)
字节序 无(单字节存储) 需考虑大端/小端(BOM)
适用场景 互联网、文本文件、数据库 操作系统内部(如 Windows/Java)

总结

所有字符的Unicode码点是唯一的,可以通过hex(ord(str))的方式得到16进制格式的码点。根据得到的码点,套用不同的编码方式,就可以得到不同编码下的字符。

高位丢失情况

先看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package solution;

public class tmp {
public static void main(String[] args) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("你");
char c = stringBuffer.charAt(0);
System.out.println("字符转十进制整数(Unicode码点的整数形式):"+(int) c);
String format = String.format("dec转hex:%04x",(int) c);
System.out.println(format);
byte c1 = (byte) c;
System.out.println("转成byte类型(高位丢失,4f60剩60,60转dec就是96):"+c1);
System.out.println((char) c1);
byte i = 127; //byte的范围是-128——127,用的补码表示法
}
}

输出结果:

1
2
3
4
字符转十进制整数(Unicode码点的整数形式):20320
dec转hex:4f60
转成byte类型(高位丢失,4f60剩60,60转dec就是96):96
`

这里很好地展示了高位丢失的情况。因为byte只有八个字节,而char有16个字节(char 在Java中本质上是一个16位的Unicode字符)。

这个性质有时候可以用来绕过一些过滤。比如D^3CTF25的d3jtar那题。

D3CTF-d3jtar | 1diot9’s Blog

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的原生反序列化。

参考

UTF-8 Overlong Encoding导致的安全问题 | 离别歌

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 22.8k 访客数 访问量