最初のステップでは、チェーン A の IBC ライト クライアントがチェーン B 上に作成され、その逆も同様です。IBC クライアントは、リモート チェーンの状態を追跡および検証するために使用されるクライアント ID によって一意に識別されます。クライアントが作成されると、4 ウェイ ハンドシェイクで開始される接続を介して接続できます。これにより、A 上のチェーン B のライト クライアントを持つチェーン A 上に ConnectionEnd が作成され、B 上のチェーン A のライト クライアントを持つ別の ConnectionEnd がチェーン B 上に作成されます。接続は一度作成されると永続的となり、2 つのライト クライアントによって暗号化されます。
Stride Airdrop 脆弱性分析
作者: ウィル
前書き: この記事では、Jump Crypto が Stride エアドロップ プログラムで発見した脆弱性について説明します。Stride は、Cosmos エコシステムでのリキッド ステーキングに使用される Cosmos チェーンです。この問題により、攻撃者は Stride 上の未取得のエアドロップをすべて盗む可能性があります。発見時点では、160万個以上のSTRD(約400万ドル相当)が危険にさらされていた。 Jump はこの脆弱性を Stride の寄稿者に非公開で報告し、その後問題は修正され、私たちの取り組みの結果として悪意のあるエクスプロイトは発生していません。元のリンクアドレス [1]
ストライドでエアドロップ
Stride は、ネットワーク活動を奨励し、広範な関係者グループにわたるガバナンスを分散化するために、ネイティブ [$STRD] トークンの大規模なエアドロップを定期的に実施しています。エアドロップの割り当てと要求のためのコードは x/claim にあります。 [2] モジュールに実装されています。エアドロップの割り当ては LoadAllocationData を通じて行われます [3] アドレスとエアドロップ割り当てを含む割り当てファイルをロードする関数によって定義されます。ほとんどのエアドロップでは、読み込まれるアドレスは Osmosis や Juno などの他の Cosmos チェーン上のユーザーを表すため、コードは最初に utils.ConvertAddressToStrideAddress 関数を使用してアドレスを Stride アドレスに変換します。
エアドロップ内のアカウントごとに、モジュールは ClaimRecord を作成します [4] 、特定のエアドロップのエアドロップ識別子、変換されたアドレス、およびユーザーに割り当てられたトークンの量が含まれます。 ClaimRecord を作成した後、対応する Stride アドレスを持つユーザーはチェーンに MsgClaimFreeAmount を送信できます。 [5] エアドロップを受け取りに来てください。
ただし、utils.ConvertAddressToStrideAddress 関数が Evmos アドレスをアクセスできない Stride アドレスにマップするため、この実装は最近の EVMOS エアドロップでは機能しませんでした。これは、EVMOS アドレスはコイン タイプ 60 を使用して導出されるのに対し、ストライド アドレスはコイン タイプ 118 を使用して導出されるためです。
影響を受けるユーザーが引き続きエアドロップを請求できるようにするために、チームは、対応する EVMOS アカウントからのクロスチェーン IBC メッセージを介して、請求されていない ClaimRecord のターゲット アドレスを更新する機能を追加しました。この更新メカニズムは、x/autopilot モジュールの一部として実装されています。 x/オートパイロット [6] 着信 IBC ICS-20 送信をインターセプトし、そのメモまたはレシーバー フィールドから Stride 固有の命令を抽出しようとします (レシーバー フィールドは、v5 より前の IBC バージョンではメモ フィールドとしても機能します)。
func(imIBCModule)OnRecvPacket(
ctxsdk.コンテキスト、
パケットチャネルタイプ.パケット、
relayersdk.AccAddress、
)ibcexported.Acknowledgement{
// 注: 確認応答は IBChandler の実行中に同期的に書き込まれます。
datatransfertypes.FungibleTokenPacketData
iferr:=transfertypes.ModuleCdc.UnmarshalJSON(packet.GetData(),&data);err!=nil{
returnchanneltypes.NewErrorAcknowledgement(err)
}
[..]
//ibc-gov5hasa転送情報を保存できるメモフィールド
//古いバージョンの fibc-go の場合、データは受信フィールドに保存する必要があります
メタデータ文字列
ifdata.Memo!=""{//ibc-gov5+
メタデータ=データ.メモ
}else{//beforeibc-gov5
メタデータ=データ.レシーバー
}
[..]
//parseoutanyforwardinginfo
packetForwardMetadata,err:=types.ParsePacketMetadata(メタデータ)
iferr!=nil{
returnchanneltypes.NewErrorAcknowledgement(err)
}
//解析されたメタデータが nil の場合、転送ロジックがないことを意味します
//パケットを次のミドルウェアに渡します
ifpacketForwardMetadata==nil{
returnim.app.OnRecvPacket(ctx,packet,relayer)
}
//JSON メタデータ フィールドを受信者のアドレスに置き換えてパケット データを変更します
//パケットがスタックの下に継続できるようにするため
新しいデータ:=データ
newData.Receiver=packetForwardMetadata.Receiver
bz,err:=transfertypes.ModuleCdc.MarshalJSON(&newData)
iferr!=nil{
returnchanneltypes.NewErrorAcknowledgement(err)
}
newPacket := パケット
newPacket.Data=bz
//最初に新しいパケットをミドルウェアスタックに渡します
ack:=im.app.OnRecvPacket(ctx,newPacket,relayer)
if!ack.Success(){
返す
}
autopilotParams:=im.keeper.GetParams(ctx)
// 転送が成功した場合は、該当する場合は対応するモジュールにルーティングします
switchroutingInfo:=packetForwardMetadata.RoutingInfo.(type){
casetypes.StakeibcPacketMetadata:
[...]
casetypes.ClaimPacketMetadata:
//クレームルーティングが非アクティブな場合(ただし、テーマ内のルーティング情報を含むパケット)はACKエラーを返します
[..]
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("Errorupdating airdropclaimfromautopilotfor%s:%s",newData.Sender,err.Error()))
returnchanneltypes.NewErrorAcknowledgement(err)
}
返す
デフォルト:
returnchanneltypes.NewErrorAcknowledgement(errorsmod.Wrapf(types.ErrUnsupportedAutopilotRoute,"%T",routingInfo))
}
}
含まれているメタデータが受信転送がエアドロップ要求であることを示している場合、モジュールは TryUpdateAirdropClaim 関数を呼び出します。
func(kKeeper)TryUpdateAirdropClaim(
ctxsdk.コンテキスト、
datatransfertypes.FungibleTokenPacketData、
packetMetadatatypes.ClaimPacketMetadata、
)エラー{
[..]
//グラブル関連アドレス
senderStrideAddress:=utils.ConvertAddressToStrideAddress(data.Sender)
ifsenderStrideAddress==""{
returnerrorsmod.Wrapf(sdkerrors.ErrInvalidAddress,fmt.Sprintf("無効な送信者アドレス(%s)",data.Sender))
}
newStrideAddress:=packetMetadata.StrideAddress
//エアドロップを更新する
airdropId:=packetMetadata.AirdropId
k.Logger(ctx).Info(fmt.Sprintf("updating airdropaddress%s(orig%s)to%sforairdrop%s",
senderStrideAddress,data.Sender,newStrideAddress,airdropId))
returnk.claimKeeper.UpdateAirdropAddress(ctx,senderStrideAddress,newStrideAddress,airdropId)
}
この関数は、送信側 IBC パケットのアドレスを senderStrideAddress という名前のストライド アドレスに変換し、パケット メタデータから airdropId と新しいエアドロップ アドレス newStrideAddress を抽出します。次に、UpdateAirdropAddress を呼び出して、senderStrideAddress と airdropId の組み合わせに一致するオープンな ClaimRecord を新しいアドレスに更新します。
ClaimRecord の更新により、newStrideAddress がエアドロップを要求できるようになりました。この更新メカニズムは、IBC パケット内で指定された送信者アドレスによってのみ保護されることに注意することが重要です。 Stride は、エアドロップへの更新が実際の受信者によってトリガーされたことを確認するために他の検証を実行しません。
これがなぜ深刻な脆弱性なのかを理解するには、ブロックチェーン間通信プロトコルである IBC を詳しく調べる必要があります。
IBC セキュリティ
IBC は、軽量のクライアントベースのクロスチェーン通信メカニズムです。古典的なネットワーク プロトコルと同様に、コア IBC モジュール は多くの低レベルの詳細を抽象化し、開発者がその上に独自の統合を簡単に構築できるようにします。 1 つの IBC 対応チェーン (チェーン A) を別の IBC 対応チェーン (チェーン B) に接続すると、次のようになります。
作成されたソロマシンクライアントIBCenabledchain[ClientID=06-solomachine-6]
テンダーミントクライアントソロマシンを作成しました[ClientID=07-tendermint-M48f]
IBCenabledchain で初期化された接続 [ConnectionID=connection-4]
ソロマシン上で初期化された接続[ConnectionID=connection-Kinb]
IBCenabledchain で接続を確認しました [ConnectionID=connection-4]
ソロマシンで接続を確認しました[ConnectionID=connection-Kinb]
IBCenabledchain で初期化されたチャネル [ChannelID=channel-0]
ソロマシンで初期化されたチャネル[ChannelID=channel-wwl6]
IBCenabledchain で確認済みチャネル[チャネル ID=channel-0]
確認済みチャンネルオンソロマシン[ChannelID=channel-wwl6]
接続が確立されました!
最初のステップでは、チェーン A の IBC ライト クライアントがチェーン B 上に作成され、その逆も同様です。IBC クライアントは、リモート チェーンの状態を追跡および検証するために使用されるクライアント ID によって一意に識別されます。クライアントが作成されると、4 ウェイ ハンドシェイクで開始される接続を介して接続できます。これにより、A 上のチェーン B のライト クライアントを持つチェーン A 上に ConnectionEnd が作成され、B 上のチェーン A のライト クライアントを持つ別の ConnectionEnd がチェーン B 上に作成されます。接続は一度作成されると永続的となり、2 つのライト クライアントによって暗号化されます。
接続を介した通信もさまざまなチャネルに分割されます。チャネルは、基礎となる接続、送信元ポートおよび宛先ポートによって識別されます。各ポートは、IBC を介して接続された対応するチェーン上のモジュールを識別します。接続に関連付けられた ChannelEnd は両方のチェーン上に作成され、channel-id によって識別されます。これで、確立されたチャネルを通じて 2 つのチェーン間でデータを転送できるようになります。
IBC はデフォルトではパーミッションレス プロトコルであることを覚えておくことが重要です。これは、誰でも事前の認可や承認なしに 2 つの IBC 対応チェーンを接続できることを意味します。実際、IBC はいわゆる Solo Machine をサポートしています。 [7] 標準では、クライアントはブロックチェーンを表しませんが、単一のホストまたはマシンを表します。 IBC パケットの内容は送信者 (通常はソース チェーン上のソース モジュール) によって完全に制御されるため、受信 IBC パケットに対して特権操作を実行するモジュールは、メッセージが信頼できるチャネルからのものであることを常に検証する必要があります。
脆弱性
ただし、Stride に関する限り、x/autopilot モジュールにはチャネル チェックがありません。このコードは、特定の送信者アドレスを持つ ICS-20 IBC パケットは、そのアドレスを制御できる人だけが送信できることを前提としています。これは、EVMOS などの信頼できるパートナー チェーン上のトランスポート モジュールのみを考慮した場合に当てはまりますが、攻撃者は完全に制御された IBC パケット データを送信するだけで、その制御下にある悪意のある IBC クライアントを使用することができます。この脆弱性の悪用は比較的簡単です。
## バグの修正
私たちの即時報告を受け取ると、Stride の寄稿者は、資金が危険にさらされていないことを確認するために、Airdrop 再販業者のウォレットからすべての資金をすぐに引き出しました。 IBC エアドロップ アドレス更新パケットが正しい信頼された IBC チャネル経由で到着するようにするための長期修正が実装されました。
## 結論は
IBC を通じたクロスチェーン通信の強力なサポートは、Cosmos エコシステム独自の利点です。 IBC は堅牢な暗号化プリミティブに基づいて構築されていますが、IBC と安全に統合するには、基礎となる信頼モデルをよく理解する必要があります。 IBC 上に構築する開発者および IBC 統合を検討しているセキュリティ エンジニアは、悪意のある IBC クライアントまたはチャネルにさらされる攻撃対象領域を慎重に検討する必要があります。この問題に対する専門的な対応と迅速な対応をしていただいた Stride の寄稿者に感謝いたします。