The blockchain paradigm as we know it today has been around for slightly more than a decade, during that time period we have seen a number of exploits and bugs ranging from the traditional integer overflow, smart contract reentrancy, unsound hash functions, etc, ...
Each and every spectacular bug and exploit provided a learning opportunity and new perspective regarding known mistakes to avoid at all costs in future systems & application design, architecture, and security considerations.
"Those that fail to learn from history are doomed to repeat it." Winston Churchill
I will be writing a series of articles, where I analyze the most spectacular exploits and bugs, how they could have been avoided, and what we learned from them.
- The Bitcoin core Integer Overflow
- Ethereum DAO Hack
- Parity MultiSig Bug
- IOTA In-House Hash Function
- Lisk Hash Collisions
- Iron Finance Bank Run
- bZx & Pancake Bunny flash loan exploits
For today's article, we will be focusing mainly on our first case study, the Bitcoin integer overflow bug.
Case Study 1: Bitcoin Core Integer Overflow
It's known that Bitcoin has a supply cap limit of 21 million Bitcoin, hardcoded in the bitcoin core code, In August 2010, an attacker was able to mint 184 billion Bitcoin by exploiting a bug in the early Bitcoin core code.
Luckily the anonymous Bitcoin creator Satoshi Nakamoto was able to push a fix for the bug in less than 5h after the exploit, a soft fork was orchestrated to return the chain to the previous state before the exploit.
What really happened?
On the 15th of August 2010, Jeff Garzik, now CEO of Bloq.com, reported a non-ordinary occurrence on block Number 74638 on the Bitcoin Talk forum.
An attacker was able to exploit the lack of integer overflow checks to create transaction outputs way larger than the spent inputs amounts.
{
"hash" : "0000000000790ab3f22ec756ad43b6ab569abf0bddeb97c67a6f7b1470a7ec1c",
"ver" : 1,
"prev_block" : "0000000000606865e679308edf079991764d88e8122ca9250aef5386962b6e84",
"mrkl_root" : "618eba14419e13c8d08d38c346da7cd1c7c66fd8831421056ae56d8d80b6ec5e",
"time" : 1281891957,
"bits" : 469794830,
"nonce" : 28192719,
"n_tx" : 2,
"tx" : [
{
"hash" : "012cd8f8910355da9dd214627a31acfeb61ac66e13560255bfd87d3e9c50e1ca",
"ver" : 1,
"vin_sz" : 1,
"vout_sz" : 1,
"lock_time" : 0,
"in" : [
{
"prev_out" : {
"hash" : "0000000000000000000000000000000000000000000000000000000000000000",
"n" : 4294967295
},
"coinbase" : "040e80001c028f00"
}
],
"out" : [
{
"value" : 50.51000000,
"scriptPubKey" : "0x4F4BA55D1580F8C3A8A2C78E8B7963837C7EA2BD8654B9D96C51994E6FCF6E65E1CF9A844B044EEA125F26C26DBB1B207E4C3F2A098989DA9BA5BA455E830F7504 OP_CHECKSIG"
}
]
},
{
"hash" : "1d5e512a9723cbef373b970eb52f1e9598ad67e7408077a82fdac194b65333c9",
"ver" : 1,
"vin_sz" : 1,
"vout_sz" : 2,
"lock_time" : 0,
"in" : [
{
"prev_out" : {
"hash" : "237fe8348fc77ace11049931058abb034c99698c7fe99b1cc022b1365a705d39",
"n" : 0
},
"scriptSig" : "0xA87C02384E1F184B79C6ACF070BEA45D5B6A4739DBFF776A5D8CE11B23532DD05A20029387F6E4E77360692BB624EEC1664A21A42AA8FC16AEB9BD807A4698D0CA8CDB0021024530 0x965D33950A28B84C9C19AB64BAE9410875C537F0EB29D1D21A60DA7BAD2706FBADA7DF5E84F645063715B7D0472ABB9EBFDE5CE7D9A74C7F207929EDAE975D6B04"
}
],
"out" : [
{
"value" : 92233720368.54277039,
"scriptPubKey" : "OP_DUP OP_HASH160 0xB7A73EB128D7EA3D388DB12418302A1CBAD5E890 OP_EQUALVERIFY OP_CHECKSIG"
},
{
"value" : 92233720368.54277039,
"scriptPubKey" : "OP_DUP OP_HASH160 0x151275508C66F89DEC2C5F43B6F9CBE0B5C4722C OP_EQUALVERIFY OP_CHECKSIG"
}
]
}
],
"mrkl_tree" : [
"012cd8f8910355da9dd214627a31acfeb61ac66e13560255bfd87d3e9c50e1ca",
"1d5e512a9723cbef373b970eb52f1e9598ad67e7408077a82fdac194b65333c9",
"618eba14419e13c8d08d38c346da7cd1c7c66fd8831421056ae56d8d80b6ec5e"
]
}
The attack worked by relying on the behavior of extremely large numbers being implicitly cast to extremely small numbers when they overflow the container type, and thus passing the validation checks that require the sum of outputs values to always be lower than the sum of inputs.
How could have this bug be prevented?
This could have simply be prevented by adding extra checks on the summed-up values against possible container type overflows.
// Check for negative or overflow input values
if (txPrev.vout[prevout.n].nValue < 0)
return error("ConnectInputs() : txin.nValue negative");
if (txPrev.vout[prevout.n].nValue > MAX_MONEY)
return error("ConnectInputs() : txin.nValue too high");
if (nValueIn > MAX_MONEY)
return error("ConnectInputs() : txin total too high");
The above code was the patch that was later submitted by Satoshi Nakamoto (1), just 5h after the incident was reported. A soft fork followed afterward.
What did we learn from this exploit?
One major lesson here is to always be careful about possible cases of integer overflows. This is important especially since many programming languages won't fail on container type overflow, but they would simply implicitly cast it and mess up the internal representation.
Always watch out for possible container type value overflows 💡
I hope y'all learned something from this brief analysis of the Bitcoin core integer overflow bug, please stay tuned for more exploit analysis.