跳到正文
Hacker News· SuperBeetleGame·· 7 天前精选AI 评分88

Veria Labs 称其 AI 智能体发现 XRP Ledger 可无限增发 XRP 的漏洞

The biggest hack in crypto history that never happened

AI 导读

Veria Labs 称其 AI 智能体在 rippled 代码中发现两个可链式利用的整数溢出漏洞,攻击者可用一笔交易凭空生成 18.45 万亿 XRP,使 XRP 约 940 亿美元市值面临风险。

推荐理由

Veria Labs 披露其 AI 智能体在 XRP Ledger 中发现可无限增发 XRP 的漏洞,读者可了解 AI 漏洞挖掘的实战路径与披露流程。

正文 · 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(kInitialXrp 1017 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,而不变量只看到手续费销毁并提交账本。

总结:

  1. 设置 256 个 offer:发行一个无价值代币,附带 256 个 offer,每个以 256+1 drops 的 XRP 出售。
  2. BookStep 使费用溢出:sum(savedIns) 将 264+256 回绕为 256 drops。
  3. XRPNotCreated 使检查溢出:264−fee 的净变化回绕为 −fee 并通过。
  4. 账本提交:不变量通过,铸造的 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