#35601 [BC-Critical] Consensus algorithm doesn't deduplicate votes, allowing a malicious validator to completely falsify transactions
Submitted on Sep 30th 2024 at 13:59:00 UTC by @throwing5tone7 for Audit Comp | Shardeum: Core II
Report ID: #35601
Report Type: Blockchain/DLT
Report severity: Critical
Target: https://github.com/shardeum/shardus-core/tree/dev
Impacts:
Direct loss of funds
Permanent freezing of funds (fix requires hardfork)
Description
Brief/Intro
When validating a receipt, the new consensus algorithm allows duplicate votes and counts them as though they are distinct. This allows a maliciously modified validator node to submit a receipt that contains only votes from itself, and trick the other nodes into thinking that this is a valid receipt that a majority of validators have voted for. The transaction end state linked to the bogus vote will then be assumed to be agreed upon by a quorum of validators, whereas in fact it is under the attacker's control and no quorum was achieved. The attacking node can fake whatever transaction end state it likes for its vote - so for example, it can create SHM out of thin air or just burn funds from another account. This allows a single malicious validator to falsify transaction states on the blockchain for accounts that are in its shard, leading to a wide range of possible bad outcomes for the network.
Vulnerability Details
The main issue lies in https://github.com/shardeum/shardus-core/blob/23e06ded6744d8521cff9d749c1f1dd482c5fcb6/src/state-manager/TransactionConsensus.ts#L1742 and onwards, we can see that the code here (part of the `verfiyAppliedReceipt` function) is responsible for counting up the votes in the receipt to see if a quorum was reached:
``` for (let i = 0; i < receipt.signaturePack.length; i++) { const sign = receipt.signaturePack[i] if (!executionGroupNodes.has(sign.owner)) continue appliedVoteHash.voteTime = receipt.voteOffsets[i] const signedObject = { ...appliedVoteHash, sign }; if (this.crypto.verify(signedObject, sign.owner)) { validSignatures++; } }
```
You can see that there is no check on whether a single node has voted twice, so the attacker node can just repeat a single signed vote enough times to represent a majority and trick other nodes into thinking that is a legitimate receipt.
Exploit steps
Based on the bug, validators can can be tricked into accepting a receipt that is invalid. In order to exploit this to malicious effect, an attacker needs to be running a node in the network, and to be able to make that node act maliciously. The threat model is that the attacker modifies the validator code subtly, so that it normally behaves as a legitimate validator, but can be made to inject an attacking transaction into the network based on some trigger (in my PoC it is a post endpoint that receives an EVM transaction).
When the attacker's node is performing an attack it does essentially the following:
Create or capture some legitimate transaction to submit to the network (e.g. an EVM transaction to transfer an SHM from one of their accounts to another)
Spread that transaction data to the rest of the nodes in it's shard in the normal way, so that they know that consensus will be happening on the transaction
Immediately create a vote for a new state of the Shardeum accounts that reflects the outcome of the attack (e.g. in my PoC I just add a large amount of SHM to both account balances before applying the legitimate EVM transaction)
Create a receipt by vote-stuffing of this single vote enough times to reach a quorum of the shard, and send it to all of the other validators in the shard
The other validators will typically accept this as a legit receipt for a legit transaction, but their end states for the transaction will not match the voted state, which will mean that they attempt to repair the transaction
At this point, they will ask the attacker's node (the sender of the receipt in question) for the "correct" state data, at which point the attacker's node can reply with it's own bogus state
The other validators accept the bogus state, and whatever effect the attacker wanted to achieve has been committed to the blockchain
Exploitability on a larger network
I have only demonstrated this on a smaller network for now, since I thought that reporting such a critical bug quickly would be more productive than tuning the PoC exploit for larger network sizes. However, I believe the attack will work against larger network sizes based on these assumptions:
The attacker can only affect the accounts that belong to the shard that their validator node processes - however, this should not restrict them, as once they know what shard they process they should be able to create an account for themselves that belongs to the shard (e.g. by creating random accounts repeatedly until one matches their requirements)
The attacker can broadcast the relevant messages to all of the shard participants, ensuring that their bogus receipt is received before any other validators in the shard start gossiping possible results to each other
I assume this because the attacker can send the relevant messages in any order they like and because each of the other nodes delays before doing much processing on a transaction - there are several checks in the `processTransactions` function of `TransactionQueue` which check that the queue entry is a certain age before continuing processing of it. Hence there should be enough time for the attacker's receipt message to arrive before another node gets the chance to create a different receipt.
Given that the attack model for this is a hacked validator node, the attacker's code clearly has all of the data available to know who needs to receive the bogus messages (i.e. who is in their shard)
Impact Details
Using this exploit, the attacker can force the network to accept an invalid state transition for any accounts that are in the shard of their malicious validator node. This means they could:
Create coins out of nothing - this is the effect demo'd in the POC
Empty out balances of victims, i.e. set them to zero without transferring the funds elsewhere
Deploy completely broken / malicious EVM contract code to do whatever they want
Destroy existing contracts (since they can change the state of the contract account) potentially locking / destroying tokens or other contract-managed assets
Change contract storage directly - e.g. to freeze / steal ERC-20 tokens or similar
In essence, they can perform any EVM-level effects on accounts in their shard. The above are examples. There may also be some Shardus chain internal transactions they can fake in a similar way, although I haven't confirmed it.
References
https://youtu.be/8-_fydHRUbU - the video of my PoC
Link to Proof of Concept
https://gist.github.com/byronrthomas/95752480309f6711367895b4dcf30ac6
Proof of Concept
Proof of Concept
See the demo video in https://youtu.be/8-_fydHRUbU for a direct demonstration
The PoC uses a hacked validator node to connect to a network containing legitimate nodes. Most of the PoC functionality is built into this maliciously modified validator codebase. Although a range of effects are possible using this bug, in this PoC I demonstrate the ability to change account balances in illegitimate ways.
PoC demo instructions
In order to recreate the PoC demo, you need to set up a legitimate network that the hacked validator is allowed to join, and modify a validator by using the patches in my GIST.
Configure a legitimate network
NOTE - I assume that the developers can do this another way - what is required is a network running non-modified code that the attacker's node can join (it therefore needs to at least allow connections from localhost IP addresses, which is covered by my config patch). The other requirement is that the attacker has an account on the shard that they validate which has some SHM balance.
To follow the steps I use in the demo video:
Get source code for real shardeum down (https://github.com/shardeum/shardeum) to a folder of your choice, e.g. `LEGIT_Shardeum`
Install dependencies using `npm ci`
Apply the config to use a smaller net for testing, and to allow localhost network addresses for nodes & archivers - `git apply config-small-net-local-nodes.patch`
Apply the patch to genesis.json to grant the attacker some funds - `git apply net-genesis.patch`
Patch a validator codebase to run the attack
Maliciously modify shardus-core code:
Download a fresh copy of shardus-core repo (https://github.com/shardeum/shardus-core) to a folder where you modify it, e.g. `HACKED_shardus-core`
Apply the patch to this `HACKED_shardus-core` folder - `git apply shardus-core.patch`
Run `npm install` to ensure it's all up-to-date with the patches and to fetch all dependencies
Maliciously modify shardeum app code:
Download a fresh copy of shardeum repo (https://github.com/shardeum/shardeum) to a folder where you modify it, e.g. `HACKED_shardeum`
Run `npm ci` to get the initial dependencies installed
Apply the code patch to this `HACKED_shardeum-folder` - `git apply shardeum-src-changes.patch`
Ensure that this hacked shardeum code, uses the hacked shardus-core code rather than the legit dependency - `git apply shardeum-package-changes.patch`
IMPORTANT: this patch assumes that the hacked shardus-core folder can be found at path `../HACKED_shardus-core` - you will need to manually edit package.json to set a different path if it's not in that location
`npm install` to get everything built & up to date
Configure a validator CLI to connect to the legit network but run the maliciously modified code:
Configure validator-cli to talk to local nodes and to run over hacked source (assuming the HACKED_shardeum folder has the relative path shown)
`ln -s "$(cd ../HACKED_shardeum && pwd)" ../validator`
Apply the patch to configure validator to talk to local networks `git apply validator-cli-config.patch`
Run `npm run compile` to get everything up to date and ready to launch
Start up the legitimate nodes in a network
Devs might do this step differently, but to follow along with the demo video, in the `LEGIT_Shardeum` folder launch a network that still has space for one node to join (the attacker's node)
`shardus create-net 10 --no-start && shardus start --dir instances 9`
Start up attacker node - maliciously modified validator
In the validator-cli folder:
Run `operator-cli start`
Wait for network to reach an appropriate state
I wait until all of the nodes have reached active state, and wait until 15 cycles after the network mode is first reported as `processing` according to the monitoring logs
Start up JSON RPC server
Run `npm run start` in JSON-RPC folder
Setup and run attack
Setup a folder for the scripts and infra for attack
Download `poc.js`, `checkBalance.js` and `package.json` from the GIST and put into an attack-scripts folder
Ensure you use node 18.16.1 for this folder
Run `npm install` to fetch all dependencies
Ensure network is in a good state before proceeding, and then:
Run `node checkBalance.js` to see the account initial balance - should be `10000000000000000000000000`
Run `node poc.js 127.0.0.1:9050` - assuming that your hacked validator node has been started at this address & port
This sends a legitimate EVM transaction to the attacker node that transfers 1 wei from attacker to a random address
You will see the transaction in the output of the script, take a note of address in the `to` field of transaction
You should see a log that says "ATTACK working it's way through the network - poll for results."
Wait a minute or so and then run `node checkBalance.js`
If the attack succeeds, attacker's balance will have increased
Using the recipient address captured from the attack output, run `node checkBalance.js RECIPIENT_ADDRESS`
The recipient's balance has also increased
So SHM has been created out of thin air - since post_balance_sender + post_balance_recipient > pre_balance_sender + pre_balance_recipient!
PoC deep dive
NOTE: this is very much a PoC, so the code is pretty rough and mostly copy-pasted and then modified versions of the original code. There is a lot of mechanics needed to make the validator act as a legitimate validator when not performing the attack, so I haven't attempted to extract a huge amount of code out of shardeum / shardus to provide a completely independent script that would perform the attack by sending messages itself.
The attack uses a legitimate EVM transaction (transfer 1 wei from attacker account to a random wallet) but pretends that this leads to an EVM state where both accounts end up with an extra `100000000000000000000000000` wei SHM. This is achieved by the line
``` wrappedEVMAccount.account.balance = (wrappedEVMAccount.account.balance || BigInt(0)) + BigInt(100000000000000000000000000) ```
Which is in my `ATTACK_apply` function in the patch applied to the Shardeum app codebase (see shardeum-src-changes.patch).
In order to run the attack the PoC code patches to Shardeum and Shardus core achieve the following:
Expose a new web endpoint on the validator node - line starting `shardus.registerExternalPost('run-attack', externalApiMiddleware, async (req, res) => {` in my Shardeum app patch (see shardeum-src-changes.patch)
This endpoint receives a normal EVM transaction, and then calls into shardus core via a newly created function `shardus.run_ATTACK`
Within the implementation of `run_ATTACK` inside the patch to the shardus-core codebase (see shardus-core.patch in the GIST) the code does the following:
Construct a new QueueEntry object for further processing `TransactionQueue.ATTACKER_createQueueEntry` and return it - does not add this to any processing queues
Save this queue entry to `TransactionQueue.ATTACKERS_transaction` so that if any node asks for it we can return the details
Preprocess it to add sufficient data to be able to create a vote - `TransactionQueue.ATTACKER_preprocessTransaction`
Within this processing, it calls back into the Shardeum app to generate a bogus result by calling `app.ATTACK_apply` - this function does everything as normal but also adds 100000000000000000000000000 wei to each account it processes
Pass this into another routine to construct a legitimate vote (but based on this bogus EVM end state) - `ATTACKER_createVote`
Does not send the vote to other participants, instead creates a bogus receipt that is a vote stuffing (i.e. repetition) of this one single legitimate vote - `ATTACKER_tryProduceReceipt`
Gossips this to all of the other participants
If any other nodes ask for details on the bogus transaction, return the copy saved into `TransactionQueue.ATTACKERS_transaction`