> 在 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.FungibleTokenPacketDataiferr:=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.Receiverbz,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.AirdropIdk.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] 独奏机:
Stride 空投漏洞解析
撰文: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 客户端。利用此漏洞相对简单:
漏洞修复
在收到我们的及时报告后,Stride 贡献者迅速从 Airdrop 经销商钱包中取出所有资金,以确保没有资金处于风险之中。实施的长期修复确保 IBC 空投地址更新数据包通过正确的可信 IBC 通道到达。
结论
通过 IBC 对跨链通信的强大支持是 Cosmos 生态系统的独特优势。虽然 IBC 建立在可靠的加密原语之上,但与其安全集成需要对底层信任模型有很好的理解。在 IBC 之上构建的开发人员和审查 IBC 集成的安全工程师应该仔细审查暴露给恶意 IBC 客户端或渠道的攻击面。我们要感谢 Stride 贡献者对这个问题的专业处理和快速响应。