Veria Labs 称其 AI 智能体发现 XRP Ledger 可无限增发 XRP 的漏洞
The biggest hack in crypto history that never happened
Veria Labs 称其 AI 智能体在 rippled 代码中发现两个可链式利用的整数溢出漏洞,攻击者可用一笔交易凭空生成 18.45 万亿 XRP,使 XRP 约 940 亿美元市值面临风险。
Veria Labs 披露其 AI 智能体在 XRP Ledger 中发现可无限增发 XRP 的漏洞,读者可了解 AI 漏洞挖掘的实战路径与披露流程。
上个月,我们的 AI 发现了一个漏洞,可让任何人凭空铸造无限量的 XRP。
XRP 是第五大加密货币,规模超过 Solana 和 USDC,市值达 940 亿美元。它所运行的 XRP Ledger 由 Ripple 于 2012 年创建,是最古老的区块链之一。
940 亿美元岌岌可危
XRP 发行时总量固定为 1000 亿枚。该网络不支持挖矿或质押,且每笔交易都会销毁少量手续费,因此供应量理应只减不增。
我们发现的这个漏洞可让任何人通过单笔交易创建 18 万亿枚 XRP,是总供应量的 184 倍。
这让全部 940 亿美元都面临风险,是史上最大加密货币黑客事件(Bybit,15 亿美元)的 60 多倍。
据我们所知,此前从未有白帽发现过比这更大的漏洞。我们所知的上一项纪录是 2021 年 Polygon 的一个漏洞,当时危及 240 亿美元。
一个存在了近十年的漏洞
该漏洞在 XRP Ledger 上已存在近十年,位于 2015 年和 2017 年编写的代码中。
这是加密货币领域审查最严密的代码库之一。 XRP Ledger 已支付超过 100 万美元的漏洞赏金,仅自 2024 年以来就进行了十多次审计和审计竞赛,其中一次竞赛的奖金池高达 55 万美元。
我们怀疑无人发现它的一个原因是,它串联了两个单独来看都无害的漏洞。第一个漏洞让一笔支付几乎不花费任何费用,却能支付出数万亿。第二个漏洞存在于本应正好捕捉这种情况的安全检查中,而它以同样的方式失效了。
我们的 AI 如何发现它
我们将智能体指向 rippled,即运行 XRP Ledger 的软件。它发现了这两个漏洞,弄清了如何将它们串联起来,并在本地网络上构建了一个可用的漏洞利用程序来证明这一点。
它在周一上午 11 点标记了这个漏洞。我不相信,于是把概念验证读了三遍。
到凌晨 4 点,我仍然找不到任何问题,于是叫醒了我们的创始工程师和我一起过一遍。直接影响 940 亿美元的漏洞并不常见,我们俩都无法相信一个漏洞竟然在 XRP Ledger 中潜伏了近十年而无人察觉。
到早上,我们确信它是真的。
Veria 智能体不仅搜寻影响最大的漏洞,还搜寻如何将较低严重性的漏洞串联成更严重的问题。在我们披露这个漏洞后,我们将通用编码智能体直接指向存在漏洞的代码,它们仍然找不到。
三天内修复
我们通过 XRPL 的漏洞赏金计划报告了该漏洞,他们的团队行动迅速。
- 9 月 22 日: 当天报告并确认
- 9 月 23 日: 修复合并
- 9 月 25 日: 修复发布,超过 80% 的验证者已升级
通常,对 XRP Ledger 处理交易方式的更改要经过修订流程,验证者会在数周内对该更改进行投票。
自该流程引入以来的十多年里,XRPL 首次跳过它并发布了紧急修复。 正如他们的披露报告所说,“攻击者本可以在单笔经过验证的交易中创建远超总供应量的可花费 XRP。”
RippleX 未发现该漏洞曾被利用的证据。我们感谢 XRPL 团队的快速响应和分类处理。
史上最大的 AI 漏洞赏金
我们获得了 XRPL 漏洞赏金计划提供的 最高 25 万美元奖金。据我们所知,这是有史以来针对完全由 AI 智能体发现的漏洞所支付的最高金额。
AI 是把双刃剑
今年夏天早些时候,一名攻击者利用一个在固件中潜伏了五年的漏洞,从 Coldcard 钱包中盗走了 1,367 枚 BTC(价值 8,900 万美元),Coinkite 怀疑 AI 帮助找到了这个漏洞。
AI 正在让旧漏洞的发现成本大幅降低,而攻击者拥有与你相同的工具。 即便是像 XRP Ledger 这样经过严格审计的代码库,也隐藏着一个长达十年的严重漏洞。
幸运的是,好黑客的数量远远多于坏黑客。 这个漏洞是由报告它的人发现的,XRPL 团队在任何人能够利用它之前就修复了它。只要防御者能像攻击者一样迅速地采用这些工具,好人就占据上风。
本文的其余部分是技术深度剖析:这两个溢出是如何工作的,它们如何串联在一起,以及修复方案如何堵住它们。
技术深度剖析
背景:XRP 与 DEX
XRP 可分割为一百万个“drop”,就像 Solana 中的 lamport 一样:
1 XRP=1,000,000 drops
全部 1,000 亿枚 XRP 都是在创世账本中创建的,任何交易都不应创建更多。开源代码库 rippled 实现了整条链:交易处理、共识、账本存储和 RPC 接口。
XRPL 还支持称为 IOU 的发行货币。IOU 代表发行方以某种货币代码承担的债务。任何人都可以发行 IOU,而 IOU 可在 XRPL 内置的去中心化交易所(DEX)上与 XRP 进行交易。
这个 DEX 最终成为攻击路径,将攻击者发行的 IOU 变成无限铸造的 XRP。
背景:报价处理与 BookStep 转换
跨货币支付可以消耗 DEX 中的报价。在内部,支付引擎将每次订单簿转换建模为一个 BookStep。
例如,一笔支付可能花费 XRP,消耗出售已发行 USD 代币的报价,并将该 USD 交付到目的地。当多个报价具有相同的质量(汇率)时,BookStep 会聚合它们的输入和输出。
该实现将单次 BookStep 遍历限制为 1,000 个报价。
背景:交易不变量
交易应用后,rippled 会在提交结果之前运行不变量检查。如果任何检查失败,交易的更改将被丢弃。
其中一项检查是 XRPNotCreated 不变量。它会遍历交易修改的每个账本条目,并汇总原生 XRP 的净变化。对于任何有效交易,该净变化必须恰好等于手续费的负值:
Σ(XRP 之后)−Σ(XRP 之前)=−手续费
若结果为正,则表明交易创建了 XRP,该交易会被拒绝。
另一个独立的不变量 XRPBalanceChecks 可防止单个账户持有超过最初 1,000 亿枚 XRP 的总供应量。
BookStep 聚合代码写于 2015 年 11 月,而 XRPNotCreated 不变量写于 2017 年 2 月。作为对比,以太坊于 2015 年 7 月推出,Solana 于 2020 年 3 月推出。
溢出 #1:BookStep 中的整数溢出
当一笔支付跨越订单簿时,BookStep 不会孤立地结算每个报价。当它遍历一条链中的报价时,它会将它们的输入累积到一个运行中的多重集合中,并将其合并为一个单一的总量。
boost::container::flat_multiset<TIn> savedIns;
// ...
savedIns.insert(stpAmt.in);
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts));
其中 result.in 是源账户最终被收取的总金额。
注意 sum() 函数。这是 BookStep 用来对一组金额求和的辅助函数:
template <class TCollection>
static auto
sum(TCollection const& col)
{
using TResult = std::decay_t<decltype(*col.begin())>;
if (col.empty())
return TResult{beast::kZero};
return std::accumulate(col.begin() + 1, col.end(), *col.begin());
};
当集合包含 XRPAmount 个值时,std::accumulate 会调用 XRPAmount::operator+=。底层值是一个有符号 64 位整数:
using value_type = std::int64_t;
该加法不检查结果是否仍能容纳于有符号 64 位类型中。
这意味着,如果单次 BookStep 遍历消耗了足够多的报价,其合并的 XRP 输入超过有符号 64 位范围,累计总额就会回绕成一个小值。
这些报价本身在 consumeOffer 内部逐个结算,每个都按其自身真实的输入金额:
auto const dr = offer.send(
sb, book_.in.getIssuer(), offer.owner(),
toSTAmount(ofrAmt.in, book_.in), j_);
由于此路径中聚合金额实际上被忽略,每个报价所有者都会被记入 ofrAmt.in,即其报价所要求的完整未回绕金额。
有符号整数溢出在 C++ 中是未定义行为,因此这会崩溃还是静默回绕取决于二进制文件的构建方式。我们针对官方发布二进制文件进行了测试,并确认溢出会静默回绕,不会崩溃。*
*值得注意的是,如果 rippled 在构建时启用了溢出捕获(例如 -ftrapv),此漏洞就会变成节点崩溃(DoS)。
溢出 #2:XRPNotCreated
溢出 BookStep 会凭空创造 XRP,但这绝不应存活到已提交的账本中。XRPNotCreated 不变量的存在正是为了捕获它。
不幸的是,它的累加器也是有符号 64 位整数,而且该不变量检查的是累计总额,而非单个状态变化:
class XRPNotCreated
{
std::int64_t drops_ = 0;
};
当不变量遍历被修改的条目时,visitEntry 会减去每个账户在交易前的余额,并加上其交易后的余额:
case ltACCOUNT_ROOT:
drops_ -= (*before)[sfBalance].xrp().drops(); // before
// ...
case ltACCOUNT_ROOT:
drops_ += (*after)[sfBalance].xrp().drops(); // after
一旦访问完每个条目,finalize 会检查净变化是否恰好等于被销毁的手续费,不多不少:
if (drops_ > 0)
return false; // XRP was created
if (-drops_ != fee.drops())
return false; // net change doesn't match the fee
在正常交易中这没问题。唯一应该消失的 XRP 是手续费,因此 -drops_ 必须等于 fee.drops(),任何正值都意味着 XRP 凭空出现。
但 drops_ 累加的是真实余额变化。当一笔支付记入足够多的账户,使其收益总和超过有符号 64 位边界时,drops_ 就会溢出。由于该不变量作用于整个状态转换,真实的净变化——一个巨大的正数——会折回成一个看起来像手续费销毁的负值。
于是 finalize 看到净变化约为 -fee,便放行了该交易。
串联两个原语
BookStep 本身可以铸造 XRP,但 XRPNotCreated 会拒绝任何创造 XRP 的交易,因此铸造永远不会提交。而 XRPNotCreated 溢出本身则无法触达。
目标是串联两个原语。方便的是,两者具有相同类型的溢出:result.in 中的 BookStep 和 drops_ 中的 XRPNotCreated 都是有符号 64 位整数。
result = TAmounts<TIn, TOut>(sum(savedIns), sum(savedOuts)); // BookStep
// ...
std::int64_t drops_ = 0; // XRPNotCreated
如果攻击者能够选择记入金额,使其总和落在 264 的倍数上,那么两个累加器就会以相同的量回绕。
构造交易
三个约束塑造了这笔交易:
- 干净回绕:两个和都是模 264,因此 N 个约 2k drops 的报价,且 N 为 2 的幂,可以使 N⋅2k 恰好落在边界上。
- 每账户上限:每个挂单方被记入其真实金额,上限为 1000 亿 XRP(
kInitialXrp1017 drops ≈256.5),因此每个报价最多为 256 drops。 - Strand 限制:
kMaxOffersToConsume将单次遍历限制为 1,000。
将这些放在一起,得到 256 个 256+1 drops 的报价:
256×(256+1)=264+256 drops
+1 使总额不会恰好为 264(会回绕为 0),而是回绕为可用的 256 drops。
同时命中两个原语
同样的 264+256 流经每个溢出:
// BookStep: The aggregate wraps to 256, but each offer still pays out in full
result.in = sum(savedIns); // 2^64 + 256 -> 256
offer.send(sb, book_.in.getIssuer(), offer.owner(),
toSTAmount(ofrAmt.in, book_.in), j_); // maker gets 2^56 + 1
// XRPNotCreated: +2^64+256 (makers) - 256 (source) - fee = 2^64 - fee -> -fee
if (-drops_ != fee.drops())
return false; // never trips: -drops_ wraps to exactly the fee
买方被收取 256 drops,256 个挂单方总共收到约 264 drops 的真实 XRP,而不变量只看到手续费销毁并提交账本。
总结:
- 设置 256 个 offer:发行一个无价值代币,附带 256 个 offer,每个以 256+1 drops 的 XRP 出售。
BookStep使费用溢出:sum(savedIns)将 264+256 回绕为 256 drops。XRPNotCreated使检查溢出:264−fee 的净变化回绕为 −fee 并通过。- 账本提交:不变量通过,铸造的 XRP 被写入 XRP Ledger。
影响
一次成功触发即可为 maker 账户记入:
264+256 drops=18,446,744,073,709.551872 XRP
在扣除源账户支出和费用后(与所创造的金额相比可忽略不计),即 18.45 万亿 XRP 平均分配到 256 个账户。
1000 亿的上限仍按账户执行。全部 18.45 万亿 XRP 均可花费,但必须分散在多个独立账户中,以确保没有任何单一账户超过上限。
该设置需要约 256 个有资金的账户,但不要求攻击者运行验证者或拥有任何特殊权限。一旦提交,它就会作为普通签名交易被处理。由于该设置可以用更多账户重新创建,攻击可重复进行,因此攻击者可以无上限地铸造原生 XRP,每次 18.45 万亿。
攻击者永远无法以市场价格卖出如此多的 XRP,因此真正的风险敞口是每一枚现有 XRP 的价值:即全部 940 亿美元市值。*
*按当时 XRP 价格 1.59 美元计算,18.45 万亿 XRP 的名义价值约为 29 万亿美元。
修复
修复已在 rippled v3.4.1 中发布:
- 在
BookStep汇总 offer 金额时添加溢出检查,因此会导致溢出的求和现在会干净地失败,而不会铸造任何东西。 - 加宽
XRPNotCreated累加器,使净变化不再能回绕成看起来像费用销毁。 - 加固其他代码路径中的余额求和。
时间线
- 2026/09/21 - Veria AI 在 rippled 上运行并识别出无限铸造
- 2026/09/22 - Veria AI 在 localnet 上构建了无限铸造的可运行 PoC
- 2026/09/22 - Cayden 验证了 PoC,并确认该漏洞可在主网上触发
- 2026/09/22 - 通过 XRPL 的漏洞赏金计划报告无限铸造,并于同日确认
- 2026/09/23 - 修复合并
- 2026/09/25 -
rippled v3.4.1作为紧急修复发布,超过 80% 的验证者已升级 - 2026/10/08 - Veria AI 获得最高严重级别赏金 250,000 美元
- 2026/10/09 - XRPL 发布其披露报告
关于我们
在 Veria Labs,我们构建 AI 渗透测试代理,可自动发现并修复您应用程序中的安全漏洞。我们由美国排名第一的竞技黑客团队成员创立,与 Phantom、Tempo 和 Provable 等团队合作,在攻击者之前发现此类漏洞。
认为我们能帮助保护您的系统?联系我们。
PoC
与 PoC 相关的所有文件均可在此 GitHub gist 中找到:
https://gist.github.com/SuperBeetleGamer000/131515ad1965a67c6f210aa7dedce67d
来源:Hacker News · verialabs.com