Move Kademlia stuff to a more OO model, etc, per reviews

All of the work is done in rex.  The msf mixin just prevents the
desire to call rex directly from the module
bug/bundler_fix
Jon Hart 2014-11-24 14:03:43 -08:00
parent e255db9429
commit 0ed356f71c
No known key found for this signature in database
GPG Key ID: 2FA9F0A3AFA8E9D3
17 changed files with 414 additions and 262 deletions

View File

@ -1,4 +1,5 @@
# -*- coding: binary -*-
require 'rex/proto/kademlia'
module Msf
@ -10,109 +11,5 @@ module Msf
###
module Auxiliary::Kademlia
include Rex::Proto::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)
message = Message.from_data(response)
# abort if this isn't a valid response
return nil unless message.type = BOOTSTRAP_RES
return nil unless message.body.size >= 23
peer_id = decode_peer_id(message.body.slice!(0,16))
tcp_port, version, num_peers = message.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 && message.body.blank?
peers = decode_bootstrap_peers(message.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
Message.new(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)
message = Message.from_data(response)
# abort if this isn't a pong
return nil unless message.type == PONG
# abort if the response is too large/small
return nil unless message.body && message.body.size == 2
# this should always be equivalent to the source port from which the PING was received
message.body.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

View File

@ -1,3 +1,8 @@
# -*- coding: binary -*-
require 'rex/proto/kademlia/bootstrap_request'
require 'rex/proto/kademlia/bootstrap_response'
require 'rex/proto/kademlia/message'
require 'rex/proto/kademlia/ping'
require 'rex/proto/kademlia/pong'
require 'rex/proto/kademlia/util'

View File

@ -0,0 +1,19 @@
# -*- coding: binary -*-
require 'rex/proto/kademlia/message'
module Rex
module Proto
module Kademlia
# Opcode for a BOOTSTRAP request
BOOTSTRAP_REQUEST = 0x01
# A Kademlia bootstrap request message
class BootstrapRequest < Message
def initialize
super(BOOTSTRAP_REQUEST)
end
end
end
end
end

View File

@ -0,0 +1,70 @@
# -*- coding: binary -*-
require 'rex/proto/kademlia/message'
require 'rex/proto/kademlia/util'
module Rex
module Proto
module Kademlia
# Opcode for a bootstrap response
BOOTSTRAP_RESPONSE = 0x09
# A Kademlia bootstrap response message
class BootstrapResponse < Message
attr_reader :peer_id
attr_reader :tcp_port
attr_reader :version
# An array of hashes containing the peer ID, IP address, UDP and TCP ports as well as the type/version
attr_reader :peers
def initialize(peer_id, tcp_port, version, peers)
@peer_id = peer_id
@tcp_port = tcp_port
@version = version
@peers = peers
end
# 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 response from given data
#
# @param data [String] the data to decode
# @return [BootstrapResponse] the bootstrap response if the data is valid, nil otherwise
def self.from_data(data)
message = Message.from_data(data)
# abort if this isn't a valid response
return unless message
return unless message.type == BOOTSTRAP_RESPONSE
return unless message.body.size >= 23
bootstrap_peer_id = Rex::Proto::Kademlia.decode_peer_id(message.body.slice!(0, 16))
bootstrap_tcp_port, bootstrap_version, num_peers = message.body.slice!(0, 5).unpack('vCv')
# protocol says there are no peers and the body confirms this, so just return with no peers
if num_peers == 0 && message.body.blank?
peers = []
else
peers_data = message.body
# peers data is too long/short, abort
return if peers_data.size % BOOTSTRAP_PEER_SIZE != 0
peers = []
until peers_data.blank?
peer_data = peers_data.slice!(0, BOOTSTRAP_PEER_SIZE)
peer_id = Rex::Proto::Kademlia.decode_peer_id(peer_data.slice!(0, 16))
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
peers << {
id: peer_id,
ip: Rex::Socket.addr_itoa(ip),
tcp_port: tcp_port,
udp_port: udp_port,
version: version
}
end
end
BootstrapResponse.new(bootstrap_peer_id, bootstrap_tcp_port, bootstrap_version, peers)
end
end
end
end
end

View File

@ -17,12 +17,12 @@ module Proto
#
##
module Kademlia
# The header that non-compressed Kad messages use
STANDARD_PACKET = 0xE4
# The header that compressed Kad messages use, which is currently unsupported
COMPRESSED_PACKET = 0xE5
class Message
# The header that non-compressed Kad messages use
STANDARD_PACKET = 0xE4
# The header that compressed Kad messages use, which is currently unsupported
COMPRESSED_PACKET = 0xE5
attr_accessor :type, :body
# @param type [String] the message type
@ -36,7 +36,7 @@ module Kademlia
return if data.length < 2
header, type = data.unpack('CC')
if header == COMPRESSED_PACKET
fail NotImplementedError, "Unable to handle #{message.length}-byte compressed Kademlia message"
fail NotImplementedError, "Unable to handle #{data.length}-byte compressed Kademlia message"
end
return if header != STANDARD_PACKET
Message.new(type, data[2, data.length])
@ -45,6 +45,10 @@ module Kademlia
def to_str
[STANDARD_PACKET, @type].pack('CC') + @body
end
def ==(other)
type == other.type && body == other.body
end
end
end
end

View File

@ -0,0 +1,19 @@
# -*- coding: binary -*-
require 'rex/proto/kademlia/message'
module Rex
module Proto
module Kademlia
# Opcode for a PING request
PING = 0x60
# A Kademlia ping message.
class Ping < Message
def initialize
super(PING)
end
end
end
end
end

View File

@ -0,0 +1,34 @@
# -*- coding: binary -*-
require 'rex/proto/kademlia/message'
module Rex
module Proto
module Kademlia
# Opcode for a PING response
PONG = 0x61
# A Kademlia pong message.
class Pong < Message
# the source port from which the PING was received
attr_reader :port
def initialize(port = nil)
super(PONG)
@port = port
end
def self.from_data(data)
message = super(data)
return if message.type != PONG
return if message.body.size != 2
Pong.new(message.body.unpack('v')[0])
end
def to_str
super + [@port].pack('v')
end
end
end
end
end

View File

@ -0,0 +1,22 @@
# -*- coding: binary -*-
module Rex
module Proto
module Kademlia
# 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 self.decode_peer_id(bytes)
peer_id = 0
return nil unless bytes.size == 16
bytes.unpack('VVVV').map { |p| peer_id = ((peer_id << 32) ^ p) }
peer_id.to_s(16).upcase
end
# TODO
# def encode_peer_id(id)
# end
end
end
end

View File

@ -46,9 +46,9 @@ class Metasploit3 < Msf::Auxiliary
def build_probe
@probe ||= case action.name
when 'BOOTSTRAP'
bootstrap
BootstrapRequest.new
when 'PING'
ping
Ping.new
end
end
@ -58,22 +58,22 @@ class Metasploit3 < Msf::Auxiliary
case action.name
when 'BOOTSTRAP'
peer_id, tcp_port, version, peers = decode_bootstrap_res(response)
info = {
peer_id: peer_id,
tcp_port: tcp_port,
version: version,
peers: peers
}
if datastore['VERBOSE']
else
print_good("#{peer} ID #{peer_id}, TCP port #{tcp_port}, version #{version}, #{peers.size} peers")
if bootstrap_res = BootstrapResponse.from_data(response)
info = {
peer_id: bootstrap_res.peer_id,
tcp_port: bootstrap_res.tcp_port,
version: bootstrap_res.version,
peers: bootstrap_res.peers
}
print_good("#{peer} ID #{bootstrap_res.peer_id}, TCP port #{bootstrap_res.tcp_port}," +
" version #{bootstrap_res.version}, #{bootstrap_res.peers.size} peers")
end
when 'PING'
udp_port = decode_pong(response)
print_good("#{peer} PONG")
# udp_port should match the port we contacted it from. TODO: validate this?
info = { udp_port: udp_port }
if pong = Pong.from_data(response)
print_good("#{peer} PONG port #{pong.port}")
# port should match the port we contacted it from. TODO: validate this?
info = { udp_port: pong.port }
end
end
return unless info

View File

@ -1,4 +1,5 @@
# -*- coding: binary -*-
#
require 'spec_helper'
require 'msf/core/auxiliary/kademlia'
@ -8,109 +9,4 @@ describe Msf::Auxiliary::Kademlia do
mod.extend described_class
mod
end
describe '#decode_pong' do
it 'does not decode overly small pongs' do
expect(kad.decode_pong("\xE4\x61\x01")).to eq(nil)
end
it 'does not decode overly large pongs' do
expect(kad.decode_pong("\xE4\x61\x01\x02\x03")).to eq(nil)
end
it 'properly decodes valid pongs' do
expect(kad.decode_pong("\xE4\x61\x9E\x86")).to eq(34462)
end
end
describe '#decode_bootstrap_peer' do
it 'does not decode overly small peer responses' do
expect(kad.decode_bootstrap_peer("this is too small")).to eq(nil)
end
it 'does not decode overly large peer responses' do
expect(kad.decode_bootstrap_peer("this is much, much, much too large")).to eq(nil)
end
it 'properly extracts peer info' do
data =
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + # peer ID
"\x04\x28\xA8\xC0" + # 192.168.40.4
"\x31\xd4" + # UDP port 54321
"\x39\x30" + # TCP port 12345
"\x08" # peer type
peer_id, ip, udp_port, tcp_port, type = kad.decode_bootstrap_peer(data)
expect(peer_id).to eq('3020100070605040B0A09080F0E0D0C')
expect(ip).to eq('192.168.40.4')
expect(udp_port).to eq(54321)
expect(tcp_port).to eq(12345)
expect(type).to eq(8)
end
end
describe '#decode_bootstrap_peers' do
it 'does not decode overly small bootstrap responses' do
expect(kad.decode_bootstrap_peer("this is too small")).to eq(nil)
end
it 'does not decode overly large bootstrap responses' do
expect(kad.decode_bootstrap_peer("this is large enough but truncated")).to eq(nil)
end
it 'properly extracts peers info' do
data =
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + # peer ID
"\x04\x28\xA8\xC0" + # 192.168.40.4
"\x31\xd4" + # UDP port 54321
"\x39\x30" + # TCP port 12345
"\x08" + # peer type
"\x01\x01\x02\x02\x03\x03\x04\x04\x05\x05\x06\x06\x07\x07\x08\x08" + # peer ID
"\x05\x28\xA8\xC0" + # 192.168.40.5
"\x5c\x11" + # UDP port 4444
"\xb3\x15" + # TCP port 5555
"\x09" # peer type
peers = kad.decode_bootstrap_peers(data)
expect(peers.size).to eq(2)
peer1_id, peer1_ip, peer1_udp, peer1_tcp, peer1_type = peers.first
expect(peer1_id).to eq('3020100070605040B0A09080F0E0D0C')
expect(peer1_ip).to eq('192.168.40.4')
expect(peer1_udp).to eq(54321)
expect(peer1_tcp).to eq(12345)
expect(peer1_type).to eq(8)
peer2_id, peer2_ip, peer2_udp, peer2_tcp, peer2_type = peers.last
expect(peer2_id).to eq('2020101040403030606050508080707')
expect(peer2_ip).to eq('192.168.40.5')
expect(peer2_udp).to eq(4444)
expect(peer2_tcp).to eq(5555)
expect(peer2_type).to eq(9)
end
end
describe '#decode_bootstrap_res' do
it 'properly decodes valid bootstrap responses' do
data = IO.read(File.join(File.dirname(__FILE__), 'kademlia_bootstrap_res.bin'))
peer_id, tcp, version, peers = kad.decode_bootstrap_res(data)
expect(peer_id).to eq('B54A83462529B21EF51FD54B956B07B0')
expect(tcp).to eq(4662)
expect(version).to eq(8)
# don't bother checking every peer
expect(peers.size).to eq(20)
end
end
describe '#decode_peer_id' do
it 'decodes a peer ID properly' do
bytes = "\x00\x60\x89\x9B\x0A\x0B\xBE\xAE\x45\x35\xCB\x0E\x07\xA1\x77\x71"
peer_id = "9B896000AEBE0B0A0ECB35457177A107"
expect(kad.decode_peer_id(bytes)).to eq(peer_id)
end
end
describe '#encode_peer' do
skip 'encodes a peer ID properly' do
bytes = "\x00\x60\x89\x9B\x0A\x0B\xBE\xAE\x45\x35\xCB\x0E\x07\xA1\x77\x71"
peer_id = "9B896000AEBE0B0A0ECB35457177A107"
expect(kad.encode_peer_id(peer_id)).to eq(bytes)
end
end
end

View File

@ -0,0 +1,23 @@
# -*- coding: binary -*-
require 'spec_helper'
require 'rex/proto/kademlia/bootstrap_request'
describe Rex::Proto::Kademlia::BootstrapRequest do
subject(:bootstrap) do
described_class.new
end
describe '#initialize' do
it 'constructs properly' do
expect(bootstrap.type).to eq(Rex::Proto::Kademlia::BOOTSTRAP_REQUEST)
expect(bootstrap.body).to eq('')
end
end
describe '#to_str' do
it 'packs properly' do
expect(bootstrap.to_str).to eq("\xE4\x01")
end
end
end

View File

@ -0,0 +1,38 @@
# -*- coding: binary -*-
require 'spec_helper'
require 'rex/proto/kademlia/bootstrap_response'
describe Rex::Proto::Kademlia::BootstrapResponse do
describe '#from_data' do
it 'properly decodes real valid bootstrap responses' do
data = IO.read(File.join(File.dirname(__FILE__), 'kademlia_bootstrap_res.bin'))
response = described_class.from_data(data)
expect(response.peer_id).to eq('B54A83462529B21EF51FD54B956B07B0')
expect(response.tcp_port).to eq(4662)
expect(response.version).to eq(8)
# don't bother checking every peer
expect(response.peers.size).to eq(20)
peer = response.peers.first
expect(peer[:id]).to eq('B0A5518388D66BC211B0B9F75B3DCB10')
expect(peer[:ip]).to eq('149.91.116.59')
expect(peer[:tcp_port]).to eq(4882)
expect(peer[:udp_port]).to eq(4992)
expect(peer[:type]).to eq(8)
peer = response.peers.last
expect(peer[:id]).to eq('9B896000AEBE0B0A0ECB35457177A107')
expect(peer[:ip]).to eq('83.46.192.208')
expect(peer[:tcp_port]).to eq(3662)
expect(peer[:udp_port]).to eq(3672)
expect(peer[:type]).to eq(8)
end
it 'does not decode overly small bootstrap responses' do
expect(described_class.from_data('this is too small')).to eq(nil)
end
it 'does not decode malformed bootstrap responses' do
expect(described_class.from_data('this is large enough but truncated')).to eq(nil)
end
end
end

View File

@ -2,49 +2,88 @@
require 'spec_helper'
require 'rex/proto/kademlia/message'
describe Rex::Proto::Kademlia do
subject(:kad) do
mod = Module.new
mod.extend described_class
mod
end
describe Rex::Proto::Kademlia::Message do
describe '#encode_message' do
let(:no_body) { "\xE4\x01" }
let(:body) { "\xE4\x01p2p" }
it 'properly encodes messages without a body' do
expect(kad.encode_message(1)).to eq("\xE4\x01")
context 'with a body' do
let(:type) { 1 }
let(:body) { 'test' }
let(:data) { "\xE4\x01test" }
subject(:message) do
described_class.new(type, body)
end
it 'properly encodes messages with a body' do
expect(kad.encode_message(1, 'p2p')).to eq("\xE4\x01p2p")
describe '#initialize' do
it 'constructs properly' do
expect(message.type).to eq(type)
expect(message.body).to eq(body)
end
end
describe '#to_str' do
it 'packs properly' do
expect(message.to_str).to eq(data)
end
end
describe '#from_data' do
it 'unpacks supported messages properly' do
unpacked = described_class.from_data(data)
expect(unpacked.type).to eq(type)
expect(unpacked.body).to eq(body)
end
it 'raises on compressed messages' do
expect do
described_class.from_data("\xE5\x01test")
end.to raise_error(NotImplementedError)
end
end
describe '#==' do
it 'respects equality' do
expect(described_class.new(1, 'test')).to eq(described_class.new(1, 'test'))
expect(described_class.new(1, 'test')).not_to eq(described_class.new(1, 'not'))
expect(described_class.new(1, 'test')).not_to eq(described_class.new(2, 'test'))
expect(described_class.new(1, 'test')).not_to eq(described_class.new(2, 'not'))
end
end
end
describe '#decode_message' do
it 'does not decode overly short messages' do
expect(kad.decode_message('f')).to eq(nil)
context 'without a body' do
let(:type) { 2 }
let(:body) { '' }
let(:data) { "\xE4\x02" }
subject(:message) do
described_class.new(type, body)
end
it 'does not decode unknown messages' do
expect(kad.decode_message("this is not kademlia")).to eq(nil)
describe '#initialize' do
it 'constructs properly' do
expect(message.type).to eq(type)
expect(message.body).to eq(body)
end
end
it 'raises on compressed messages' do
expect do
kad.decode_message("\xE5\x01blahblah")
end.to raise_error(NotImplementedError)
describe '#to_str' do
it 'packs properly' do
expect(message.to_str).to eq(data)
end
end
it 'properly decodes valid messages without a body' do
type, payload = kad.decode_message("\xE4\xFF")
expect(type).to eq(0xFF)
expect(payload).to eq('')
end
describe '#from_data' do
it 'unpacks supported messages properly' do
unpacked = described_class.from_data(data)
expect(unpacked.type).to eq(type)
expect(unpacked.body).to eq(body)
end
it 'properly decodes valid messages wth a body' do
type, payload = kad.decode_message("\xE4\xFFtesttesttest")
expect(type).to eq(0xFF)
expect(payload).to eq('testtesttest')
it 'raises on compressed messages' do
expect do
described_class.from_data("\xE5\x01")
end.to raise_error(NotImplementedError)
end
end
end
end

View File

@ -0,0 +1,23 @@
# -*- coding: binary -*-
require 'spec_helper'
require 'rex/proto/kademlia/ping'
describe Rex::Proto::Kademlia::Ping do
subject(:ping) do
described_class.new
end
describe '#initialize' do
it 'constructs properly' do
expect(ping.type).to eq(Rex::Proto::Kademlia::PING)
expect(ping.body).to eq('')
end
end
describe '#to_str' do
it 'packs properly' do
expect(ping.to_str).to eq("\xE4\x60")
end
end
end

View File

@ -0,0 +1,40 @@
# -*- coding: binary -*-
require 'spec_helper'
require 'rex/proto/kademlia/pong'
describe Rex::Proto::Kademlia::Pong do
let(:port) { 12345 }
subject(:pong) do
described_class.new(port)
end
describe '#initialize' do
it 'constructs properly' do
expect(pong.type).to eq(Rex::Proto::Kademlia::PONG)
expect(pong.port).to eq(port)
end
end
describe '#to_str' do
it 'packs properly' do
expect(pong.to_str).to eq("\xE4\x61\x39\x30")
end
end
describe '#from_data' do
it 'unpacks supported valid pongs properly' do
unpacked = described_class.from_data("\xE4\x61\x9E\x86")
expect(unpacked.type).to eq(Rex::Proto::Kademlia::PONG)
expect(unpacked.port).to eq(34462)
end
it 'does not decode overly small pongs' do
expect(described_class.from_data("\xE4\x61\x01")).to eq(nil)
end
it 'does not decode overly large pongs' do
expect(described_class.from_data("\xE4\x61\x01\x02\x03")).to eq(nil)
end
end
end

View File

@ -0,0 +1,23 @@
# -*- coding: binary -*-
#
require 'spec_helper'
require 'rex/proto/kademlia/util'
describe Rex::Proto::Kademlia do
describe '#decode_peer_id' do
subject(:kad) { described_class.decode_peer_id(bytes) }
let(:bytes) { "\x00\x60\x89\x9B\x0A\x0B\xBE\xAE\x45\x35\xCB\x0E\x07\xA1\x77\x71" }
it 'decodes a peer ID properly' do
is_expected.to eq('9B896000AEBE0B0A0ECB35457177A107')
end
end
describe '#encode_peer' do
skip 'encodes a peer ID properly' do
bytes = "\x00\x60\x89\x9B\x0A\x0B\xBE\xAE\x45\x35\xCB\x0E\x07\xA1\x77\x71"
peer_id = "9B896000AEBE0B0A0ECB35457177A107"
expect(kad.encode_peer_id(peer_id)).to eq(bytes)
end
end
end