--- -- This library implements a minimal subset of the BitCoin protocol -- It currently supports the version handshake and processing Addr responses. -- -- The library contains the following classes: -- -- * NetworkAddress - Contains functionality for encoding and decoding the -- BitCoin network address structure. -- -- * Request - Classs containing BitCoin client requests -- o Version - The client version exchange packet -- -- * Response - Class containing BitCoin server responses -- o Version - The server version exchange packet -- o VerAck - The server version ACK packet -- o Addr - The server address packet -- o Inv - The server inventory packet -- -- * Helper - The primary interface to scripts -- --@author Patrik Karlsson --@author Andrew Orr --@copyright Same as Nmap--See https://nmap.org/book/man-legal.html -- -- Version 0.2 -- -- Created 11/09/2011 - v0.1 - created by Patrik Karlsson -- Revised 17/02/2012 - v0.2 - fixed count parsing -- - changed version/verack handling to support -- February 20th 2012 bitcoin protocol switchover local ipOps = require "ipOps" local match = require "match" local nmap = require "nmap" local os = require "os" local stdnse = require "stdnse" local string = require "string" local table = require "table" local openssl = stdnse.silent_require('openssl') _ENV = stdnse.module("bitcoin", stdnse.seeall) -- A class that supports the BitCoin network address structure NetworkAddress = { NODE_NETWORK = 1, -- Creates a new instance of the NetworkAddress class -- @param host table as received by the action method -- @param port table as received by the action method -- @return o instance of NetworkAddress new = function(self, host, port) local o = { host = "table" == type(host) and host.ip or host, port = "table" == type(port) and port.number or port, service = NetworkAddress.NODE_NETWORK, } setmetatable(o, self) self.__index = self return o end, -- Creates a new instance of NetworkAddress based on the data string -- @param data string of bytes -- @return na instance of NetworkAddress fromString = function(data) assert(26 == #data, "Expected 26 bytes of data") local na = NetworkAddress:new() local ipv6_prefix, ipv4_addr na.service, ipv6_prefix, ipv4_addr, na.port = string.unpack("I2", data) if ipv6_prefix == "\0\0\0\0\0\0\0\0\0\0\xff\xff" then -- IPv4 na.host = ipOps.str_to_ip(ipv4_addr) else na.host = ipOps.str_to_ip(ipv6_prefix .. ipv4_addr) end return na end, -- Converts the NetworkAddress instance to string -- @return data string containing the NetworkAddress instance __tostring = function(self) local ipv6_addr = ipOps.ip_to_str(self.host) return string.pack("I2", self.service, ipv6_addr, self.port ) end } -- The request class container Request = { -- The version request Version = { -- Creates a new instance of the Version request -- @param host table as received by the action method -- @param port table as received by the action method -- @param lhost string containing the source IP -- @param lport number containing the source port -- @return o instance of Version new = function(self, host, port, lhost, lport) local o = { host = host, port = port, lhost= lhost, lport= lport, } setmetatable(o, self) self.__index = self return o end, -- Converts the Version request to a string -- @return data as string __tostring = function(self) local magic = 0xD9B4BEF9 local cmd = "version" local len = 85 -- ver: 0.4.0 local ver = 0x9c40 cmd = cmd .. ('\0'):rep(12 - #cmd) -- NODE_NETWORK = 1 local services = 1 local timestamp = os.time() local ra = NetworkAddress:new(self.host, self.port) local sa = NetworkAddress:new(self.lhost, self.lport) local nodeid = openssl.rand_bytes(8) local useragent = "\0" local lastblock = "\0\0\0\0" -- Construct payload in order to calculate checksum for the header local payload = (string.pack("I4 c12 I4 I4", data) header.cmd = string.unpack("z", cmd) return header end, }, Alert = { type = "Alert", -- Creates a new instance of Version based on data string -- @param data string containing the raw response -- @return o instance of Version new = function(self, data) local o = { data = data, } setmetatable(o, self) self.__index = self o:parse() return o end, -- Parses the raw data and builds the Version instance parse = function(self) local pos = Response.Header.size + 1 self.header = Response.Header.parse(self.data) local data pos, data = Util.decodeVarString(self.data, pos) -- -- TODO: Alert decoding goes here -- return end, }, -- The version response message Version = { -- Creates a new instance of Version based on data string -- @param data string containing the raw response -- @return o instance of Version new = function(self, data) local o = { data = data } setmetatable(o, self) self.__index = self o:parse() return o end, -- Parses the raw data and builds the Version instance parse = function(self) local ra, sa, cmd, nodeid, pos -- After 2012-02-20, version messages contain checksums self.magic, cmd, self.len, self.checksum, self.ver_raw, self.service, self.timestamp, ra, sa, nodeid, pos = string.unpack(" 31402 ) then local timestamp, data timestamp, data, pos = string.unpack("= 0xfd then count, pos = string.unpack("timeout - the socket timeout in ms -- @return instance of Helper new = function(self, host, port, options) local o = { host = host, port = port, options = options or {} } setmetatable(o, self) self.__index = self return o end, -- Connects to the BitCoin Server -- @return status true on success false on failure -- @return err string containing the error message in case status is false connect = function(self) self.socket = nmap.new_socket() self.socket:set_timeout(self.options.timeout or 10000) local status, err = self.socket:connect(self.host, self.port) if ( not(status) ) then return false, err end status, self.lhost, self.lport = self.socket:get_info() return status, (status and nil or self.lhost) end, -- Performs a version handshake with the server -- @return status, true on success false on failure -- @return version instance if status is true -- err string containing an error message if status is false exchVersion = function(self) if ( not(self.socket) ) then return false end local req = Request.Version:new( self.host, self.port, self.lhost, self.lport ) local status, err = self.socket:send(tostring(req)) if ( not(status) ) then return false, "Failed to send \"Version\" request to server" end local version status, version = Response.recvPacket(self.socket) if not status or not version then return false, "Failed to read \"Version\" response from server: " .. (version or "nil") elseif version.cmd ~= "version" then return false, ('"Version" request got %s from server'):format(version.cmd) end if ( version.ver_raw > 29000 ) then local status, verack = Response.recvPacket(self.socket) end local verack = Request.VerAck:new() local status, err = self.socket:send(tostring(verack)) if ( not(status) ) then return false, "Failed to send \"Version\" request to server" end self.version = version.ver_raw return status, version end, getNodes = function(self) local req = Request.GetAddr:new( self.host, self.port, self.lhost, self.lport ) local status, err = self.socket:send(tostring(req)) if ( not(status) ) then return false, "Failed to send \"GetAddr\" request to server" end local status, response = Response.recvPacket(self.socket, self.version) local all_addrs = {} local limit = 10 -- Usually sends an addr response with 1 address, -- then some other stuff like getheaders or ping, -- then one with hundreds of addrs. while status and #all_addrs <= 1 and limit > 0 do limit = limit - 1 status, response = Response.recvPacket(self.socket, self.version) if status and response.cmd == "addr" then for _, addr in ipairs(response.addresses) do all_addrs[#all_addrs+1] = addr end end end return #all_addrs > 0, all_addrs end, -- Reads a message from the server -- @return status true on success, false on failure -- @return response instance of response packet if status is true -- err string containing the error message if status is false readMessage = function(self) assert(self.version, "Version handshake has not been performed") return Response.recvPacket(self.socket, self.version) end, -- Closes the connection to the server -- @return status true on success false on failure -- @return err code, if status is false close = function(self) return self.socket:close() end } return _ENV;