Stride 空投漏洞解析

在 IBC 之上构建的开发人员和审查 IBC 集成的安全工程师应该仔细审查暴露给恶意 IBC 客户端或渠道的攻击面。

撰文:Will

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

Stride 上的空投

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

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

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

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

func(imIBCModule)OnRecvPacket(

ctxsdk.上下文,

packetchanneltypes.Packet,

relayersdk.AccAddress,

)ibcexported.Acknowledgement{

//注意:确认将在 IB 处理过程中同步写入。

datatransfertypes.FungibleTokenPacketData

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

returnchanneltypes.NewErrorAcknowledgement(错误)

}

[..]

//ibc-gov5hasaMemofieldthatcanstoreforwardinginfo

//对于旧版本的ibc-go,数据必须存储在receiverfield

元数据字符串

ifdata.Memo!=""{//ibc-gov5+

元数据=数据.备忘录

}else{//beforeibc-gov5

元数据=data.Receiver

}

[..]

//解析任何转发信息

packetForwardMetadata,错误:=类型。ParsePacketMetadata(元数据)

iferr!=无{

returnchanneltypes.NewErrorAcknowledgement(错误)

}

//如果parsed metadata为nil,说明没有转发逻辑

//将数据包向下传递到下一个中间件

ifpacketForwardMetadata==nil{

returnim.app.OnRecvPacket(ctx,packet,relayer)

}

//通过将 JSON 元数据字段替换为接收地址来修改数据包数据

//toallowthepackettocontinuedownthestack

新数据:=数据

newData.Receiver=packetForwardMetadata.Receiver

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

iferr!=无{

returnchanneltypes.NewErrorAcknowledgement(错误)

}

新数据包:=数据包

newPacket.Data=bz

//首先将新数据包传递到中间件堆栈

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

如果!ack.成功(){

返回

}

自动驾驶参数:=im.keeper.GetParams(ctx)

//如果传输成功,则路由到相应的模块,如果适用

switchroutingInfo:=packetForwardMetadata.RoutingInfo.(类型){

casetypes.StakeibcPacketMetadata:

[...]

casetypes.ClaimPacketMetadata:

//Ifclaimsroutingisinactive(butthepackethadroutinginfointhememo)返回一个ackerror

[..]

im.keeper.Logger(ctx).Info(fmt.Sprintf("Forwaringpacketfrom%stoclaim",newData.Sender))

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

im.keeper.Logger(ctx).Error(fmt.Sprintf("Errorupdatingairdropclaimfromautopilotfor%s:%s",newData.Sender,err.Error()))

returnchanneltypes.NewErrorAcknowledgement(错误)

}

返回

默认:

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

}

}

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

func(kKeeper)TryUpdateAirdropClaim(

ctxsdk.上下文,

datatransfertypes.FungibleTokenPacketData,

packetMetadatatypes.ClaimPacketMetadata,

)错误{

[..]

//抓取相关地址

senderStrideAddress:=utils.ConvertAddressToStrideAddress(数据.发件人)

ifsenderStrideAddress==””{

returnerrorsmod.Wrapf(sdkerrors.ErrInvalidAddress,fmt.Sprintf("invalidsenderaddress(%s)",data.Sender))

}

newStrideAddress:=packetMetadata.StrideAddress

//更新空投

airdropId:=packetMetadata.AirdropId

k.Logger(ctx).Info(fmt.Sprintf("updatingairdropaddress%s(orig%s)to%sforairdrop%s",

senderStrideAddress,data.Sender,newStrideAddress,airdropId))

returnk.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)看起来有点像这样:

CreatedsolomachineclientonIBCenabledchain[ClientID=06-solomachine-6]

在单机上创建了 tendermint 客户端 [ClientID=07-tendermint-M48f]

启用IBC的链上的初始化连接[ConnectionID = connection-4]

在单机上初始化连接 [ConnectionID = connection-Kinb]

在启用 IBC 的链上确认连接 [ConnectionID=connection-4]

Confirmedconnectiononsolomachine[ConnectionID=connection-Kinb]

在 IBCenabledchain 上初始化的通道 [ChannelID=channel-0]

已初始化频道在单机[ChannelID=channel-wwl6]

已确认通道启用 IBC 链 [ChannelID = channel-0]

确认频道onsolomachine[ChannelID=channel-wwl6]

连接已建立!

第一步,在链 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 贡献者对这个问题的专业处理和快速响应。

微信外链

[1] 原文链接地址:

[2] x/声明:

[3] 加载分配数据:

[4] 理赔记录:

[5] MsgClaimFreeAmount:

[6] x/自动驾驶仪:

[7] 独奏机:

查看原文
  • 赞赏
  • 评论
  • 分享
评论
请输入评论内容
no_comments
暂无评论