|
| 1 | +#!/usr/bin/env python3 |
| 2 | + |
| 3 | +import socket |
| 4 | + |
| 5 | +# The base class exists inside the commander container when deployed, |
| 6 | +# but requires a relative path inside the python source code for other functions. |
| 7 | +try: |
| 8 | + from commander import Commander |
| 9 | +except ImportError: |
| 10 | + from resources.scenarios.commander import Commander |
| 11 | + |
| 12 | +# The entire Bitcoin Core test_framework directory is available as a library |
| 13 | +from test_framework.messages import MSG_TX, CInv, hash256, msg_getdata |
| 14 | +from test_framework.p2p import MAGIC_BYTES, P2PInterface |
| 15 | + |
| 16 | + |
| 17 | +# This message is provided to the user when they describe the scenario |
| 18 | +def cli_help(): |
| 19 | + return "Demonstrate network reconnaissance using a scenario and P2PInterface" |
| 20 | + |
| 21 | + |
| 22 | +def get_signet_network_magic_from_node(node): |
| 23 | + template = node.getblocktemplate({"rules": ["segwit", "signet"]}) |
| 24 | + challenge = template["signet_challenge"] |
| 25 | + challenge_bytes = bytes.fromhex(challenge) |
| 26 | + data = len(challenge_bytes).to_bytes() + challenge_bytes |
| 27 | + digest = hash256(data) |
| 28 | + return digest[0:4] |
| 29 | + |
| 30 | + |
| 31 | +# The actual scenario is a class like a Bitcoin Core functional test. |
| 32 | +# Commander is a subclass of BitcoinTestFramework instide Warnet |
| 33 | +# that allows to operate on containerized nodes instead of local nodes. |
| 34 | +class Reconnaissance(Commander): |
| 35 | + def set_test_params(self): |
| 36 | + # This setting is ignored but still required as |
| 37 | + # a sub-class of BitcoinTestFramework |
| 38 | + self.num_nodes = 1 |
| 39 | + |
| 40 | + # Scenario entrypoint |
| 41 | + def run_test(self): |
| 42 | + self.log.info("Getting peer info") |
| 43 | + |
| 44 | + # Just like a typical Bitcoin Core functional test, this executes an |
| 45 | + # RPC on a node in the network. The actual node at self.nodes[0] may |
| 46 | + # be different depending on the user deploying the scenario. Users in |
| 47 | + # Warnet may have different namepsace access but everyone should always |
| 48 | + # have access to at least one node. |
| 49 | + peerinfo = self.nodes[0].getpeerinfo() |
| 50 | + for peer in peerinfo: |
| 51 | + # You can print out the the scenario logs with `warnet logs` |
| 52 | + # which have a list of all this node's peers' addresses and version |
| 53 | + self.log.info(f"{peer['addr']} {peer['subver']}") |
| 54 | + |
| 55 | + # We pick a node on the network to attack |
| 56 | + victim = peerinfo[0] |
| 57 | + |
| 58 | + # regtest or signet |
| 59 | + chain = self.nodes[0].chain |
| 60 | + |
| 61 | + # The victim's address could be an explicit IP address |
| 62 | + # OR a kubernetes hostname (use default chain p2p port) |
| 63 | + if ":" in victim["addr"]: |
| 64 | + dstaddr = victim["addr"].split(":")[0] |
| 65 | + else: |
| 66 | + dstaddr = socket.gethostbyname(victim["addr"]) |
| 67 | + if chain == "regtest": |
| 68 | + dstport = 18444 |
| 69 | + if chain == "signet": |
| 70 | + dstport = 38333 |
| 71 | + MAGIC_BYTES["signet"] = get_signet_network_magic_from_node(self.nodes[0]) |
| 72 | + |
| 73 | + # Now we will use a python-based Bitcoin p2p node to send very specific, |
| 74 | + # unusual or non-standard messages to a "victim" node. |
| 75 | + self.log.info(f"Attacking {dstaddr}:{dstport}") |
| 76 | + attacker = P2PInterface() |
| 77 | + attacker.peer_connect(dstaddr=dstaddr, dstport=dstport, net=chain, timeout_factor=1)() |
| 78 | + attacker.wait_until(lambda: attacker.is_connected, check_connected=False) |
| 79 | + |
| 80 | + # Send a harmless network message we expect a response to and wait for it |
| 81 | + # Ask for TX with a 0 hash |
| 82 | + msg = msg_getdata() |
| 83 | + msg.inv.append(CInv(t=MSG_TX, h=0)) |
| 84 | + attacker.send_and_ping(msg) |
| 85 | + attacker.wait_until(lambda: attacker.message_count["notfound"] > 0) |
| 86 | + self.log.info(f"Got notfound message from {dstaddr}:{dstport}") |
| 87 | + |
| 88 | + |
| 89 | +if __name__ == "__main__": |
| 90 | + Reconnaissance().main() |
0 commit comments