126 lines
4.4 KiB
Ruby
126 lines
4.4 KiB
Ruby
|
# -*- coding: binary -*-
|
||
|
|
||
|
module Rex
|
||
|
module Proto
|
||
|
##
|
||
|
#
|
||
|
# Minimal support for the newer Kademlia protocol, referred to here and often
|
||
|
# elsewhere as Kademlia2. It is unclear how this differs from the old protocol.
|
||
|
#
|
||
|
# Protocol details are hard to come by because most documentation is academic
|
||
|
# in nature and glosses over the low-level network details. The best
|
||
|
# documents I found on the protocol are:
|
||
|
#
|
||
|
# http://gbmaster.wordpress.com/2013/05/05/botnets-surrounding-us-an-initial-focus-on-kad/
|
||
|
# http://gbmaster.wordpress.com/2013/06/16/botnets-surrounding-us-sending-kademlia2_bootstrap_req-kademlia2_hello_req-and-their-strict-cousins/
|
||
|
# http://gbmaster.wordpress.com/2013/11/23/botnets-surrounding-us-performing-requests-sending-out-kademlia2_req-and-asking-contact-where-art-thou/
|
||
|
#
|
||
|
##
|
||
|
module Kademlia
|
||
|
# Opcode for a BOOTSTRAP request
|
||
|
BOOTSTRAP_REQ = 0x01
|
||
|
# Opcode for a BOOTSTRAP response
|
||
|
BOOTSTRAP_RES = 0x09
|
||
|
# Opcode for a PING request
|
||
|
PING = 0x60
|
||
|
# Opcode for a PING response
|
||
|
PONG = 0x61
|
||
|
# The minimum size of a peer in a KADEMLIA2_BOOTSTRAP_RES message:
|
||
|
# peer ID (16-bytes), IP (4 bytes), UDP port (2 bytes), TCP port (2 bytes)
|
||
|
# and version (1 byte)
|
||
|
BOOTSTRAP_PEER_SIZE = 25
|
||
|
|
||
|
# Builds a BOOTSTRAP request
|
||
|
#
|
||
|
# @return [String] a BOOTSTRAP request
|
||
|
def bootstrap
|
||
|
Message.new(BOOTSTRAP_REQ)
|
||
|
end
|
||
|
|
||
|
# Decodes a BOOTSTRAP response
|
||
|
#
|
||
|
# @param response [String] the response to decode
|
||
|
# @return [Array] the discovered peer ID, TCP port, version and a list of peers
|
||
|
# if the response if valid, nil otherwise
|
||
|
def decode_bootstrap_res(response)
|
||
|
type, body = decode_message(response)
|
||
|
# abort if this isn't a valid response
|
||
|
return nil unless type = BOOTSTRAP_RES
|
||
|
return nil unless body.size >= 23
|
||
|
peer_id = decode_peer_id(body.slice!(0,16))
|
||
|
tcp_port, version, num_peers = body.slice!(0,5).unpack('vCv')
|
||
|
# protocol says there are no peers and the body confirms this, so just return with no peers
|
||
|
return [ tcp_port, version, []] if num_peers == 0 && body.blank?
|
||
|
peers = decode_bootstrap_peers(body)
|
||
|
# abort if the peer data was invalid
|
||
|
return nil unless peers
|
||
|
[ peer_id, tcp_port, version, peers ]
|
||
|
end
|
||
|
|
||
|
# Builds a PING request
|
||
|
#
|
||
|
# @return [String] a PING request
|
||
|
def ping
|
||
|
encode_message(PING)
|
||
|
end
|
||
|
|
||
|
# Decode a PING response, PONG
|
||
|
#
|
||
|
# @param response [String] the response to decode
|
||
|
# @return [Integer] the source port from the PING response if the response is valid, nil otherwise
|
||
|
def decode_pong(response)
|
||
|
type, port = decode_message(response)
|
||
|
# abort if this isn't a pong
|
||
|
return nil unless type == PONG
|
||
|
# abort if the response is too large/small
|
||
|
return nil unless port && port.size == 2
|
||
|
# this should always be equivalent to the source port from which the PING was received
|
||
|
port.unpack('v')[0]
|
||
|
end
|
||
|
|
||
|
# Decode a list of peers from a BOOTSTRAP response
|
||
|
#
|
||
|
# @param peers_data [String] the peers data from a BOOTSTRAP response
|
||
|
# @return [Array] a list of the peers and their associated metadata extracted
|
||
|
# from the response if valid, nil otherwise
|
||
|
def decode_bootstrap_peers(peers_data)
|
||
|
# sanity check total size
|
||
|
return nil unless peers_data.size % BOOTSTRAP_PEER_SIZE == 0
|
||
|
peers = []
|
||
|
until peers_data.blank?
|
||
|
peers << decode_bootstrap_peer(peers_data.slice!(0, BOOTSTRAP_PEER_SIZE))
|
||
|
end
|
||
|
peers
|
||
|
end
|
||
|
|
||
|
# Decodes a single set of peer data from a BOOTSTRAP reseponse
|
||
|
#
|
||
|
# @param peer-data [String] the peer data for one peer from a BOOSTRAP response
|
||
|
# @return [Array] the peer ID, IPv4 addresss, UDP port, TCP port and version of this peer
|
||
|
def decode_bootstrap_peer(peer_data)
|
||
|
# sanity check the size of this peer's data
|
||
|
return nil unless peer_data.size == BOOTSTRAP_PEER_SIZE
|
||
|
# TODO; interpret this properly
|
||
|
peer_id = peer_data.slice!(0, 16)
|
||
|
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
|
||
|
[ decode_peer_id(peer_id), Rex::Socket.addr_itoa(ip), udp_port, tcp_port, version ]
|
||
|
end
|
||
|
|
||
|
# Decodes an on-the-wire representation of a Kademlia peer to its 16-character hex equivalent
|
||
|
#
|
||
|
# @param bytes [String] the on-the-wire representation of a Kademlia peer
|
||
|
# @return [String] the peer ID if valid, nil otherwise
|
||
|
def decode_peer_id(bytes)
|
||
|
peer_id = 0
|
||
|
return nil unless bytes.size == 16
|
||
|
bytes.unpack('VVVV').map { |p| peer_id <<= 32; peer_id ^= p; }
|
||
|
peer_id.to_s(16).upcase
|
||
|
end
|
||
|
|
||
|
# TODO
|
||
|
# def encode_peer_id(id)
|
||
|
# end
|
||
|
end
|
||
|
end
|
||
|
end
|