Skip to content

Commit ab49d01

Browse files
committed
Add beginnings of Kademlia gather module and protocol support
1 parent a91a5f3 commit ab49d01

File tree

5 files changed

+310
-0
lines changed

5 files changed

+310
-0
lines changed

lib/rex/proto/kademlia.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- coding: binary -*-
2+
3+
require 'rex/proto/steam/message'

lib/rex/proto/kademlia/message.rb

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# -*- coding: binary -*-
2+
3+
module Rex
4+
module Proto
5+
##
6+
#
7+
# Minimal support for the newer Kademlia protocol, referred to here and often
8+
# elsewhere as Kademlia2. It is unclear how this differs from the old protocol.
9+
#
10+
# Protocol details are hard to come by because most documentation is academic
11+
# in nature and glosses over the low-level network details. The best
12+
# documents I found on the protocol are:
13+
#
14+
# http://gbmaster.wordpress.com/2013/05/05/botnets-surrounding-us-an-initial-focus-on-kad/
15+
# http://gbmaster.wordpress.com/2013/06/16/botnets-surrounding-us-sending-kademlia2_bootstrap_req-kademlia2_hello_req-and-their-strict-cousins/
16+
# http://gbmaster.wordpress.com/2013/11/23/botnets-surrounding-us-performing-requests-sending-out-kademlia2_req-and-asking-contact-where-art-thou/
17+
#
18+
##
19+
module Kademlia
20+
STANDARD_PACKET = 0xE4
21+
# TODO: support this format
22+
COMPRESSED_PACKET = 0xE5
23+
24+
BOOTSTRAP_REQ = 0x01
25+
BOOTSTRAP_RES = 0x09
26+
PING = 0x60
27+
PONG = 0x61
28+
29+
# The minimum size of a peer in a KADEMLIA2_BOOTSTRAP_RES message:
30+
# peer ID (16-bytes), IP (4 bytes), UDP port (2 bytes), TCP port (2 bytes)
31+
# and version (1 byte)
32+
BOOTSTRAP_PEER_SIZE = 25
33+
34+
def decode_message(message)
35+
# minimum size is header (1) + opcode (1) + stuff (0+)
36+
return if message.length < 2
37+
header, opcode = message.unpack('CC')
38+
if header == COMPRESSED_PACKET
39+
fail NotImplementedError, "Unable to handle compressed #{message.length}-byte compressed Kademlia message"
40+
end
41+
return if header != STANDARD_PACKET
42+
[opcode, message[2, message.length]]
43+
end
44+
45+
def encode_message(type, payload = '')
46+
[STANDARD_PACKET, type].pack('CC') + payload
47+
end
48+
49+
def bootstrap
50+
encode_message(BOOTSTRAP_REQ)
51+
end
52+
53+
def decode_bootstrap_res(message)
54+
opcode, payload = decode_message(message)
55+
# abort if this isn't a valid response
56+
return nil unless opcode = BOOTSTRAP_RES
57+
return nil unless payload.size >= 23
58+
# XXX: unsure how to get the 128-bit contact ID just yet...
59+
peer_id = payload.slice!(0,16)
60+
tcp_port, version, num_peers = payload.slice!(0,5).unpack('vCv')
61+
# protocol says there are no peers and the payload confirms this, so just return with no peers
62+
return [ tcp_port, version, []] if num_peers == 0 && payload.blank?
63+
peers = decode_bootstrap_peers(payload)
64+
# abort if the peer data was invalid
65+
return nil unless peers
66+
[ peer_id, tcp_port, version, peers ]
67+
end
68+
69+
# Returns a PING message
70+
def ping
71+
encode_message(PING)
72+
end
73+
74+
# Decodes a PONG message, returning the port used by the peer
75+
def decode_pong(message)
76+
opcode, port = decode_message(message)
77+
# abort if this isn't a pong
78+
return nil unless opcode == PONG
79+
# abort if the response is too large/small
80+
return nil unless port && port.size == 2
81+
# the port will be the port the true endpoint is actually listening on,
82+
# which may not be the same as the port you contacted it on (NAT/etc)
83+
port.unpack('v')[0]
84+
end
85+
86+
def decode_bootstrap_peers(peers_data)
87+
# sanity check total size
88+
return nil unless peers_data.size % BOOTSTRAP_PEER_SIZE == 0
89+
peers = []
90+
until peers_data.blank?
91+
peers << decode_bootstrap_peer(peers_data.slice!(0, BOOTSTRAP_PEER_SIZE))
92+
end
93+
peers
94+
end
95+
96+
def decode_bootstrap_peer(peer_data)
97+
# sanity check the size of this peer's data
98+
return nil unless peer_data.size == BOOTSTRAP_PEER_SIZE
99+
# TODO; interpret this properly
100+
peer_id = peer_data.slice!(0, 16)
101+
ip, udp_port, tcp_port, version = peer_data.unpack('VvvC')
102+
[ peer_id, Rex::Socket.addr_itoa(ip), udp_port, tcp_port, version ]
103+
end
104+
end
105+
end
106+
end
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rex/proto/kademlia'
8+
9+
class Metasploit3 < Msf::Auxiliary
10+
include Msf::Auxiliary::Report
11+
include Msf::Auxiliary::UDPScanner
12+
include Rex::Proto::Kademlia
13+
14+
def initialize(info = {})
15+
super(
16+
update_info(
17+
info,
18+
'Name' => 'Gather Kademlia Server Information',
19+
'Description' => %q(
20+
This module uses the Kademlia BOOTSTRAP and PING messages to identify
21+
and extract information from Kademlia speaking UDP endpoints,
22+
typically belonging to eMule/eDonkey/BitTorrent servers or other P2P
23+
applications.
24+
),
25+
'Author' => 'Jon Hart <jon_hart[at]rapid7.com',
26+
'References' =>
27+
[
28+
# There are lots of academic papers on the protocol but they tend to lack usable
29+
# protocol details. This is the best I've found
30+
['URL', 'http://gbmaster.wordpress.com/2013/06/16/botnets-surrounding-us-sending-kademlia2_bootstrap_req-kademlia2_hello_req-and-their-strict-cousins/#more-125']
31+
],
32+
'License' => MSF_LICENSE,
33+
'Actions' => [
34+
['BOOTSTRAP', 'Description' => 'Use a Kademlia2 BOOTSTRAP'],
35+
['PING', 'Description' => 'Use a Kademlia2 PING']
36+
],
37+
'DefaultAction' => 'BOOTSTRAP'
38+
)
39+
)
40+
41+
register_options(
42+
[
43+
Opt::RPORT(4672)
44+
], self.class)
45+
end
46+
47+
def build_probe
48+
@probe ||= case action.name
49+
when 'BOOTSTRAP'
50+
bootstrap
51+
when 'PING'
52+
ping
53+
end
54+
end
55+
56+
def scanner_process(response, src_host, src_port)
57+
info = message_decode(response)
58+
return unless info
59+
@results[src_host] ||= []
60+
if datastore['VERBOSE']
61+
print_good("#{src_host}:#{src_port} found '#{info.inspect}'")
62+
else
63+
print_good("#{src_host}:#{src_port} found '#{info[:name]}'")
64+
end
65+
@results[src_host] << info
66+
end
67+
68+
def scanner_postscan(_batch)
69+
@results.each_pair do |host, info|
70+
report_host(host: host)
71+
report_service(
72+
host: host,
73+
proto: 'udp',
74+
port: rport,
75+
name: 'Kademlia',
76+
info: info
77+
)
78+
end
79+
end
80+
end
Binary file not shown.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# -*- coding: binary -*-
2+
require 'spec_helper'
3+
require 'rex/proto/kademlia/message'
4+
5+
describe Rex::Proto::Kademlia do
6+
subject do
7+
mod = Module.new
8+
mod.extend described_class
9+
mod
10+
end
11+
12+
describe '#encode_message' do
13+
it 'should properly encode messages' do
14+
expect(subject.encode_message(1)).to eq("\xE4\x01")
15+
expect(subject.encode_message(1, 'p2p')).to eq("\xE4\x01p2p")
16+
end
17+
end
18+
19+
describe '#decode_message' do
20+
it 'should not decode overly short messages' do
21+
expect(subject.decode_message('f')).to eq(nil)
22+
end
23+
24+
it 'should not decode unknown messages' do
25+
expect(subject.decode_message("this is not kademlia")).to eq(nil)
26+
end
27+
28+
it 'should raise on compressed messages' do
29+
expect {
30+
subject.decode_message("\xE5\x01blahblah")
31+
}.to raise_error(NotImplementedError)
32+
end
33+
34+
it 'should properly decode valid messages' do
35+
type, payload = subject.decode_message("\xE4\xFF")
36+
expect(type).to eq(0xFF)
37+
expect(payload).to eq('')
38+
39+
_, payload = subject.decode_message("\xE4\xFFtesttesttest")
40+
expect(payload).to eq('testtesttest')
41+
end
42+
end
43+
44+
describe '#decode_pong' do
45+
it 'should not decode overly large/small pongs' do
46+
expect(subject.decode_pong("\xE4\x61\x01")).to eq(nil)
47+
expect(subject.decode_pong("\xE4\x61\x01\x02\x03")).to eq(nil)
48+
end
49+
50+
it 'should properly decode valid pongs' do
51+
expect(subject.decode_pong("\xE4\x61\x9E\x86")).to eq(34462)
52+
end
53+
end
54+
55+
describe '#decode_bootstrap_peer' do
56+
it 'should not decode overly large/small peer' do
57+
expect(subject.decode_bootstrap_peer("this is too small")).to eq(nil)
58+
expect(subject.decode_bootstrap_peer("this is much, much, much too large")).to eq(nil)
59+
end
60+
61+
it 'should properly extract peer info' do
62+
data =
63+
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + # peer ID
64+
"\x04\x28\xA8\xC0" + # 192.168.40.4
65+
"\x31\xd4" + # UDP port 54321
66+
"\x39\x30" + # TCP port 12345
67+
"\x08" # peer type
68+
peer_id, ip, udp_port, tcp_port, type = subject.decode_bootstrap_peer(data)
69+
#expect(peer_id).to eq('something')
70+
expect(ip).to eq('192.168.40.4')
71+
expect(udp_port).to eq(54321)
72+
expect(tcp_port).to eq(12345)
73+
expect(type).to eq(8)
74+
end
75+
end
76+
77+
describe '#decode_bootstrap_peers' do
78+
it 'should not decode overly small peers' do
79+
expect(subject.decode_bootstrap_peer("this is too small")).to eq(nil)
80+
expect(subject.decode_bootstrap_peer("this is large enough but truncated")).to eq(nil)
81+
end
82+
83+
it 'should properly extract peers info' do
84+
data =
85+
"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F" + # peer ID
86+
"\x04\x28\xA8\xC0" + # 192.168.40.4
87+
"\x31\xd4" + # UDP port 54321
88+
"\x39\x30" + # TCP port 12345
89+
"\x08" + # peer type
90+
"\x01\x01\x02\x02\x03\x03\x04\x04\x05\x05\x06\x06\x07\x07\x08\x08" + # peer ID
91+
"\x05\x28\xA8\xC0" + # 192.168.40.5
92+
"\x5c\x11" + # UDP port 4444
93+
"\xb3\x15" + # TCP port 5555
94+
"\x09" # peer type
95+
peers = subject.decode_bootstrap_peers(data)
96+
expect(peers.size).to eq(2)
97+
peer1_id, peer1_ip, peer1_udp, peer1_tcp, peer1_type = peers.first
98+
expect(peer1_ip).to eq('192.168.40.4')
99+
expect(peer1_udp).to eq(54321)
100+
expect(peer1_tcp).to eq(12345)
101+
expect(peer1_type).to eq(8)
102+
peer2_id, peer2_ip, peer2_udp, peer2_tcp, peer2_type = peers.last
103+
expect(peer2_ip).to eq('192.168.40.5')
104+
expect(peer2_udp).to eq(4444)
105+
expect(peer2_tcp).to eq(5555)
106+
expect(peer2_type).to eq(9)
107+
end
108+
end
109+
110+
describe '#decode_bootstrap_res' do
111+
it 'should properly decode valid bootstrap responses' do
112+
data = IO.read(File.join(File.dirname(__FILE__), 'kademlia_bootstrap_res.bin'))
113+
peer_id, tcp, version, peers = subject.decode_bootstrap_res(data)
114+
#expect(peer_id).to eq('XXXX')
115+
expect(tcp).to eq(4662)
116+
expect(version).to eq(8)
117+
# don't bother checking every peer
118+
expect(peers.size).to eq(20)
119+
end
120+
end
121+
end

0 commit comments

Comments
 (0)