Prior to hardfork 26 (and since hardfork 20)[1], the implementation of resource credits in Hive had multiple security issues stemming from a logic bug in the resource credits implementation:
- Anyone could transact using any another account's resource credits
- This could be used to spam the chain with useless transactions
- This could be used to prevent any user from transacting by using all their resource credits
- An attacker who compromised an account could prevent the recovery system from being used to recover the account
Thanks to @blocktrades and the Hive developers for working to get these issues fixed! :)
This issue has not been fixed on Steem, or any other post-HF20 fork of Steem.
The rest of this post is quite technical and assumes familiarity with how Hive and resource credits work.
Cause
Both of these issues stemmed from essentially the same root cause: the way resource credits were calculated for recover_account
, the operation that can be used by the rightful owner of a compromised account to recover it. The Hive recovery flow looks like:
- An attacker compromises an account and broadcasts a
account_update
to change the owner key. The compromised account pays the resource credits for this transaction. - The rightful owner tells their recover partner (usually the account who created their account) about the compromise, and the public key for their new owner key.
- The recover partner broadcasts a
request_account_recovery
operation. The recovery partner pays the resource credits for this transaction. - The rightful owner broadcasts a
recover_account
operation, signed with their old owner key and the new owner key they told their recovery partner about.
We can "attack" our own accounts and use the recovery system to exploit this issue without actually having our account compromised.
What account should pay resource credits for step 4? Here's how pre-HF25 Hive does it:
account_name_type operator()( const recover_account_operation& op )const
{
for( const auto& account_weight : op.new_owner_authority.account_auths )
return account_weight.first;
for( const auto& account_weight : op.recent_owner_authority.account_auths )
return account_weight.first;
return op.account_to_recover;
}
op.new_owner_authority
is the authority that the compromised owner authority will be replaced with; op.recent_owner_authority
is the previous owner authority that the owner controlled before it was changed by the person who compromised the account.
We can take advantage of the way authorities work to make a transaction such that the problematic code assigns the RC cost to a user of our choice. To understand, let's first have a digression on Hive authorities.
How authorities work
In Hive, an authority is a set of public keys and accounts. Each key/account has a weight. A transaction is considered to be signed by an authority if the sum of the weights of the keys/accounts that signed it are at least the authority's weight threshold. Hive accounts have three authorities: owner, active, and posting[2]. The owner authority can be used to completely control the account.
Abusing pointless authorities
We can create authorities that are pointless complicated. For example:
Subauthority | Weight |
---|---|
Key 1 | 10 |
@alice | 1 |
Weight threshold | 10 |
The @alice subauthority is pointless and could be removed, but the Hive software doesn't require the absence of extraneous subauthorities.
The bug
Okay, here's the bug: the first account subauthority (if it exists) specified when recovering an account pays the resource credits for the whole transaction, even if transaction wasn't signed by that account. That's what the const auto& account_weight : op.new_owner_authority.account_auths
part of the code does: check for an account authority to charge the RCs to.
An attacker can recover an account they own to a new owner authority. For example, if they use the example authority above, the code that determines the RC cost of a recover_account
operation will still force @alice to pay the RCs for the account recovery, even though @alice didn't sign the transaction.
Making our attack more efficient
A single Hive transaction can have multiple, potentially unrelated, operations in it. For example, if you wanted to trade HIVE for HBD with someone, you can create a transaction where you send HIVE and the counterparty sends HBD. The two-operation transaction would only be valid if it was signed by both parties.
The account charged RCs for the first operation in a transaction gets charged for all of them. So by including spam operations in the same transaction as a recover_account
we can drain as much RCs as can fit into single block. By repeating this, we can drain large amounts of RCs, while only paying for relatively small request_account_recovery
operations ourself. See this example transaction where I drained nearly all of the RCs from @steemit (I control @memos):
If we wanted to do this repeatedly to drain RCs, there are two optimizations we'd need to make:
- Instead of paying RCs for the
request_account_recovery
operation, we can tack them onto the end of therecover_account
operation. We then only need to pay for the recovery request for the first iteration. - An account can only be recovered once every 3603 seconds (1 hour + 1 block). To continuously exploit this, we could create 1201 accounts, and recover one each block. Normally, one has to pay of fee of 3 HIVE to create an account, but we can also use
claim_account
andcreate_claimed_account
to create an account using resource credits. By using this exploit with one account a few times, we can then create as many accounts as we need.
Fix
When fixing this issue we need to take care not to:
- Allow an attacker to prevent an account from being recovered by draining its RCs
- Allow free spam transactions
I wrote a patch to fix this issue by just making transactions with a single account recovery operation not cost RCs. Since each recover_account
has a matching request_account_recovery
(where RCs are paid by the recovery partner), all account recoveries have RCs paid on them, which I thought would prevent spamming the chain with frivolous account recoveries.
It turns out my fix didn't fully fix the issue. You can make arbitrarily big transactions that only have a single recover_account
operation, either by having your transaction signed with a lot of signatures or making your new owner authority really long. Both issues were fixed.
Conclusion
I hope you enjoyed reading about this issue as much as I enjoyed finding it! I've been looking into the security of the hived code recently, and noticed this issue while doing so. If you have any questions, I'd be happy to answer them.
The text of this document is dedicated to the public domain under the CC0 Public Domain Dedication.
@smitop! The Hive.Pizza team manually upvoted your post.
Please vote for pizza.witness!
Dear @smitop,
May I ask you to review and support the new proposal (https://peakd.com/me/proposals/240) so I can continue to improve and maintain this service?
You can support the new proposal (#240) on Peakd, Ecency,
Thank you!
Congratulations @smitop! You have completed the following achievement on the Hive blockchain and have been rewarded with new badge(s):
Your next target is to reach 23000 upvotes.
You can view your badges on your board and compare yourself to others in the Ranking
If you no longer want to receive notifications, reply to this comment with the word
STOP
Check out the last post from @hivebuzz:
Support the HiveBuzz project. Vote for our proposal!