Overview
RustRoll implements a simplified blockchain/rollup node written in Rust. Users can submit transactions signed with Ed25519, and account ownership is determined by a derived address. The goal is to steal funds from a privileged “Vault” account.
| Category | Difficulty | Flag |
|---|---|---|
| Blockchain | Medium | nexus{Th1s-Is_A-Hard#Fl4g!} |
Analysis
Transaction Structure
The challenge defines a custom transaction format (120 bytes):
struct Transaction { from_addr: u32, // 4 bytes (Little Endian) to_addr: u32, // 4 bytes (Little Endian) amount: u64, // 8 bytes (Little Endian) nonce: u64, // 8 bytes (Little Endian) pubkey: [u8; 32], // Ed25519 Public Key signature: [u8; 64]// Ed25519 Signature}Address Derivation
The from_addr field is only a 32-bit integer. Investigating the source (or black-box testing) reveals the address derivation logic involves Blake3 hashing:
# Derived Address Logicaddr = unpack_u32(blake3(pubkey)[0:4]) & 0xFFFFF[!WARNING] The address space is explicitly masked to
0xFFFFF, meaning there are only 2²⁰ (1,048,576) possible addresses!
Reconnaissance
Connecting to the challenge instance reveals the target Vault:
addr=622488 balance=999358175823 nonce=49- Target Address:
622488 - Goal: Steal funds (transfer to our account).
The Vulnerability
Since the address space is small (20 bits), we can easily perform an Address Collision Attack.
The system verifies transactions by checking if the Provided Public Key derives to the from_addr. It does not check if you are the original owner of that address (because addresses are just hashes of public keys).
If we find a different Ed25519 keypair that creates the same 20-bit hash as the Vault’s address, the system will accept our signatures as valid for the Vault account.
graph TD A[Generate Random Keypair] --> B[Calculate BLAKE3 Hash] B --> C[Truncate to 20 bits] C --> D{Equals 622488?} D -- No --> A D -- Yes --> E[Collision Found!] E --> F[Sign Transaction as Vault]Exploit
1. Generating a Collision
We can bruteforce this in Python locally. With possibilities, it takes only a few seconds.
from blake3 import blake3from nacl.signing import SigningKeyimport struct
VAULT_ADDR = 622488
def addr_from_pk(pk: bytes) -> int: # First 4 bytes of Blake3 hash, masked to 20 bits return struct.unpack("<I", blake3(pk).digest()[:4])[0] & 0xFFFFF
print("[-] Bruteforcing collision...")while True: sk = SigningKey.generate() pk = sk.verify_key.encode()
if addr_from_pk(pk) == VAULT_ADDR: print(f"[+] Found Private Key: {sk.encode().hex()}") print(f"[+] Found Public Key: {pk.hex()}") break2. Crafting the Transaction
Once we have the keypair, we create a transaction transferring funds from the Vault (622488) to our target address (1337).
[!NOTE] Don’t forget to use the correct
nonce! The recon showed the vault hadnonce=49.
Solution Script
#!/usr/bin/env python3import socketimport structimport timefrom nacl.signing import SigningKeyfrom blake3 import blake3
HOST = "4.211.248.144"PORT = 8080
TARGET_TO = 1337TARGET_AMOUNT = 13_371_337
def addr_from_pk(pk: bytes) -> int: return struct.unpack("<I", blake3(pk).digest()[:4])[0] & 0xFFFFF
def recv_all(s, timeout=0.3): s.settimeout(timeout) data = b"" try: while True: data += s.recv(4096) except: pass return data.decode()
# 1. Connect and Recons = socket.socket()s.connect((HOST, PORT))recv_all(s)s.sendall(b"LIST\n")time.sleep(0.2)data = recv_all(s)s.close()
vault_addr = Nonevault_nonce = Nonemax_balance = -1
# Parse LIST output to find the rich vaultfor line in data.splitlines(): if line.startswith("addr="): p = line.split() addr = int(p[0].split("=")[1]) bal = int(p[1].split("=")[1]) nonce = int(p[2].split("=")[1]) if bal > max_balance: max_balance = bal vault_addr = addr vault_nonce = nonce
print(f"[*] Target Vault: {vault_addr} (Balance: {max_balance}, Nonce: {vault_nonce})")
# 2. Collision Attackprint("[*] Searching for key collision...")collision_sk = Nonecollision_pk = None
while True: sk = SigningKey.generate() pk = sk.verify_key.encode() if addr_from_pk(pk) == vault_addr: collision_sk = sk collision_pk = pk break
print("[+] Collision found!")
# 3. Send Malicious Transactionpayload = struct.pack( "<IIQQ", vault_addr, TARGET_TO, TARGET_AMOUNT, vault_nonce)
sig = collision_sk.sign(payload).signaturetx_blob = payload + collision_pk + sig
print(f"[+] TX Blob: {tx_blob.hex()}")# Sending this blob to the server would trigger the flagFlag: nexus{Th1s-Is_A-Hard#Fl4g!}