Vulnerability Disclosure: Wasting ViaBTC's 60 EH/s hashrate by sending a P2P message
Sloppy Pseudo Verification (SPV) mining
Wednesday, March 20, 2024In January, while investigating a misbehaving client on the Bitcoin P2P network, I found a vulnerability in ViaBTC’s, the fourth largest Bitcoin mining pool, SPV mining code that allowed a remote attacker to waste ViaBTC’s 60 EH/s hashrate by sending a single, crafted Bitcoin P2P message. I responsibly disclosed this to ViaBTC, and they awarded a bug bounty of 2000 USDT.
Improper Input Validation in the SPV mining module of the ViaBTC mining server (not fixed on GitHub, fixed in a closed-source version) allows a remote attacker to waste the pools hashrate by letting it mine on an old block (i.e. DoS) by sending a modified, old block via the P2P network.
ViaBTC’s SPV mining module and Bitcoin P2P client, bitpeer, did not check the merkle root of received blocks, allowing an attacker to make ViaBTC’s miners mine on an old block by sending a modified previous block. While ViaBTC fixed this vulnerability in their systems, the open source viabtc_mining_server is still affected.
ViaBTC Mining Server
In March 2021, ViaBTC published a version of its mining server on GitHub. While this repository hasn’t been updated since summer 2021, the underlying software, likely with a few modifications and patches, is still in use by ViaBTC. The mining server is made up of multiple modules. For the disclosed vulnerability, the interesting modules are the bitpeer module and the jobmaster module. The jobmaster module is responsible for producing mining jobs by talking to a Bitcoin Core node. The bitpeer module is a Bitcoin P2P client and connects to multiple other nodes on the Bitcoin P2P network. The bitpeer client broadcast ViaBTC’s newly found blocks to Bitcoin nodes and notifies the jobmaster about new blocks received over the network.
SPV mining
Mining pools want to minimize the time they spent mining on an old block, when a new one has been
found by another pool. To quickly switch to a new block, ViaBTC’s bitpeer module sends details, like the new block
height and the block hash, to the jobmaster. If the new block height is current block height + 1
, the jobmaster switches to SPV (Simple Payment Verification) mining mode. In SPV mining mode, a mining job is issued to miners
without having validated the new block. Without having validated the transactions in the previous
block, the SPV mining job can only be an empty block (only a coinbase transaction).
Vulnerability
The process_block()
function in bp_peer.c
is called when a new block message arrives over the
P2P network. In this function, it’s first checked that the message contains a block header with enough
proof-of-work (higher than the current difficulty requirement). Then, the BIP-34 block height is
extracted from the coinbase. If this height is larger than the best know height, the block is send
to the jobmaster by calling send_block_nitify()
.
Here, checking that the block header has a valid and enough proof-of-work to be an SPV-valid block defends against most attacks. The cost of mining a block header with enough proof-of-work is high. For an attacker, it does not make sense to spend that hashrate to attack ViaBTC with an otherwise invalid block. A rational actor would use the hashrate to mine for them selves.
However, ViaBTC only checks the header. They don’t verify that the transactions in the block match the merkle root in the header. This means, the coinbase transaction and thus the BIP-34 block height in the coinbase transaction can be modified. Since they use the height in the coinbase to determine if they should SPV mine, the pool can be tricked into mining on a valid, but old, header with an arbitrary coinbase transaction attached.
Exploit
To exploit this vulnerability, an attacker needs to send a crafted block message to a ViaBTC bitpeer
client. ViaBTC runs multiple instances of bitpeer in different data centers distributed around the
globe. Each instance opens connections to multiple listening nodes on the P2P Bitcoin network. On my
nodes I counted zero to two connections from bitpeer instances per node. In total, I saw seven
different IP addresses, which indicates there are at least seven instances running. These bitpeer
peers can be detected as they always use the same fake user agent of /Satoshi:0.19.0.1/
and only
set the WITNESS
service flag. The IP addresses always belong to AWS-owned IP ranges. Connections
from bitpeer instances can, for example, be listed with the following command.
$ bitcoin-cli getpeerinfo | jq '.[] | select(.subver == "/Satoshi:0.19.0.1/" and .services == "0000000000000008")'
A block header used for this attack should be from the same difficulty adjustment period to meet the
difficulty requirements. It does not matter which coinbase transaction is chosen. The BIP-34
height in the coinbase input needs to be current network height + 1. To send this block to a bitpeer
instance, the recently introduced, test-only sendmsgtopeer
Bitcoin Core RPC call can be used.
$ bitcoin-cli sendmsgtopeer <peer-id> "block" <block hex>
Local and remote verification
To make sure the vulnerability is practicably exploitable before I disclose it, and not only a theoretical issue, I tested it against a local test setup using the viabtc_mining_server code available on GitHub.
I build, installed, and configured a bitpeer and jobmaster instance to use a local Bitcoin Core node. The bitpeer instance was configured to connect to a second local Bitcoin Core node via P2P. To be able to start the bitpeer and jobmaster instances I needed to comment out some startup actions for services I was unable to configure (mostly due to missing documentation). Once set up, I constructed a block composed of a real block header and a coinbase transaction encoding a height a few hundred blocks into the future. I send the block to the bitpeer instance, and it notified the jobmaster about the new block. I couldn’t tell from my patched jobmaster instance if it would have switched to SPV mode as it crashed due not being configured correctly. This made it hard to verify the vulnerability locally.
At this point I wasn’t sure if this vulnerability could even be exploited against ViaBTC. Surely they are running an updated and maintained version of their mining server? I contemplated trying to verify the vulnerability against ViaBTC’s production mining pool. As far as I could tell, there is no ViaBTC testnet pool available. I knew that ViaBTC would, by default, only SPV mine for 30 seconds, which made the possible damage done manageable: about $1000 of lost revenue per attack. If the vulnerability could be exploited, I could supply ViaBTC with log timestamps and the information about the block I send. Showing that the vulnerability can be exploited against their production pool increases the chances of them taking this seriously. I feared they wouldn’t take this seriously if I only a reported a vulnerability in unmaintained version of their mining server they uploaded to GitHub three years ago1.
I ultimately decided to go ahead with trying to verify the vulnerability against the production pool. I attempted this only once, while taking great care not to unnecessarily harm ViaBTC or it’s customers.
I choose block 826284 mined a few hours earlier. This block has only a coinbase transaction and is
comparatively quite small, which made it easy to handle in a text editor and on the command line.
However, vulnerability can be exploited with larger blocks just as well. At the time, block 826337
was the most recent block. My Bitcoin Core node saw it at 2024-01-19 00:55:46
. The 53 block
difference between these blocks was big enough to make it clear to ViaBTC that a successful
exploitation isn’t just a small reorg.
Block 826284 encodes an original block height of ac9b0c
in the coinbase transaction. I set the
height to e29b0c
(826338; current height of 826337 + 1) in the coinbase using a text editor. Note
that the BIP34 height in the coinbase is encoded in little-endian.
original ac9b0c - 826284
current e19b0c - 826337
modified e29b0c - 826338
The following shows the modified block 826284. The only difference to the original block is that the
coinbase height has been modified from ac9b0c
to e29b0c
.
0000cd2765e111fa8f2870ae4fdaa564907e501ac5ab12d0cab6020000000000000000007908d44825dc7bc91fb883fa5b2
083f0f95641aa4ac518bb968c2f29ae61a00f4357a96569d80317397f083301010000000100000000000000000000000000
00000000000000000000000000000000000000ffffffff6403█e29b0c█2cfabe6d6d654b01255ebb409c165395395b3f3c2
301f032f53df45cba5ed5d266dc2c786010000000f09f909f092f4632506f6f6c2f73000000000000000000000000000000
00000000000000000000000000000000000000000500ae970800000000000422020000000000001976a914c6740a12d0a7d
556f89782bf5faf0e12cf25a63988ac1ebc4025000000001976a914c825a1ecf2a6830c4401620c3a16f1995057c2ab88ac
00000000000000002f6a2d434f524501a21cbd3caa4fe89bccd1d716c92ce4533e4d4733bdb2a04b4ccf74792cc6753c27c
5fd5f1d6458bf00000000000000002c6a4c2952534b424c4f434b3acd2e3ba1354794d09aabccd650c2155ae16cd9830cc9
b0d57aecd423005ba3a64940a53f
To track which previous block ViaBTC mines on, I set up a patched version of achow101’s stratum-watcher. This connects to the ViaBTC stratum server and listens for new mining jobs. My patch prints which previous block is specified in the mining jobs. If ViaBTC sends out a new mining job with the block hash of 826284, I’d know that ViaBTC is vulnerable.
I send the modified block to one of my bitpeer peers using the sendmsgtopeer
RPC call on
2024-01-19 at 00:56:28
UTC. My Bitcoin Core node with net
debug logging showed that the block
was sent: sending block (409 bytes)
.
At the same time, I saw that the ViaBTC stratum servers I was connected to send out a new mining
job switching to mining on block 0000000000000000000231524f6ba483c6d6e84b68622ec7128a7269bcb9a9d8
.
This meant I had successfully confirmed that the vulnerability is exploitable against the production
ViaBTC mining pool. All other pools kept mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
.
[..]
00:56:25,877: btc.f2pool.com mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
00:56:25,924: btc-eu.f2pool.com mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
█ 00:56:28,678: btc.viabtc.io mining on 0000000000000000000231524f6ba483c6d6e84b68622ec7128a7269bcb9a9d8
█ 00:56:28,687: btc.viabtc.com mining on 0000000000000000000231524f6ba483c6d6e84b68622ec7128a7269bcb9a9d8
█ 00:56:28,699: btc.viabtc.com mining on 0000000000000000000231524f6ba483c6d6e84b68622ec7128a7269bcb9a9d8
█ 00:56:28,904: btc.viabtc.io mining on 0000000000000000000231524f6ba483c6d6e84b68622ec7128a7269bcb9a9d8
00:56:32,710: solo.ckpool.org mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
00:56:35,854: stratum.kano.is mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
[..]
Exactly 30 seconds later (ViaBTC’s default SPV-mining timeout), ViaBTC switched back to the correct previous
block 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
. In these 30 seconds,
ViaBTC’s miners wasted about 1.8 sextillion hashes (1.8×10²¹ or 1.8 zeta hashes) mining on an old
block. No block was found during these 30 seconds by neither ViaBTC nor another pool. If a ViaBTC
miner had found an empty block, the block would have been invalid2 as the height
commitment in the coinbase would been 826338 and not 826285 as expected with the previous block hash
in the header.
00:56:49,503: [..]slushpool.com mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
00:56:51,422: [..]slushpool.com mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
█ 00:56:58,645: btc.viabtc.io mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
█ 00:56:58,677: btc.viabtc.com mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
█ 00:56:58,721: btc.viabtc.com mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
█ 00:56:58,899: btc.viabtc.io mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
00:57:02,589: solo.ckpool.org mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
00:57:05,867: stratum.kano.is mining on 0000000000000000000005575548ec79ea5afb112f91422e12aad67080fda017
I stopped digging further at this point and responsibly disclosed the vulnerability to ViaBTC.
Impact
The vulnerability has a denial-of-service impact with a measurable associated business cost. By default, ViaBTC miners are paid according to PPS+ (Pay Per Share Plus): this means, miners are paid even if ViaBTC does not find a block or isn’t rewarded for finding a block. When the vulnerability was discovered, ViaBTC had around 12% of the network hashrate. Assuming 144 blocks per day, that are 12 blocks per day found by ViaBTC. With each block worth at least 6.25 BTC, that’s 75 BTC or $3M USD per day at $40k USD/BTC. This is ~$35 USD per second. Exploiting the vulnerability once for 30 seconds costs ViaBTC at least $1000 USD.
When a bitpeer instance sends a SPV mining notification, it increases the best known height and does not notify for the same height again. This means, a single bitpeer instance can only be used once per block to exploit the vulnerability. However, since there are at least seven bitpeer instances running, in theory, a malicious actor (e.g., a competing mining pool) could have exploited the vulnerability at least seven times per block. A attack over a longer time-frame would be detectable not only by ViaBTC but also on e.g., fork.observer as ViaBTC would have an abnormal stale block rate. An attacker could also be more careful and exploit this sporadically over a longer time-frame, which could cause serious financial damage if undetected.
Communication with ViaBTC
ViaBTC offers a bug bounty program (mainly scoped for their viabtc.com
website) where they list a
Zendesk support email to report vulnerabilities to. I didn’t find a dedicated security contact or
way to send an encrypted message. My responsible disclosure included all information I know about
the vulnerability, details how I verified the vulnerability against the ViaBTC production pool, how
they can verify it against a local setup, a discussion of a potential impact if exploited multiple
times, a recommendation on how to fix the vulnerability, and some general notes on running old C
code with seemingly no test or fuzz coverage for critical business infrastructure. I also included
that it would be nice if they could notify potential other pools using the same software.
Additionally, I choose to send the ViaBTC CEO and author of the ViaBTC mining server, Haipo Yang, a
short summary of the vulnerability and it’s potential impact.
The next day, ViaBTC confirmed they’ve received the vulnerability. Three days later, ViaBTC assigned it severity level 1 of 3 and awarded a 500 USDT bounty. I followed up asking for a reasoning behind the level 1 classification. They re-classified the vulnerability as level 2 and raised the bug bounty reward to 2000 USDT. They also noted that their risk scoring system and monitoring had detected my verification attempt. Furthermore, they claim, a fix was implemented immediately, even before I reported the vulnerability. If true, then the classification as level 2 under “certain asset losses” makes sense. The vulnerability couldn’t have been exploited over a longer time range causing level 3 “serious asset loss” (i.e., ViaBTC paying their PPS+ miners out of their pockets and their PPLNS miners moving away due ViaBTC’s “bad luck”).
While ViaBTC responded quick, funneling the communication with the “Security Risk Team” through a support agent isn’t ideal. Their responses were short and at first I thought they don’t understand the vulnerability, maybe due to a noticeable language barrier. ViaBTC didn’t provide details on how they fixed the vulnerability (they, obviously, don’t need to) and didn’t respond to my offer to re-test the vulnerability. I choose to re-test nonetheless before publishing this disclosure. Ignoring the rough edges, it was still a positive experience working with them.
Timeline
- 2024-01-18: vulnerability detected and successfully exploited locally
- 2024-01-19:
- Confirmed ViaBTC production mining pool is vulnerable
- Responsible disclosure of the vulnerability to ViaBTC
- Disclosure of a vulnerability summary to Haipo Yang, ViaBTC CEO and pool software author
- Vulnerability was fixed immediately after being detected by ViaBTC’s “risk scoring system” (claimed by ViaBTC)
- 2024-01-20: ViaBTC confirms the vulnerability has been received
- 2024-01-24: ViaBTC classifies vulnerability as level 2 and awards a 2000 USDT bug bounty
- 2024-02-06: I re-tested that the vulnerability is indeed fixed
- 2024-03-20: Public disclosure
Notes
To my knowledge, only ViaBTC was affected. I hope ViaBTC informed other known users of their software about the vulnerability. I contacted a few people in the mining pool community who asked around if anyone is using the ViaBTC mining server, however it doesn’t seem there’s much usage besides ViaBTC. As far as I can tell, all bitpeer connections I receive are from ViaBTC. However, someone might be using the GitHub version to host a mining pool for another coin. I opened an issue in the repository to make potential users aware.
I can’t know for sure if this hasn’t been expoited in the wild, but I don’t have any reason to assume it was before I validated the vulnerability against the ViaBTC production mining pool. If ViaBTC’s “risk scoring system” really detected my attempt, it would likely have detected other attacks too.
Reflection
I think, it was the right decision to test if ViaBTC’s production pool is vulnerable. The claimed internal alarms made sure they took this vulnerability serious and fixed it before I could report it. Additionally, it removed the necessity for ViaBTC to verify a theoretical vulnerability in a test environment – they had confirmation that it works. Still, I could have been more careful when exploiting the vulnerability by choosing the current block to mine an empty block on. This would have cost them the transaction fees, but not the whole block reward. Yet, this might also have not been alerted by their “risk scoring system”.
Running the C code, as uploaded on GitHub, as a production mining server seems scary to me. As far as I can tell, there is no unit testing (there seems to be a few years of in-the-wild-usage-testing though…), no fuzz-testing, error handling seems to be done by returning a failed line number, etc. I personally wouldn’t run this in production. Especially not with 12% of Bitcoin’s hashrate. I suspect the vulnerability existed since Haipo originally wrote the ViaBTC mining server in 2016. While I briefly checked that there weren’t any scary buffer overflows in the code that accepts P2P messages before publishing this disclosure, I decided not to spent more time digging deeper. Usually, disclosing a vulnerability ends up bringing in more eyes on the code. In this case, it wouldn’t surprise me if there are more vulnerabilities to be found.
This once again confirms my impression that some mining pools aren’t as technologically advanced as one might think. Especially older pools might still be stuck with a bit of technical dept. However, ViaBTC seems to have adequate an alerting and monitoring setup.
Credits
Will Clark reminded me about the weird P2P network behavior of what we found out to be ViaBTC bitpeer P2P clients. Furthermore, I found the discussion with Will about the vulnerability and how to best handle it helpful. The MIT DCI provides me with servers I use to run Bitcoin Core nodes alongside some monitoring tools. Having access to multiple nodes was helpful in collecting the initial data about the bitpeer clients. My day-to-day work of, for example, monitoring Bitcoin network anomalies, was funded by Sprial and Human Rights Foundation when I found the vulnerability and is now funded by OpenSats while writing this disclosure. Without their support, I wouldn’t be able to work on Bitcoin full-time. Consider donating to them if you want to support my and others work on Bitcoin.
However, who knows who is still using this… ↩︎
A previous version of this post clailmed that the block would have been valid but stale. This was incorrect. AJ made me aware of this in https://twitter.com/ajtowns/status/1770780046707302514. ↩︎