Cosmos · Report #2917368
Replacing ICA active channel during the upgrade and a bit more
How it was found
Summary of Impact
There are 2 potential issues here.
-
Active channel is set on controller during the channel ack. And it's a check than act operation , and it's atomic. But GetOpenActiveChannel only returns the channel if it's open. Not when it's flushing.
-
Active channel is set on host during the channel open confirm but the check if the channel exists is done in try. It does include FLUSH* states but it's not atomic.
Steps to Reproduce
We monitor newly inited channels on the source chain. We iterate over returned channels and run the attack for a matched one. We submit ICA creation once everything is up(see the video) and this place is responsible for performing victim's part. I used version as is because it would change during the upgrade. And the main idea to have this new channel different from the legit channel. The attack consists of 2 steps:
- We do the preparation work. Because check than act operation on the host is not atomic we have a window of opportunity between ICA channel init on the controller and ICA channel open confirm on the host. And during that window we'll do chan init with the ICA port and connection on the controller and chan try on the host.
Then we wait for the victim channel to start upgrading. The script will initiate upgrade proposal for a new encoding. But our channel will have an old encoding. Encoding is needed to deserialize the tx on the host.
- Once the proposal passes through, the attack picks up new upgrade sequence and performs the whole channel update. And in parallel it completes the handshake for the malicious channel.
Legit channel: channel-1, my channel channel-2
I run relayer in parallel too because I need ICA channel to complete like it would do in real world. But also the relaye picks up mu channels. I stop it after legit channel is open but it prints the following message during the handshke:
error Error sending messages {"path_name": "demo", "src_chain_id": "ibc-1", "dst_chain_id": "ibc-0", "src_client_id": "07-tendermint-0", "dst_client_id": "07-tendermint-0", "error": "rpc error: code = Unknown desc = rpc error: code = Unknown desc = failed to execute message; message index: 1: channel open ack callback failed for port ID: icacontroller-cosmos1zr776t6fpjtyx8vqfhn0ene3jzm5q7nc2674d0, channel ID: channel-2: existing active channel channel-1 for portID icacontroller-cosmos1zr776t6fpjtyx8vqfhn0ene3jzm5q7nc2674d0: active channel already set for this owner [
Which is correct. The error comes exactly from here But after once upgrade is started, I complete the handshake it passes:
channel acked
successfully acked channe on the source chainl channel-2 for port icacontroller-cosmos1zr776t6fpjtyx8vqfhn0ene3jzm5q7nc2674d0 and connection connection-0
{"level":"info","msg":"Successful transaction","chain_id":"ibc-0","gas_used":105368,"fees":"3310stake","fee_payer":"\u001bZf\u0007\u00144u \ufffd\ufffd\ufffdx\ufffdh\ufffd\ufffdT\ufffdM\u0012","height":134,"msg_types":["/ibc.core.channel.v1.MsgChannelOpenAck"],"tx_hash":"1E1968F24B19D38D0C1ABA0212884E6D295796660862F99C249002739548EE40"}
u
Then it passes open confirm on the host too. And we set a new channel that has different settings as active for this ICA. (have Go installed, and make sure no simd running)
-
I had to make an adjustment in ibc-go in order to submiit a proposal for upgrade. Took the logic from wasm light client. I was getting an eror that the proposal has to be signed by the gov module(which makes sense). So not sure if that's a bug or authority account has to be set up. Either way either check out my modified forkor set up your account to be a gov authority if possible. So you would be able to submit the upgrade. And build ibc-go
make build. Take a note of the path to simd (in build folder) and change it here (Also just a note that our malicious channel won't be affected by the upgrade because it's not open. ) -
Checkout my relayer ica branch. Only demo folder is needed for this attack. But also a rly binary in path which you can build from the official relayer or my relayer.
-
Checkout https://github.com/unknownfeature/ibc-tools/tree/one, switch to branch
oneand put attached main.go to the root. -
Modify rootFolder in the main.go to point to the correct folder with the scripts.
-
Go to the relayer/examples/demo(rootFolder) and run ./dev-env. And wait until you see this message: "Run 'rly start and run active_channel.go, press ENTER after everything is up"
-
Run main.go, it will print 2 lines and will be waiting. After that open another tab and start
rly start. We need it to finish the ICA handshake. But also it will be trying to finish the handshake we initiated in the attack which will error at first but then once we pass the ack it will be trying to continue our handshake. Which will accomplish the same thing as us but just my little attack might error, and it won't look good for the demo purposes. But the result of the attack will be the same even if the relayer finishes it. So I just wait for it to open the legitimate channel and stop it after it prints " Successfully created new channel {"chain_name": "ibc-1", ". Should be on ibc-1 which means that the handshake is over. -
Once everything is up hit 'ENTER" where dev-env requested it. And look at the output of the attack. It will take some time. I set proposal waiting period to 30s. But also I wait for the ICA channels for 20s.
I also wanted to say that sometimes my chan ack arrived to the source chain faster than the legit one. That's why I sleep here. But it's an interesting use case as well. In some cases active channel will be set to a wrong one during the handshake. Very opportunistic. And who knows how it'll be acting in real life.
Workarounds
Doesn't look like
Supporting Material/References
- active_channel_upgrade.mov - video of the POC
- main.go - the POC
Impact
The app gives opportunities to replace established active channel with the malicious one during the upgrade (or sometimes set a wrong active channel during the handshake) on the ICA. We can't change ICA address, we can't change connection. The only thing we can change is encoding. Which can prevent host chain from deserializing ICA transactions. So it will need another upgrade to be able to work with it. Plus it will require some investigation taking into account that there is no query method to see what's the active channel. So it will take some time to figure it out and make it work again. Plus we can pre setup not one but several channels. And use each for each upgrade.