Stride空投漏洞解析

Stride 定期对其原生 [$STRD]代币进行大量空投,以激励网络活动并在广泛的各方群体中分散治理。分配和领取空投的代码在 x/claim模块中实现。空投分配是通过 LoadAllocationData函数定义的,该函数加载一个包含地址和空投分配的分配文件。

前言:本文描述了 Jump Crypto 在 Stride 空投程序中发现的一个漏洞:Stride 是一个 Cosmos 链,用于在 Cosmos 生态系统中进行流动性质押。这个问题可能会让攻击者窃取 Stride 上所有无人认领的空投。在发现时,超过 160 万 STRD(相当于大约 400 万美元)处于危险之中。Jump 私下向 Stride 贡献者报告了该漏洞,该问题现已修复,通过努力没有发生恶意利用的事件。

Stride 上的空投

Stride 定期对其原生 [$STRD]代币进行大量空投,以激励网络活动并在广泛的各方群体中分散治理。分配和领取空投的代码在 x/claim模块中实现。空投分配是通过 LoadAllocationData函数定义的,该函数加载一个包含地址和空投分配的分配文件。对于大多数空投,加载的地址描述了其他 Cosmos 链上的用户,例如 Osmosis 或 Juno,因此代码首先使用 utils.ConvertAddressToStrideAddress 函数将它们转换为 Stride 地址。

对于空投中的每个帐户,该模块都会创建一个 ClaimRecord,其中包含特定空投的空投标识符、转换后的地址以及分配给用户的代币数量。创建 ClaimRecord 后,具有相应 Stride 地址的用户可以通过向链发送 MsgClaimFreeAmount来领取他们的空投。

但是,此实现在最近的 EVMOS 空投期间不起作用,因为 utils.ConvertAddressToStrideAddress 函数将 Evmos 地址映射到不可访问的 Stride 地址。这是因为 EVMOS 地址是使用硬币类型 60 派生的,而 Stride 地址是使用硬币类型 118 派生的。 

为了让受影响的用户仍然可以领取空投,该团队添加了通过跨链 IBC 更新无人认领的 ClaimRecord 的目标地址的功能来自相应 EVMOS 帐户的消息。此更新机制作为 x/autopilot 模块的一部分实现。x/autopilot拦截传入的 IBC ICS-20 传输并尝试从其备忘录或接收方字段中提取特定于 Stride 的指令(接收方字段在 v5 之前的 IBC 版本中兼作备忘录字段):

func (im IBCModule) OnRecvPacket(

 ctx sdk.Context,

 packet channeltypes.Packet,

 relayer sdk.AccAddress,

) ibcexported.Acknowledgement {

 // NOTE: acknowledgment will be written synchronously during IBC handler execution.

 var data transfertypes.FungibleTokenPacketData

 if err := transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(), &data); err != nil {

  return channeltypes.NewErrorAcknowledgement(err)

 }

 [..]

 // ibc-go v5 has a Memo field that can store forwarding info

 // For older version of ibc-go, the data must be stored in the receiver field

 var metadata string

 if data.Memo != "" { // ibc-go v5+

  metadata = data.Memo

 } else { // before ibc-go v5

  metadata = data.Receiver

 }

 [..]

 // parse out any forwarding info

 packetForwardMetadata, err := types.ParsePacketMetadata(metadata)

 if err != nil {

  return channeltypes.NewErrorAcknowledgement(err)

 }

 // If the parsed metadata is nil, that means there is no forwarding logic

 // Pass the packet down to the next middleware

 if packetForwardMetadata == nil {

  return im.app.OnRecvPacket(ctx, packet, relayer)

 }

 // Modify the packet data by replacing the JSON metadata field with a receiver address

 // to allow the packet to continue down the stack

 newData := data

 newData.Receiver = packetForwardMetadata.Receiver

 bz, err := transfertypes.ModuleCdc.MarshalJSON(&newData)

 if err != nil {

  return channeltypes.NewErrorAcknowledgement(err)

 }

 newPacket := packet

 newPacket.Data = bz

 // Pass the new packet down the middleware stack first

 ack := im.app.OnRecvPacket(ctx, newPacket, relayer)

 if !ack.Success() {

  return ack

 }

 autopilotParams := im.keeper.GetParams(ctx)

 // If the transfer was successful, then route to the corresponding module, if applicable

 switch routingInfo := packetForwardMetadata.RoutingInfo.(type) {

 case types.StakeibcPacketMetadata:

  [...]

 case types.ClaimPacketMetadata:

  // If claim routing is inactive (but the packet had routing info in the memo) return an ack error

  [..]

  im.keeper.Logger(ctx).Info(fmt.Sprintf("Forwaring packet from %s to claim", newData.Sender))

  if err := im.keeper.TryUpdateAirdropClaim(ctx, newData, routingInfo); err != nil {

   im.keeper.Logger(ctx).Error(fmt.Sprintf("Error updating airdrop claim from autopilot for %s: %s", newData.Sender, err.Error()))

   return channeltypes.NewErrorAcknowledgement(err)

  }

  return ack

 default:

  return channeltypes.NewErrorAcknowledgement(errorsmod.Wrapf(types.ErrUnsupportedAutopilotRoute, "%T", routingInfo))

 }

}

如果包含的元数据表明传入传输是空投声明,则模块调用 TryUpdateAirdropClaim 函数:

func (k Keeper) TryUpdateAirdropClaim(

 ctx sdk.Context,

 data transfertypes.FungibleTokenPacketData,

 packetMetadata types.ClaimPacketMetadata,

) error {

 [..]

 // grab relevant addresses

 senderStrideAddress := utils.ConvertAddressToStrideAddress(data.Sender)

 if senderStrideAddress == "" {

  return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, fmt.Sprintf("invalid sender address (%s)", data.Sender))

 }

 newStrideAddress := packetMetadata.StrideAddress

 // update the airdrop

 airdropId := packetMetadata.AirdropId

 k.Logger(ctx).Info(fmt.Sprintf("updating airdrop address %s (orig %s) to %s for airdrop %s",

  senderStrideAddress, data.Sender, newStrideAddress, airdropId))

 return k.claimKeeper.UpdateAirdropAddress(ctx, senderStrideAddress, newStrideAddress, airdropId)

}

函数转换发送方 IBC 数据包的地址到名为 senderStrideAddress 的 Stride 地址,并从数据包元数据中提取 airdropId 和新的空投地址 newStrideAddress。然后它调用 UpdateAirdropAddress 来更新一个打开的 ClaimRecord,该记录与 senderStrideAddress 和 airdropId 的组合匹配到新地址。

随着 ClaimRecord 的更新,newStrideAddress 现在可以领取空投了。需要注意的重要一点是,此更新机制仅受 IBC 数据包内指定的发件人地址的保护。Stride 不执行任何其他验证来确保空投的更新是由真正的接收者触发的。

要了解为什么这是一个严重的漏洞,我们需要仔细研究 IBC,即区块链间通信协议。

IBC 安全

IBC 是一种基于轻客户端的跨链通信机制。与经典网络协议类似,核心 IBC 模块抽象了许多底层细节,使开发人员可以轻松地在其上构建自己的集成。将一个支持 IBC 的链(链 A)连接到另一个支持 IBC 的链(链 B)看起来有点像这样:

Created solo machine client on IBC enabled chain [Client ID = 06-solomachine-6]

Created tendermint client on solo machine [Client ID = 07-tendermint-M48f]

Initialized connection on IBC enabled chain [Connection ID = connection-4]

Initialized connection on solo machine [Connection ID = connection-Kinb]

Confirmed connection on IBC enabled chain [Connection ID = connection-4]

Confirmed connection on solo machine [Connection ID = connection-Kinb]

Initialized channel on IBC enabled chain [Channel ID = channel-0]

Initialized channel on solo machine [Channel ID = channel-wwl6]

Confirmed channel on IBC enabled chain [Channel ID = channel-0]

Confirmed channel on solo machine [Channel ID = channel-wwl6]

Connection established!

第一步,在链 B 上创建链 A 的 IBC 轻客户端,反之亦然. IBC 客户端由其客户端 ID 唯一标识,用于跟踪和验证远程链的状态。创建客户端后,它们可以通过一个连接来连接,该连接是通过四次握手启动的。这在链 A 上创建了一个 ConnectionEnd,链 B 的轻客户端在 A 上,另一个在链 B 上,链 A 的轻客户端在 B 上。连接一旦创建就会持久,并受到两个轻客户端的加密保护。

通过连接进行的通信还分为不同的通道。通道由底层连接以及源端口和目标端口标识。每个端口标识通过 IBC 连接的相应链上的一个模块。与 Connection 关联的 ChannelEnd 在两个链上创建并通过 channel-id 标识。现在可以通过已建立的通道在两个链之间传输数据。

重要的是要记住,默认情况下 IBC 是一种无需许可的协议。这意味着任何人都可以连接任何两条支持 IBC 的链,而无需事先授权或批准。实际上,IBC 支持所谓的 Solo Machines[7] 标准,客户端不代表区块链,而是代表单个主机或机器。由于 IBC 数据包内容完全由发送方(通常是源链上的源模块)控制,因此根据传入的 IBC 数据包执行特权操作的模块始终需要验证消息是否来自可信通道。

漏洞

然而,就 Stride 而言 x/autopilot 模块中缺少通道检查。该代码假定具有特定发件人地址的 ICS-20 IBC 数据包只能由对该地址有控制权的人发送。如果我们只考虑 EVMOS 等可信赖合作伙伴链上的传输模块,这是正确的,但攻击者可以简单地发送完全受控的 IBC 数据包数据,以使用他们控制下的恶意 IBC 客户端。利用此漏洞相对简单:

  1. 创建恶意 IBC 客户端
  2. 使用恶意客户端 Craft 创建到 Stride IBC 传输模块的 IBC 通道,
  3. 并使用无人认领的 ClaimRecords 的地址作为发件人字段发送恶意 IBC 传输。使用 ClaimMetadata 备忘录字段触发自动驾驶并将空投地址更新为攻击者控制的 Stride 帐户。
  4. 通过向 x/claim 模块发送 MsgClaimFreeAmount 来窃取空投

漏洞修复

在收到我们的及时报告后,Stride 贡献者迅速从 Airdrop 经销商钱包中取出所有资金,以确保没有资金处于风险之中。实施的长期修复确保 IBC 空投地址更新数据包通过正确的可信 IBC 通道到达。

结论

通过 IBC 对跨链通信的强大支持是 Cosmos 生态系统的独特优势。虽然 IBC 建立在可靠的加密原语之上,但与其安全集成需要对底层信任模型有很好的理解。在 IBC 之上构建的开发人员和审查 IBC 集成的安全工程师应该仔细审查暴露给恶意 IBC 客户端或渠道的攻击面。我们要感谢 Stride 贡献者对这个问题的专业处理和快速响应。

(声明:请读者严格遵守所在地法律法规,本文不代表任何投资建议)

(0)
上一篇 2023年5月21日 下午1:18
下一篇 2023年5月21日 下午7:54

相关推荐

  • Web3.0世界日报(11月15日)

    加密社区用户:Crypto.com出现提款延迟。以太坊自合并以来供应量实现通缩超5900枚ETH。巴哈马最高法院已批准两名普华永道清算人监督FTX资产清算。

    2022年11月15日
    878
  • 剖析Web3用户体验四个层次:如何创造良好的用户体验?

    考虑一个标准的应用程序。不是Web3的,只是我们经常使用的智能手机上的流行应用程序。这款应用的实际UI只是一长串体验的最后一个元素,从现实世界开始,穿过无数的物理空间,进入数字交互,经过一大堆不同的硬件和软件,直到最终发现自己的拇指停留在别人设计的按钮上。

    2022年9月21日
    435
  • 扩容终局之外,Optimistic Rollup相比ZK Rollup有哪些用例优势?

    a16z 的 @NoahCitron 认为 Optimistic Rollup 可能更适合高吞吐量和低可组合性应用程序,链上游戏就是这类用例。他提到 ZK Rollup 可能会在 DeFi 等需要原子可组合性的应用程序中胜出。如果一个应用程序并不真正需要与其他应用程序组合,那么将它隔离在自己的执行环境中是合理的。

    2023年7月27日
    309

发表回复

登录后才能评论
微信

联系我们
邮箱:whylweb3@163.com
微信:gaoshuang613