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 提供三种主要反序列化方法:

  1. readObject - 需要指定 Class 对象
  2. readObjectOrNull - 需要指定 Class 对象,允许返回 null
  3. readClassAndObject - 从 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);
    }
}

关键差异点

  1. ASCII 模式:

    • 直接写入字符串字节
    • 最后一个字节设置 0x80 标志位
  2. 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++;
    }
}

可混淆属性分析

主要混淆点

  1. 类名字符串 - 通过 readString 方法处理
  2. 字符串类型变量 - 同样通过 readString 方法
  3. Class 类型变量 - 通过 readClass 方法处理

其他可混淆类型

DefaultSerializers 中可混淆的类型包括:

  • StringBufferStringBuilder
  • Charset
  • URL
  • 数组类型 (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. 1字节字符:(byte)c
  2. 2字节字符:(0xC0 | (c >> 6)), (0x80 | (c & 0x3F))
  3. 3字节字符:(0xE0 | (c >> 12)), (0x80 | ((c >> 6) & 0x3F)), (0x80 | (c & 0x3F))

混淆前后对比

混淆前 (ASCII 模式):

[类名字节直接存储]

混淆后 (UTF-8 模式):

[UTF-8 长度][Overlong 编码的类名字节]

进阶应用

Java 原生序列化集成

Kryo 支持 Java 原生序列化,可通过重写 JavaSerializerwrite 方法实现原生序列化的混淆:

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 格式的转换:

  1. 读取原始 ASCII 流的最后一个字符(设置了 0x80 标志位)
  2. 计算字符串长度并写入 writeUtf8Length
  3. 将剩余字符按 UTF-8 Overlong 编码重新写入

防御建议

  1. 输入验证:对反序列化的类名和字符串进行严格校验
  2. 长度检查:检测 UTF-8 编码的字符长度是否合理
  3. 编码规范化:对输入数据进行规范化处理
  4. 安全配置:限制 Kryo 反序列化的类范围

结语

Kryo 的 UTF-8 Overlong Encoding 混淆技术展示了反序列化过程中编码处理可能带来的安全问题。理解这些底层机制有助于开发更安全的序列化/反序列化实现。

参考

  1. 《探索 Java 反序列化绕 WAF 新姿势》
  2. phith0n 的 UTF-8 Overlong Encoding 研究
Kryo UTF-8 Overlong Encoding 混淆技术分析 前言 本文深入分析 Kryo 反序列化中的 UTF-8 Overlong Encoding 混淆技术,该技术灵感来源于 1ue 和 phith0n 师傅的研究成果。Kryo 作为 Java 的高效序列化框架,其反序列化过程同样存在可被混淆的环节。 Kryo 反序列化方法 Kryo 提供三种主要反序列化方法: readObject - 需要指定 Class 对象 readObjectOrNull - 需要指定 Class 对象,允许返回 null readClassAndObject - 从 IO 流中动态获取对象类型(主要研究对象) UTF-8 模式下的 IO 流写入流程 关键写入方法分析 writeString 方法是关键入口,处理逻辑如下: 关键差异点 ASCII 模式: 直接写入字符串字节 最后一个字节设置 0x80 标志位 UTF-8 模式: 先调用 writeUtf8Length 写入长度信息 处理非 ASCII 字符时调用 writeString_slow UTF-8 模式下的 IO 流读取 反序列化调用栈 关键读取方法 readString 方法根据标志位判断读取模式: UTF-8 解码过程 readUtf8_slow 方法处理 Overlong Encoding: 可混淆属性分析 主要混淆点 类名字符串 - 通过 readString 方法处理 字符串类型变量 - 同样通过 readString 方法 Class 类型变量 - 通过 readClass 方法处理 其他可混淆类型 在 DefaultSerializers 中可混淆的类型包括: StringBuffer 和 StringBuilder Charset URL 数组类型 ( DefaultArraySerializer ) Map 的键值对 ( MapSerializer ) 混淆实现方案 基本实现方法 重写 Output 源码,强制所有字符串写入走 writeString_slow 路径: 字节替换策略 在 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 模式): 进阶应用 Java 原生序列化集成 Kryo 支持 Java 原生序列化,可通过重写 JavaSerializer 的 write 方法实现原生序列化的混淆: 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 研究