学了1ue和phith0n师傅介绍的知识点,让我受益匪浅。Java的原生反序列化是从IO流中读出对象的过程,除了Java原生的反序列化以外,kryo的反序列化也是从IO流中去读取对象,那么kryo会不会也存在相关的混淆方案呢
kryo提供了三种方法来反序列化,分别是:readObject、readObjectOrNull、readClassAndObject,其中readObject跟readObjectOrNull都需要绑定一个Class对象作为反序列化还原的对象,而readClassAndObject则是从IO流中获取对象,那么这里主要跟进readClassAndObject进行分析
Kryo kryo = new Kryo();
Input input = new Input(new FileInputStream("kryo.out"));
Test test = (Test)kryo.readClassAndObject(input);
input.close();
一个demo
package org.example;
import java.io.Serializable;
public class Test implements Serializable {
public String name = "Nivia";
transient String id = "abc";
public String getName() {
return name;
}
}
数据写入时,获取类名的调用栈
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;
}
// Detect 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;
}
}
}
if (ascii) {
if (capacity - position < charCount)
writeAscii_slow(value, charCount);
else {
value.getBytes(0, charCount, buffer, position);
position += charCount;
}
buffer[position - 1] |= 0x80;
} else {
writeUtf8Length(charCount + 1);
int charIndex = 0;
if (capacity - position >= charCount) {
// Try to write 8 bit chars.
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);
}
}
形参value为类名,只要类名中全是ascii字符就走ascii字符处理,否则尝试走UTF-8处理模式
这里有个坑点,就是纯ASCII和UTF-8模式的数据写入是存在差异的,不是直接修改就能成功,为了了解UTF-8处理模式,我这里重写了源码,让if语句中的ascii布尔值进行了取反
先是调用writeUtf8Length方法,方法将指定的UTF-8长度写入到IO流
然后遍历类名,当发现不是ascii字符会break
如果出现break情况,就会调用writeString_slow方法
方法就是unicode码转换成UTF-8编码的原理
按照Overlong Encoding的原理,对IO流数据进行替换
然后调试一下反序列化的过程,相关调用栈
在Input#readString方法中
如果是正常的ascii序列化,buffer记录完classid后就直接存储类名了,而UTF-8会先记录UTF-8的长度。而在反序列化阶段,kryo用这个差异判断进行哪种模式的反序列化
readUtf8Length方法读取出长度,主要处理逻辑在readUtf8方法
如果发现是Overlong Encoding的情况,会调用readUtf8_slow方法
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++;
}
}
跟UTF8-Overlong-Encoding的原理就一样啦
除了类名我们还能混淆什么?
相关存在混淆的方法都位于Input的类下,除了readString方法以外还有readStringBuilder,跟其对应的就是相关的Output#write方法,只要经过相关write方法处理并加进了序列化IO流,就应该可以实现IO流混淆
获取完类名以后,会通过对象类型获取序列化器,在一系列的Serializer#read方法还原对象后去还原属性,可以简单去序列化器里面进行一些搜索,这里只收集了部分有价值的
混淆字符串类型的变量,相关调用栈
混淆Class类型的变量
在DefaultSerializers中,更多的还有StringBuffer和StringBuilder、Charset、URL
其他序列化器:
等等
其实这里很简单,重写Output源码,让数据写入IO流的操作全走writeString_slow方法
分别对应着1、3、2字节的写入方法,然后注释掉1、3字节写入的部分就能实现数据混淆
混淆前后对比
同样kryo支持Java原生反序列化,Java原生反序列化也同样存在Overlong Encoding的混淆方法,具体实现可以重写JavaSerializer序列化器write方法下的源码,替换掉ObjectOutputStream对象即可
FileOutputStream barr = new FileOutputStream("kryo.out");
Output output = new Output(barr);
kryo.writeClassAndObject(output, test);
output.close();
用户自行生成的序列化数据,是以ascii模式生成,能否实现从ascii流转换成UTF-8形式的流
UTF-8形式
ASCII形式
原理也很简单,还原最后一位字符,把Utf8Length写到前头就OK了
最后一位的操作
逆向操作
如果存在错误或者建议还请师傅们提issue交流指导🙏
https://www.leavesongs.com/PENETRATION/utf-8-overlong-encoding.html