A bitcoin wallet is a "place" to store Bitcoins. I say "place" because it is not actually a physical place but solely defined by a private key, which is used to sign transactions. So anyone who knows the key has complete access to the wallet.
So, is it possible to brute-force a private key for a wallet?
Spoiler: No, not really, that's part of the "mathematical security" Bitcoin offers.
There's this video about how big 2**256 really is:
The creation is sometimes also done via choosing a series of words, like for Electrum, where a "wallet seed" contains of 12 words from a list of words, which also can be written down on paper. It can then later be used to re-create the wallet and gain access to the Bitcoins, even if the original PC is not around anymore.
While reading some related articles I stumbled on a StackExchange post, and an answer on a similar question got me thinking:
It is possible to brute force some Bitcoin addresses, because some people generate their private keys in an insecure manner. Any (non-zero) 32 bytes can be a private key. So running sha256 over a passphrase gives an apparently random, but brute force-able private key.
So, why not just try that?
This is what we need:
- An updated list of all addresses which have some Bitcoins
- A list of commonly used passphrases
- A script to generate public addresses from sha256'ed passphrases
Getting all addresses with a non-zero balance
First, we'd need a list with all current addresses with a balance. Anyone with a up-to-date full node of the Bitcoin network already has it, and luckily, there are people sharing that data just in the format we need. For example, you can download an up-to-date list here, it will be around 1.2 GB currently (zipped).
The list contains entries like this:
address balance 34xp4vRoCGJym3xR7yCVPFHoCNxv4Twseo 26547968732321 bc1qgdjqv0av3q56jvd82tkdjpy7gdp9ut8tlqmgrpmv24sq90ecnvqqjwvw97 16800998536372 1P5ZEDWTKTFGxQjZphgWPQUpe554WKDfHQ 12084557276776 37XuVSEpWW4trkfmvWzegTHQt7BdktSKUs 9450511456719 38UmuUqPCrFmQo4khkomQwZ4VbY2nZMJ67 8406701772536 1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF 7995721697629 ...
Getting a list of passwords
Next up we need a list of common passphrases. As there are many such lists available I'll just go with one from the NCSC (UK), available via a web.archive.org-Link.
It contains entries such as:
123456 123456789 qwerty password 111111 12345678
Calculating the public address
private_hash = hashlib.sha256(bytes(word, 'ascii')).hexdigest()
Next, we want to create a Bitcoin wallet using that hash. Luckily we don't have to go through wallet generation ourselves, but there is also a library which does the heavy lifting, bitcoinaddress:
from bitcoinaddress import Wallet wallet = Wallet(private_hash)
This will generate a wallet with the most basic information, and also some pre-generated public addresses, which we can use to check the list later:
Private Key HEX: 30caae2fcb7c34ecadfddc45e0a27e9103bd7cfc87730d7818cc096b1266a683 Private Key WIF: 5JBmuBc64pVrKLyDc8ktyXJmAeEwKQogn6jsk6taeq8zRMtGZrE Private Key WIF compressed: KxrZBKosMreLfy98e5EkQUmPwZNWkG54a4LEyEzfXyUGSRvfnCve Public Key: 04a32ed011213146495f58d3ed83a6cc3fc0fd107d5fa2887bbc2fcea81e8bc84f650e81f4ddc84424daab546945f0d7d9dfd4dce39ce3776ee6b8ba78e6eddc7a Public Key compressed: 02a32ed011213146495f58d3ed83a6cc3fc0fd107d5fa2887bbc2fcea81e8bc84f Public Address 1: 1TnnhMEgic5g4ttrCQyDopwqTs4hheuNZ Public Address 1 compressed: 17xYNJ5h9eW9CbhntR87YE1myvGtNkrpnK Public Address 3: 3QmjwSLdEbBP9qBdqoNuwLRr1AFX4aATrM Public Address bc1 P2WPKH: bc1qf3gazzex87xqdlusm4qa2qlpj6r63jgegx2nur Public Address bc1 P2WSH: bc1qeqmm7yxpyn7caxcd9gz8mytklnfevqtylrptt9lxdnfvtje2vxkq252l53
So, now that we a public address, we can just search the list of addresses, right? Unfortunately, searching 1.7 GiB of text (uncompressed) takes a really long time, so we need to speed that up.
bisect_left() from bisect on a sorted list of addresses, we can use a bisect search, meaning that we just keep cutting the list in half until we find (or don't find) the address we're looking for.
Putting it together
After letting the script run, there is currently no balance on any of the widely used passwords.
But when first preparing this blog post, there was actually a match:
secret: 777 private_hash: eaf89db7108470dc3f6b23ea90618264b3e8f8b6145371667c4055e9c5ce9f52 wallet: Private Key HEX: eaf89db7108470dc3f6b23ea90618264b3e8f8b6145371667c4055e9c5ce9f52 Private Key WIF: 5Kbmc1FLfTFTXy9sCZsmxgUjZfP4hLtMPkxpgKjDeV954bQVSQn Public Key: 042661ad780e66d678af5acc9b6259c93a16001ea261914844a413af90757f25fc6cc2cc571016903625702cf2e60601224cefdd7a99a42a3f4ca1c21f2e06889c Public Key compressed: 022661ad780e66d678af5acc9b6259c93a16001ea261914844a413af90757f25fc Public Address 1: 164694JJjkWpSJH3HufCEK5AdJvUiG9Ucd
You can see the balance here:
You can see that it had a balance of about $4 in April last year.
When I first saw that, I literally wrote:
The balance is so low though, that even if someone decided to steal it, it wouldn't be enough to pay for the transaction fees.
Apparently I was wrong! I had in mind that transaction costs for Bitcoin were about $20 back then, but somehow somebody was able to transfer it to another account.
Anyway, it was interesting to see that all it takes to access an account is really one long number - and if that is generated in an insecure manner, it doesn't take much to get to a private wallet key like shown above.
Thanks for reading!
All the code can be found here.