diff options
Diffstat (limited to 'nselib/bitcoin.lua')
-rw-r--r-- | nselib/bitcoin.lua | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/nselib/bitcoin.lua b/nselib/bitcoin.lua new file mode 100644 index 0000000..e22589f --- /dev/null +++ b/nselib/bitcoin.lua @@ -0,0 +1,628 @@ +--- +-- 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 <patrik@cqure.net> +--@author Andrew Orr <andrew@andreworr.ca> +--@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 <patrik@cqure.net> +-- 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("<I8 c12 c4 >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("<I8 c16 >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 I8 I8", ver, services, timestamp) + .. tostring(ra) .. tostring(sa) .. nodeid .. useragent .. lastblock) + + -- Checksum is first 4 bytes of sha256(sha256(payload)) + local checksum = openssl.digest("sha256", payload) + checksum = openssl.digest("sha256", checksum) + + -- Construct the header without checksum + local header = string.pack("<I4 c12 I4", magic, cmd, len) + + -- After 2012-02-20, version messages require checksums + header = header .. checksum:sub(1,4) + + return header .. payload + end, + }, + + -- The GetAddr request + GetAddr = { + + -- 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 = "getaddr" + local len = 0 + local chksum = 0xe2e0f65d + cmd = cmd .. ('\0'):rep(12 - #cmd) + + return string.pack("<I4 c12 I4 I4", magic, cmd, len, chksum) + end + }, + + VerAck = { + + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + local cmd = "verack" + cmd = cmd .. ('\0'):rep(12 - #cmd) + return string.pack("<I4 c12 I4 I4", 0xD9B4BEF9, cmd, 0, 0xe2e0f65d) + end, + + }, + + -- The pong message is sent in response to a ping message. + Pong = { + new = function(self) + local o = {} + setmetatable(o, self) + self.__index = self + return o + end, + + __tostring = function(self) + local magic = 0xD9B4BEF9 + local cmd = "pong" + local len = 0 + local chksum = 0xe2e0f65d + cmd = cmd .. ('\0'):rep(12 - #cmd) + + return string.pack("<I4 c12 I4 I4", magic, cmd, len, chksum) + end, + + } + +} + +-- The response class container +Response = { + + Header = { + size = 24, + new = function(self) + local o = { + magic = 0, + cmd = "", + length = 0, + checksum = 0, + } + setmetatable(o, self) + self.__index = self + return o + end, + + parse = function(data) + local header = Response.Header:new() + + local cmd + header.magic, cmd, header.length, header.checksum = string.unpack(">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("<I4 c12 I4 I4 I4 I8 I8 c26 c26 c8", self.data) + pos, self.user_agent = Util.decodeVarString(self.data, pos) + self.lastblock, pos = string.unpack("<I4", self.data, pos) + self.nodeid = stdnse.tohex(nodeid) + self.cmd = string.unpack("z", cmd) + + local function decode_bitcoin_version(n) + if ( n < 31300 ) then + local minor, micro = n // 100, n % 100 + return ("0.%d.%d"):format(minor, micro) + else + local minor, micro = n // 10000, (n // 100) % 100 + return ("0.%d.%d"):format(minor, micro) + end + end + + self.ver = decode_bitcoin_version(self.ver_raw) + self.sa = NetworkAddress.fromString(sa) + self.ra = NetworkAddress.fromString(ra) + end, + }, + + -- The verack response message + VerAck = { + + -- Creates a new instance of VerAck 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 VerAck instance + parse = function(self) + local cmd + -- After 2012-02-20, VerAck messages contain checksums + self.magic, cmd, self.checksum = string.unpack("<I4 c12 I4", self.data) + self.cmd = string.unpack("z", cmd) + end, + }, + + -- The Addr response message + Addr = { + + -- Creates a new instance of VerAck based on data string + -- @param data string containing the raw response + -- @return o instance of Addr + new = function(self, data, version) + local o = { data = data, version=version } + setmetatable(o, self) + self.__index = self + o:parse() + return o + end, + + -- Parses the raw data and builds the Addr instance + parse = function(self) + local pos, count + local cmd + self.magic, cmd, self.len, self.chksum, pos = string.unpack("<I4 c12 I4 I4", self.data) + self.cmd = string.unpack("z", cmd) + pos, count = Util.decodeVarInt(self.data, pos) + + self.addresses = {} + for c=1, count do + if ( self.version > 31402 ) then + local timestamp, data + timestamp, data, pos = string.unpack("<I4 c26", self.data, pos) + local na = NetworkAddress.fromString(data) + table.insert(self.addresses, { ts = timestamp, address = na }) + end + end + + end, + }, + + -- The inventory server packet + Inv = { + + -- Creates a new instance of Inv based on data string + -- @param data string containing the raw response + -- @return o instance of Inv + new = function(self, data, version) + local o = { data = data, version=version } + setmetatable(o, self) + self.__index = self + o:parse() + return o + end, + + -- Parses the raw data and builds the Inv instance + parse = function(self) + local cmd + self.magic, cmd, self.len, self.chksum = string.unpack("<I4 c12 I4 I4", self.data) + self.cmd = string.unpack("z", cmd) + -- TODO parse inv_vect + end, + }, + + -- Receives the packet and decodes it + -- @param socket socket connected to the server + -- @param version number containing the server version + -- @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 + recvPacket = function(socket, version) + local status, header = socket:receive_buf(match.numbytes(24), true) + if ( not(status) ) then + return false, "Failed to read the packet header" + end + + local magic, cmd, len, checksum = string.unpack("<I4 c12 I4 I4", header) + local data = "" + cmd = string.unpack("z", cmd) + + -- the verack and ping has no payload + if ( 0 ~= len ) then + status, data = socket:receive_buf(match.numbytes(len), true) + if ( not(status) ) then + return false, "Failed to read the packet header" + end + else + -- The ping message is sent primarily to confirm that the TCP/IP connection is still valid. + if( cmd == "ping" ) then + local req = Request.Pong:new() + + local status, err = socket:send(tostring(req)) + if ( not(status) ) then + return false, "Failed to send \"Pong\" reply to server" + else + return Response.recvPacket(socket, version) + end + end + end + return Response.decode(header .. data, version) + end, + + -- Decodes the raw packet data + -- @param data string containing the raw packet + -- @param version number containing the server version + -- @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 + decode = function(data, version) + local magic, cmd = string.unpack("<I4 z", data) + if ( "version" == cmd ) then + return true, Response.Version:new(data) + elseif ( "verack" == cmd ) then + return true, Response.VerAck:new(data) + elseif ( "addr" == cmd ) then + return true, Response.Addr:new(data, version) + elseif ( "inv" == cmd ) then + return true, Response.Inv:new(data) + elseif ( "alert" == cmd ) then + return true, Response.Alert:new(data) + else + return true, ("Unknown command (%s)"):format(cmd) + end + end, +} + +Util = { + + varIntLen = { + [0xfd] = 2, + [0xfe] = 4, + [0xff] = 8, + }, + + -- Decodes a variable length int + -- @param data string of data + -- @param pos the location within the string to decode + -- @return pos the new position + -- @return count number the decoded argument + decodeVarInt = function(data, pos) + local count, pos = string.unpack("B", data, pos) + if count >= 0xfd then + count, pos = string.unpack("<I" .. Util.varIntLen[count], data, pos) + end + return pos, count + end, + + decodeVarString = function(data, pos) + local count, pos = string.unpack("B", data, pos) + local str + if count < 0xfd then + str, pos = string.unpack("s1", data, pos - 1) + else + str, pos = string.unpack("<s" .. Util.varIntLen[count], data, pos) + end + return pos, str + end, + +} + +-- The Helper class used as a primary interface to scripts +Helper = { + + -- Creates a new Helper instance + -- @param host table as received by the action method + -- @param port table as received by the action method + -- @param options table containing additional options + -- <code>timeout</code> - 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; |