惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
Google Developers Blog
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
D
Docker
F
Fortinet All Blogs
博客园 - 三生石上(FineUI控件)
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
J
Java Code Geeks
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Simon Willison's Weblog
Simon Willison's Weblog
S
Security Affairs
NISL@THU
NISL@THU
T
Tor Project blog
A
About on SuperTechFans
宝玉的分享
宝玉的分享
腾讯CDC
S
Schneier on Security
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
P
Privacy & Cybersecurity Law Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
雷峰网
雷峰网
C
Cyber Attacks, Cyber Crime and Cyber Security
Vercel News
Vercel News
Cisco Talos Blog
Cisco Talos Blog
D
DataBreaches.Net
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Google Online Security Blog
Google Online Security Blog
Recorded Future
Recorded Future
L
LINUX DO - 热门话题
Microsoft Security Blog
Microsoft Security Blog
Latest news
Latest news
C
Check Point Blog
有赞技术团队
有赞技术团队
T
The Exploit Database - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
云风的 BLOG
云风的 BLOG
SecWiki News
SecWiki News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
爱范儿
爱范儿
月光博客
月光博客
V
Vulnerabilities – Threatpost
T
Threat Research - Cisco Blogs
P
Palo Alto Networks Blog
T
The Blog of Author Tim Ferriss
C
Cisco Blogs
Webroot Blog
Webroot Blog
S
Security @ Cisco Blogs

博客园_首页

Linux实操--组管理、权限管理和定时任务 Java + EasyExcel 实现单个接口导出多个Excel Mem0 源码解析系列(二):提示词工程的深度剖析 Openclaw TaskFlow究竟是什么?和普通Skill技能有什么区别 博文阅读密码验证 - 博客园 嘉立创开源:应该是全网MicroPython教程最多的开发板 Hermes Agent 集成实践:从协议到生产 2026年AI编程工具横评:Cursor、Codex、Claude Code、Zed、Windsurf Java程序员必看的RAG入门教程 2026 AI效率神器:Superpowers + Claude Code 保姆级教程 本地大模型部署全攻略:从 0 到 1 玩转 Ollama 【从0到1构建一个ClaudeAgent】内存管理-上下文压缩 .NET 高级开发 | 设计、实现一个事件总线框架 电子小白入门之NE555 3. WorkBuddy:隐藏玩法,一键召唤专家,让 AI 以"专家身份"给你干活 和AI一起搞事情#3:Claude Teammate 游戏开发翻车实录 【OpenClaw】通过 Nanobot 源码学习架构---(7)Memory C# .NET 周刊|2026年3月3期 我在 Debian 11 上把 K8s 单机搭起来了,过程没你想的那么顺(/opt 目录版) 深度学习进阶(七)Data-efficient Image Transformer CLI+Skill搭建浏览器AI自动化框架,告别一切重复枯燥任务 告别Token账单无底洞:OpenClaw本地部署,重塑企业数据主权的唯一解 FastAPI+Vue:文件分片上传+秒传+断点续传,这坑我帮你踩平了! SBTI 爆火后,我做了个程序员版的 CBTI。。已开源 + 附开发过程 多模态检索开始进入工程期:用 Sentence Transformers 搭建可落地的 Multimodal RAG 100多行代码实现一个最简单的Agent(用ReAct) Claude Code 通关手册(八):推荐 5 个 Hooks,代码质量提升 3 倍 老板:“有人截图了!”。安全部门:“收到,马上查暗水印!” - why技术 技术之外,皆是人间 C#/.NET/.NET Core技术前沿周刊 | 第 69 期(2026年4.01-4.12) Snack JSONPath 项目架构分析 Claude Code Buddy 小析:一个非核心功能,如何体现产品的细节完成度 AI新时代下的图床管理方案-Cloudflare图床+MCP+Skills方案指南 化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量 从零实现富文本编辑器#13-React非编辑节点的内容渲染 AI开发-python-langchain框架(3-23-OpenAI Functions风格Tool Calling智能助手) .NET + AI 进阶实战:基于类的技能开发 - 打造可治理的 Agent 能力模块 【从0到1构建一个ClaudeAgent】规划与协调-技能 上周热点回顾(4.6-4.12) 电子小白的工具三件套:面包板、杜邦线、万能板 单表五亿数据的查询优化 | Mysql、StarRocks 2. WorkBuddy:从“我是谁”到“帮我干活” C# 如何减少代码运行时间:7 个实战技巧 基于HelixToolkit.SharpDX 渲染3D模型 - 笺上知微 从零开始的双臂具身VLA起源及现阶段发展综述 - SkyXZ 记对 xonsh shell 的使用, 脚本编写, 迁移及调优 - pluvium27 受够了Vibe Coding的失控?换个起点,让AI事半功倍 从开始配置漏洞环境到漏洞复现流程 - 難しい 关于10年工作经验的程序员对OpenClaw的实战经验分享以及看法 - 虚无境 Any metadata 的内存布局 C# .NET 周刊|2026年3月2期 - InCerry 我帮你测过了,测试圈排名第二的 Skill 依然很牛逼 Skill Discovery | 无监督技能发现的经典工作总结 - MoonOut PbootCMS 网站内容数量多导致访问慢?这些实用优化方案帮你提速! - 家兴网络技术工作室 上下文工程是什么?过时了么?一文讲明白! - 一枫说码 网站漏洞怎么发现并修复?一篇实用指南(附完整流程) - 家兴网络技术工作室 开了 TUN 模式还是直连?90% 的人都踩过这个坑 Github日报|2026年04月12日 - AI一族 AScript扩展多种脚本语言 - rockey627 AI 学习笔记:Agent 的记忆机制 你能被装进一个文件里吗?——7 万人把同事"蒸馏"成了 AI - 我没有三颗心脏 Claude Code 通关手册(七):给 AI 装上技能包——Skills 完全指南 - 暮色之狐 在浏览器中快速编辑代码:VSCode Web 集成实践 - Newbe36524 蒸馏自己 skill?基于 Deepseek 的蒸馏器,丐版蒸馏方式,简单便捷 - To_Carpe_Diem Spring AI Aliababa和AgentScope,哪个更好? - 苏三说技术 Etsy 把 1000 个 MySQL 分片迁进 Vitess:425TB 数据背后的真正问题不是性能,而是运维规模 MicroPython LVGL基础知识和概念:底层渲染与性能优化 - FreakStudio 数据库草图算法 Python 潮流周刊#146:CPython 引入 Rust 的进展 - 豌豆花下猫 最小生成树 - mofei1116 红日靶场七:从外网入口、容器逃逸到 AD 接管的完整利用链复盘 - YouDiscovered1t 分享四款开源且实用的 Kafka 管理工具 - 追逐时光者 vLLM 权重加载机制全解析:从挑战到理想架构 LCT 学习笔记 - ACehomoxue Avalonia UI 12.0.0 正式发布:架构演进和性能飞跃 - 张善友 当 AI Agent 把调用链拉长,延迟开始成为一门生意 conhost.exe 无法显示 U+2717 - 145a 太秀了,我把自己蒸馏成了 Skill!已开源 - 程序员鱼皮 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 基于 Ghostty 带有分割标签页和为 Claude 编程设计的通知终端 - BugShare AI 焊死入口:教育的“操作系统级”重塑 - 郝hai 初级Java开发工程师使用sql脚本编写代码的过程是简单而且不糊涂 - CoderOilStation Claude Code通关手册(六):MCP协议完全指南 - 暮色之狐 边框灯光环绕动画特效实现指南 - Newbe36524 开源:子木蒸馏版的 SEO 审计工具 seo-audit-skill v1.0 我所理解的Python元模型 【从0到1构建一个ClaudeAgent】规划与协调-TodoWrite - 程序员Seven Claude 和 Codex 在审计 Skill 上性能差异探究 - ACai_sec AScript如何实现中文脚本引擎 - rockey627 【渗透测试】HTB Season10 Garfield 全过程wp - dynasty_chenzi Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革 树状数组正确性证明 - AC-wyr 你的 AI 焦虑,可能比 AI 本身更危险——ATM 机没有消灭银行柜员,但恐慌消灭了你的判断力 - 我没有三颗心脏 一个拉胯的分库分表方案有多绝望?整个部门都在救火! - 冰河团队 动态规划入门必学之走方格问题 - Ofnoname PostgREST 与 PostgreSQL 角色权限配置全解析(生产级实践) - SheepDog1998 使用 UEFI 图形输出协议 GOP 在屏幕上显示图像的方法 - 阿源- Claude Code通关手册(五):组建你的AI专家团队,子代理系统 - 暮色之狐 一个程序员到架构师的催婚路之感悟(整整10年后的催婚相亲感悟) - MisterLip 用 Agent Skill 自动生成工作周报 - 赵康
20260413-Hyperbridge 攻击事件:发生在默克尔山上的验证绕过
ACai_sec · 2026-04-15 · via 博客园_首页

背景信息

20260413,Ethereum 链上的 Hyperbridge 协议遭受了验证绕过攻击,攻击者绕过了 MMR 证明检查,获取到了 Ethereum 链上 DOT 代币的铸币权限。随后通过增发了 1 Billion DOT 代币进行抛售,获取 110.8 ETH (约 22 万美元)。

Trace 分析

image

  1. 提交恶意的 MMR proof,绕过检查,获得 DOT 的铸币权限
  2. 铸造大量的 DOT 代币
  3. 将铸造的 DOT 代币兑换成 ETH

Merkle Mountain Range (MMR)

在进行代码分析之前,我们需要先了解一下 Merkle Mountain Range,只有了解了这个数据结构是如何运行的,才能理解攻击者是如何构造恶意的证明绕过相关的检查,

Merkle Mountain Range (MMR) 介绍

Merkle Mountain Range(MMR)是 Merkle Tree 的变体,一种仅追加(append-only)的数据结构。

经典的 Merkle Tree 是一棵完全二叉树——叶子数量必须是 2 的幂次,而且一旦建好就不方便再往里追加数据。

比如在 tornado 中,采用的方案就是预先构建好高度为 64 的 merkcle tree(叶子节点数量固定),然后未被使用的叶子节点采用 0 数据填充。
MMR 就是为了解决"高效追加 + 高效证明"这个问题而设计的。

数据结构

MMR 的数据结构为一排从高到低排列的"完美二叉树"(因为像山峰从高到低排列,所以叫"Mountain Range")。每棵树的根节点就叫一个 peak(山峰)。

image

新增过程

每次新增一个新叶子时:

  1. 把新增叶子作为一棵高度为 0 的山放到最右边。
  2. 进行合并检查:如果最右边两座山的高度相同,就把它们合并(对两个根做一次哈希,生成一个新的父节点,形成一棵更高的山)。
  3. 重复步骤 2,直到最右边两座山高度不同为止。
  4. 此时 MMR 形成了一排从高到低排列的山峰形式。

举例说明

在原有两个叶子的 MMR 添加第 3 个叶子时,旁边多出一座高度 0 的小山。

image

继续添加第 4 个叶子时,两座高度 0 的又合并成高度 1,然后两座高度 1 的再合并成高度 2,只剩一座大山。

image

数学规律

如果当前有 n 个叶子,把 n 写成二进制,每个为 1 的位对应一座山峰。例如 n = 11 = 0b1011,表示有三座山峰,高度分别为 3、1、0(对应二进制位 8+2+1)。所以山峰数量等于 n 的二进制中 1 的个数,最多 O(log n) 座。

MMR 根的计算

虽然 MMR 可能有多个 peak,但最终需要一个唯一的根哈希(root)来代表整个 MMR。
Root 的计算流程如下:

  1. 从右往左选取两个 peak 值进行哈希,获得一个 bag hash
  2. 继续选取下一个 peak 值,将 bag hash 和它哈希
  3. 不断重复步骤 2 直到所有 peak 被选取,最后得到的值就是 MMR root

MMR Inclusion Proof

MMR inclusion proof 回答的问题是:"给定一个 MMR root,证明某个叶子确实存在于 MMR 中"。
整个过程分为两个部分:Mountain ProofPeak Bagging


以图中 L5(节点 8)为例进行 MMR inclusion proof

image

阶段一:Mountain Proof

这一阶段和普通 Merkle Tree 的 proof 完全一样。

  1. 验证者手里有 L5 的原始数据,首先算出 H(8) = Hash(L5_data)。然后 prover 提供了兄弟节点 9 的哈希值,验证者计算 H(10) = Hash(H(8) || H(9))

    这里有个关键细节——左右顺序不能搞反。MMR 的节点编号天然编码了位置信息,验证者可以通过位置推算出谁在左、谁在右。

  2. 接着上一层:prover 提供 H(13),验证者算 H(14) = Hash(H(10) || H(13))

  3. 再上一层:prover 提供 H(7),验证者算 H(15) = Hash(H(7) || H(14))

节点 15 就是这座山的 peak。到此为止,山内阶段结束。验证者现在手里有了 H(15) 的值——这个值是自己一步步算出来的,不是 prover 给的,所以如果中间任何一步被篡改,最终的 H(15) 都会不匹配。


阶段二:Peak Bagging

一个 MMR 通常有多座山,图中 11 个叶子产生了 3 座山:peak 15(h=3)、peak 18(h=1)、peak 19(h=0)。

验证者已经通过阶段一已经算出了 H(15),然后在 prover 提供的 proof 中获取 H(18)H(19)的值。
bagging 阶段:从最右边的 peak 开始,两两向左折叠哈希。

  1. bag = Hash(H(18) || H(19))
  2. root = Hash(H(15) || bag)

这个 root 就是整个 MMR 的根哈希,验证者把算出的 root 和链上存储的可信 MMR root 做比较,如果相同则证明 proof 有效,L5 确实在这个 MMR 中。

代码分析

整个 MMR 验证的核心对应的是 MerkleMountainRange.CalculateRoot() 函数,第一到第四部分对应 mountain proof(阶段一),第五部分对应 peak bagging(阶段二),第六部分是最终输出。

接下来我们结合攻击者的输入进行分析,排查出漏洞出现的位置,以及攻击者是如何利用这个漏洞构造一次成功的攻击的。

重点关注以下部分的参数(完整版本的参数请自行查看)

host: 0x792a6236af69787c40cf76b69b4c8c7b28c4ca20
proof[1]:
	height:[stateMachineId: 3367, height: 9775932]
	multiproof[1]:[0x466dddba7e9a84a0f2632b59be71b8bd489e3334a1314a61253f8b827c9d3a36]
	**leafCount: 1**
requests[1]:
	request[1]:[call DOT.changeAdmin()]
	**index: 1**
	kIndex: 0

| 其中 multiproof 的值是和 height (3367, 9775932) 对应的 MMR 的 root

MerkleMountainRange.CalculateRoot() 函数分析

第一部分:单叶特判

if (leafCount == 1 && leaves.length == 1 && leaves[0].leaf_index == 0) {
	return leaves[0].hash;
}

如果整个 MMR 只有一个叶子(同时满足以下三个条件),那 root 就是这个叶子本身的 hash,不需要任何 proof 节点,直接返回。

  • leafCount == 1:MMR 只有 1 个叶子节点
  • leaves.length == 1:需要验证的叶子节点数组长度为 1
  • leaves[0].leaf_index == 0:所验证的第一个叶子节点的索引是 0
    从这个检查可以看出,如果 MMR 的叶子数量只有 1 个的时候,所传入的叶子索引值 leaves[0].leaf_index 应该为 0

从攻击者的输入来看,他所需要验证的 MMR 叶子节点数量为 1 (leafCount: 1),理应是符合这个分支的判定,直接返回的。但是攻击者通过构造了一个恶意参数 requests[1].index: 1,使得 leaves[0].leaf_index == 0 的判断为 false,从而绕过了这个判断。
也正是这个关键步骤的绕过,使得在后续的证明构建与验证过程中,requests[1] 所对应的叶子节点始终没有参与到整个证明过程中。


第二部分:Peak 分解 + 迭代器初始化

uint256[] memory subtrees = subtreeHeights(leafCount);
uint256 length = subtrees.length;
Iterator memory peakRoots = Iterator(0, new bytes32);
Iterator memory proofIter = Iterator(0, proof);

subtreeHeights(leafCount) 把叶子数做二进制分解,算出每座山的高度。比如 leafCount = 11 = 8+2+1,返回 [3, 1, 0],表示三座山,高度分别是 3、1、0。

然后初始化两个迭代器:peakRoots 是一个空数组,用来收集每座山最终算出的 peak hash;proofIter 是对传入的 proof 数组的游标,后面每次需要 proof 节点时就从里面顺序取。

由于传入的 leafCount = 1 ,所以 subtrees = [0]proofIter 指向的是传入的 root 值。


第三部分:主循环——逐座山处理

uint256 current_subtree;
for (uint256 p; p < length; ) {
	uint256 height = subtrees[p];
	current_subtree += 2 ** height;

从左到右遍历每座山。current_subtree 是一个累加器,记录"前 p+1 座山总共覆盖了多少个叶子"。比如第一座山高度 3,覆盖 2 ** 3 = 8 个叶子,第二座高度 1,再加 2 ** 1 = 2,累计 10 个。这个值用来判断哪些待证明的叶子属于当前这座山。

由于单个节点的 hight 为 0,所以 current_subtree = 1


第四部分:叶子分割 + 三路分支

MmrLeaf[] memory subtreeLeaves = new MmrLeaf;
if (leaves.length > 0) {
	(subtreeLeaves, leaves) = leavesForSubtree(leaves, current_subtree);
}

leavesForSubtreecurrent_subtree 为分界点,把 leaves 切成两半:leaf_index < current_subtree 的归入当前这座山(subtreeLeaves),剩下的留给后面的山(重新赋值给 leaves)。

然后进入三路分支:

if (subtreeLeaves.length == 0) {
	if (proofIter.data.length == proofIter.offset) {
		break;
	} else {
		push(peakRoots, next(proofIter));
	}
} else if (subtreeLeaves.length == 1 && height == 0) {
	push(peakRoots, subtreeLeaves[0].hash);
} else {
	push(peakRoots, CalculateSubtreeRoot(subtreeLeaves, proofIter, height));
}
  • 分支 A:这座山里没有需要证明的叶子。那这座山的 peak hash 应该由 prover 直接提供——从 proof 里取一个。如果 proof 已经耗尽就 break 退出。
  • 分支 B:这座山高度为 0(只有一个叶子),且恰好就是要证明的那个叶子。那 peak hash 就是叶子自身的 hash,不需要任何 proof 节点。
  • 分支 C:这座山里有需要证明的叶子,且山的高度大于 0。调用 CalculateSubtreeRoot 做标准的 Merkle 多重证明——用 subtreeLeaves 和 proof 中的兄弟节点,一层层向上合并,算出这座山的 peak hash。

三个分支都把结果 push 到 peakRoots 里。

由于传入的 leafCount: 1 代表 MMR 只有 1 个叶子节点,所以 leavesForSubtree 函数以current_subtree 为分界点切成两半的值都是空的,进入到 subtreeLeaves.length == 0 分支,把攻击者传入的 root 压入栈。

image


第五部分:Peak bagging

unchecked {
	peakRoots.offset--;
}

while (peakRoots.offset != 0) {
	bytes32 right = previous(peakRoots);
	bytes32 left = previous(peakRoots);
	unchecked {
		++peakRoots.offset;
	}
	peakRoots.data[peakRoots.offset] = keccak256(
	abi.encodePacked(right, left)
	);
}

进入这段时,peakRoots 里已经收集了所有山的 peak hash,offset 指向最后一个元素的下一个位置。先 offset-- 把游标退到最后一个有效元素上。

然后 while 循环从右向左折叠:每次取出当前位置的值(right)和它左边的值(left),哈希后写回 left 的位置,同时 offset 也回到了这个位置。下一轮再取这个新值和更左边的值继续折叠,直到 offset == 0,只剩一个值。

用具体例子说明——假设 peakRoots = [P0, P1, P2],offset 从 2 开始:第一轮取 right=P2, left=P1,算出 bag1 = H(P2, P1),写到位置 1;第二轮取 right=bag1, left=P0,算出 root = H(bag1, P0),写到位置 0。循环结束。

按照只有一个叶子节点处理,只有一个 peak,所以这部分会被跳过。


第六部分:返回 root

return peakRoots.data[0];

经过 bagging,最终结果就存在 peakRoots.data[0],这就是整个 MMR 的 root hash。

此时也就是返回了攻击者最开始传入的 root 值,也就顺利地通过了验证了。


在通过了 MMR 的校验以后,HandlerV1 合约会通过调用 host.dispatchIncoming() 函数执行request 中的内容,也就是攻击者构建的获取 DOT 铸币权限的调用。

image

接下来的操作就是铸造大量的 DOT,并进行抛售获利。

后记

链上攻击事件不会等你有空的时候再发生,发生了就只能抽空出来做攻击分析了。所以本篇文章重点在 rootcause 部分的内容,对于跨链 ISMP,MMR root,Governance 这些部分的内容未能提及,读者如果感兴趣的话可以自行了解。
唉,写攻击事件分析真难啊。倒不是难在分析,是难在写,是需要有时间有精力有想法去把它写下来。
感谢你的阅读,如果你觉得写得还行的话欢迎点赞推荐转发,这是对我最大的支持和鼓励(抱拳)。