ZKar引用句柄系统揭秘:TCReference与Handler如何在序列化流中解析对象引用
2026/8/24 17:18:45 网站建设 项目流程

ZKar引用句柄系统揭秘:TCReference与Handler如何在序列化流中解析对象引用

【免费下载链接】zkarZKar is a Java serialization protocol analysis tool implement in Go.项目地址: https://gitcode.com/gh_mirrors/zk/zkar

ZKar 是一款用 Go 语言编写的 Java 序列化协议分析工具(0xACED 0x0005字节流的解析器与重建器),无需 JDK、无需 CGO。本文揭秘 ZKar 的引用句柄系统:TCReference与 Handler 如何在序列化流中解析对象引用——这正是理解 Java 序列化"对象只写一次、后面反复引用"机制的核心。

为什么 Java 序列化需要"句柄"?

Java 序列化流(由ObjectOutputStream写出)有一个重要特性:共享与循环引用

当一个对象在流中出现第二次时,JVM 不会再次完整写出它,而是写一个 5 字节的"回指":

0x71 00 7E 00 00
  • 0x71TC_REFERENCE标签(常量定义见 serz/model.go 中的JAVA_TC_REFERENCE
  • 后面 4 字节是句柄(Handler),即该对象第一次被写出时领取的"门牌号",例如0x7E0000(十进制 8257536)

因此解析器必须维护一张"门牌号 → 对象"的登记表,这正是 ZKar 中TCReference+ Handler 系统要解决的问题。

快速上手:用 ZKar Dump 一个真实载荷

克隆仓库后即可体验(仓库地址:https://gitcode.com/gh_mirrors/zk/zkar):

git clone https://gitcode.com/gh_mirrors/zk/zkar cd zkar go run main.go dump -f testcases/ysoserial/CommonsCollections6.ser

CLI 入口实现在 main.go,dump子命令会把字节流解析成一棵结构树打印出来。对象节点上都能看到@Handler字段,例如:

TC_OBJECT - 0x73 @Handler - 8257538 ... TC_REFERENCE - 0x71 @Handler - 8257538

TC_REFERENCE后面跟的 Handler,指回的就是之前@Handler相同的那个对象——这就是 ZKar 帮你还原的"对象引用关系图"。

TCReference:一个 5 字节的"回指指针"

解析TC_REFERENCE的入口是readTCReference(位于 serz/tc_reference.go),流程只有三步:

  1. 读标签:消费 1 字节0x71
  2. 读句柄:按大端序读 4 字节,得到 Handler 值
  3. 查表回指:调用stream.GetReference(handler)从登记表中取出对象,并根据对象的具体类型(TCObjectTCClassTCClassDescTCProxyClassDescTCStringTCArrayTCEnum)设置引用类型标记

TCReference结构体本身并不内嵌对象的副本,而是持有指向已有对象的指针。若 Handler 在表中查不到,解析器会直接报错object reference %v is not found——这种"前向引用"在合法流中是不存在的,报错能帮你快速定位畸形载荷。

Handler:流的"身份证登记处"

句柄登记表的维护者是ObjectStream(serz/buffer.go),它有三个关键成员:

成员作用
handler当前可分配的句柄号,初始为JAVA_BASE_WRITE_HANDLE = 0x7E0000
references map[uint32]Object句柄 → 对象的登记表
AddReference(obj)给对象发放门牌号并登记,然后句柄号 +1
GetReference(handler)按门牌号查表(供TCReference回指使用)

ZKar 严格模拟 Java 写端的行为:每个对象在第一次被写出时登记,而不是被引用时。以普通对象为例,readTCObject读完类指针后立即调用stream.AddReference(obj)(serz/tc_object.go);字符串(serz/tc_string.go)、数组、枚举、类描述符、代理类描述符同理。这种"发放时机"一旦错位,回指就会全部对不上——这也是该项目测试套件坚持逐字节回环(解析后再序列化,必须与原文件完全一致)的原因。

另外,Java 协议定义了TC_RESET0x79)用于重置整个句柄上下文,ZKar 将其作为一等内容类型处理(见 serz/tc_content.go),不会与相邻记录合并。

引用的另一大场景:类描述符复用

对象引用并不只出现在流的顶层。Java 允许类描述符被引用:多个对象如果是同一个类,第二个对象后面只需要写TC_REFERENCE指回类描述符的句柄,不必重复写出整个类元数据。

这一场景由TCClassPointer(serz/tc_classpointer.go)承载——它表示"一个类的指针",可以是全新的类描述符,也可以是一个TCReference。当 ZKar 需要读取对象字段时,FindClassBag方法会先判断指针类型:如果是指向句柄的引用,就从登记表里取出真正的类描述符,再沿SuperClassPointer递归收集完整类链。

如何优雅遍历:Walk 防环机制

由于引用会造成对象图出现环(A 引 B、B 又引 A),朴素的递归遍历会死循环。ZKar 的解法很克制:

  • TCReference.Walk直接返回,不再深入——它指向的对象在前面已经被访问过,源码注释写得很明白:"We don't walk into TCReference, because its field are all the pointer that walked before"
  • 通用访问器FindObject/FindClassDesc(serz/walker.go)支持按条件搜索任意节点,命中后返回哨兵错误StopWalkError短路退出

这让你可以方便地回答"流里有没有某个类描述符"这类问题,而不用担心引用成环。

字节级回环:引用也能被忠实重建

ZKar 不只读,也能写。TCReference.ToBytes()只做一件事:输出0x71+ 4 字节大端句柄,保证序列化载荷在"解析 → 重建"后逐字节还原。项目内置了大量真实 ysoserial 载荷作为基准(testcases/ysoserial/),测试断言重建结果与原始文件bytes.Equal——引用句柄系统的正确性正是靠这套"黄金基准"钉死的。

相关源码地图

文件职责
serz/tc_reference.goTCReference定义与解析
serz/buffer.goObjectStream:句柄登记表与发放逻辑
serz/model.goTC_*标签常量与起始句柄0x7E0000
serz/tc_content.go顶层标签分发器(引用也是其一)
serz/tc_classpointer.go类描述符引用与类链收集
serz/walker.go防环遍历与节点搜索
serz/parser.go流头部(魔数+版本)与主解析循环

小结 🎯

ZKar 的引用句柄系统用三个概念还原了 Java 序列化的对象引用机制:ObjectStream按"首次写出即登记"的原则发放从0x7E0000起递增的 Handler;TCReference作为 5 字节的回指节点,通过查表把 4 字节数字还原成真实的对象指针;Walk 防环设计则让带引用的对象图可以安全遍历。理解这套机制后,你阅读 ZKar 打印出的任何结构树时,都能一眼看懂每个TC_REFERENCE到底"指"向谁。

【免费下载链接】zkarZKar is a Java serialization protocol analysis tool implement in Go.项目地址: https://gitcode.com/gh_mirrors/zk/zkar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询