Kryo-UTF8-Overlong-Encoding
字数 1617 2025-08-05 11:39:45
Kryo UTF-8 Overlong Encoding 混淆技术分析
前言
本文深入分析 Kryo 反序列化中的 UTF-8 Overlong Encoding 混淆技术,该技术灵感来源于 1ue 和 phith0n 师傅的研究成果。Kryo 作为 Java 的高效序列化框架,其反序列化过程同样存在可被混淆的环节。
Kryo 反序列化方法
Kryo 提供三种主要反序列化方法:
readObject- 需要指定 Class 对象readObjectOrNull- 需要指定 Class 对象,允许返回 nullreadClassAndObject- 从 IO 流中动态获取对象类型(主要研究对象)
UTF-8 模式下的 IO 流写入流程
关键写入方法分析
writeString 方法是关键入口,处理逻辑如下:
public void writeString(String value) throws KryoException {
if (value == null) {
writeByte(0x80); // 0 means null, bit 8 means UTF8.
return;
}
int charCount = value.length();
if (charCount == 0) {
writeByte(1 | 0x80); // 1 means empty string, bit 8 means UTF8.
return;
}
// ASCII 检测逻辑
boolean ascii = false;
if (charCount > 1 && charCount < 64) {
ascii = true;
for (int i = 0; i < charCount; i++) {
int c = value.charAt(i);
if (c > 127) {
ascii = false;
break;
}
}
}
// ASCII 处理路径
if (ascii) {
if (capacity - position < charCount)
writeAscii_slow(value, charCount);
else {
value.getBytes(0, charCount, buffer, position);
position += charCount;
}
buffer[position - 1] |= 0x80;
}
// UTF-8 处理路径
else {
writeUtf8Length(charCount + 1);
int charIndex = 0;
if (capacity - position >= charCount) {
// 尝试写入 8 位字符
byte[] buffer = this.buffer;
int position = this.position;
for (; charIndex < charCount; charIndex++) {
int c = value.charAt(charIndex);
if (c > 127) break;
buffer[position++] = (byte)c;
}
this.position = position;
}
if (charIndex < charCount)
writeString_slow(value, charCount, charIndex);
}
}
关键差异点
-
ASCII 模式:
- 直接写入字符串字节
- 最后一个字节设置 0x80 标志位
-
UTF-8 模式:
- 先调用
writeUtf8Length写入长度信息 - 处理非 ASCII 字符时调用
writeString_slow
- 先调用
UTF-8 模式下的 IO 流读取
反序列化调用栈
readClassAndObject
-> readClass
-> readClass
-> readName
-> readString
关键读取方法
readString 方法根据标志位判断读取模式:
public String readString() {
int available = require(1);
int position = this.position;
byte[] buffer = this.buffer;
int b = buffer[position++];
if ((b & 0x80) == 0x80) { // UTF-8
int length;
switch (b & 0x7F) {
case 0:
return null;
case 1:
this.position = position;
return "";
default:
length = b & 0x7F;
if (length > 0x7F) {
this.position = position;
length = readUtf8Length(length);
}
this.position = position;
return readUtf8(length - 1);
}
}
// ASCII 处理路径...
}
UTF-8 解码过程
readUtf8_slow 方法处理 Overlong Encoding:
private void readUtf8_slow(int charCount, int charIndex) {
char[] chars = this.chars;
byte[] buffer = this.buffer;
while (charIndex < charCount) {
if (position == limit) require(1);
int b = buffer[position++] & 0xFF;
switch (b >> 4) {
case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7:
chars[charIndex] = (char)b;
break;
case 12: case 13:
if (position == limit) require(1);
chars[charIndex] = (char)((b & 0x1F) << 6 | buffer[position++] & 0x3F);
break;
case 14:
require(2);
chars[charIndex] = (char)((b & 0x0F) << 12 |
(buffer[position++] & 0x3F) << 6 |
buffer[position++] & 0x3F);
break;
}
charIndex++;
}
}
可混淆属性分析
主要混淆点
- 类名字符串 - 通过
readString方法处理 - 字符串类型变量 - 同样通过
readString方法 - Class 类型变量 - 通过
readClass方法处理
其他可混淆类型
在 DefaultSerializers 中可混淆的类型包括:
StringBuffer和StringBuilderCharsetURL- 数组类型 (
DefaultArraySerializer) - Map 的键值对 (
MapSerializer)
混淆实现方案
基本实现方法
重写 Output 源码,强制所有字符串写入走 writeString_slow 路径:
// 修改 writeString 方法,强制使用 UTF-8 路径
public void writeString(String value) {
// 跳过 ASCII 检测,直接进入 UTF-8 处理
writeUtf8Length(value.length() + 1);
writeString_slow(value, value.length(), 0);
}
字节替换策略
在 writeString_slow 中,可以针对不同字节长度的字符进行替换:
- 1字节字符:
(byte)c - 2字节字符:
(0xC0 | (c >> 6)), (0x80 | (c & 0x3F)) - 3字节字符:
(0xE0 | (c >> 12)), (0x80 | ((c >> 6) & 0x3F)), (0x80 | (c & 0x3F))
混淆前后对比
混淆前 (ASCII 模式):
[类名字节直接存储]
混淆后 (UTF-8 模式):
[UTF-8 长度][Overlong 编码的类名字节]
进阶应用
Java 原生序列化集成
Kryo 支持 Java 原生序列化,可通过重写 JavaSerializer 的 write 方法实现原生序列化的混淆:
public void write(Kryo kryo, Output output, Object object) {
ObjectOutputStream objectStream = new ObjectOutputStream(output) {
// 重写 writeUTF 等方法实现 Overlong Encoding
};
objectStream.writeObject(object);
objectStream.flush();
}
ASCII 流到 UTF-8 流转换
实现 ASCII 格式序列化数据到 UTF-8 格式的转换:
- 读取原始 ASCII 流的最后一个字符(设置了 0x80 标志位)
- 计算字符串长度并写入
writeUtf8Length - 将剩余字符按 UTF-8 Overlong 编码重新写入
防御建议
- 输入验证:对反序列化的类名和字符串进行严格校验
- 长度检查:检测 UTF-8 编码的字符长度是否合理
- 编码规范化:对输入数据进行规范化处理
- 安全配置:限制 Kryo 反序列化的类范围
结语
Kryo 的 UTF-8 Overlong Encoding 混淆技术展示了反序列化过程中编码处理可能带来的安全问题。理解这些底层机制有助于开发更安全的序列化/反序列化实现。
参考
- 《探索 Java 反序列化绕 WAF 新姿势》
- phith0n 的 UTF-8 Overlong Encoding 研究