Skip to content

Commit 698efe8

Browse files
authored
Merge pull request #286 from zeroSteiner/feat/dcerpc/epm/1
Refactor EPM Functionality
2 parents c2663e1 + cd2a57b commit 698efe8

6 files changed

Lines changed: 255 additions & 123 deletions

File tree

examples/epm_client.rb

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/ruby
2+
3+
require 'bundler/setup'
4+
require 'ruby_smb'
5+
6+
require 'optparse'
7+
require 'pp'
8+
9+
options = {
10+
major_version: 1,
11+
minor_version: 0,
12+
max_towers: 1,
13+
}
14+
15+
parser = OptionParser.new do |opts|
16+
opts.banner = "Usage: script.rb [options] TARGET PROTOCOL UUID"
17+
18+
opts.on("--major-version N", Integer, "Specify major version number (default: #{options[:major_version]})") do |v|
19+
options[:major_version] = v
20+
end
21+
22+
opts.on("--minor-version N", Integer, "Specify minor version number ((default: #{options[:minor_version]})") do |v|
23+
options[:minor_version] = v
24+
end
25+
26+
opts.on("--max-towers N", Integer, "Set the maximum number of towers (default: #{options[:max_towers]})") do |v|
27+
options[:max_towers] = v
28+
end
29+
30+
opts.on("-h", "--help", "Prints this help") do
31+
puts opts
32+
exit
33+
end
34+
end
35+
36+
# Parse and extract positional arguments
37+
begin
38+
parser.order!(ARGV)
39+
if ARGV.size != 3
40+
raise OptionParser::MissingArgument, "TARGET, PROTOCOL, and UUID are required"
41+
end
42+
43+
options[:target], options[:protocol], options[:uuid] = ARGV
44+
rescue OptionParser::ParseError => e
45+
puts e.message
46+
puts parser
47+
exit 1
48+
end
49+
50+
dcerpc_client = RubySMB::Dcerpc::Client.new(options[:target], RubySMB::Dcerpc::Epm)
51+
dcerpc_client.connect
52+
dcerpc_client.bind
53+
dcerpc_client.ept_map(
54+
uuid: options[:uuid],
55+
maj_ver: options[:major_version],
56+
min_ver: options[:minor_version],
57+
protocol: options[:protocol].to_sym,
58+
max_towers: options[:max_towers]
59+
).each do |tower|
60+
puts "Tower: #{tower[:endpoint]}"
61+
tower.each do |key, value|
62+
next if key == :endpoint
63+
puts " #{key}: #{value}"
64+
end
65+
end

lib/ruby_smb/dcerpc.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module Dcerpc
2828
DCE_C_AUTHZ_DCE = 2
2929

3030
require 'windows_error/win32'
31+
require 'ruby_smb/dcerpc/client'
3132
require 'ruby_smb/dcerpc/error'
3233
require 'ruby_smb/dcerpc/fault'
3334
require 'ruby_smb/dcerpc/uuid'
@@ -190,6 +191,8 @@ def force_set_auth_params(auth_type, auth_level)
190191
# @raise [ArgumentError] if `:auth_type` is unknown
191192
# @raise [NotImplementedError] if `:auth_type` is not implemented (yet)
192193
def bind(options={})
194+
options = options.merge(endpoint: @endpoint) if !options[:endpoint] && defined?(:@endpoint) && @endpoint
195+
193196
@call_id ||= 1
194197
bind_req = Bind.new(options)
195198
bind_req.pdu_header.call_id = @call_id

lib/ruby_smb/dcerpc/client.rb

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class Client
1111
require 'ruby_smb/peer_info'
1212

1313
include Dcerpc
14-
include Epm
1514
include PeerInfo
1615

1716
# The default maximum size of a RPC message that the Client accepts (in bytes)
@@ -146,27 +145,31 @@ def initialize(host,
146145
# @return [TcpSocket] The connected TCP socket
147146
def connect(port: nil)
148147
return if @tcp_socket
148+
149149
unless port
150-
@tcp_socket = TCPSocket.new(@host, ENDPOINT_MAPPER_PORT)
151-
bind(endpoint: Epm)
152-
begin
153-
host_port = get_host_port_from_ept_mapper(
154-
uuid: @endpoint::UUID,
155-
maj_ver: @endpoint::VER_MAJOR,
156-
min_ver: @endpoint::VER_MINOR
157-
)
158-
rescue RubySMB::Dcerpc::Error::DcerpcError => e
159-
e.message.prepend(
160-
"Cannot resolve the remote port number for endpoint #{@endpoint::UUID}. "\
161-
"Set @tcp_socket parameter to specify the service port number and bypass "\
162-
"EPM port resolution. Error: "
163-
)
164-
raise e
150+
if @endpoint == Epm
151+
port = ENDPOINT_MAPPER_PORT
152+
else
153+
epm_client = Client.new(@host, Epm, read_timeout: @read_timeout)
154+
epm_client.connect
155+
begin
156+
epm_client.bind
157+
towers = epm_client.ept_map_endpoint(@endpoint)
158+
rescue RubySMB::Dcerpc::Error::DcerpcError => e
159+
e.message.prepend(
160+
"Cannot resolve the remote port number for endpoint #{@endpoint::UUID}. "\
161+
"Set @tcp_socket parameter to specify the service port number and bypass "\
162+
"EPM port resolution. Error: "
163+
)
164+
raise e
165+
ensure
166+
epm_client.close
167+
end
168+
169+
port = towers.first[:port]
165170
end
166-
port = host_port[:port]
167-
@tcp_socket.close
168-
@tcp_socket = nil
169171
end
172+
170173
@tcp_socket = TCPSocket.new(@host, port)
171174
end
172175

lib/ruby_smb/dcerpc/epm.rb

Lines changed: 100 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,37 +9,91 @@ module Epm
99
# Operation numbers
1010
EPT_MAP = 0x0003
1111

12+
# MS-RPCE specific error codes
13+
STATUS_NO_ELEMENTS = 0x16C9A0D6
14+
1215
require 'ruby_smb/dcerpc/epm/epm_twrt'
1316
require 'ruby_smb/dcerpc/epm/epm_ept_map_request'
1417
require 'ruby_smb/dcerpc/epm/epm_ept_map_response'
1518

16-
# Retrieve the service port number given a DCERPC interface UUID
17-
# See:
18-
# [2.2.1.2.5 ept_map Method](https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/ab744583-430e-4055-8901-3c6bc007e791)
19-
# [https://pubs.opengroup.org/onlinepubs/9629399/apdxo.htm](https://pubs.opengroup.org/onlinepubs/9629399/apdxo.htm)
19+
# Map a service to a connection end point.
2020
#
21-
# @param uuid [String] The interface UUID
22-
# @param maj_ver [Integer] The interface Major version
23-
# @param min_ver [Integer] The interface Minor version
24-
# @param max_towers [Integer] The maximum number of elements to be returned
25-
# @return [Hash] A hash with the host and port
26-
# @raise [RubySMB::Dcerpc::Error::InvalidPacket] if the response is not a
27-
# EpmEptMap packet
28-
# @raise [RubySMB::Dcerpc::Error::EpmError] if the response error status
29-
# is not STATUS_SUCCESS
30-
def get_host_port_from_ept_mapper(uuid:, maj_ver:, min_ver:, max_towers: 1)
31-
decoded_tower = EpmDecodedTowerOctetString.new(
32-
interface_identifier: {
33-
interface: uuid,
34-
major_version: maj_ver,
35-
minor_version: min_ver
36-
},
37-
data_representation: {
38-
interface: Ndr::UUID,
39-
major_version: Ndr::VER_MAJOR,
40-
minor_version: Ndr::VER_MINOR
41-
}
42-
)
21+
# @param uuid [String] The object UUID of the interface.
22+
# @param maj_ver [Integer] The major version number of the interface.
23+
# @param min_ver [Integer] The minor version number of the interface.
24+
# @param max_towers [Integer] The maximum number of results to obtain.
25+
# @param protocol [Symbol] The protocol of endpoint to obtain.
26+
#
27+
# @return [Array<Hash<Symbol,>>] The mapped endpoints. The hash keys will
28+
# depend on the protocol that was selected but an endpoint key will
29+
# always be present.
30+
# @raise [NotImplementedError] Raised if the *protocol* argument is not
31+
# supported.
32+
def ept_map(uuid:, maj_ver:, min_ver: 0, max_towers: 1, protocol: :ncacn_ip_tcp)
33+
interface_identifier = {
34+
interface: uuid,
35+
major_version: maj_ver,
36+
minor_version: min_ver
37+
}
38+
data_representation = {
39+
interface: Ndr::UUID,
40+
major_version: Ndr::VER_MAJOR,
41+
minor_version: Ndr::VER_MINOR
42+
}
43+
44+
case protocol
45+
when :ncacn_ip_tcp
46+
decoded_tower = EpmDecodedTowerOctetString.new(
47+
interface_identifier: interface_identifier,
48+
data_representation: data_representation,
49+
pipe_or_port: {
50+
identifier: 7, # 0x07: DOD TCP port
51+
pipe_or_port: 0
52+
},
53+
host_or_addr: {
54+
identifier: 9, # 0x09: DOD IP v4 address (big-endian)
55+
host_or_addr: 0
56+
}
57+
)
58+
59+
process_tower = lambda do |tower|
60+
port = tower.pipe_or_port.pipe_or_port.value
61+
address = IPAddr.new(tower.host_or_addr.host_or_addr.value, Socket::AF_INET)
62+
{
63+
port: port,
64+
address: address,
65+
# https://learn.microsoft.com/en-us/windows/win32/midl/ncacn-ip-tcp
66+
endpoint: "ncacn_ip_tcp:#{address}[#{port}]"
67+
}
68+
end
69+
when :ncacn_np
70+
decoded_tower = EpmDecodedTowerOctetString.new(
71+
interface_identifier: interface_identifier,
72+
data_representation: data_representation,
73+
pipe_or_port: {
74+
identifier: 0x0f, # 0x0f: NetBIOS pipe name
75+
pipe_or_port: [0]
76+
},
77+
host_or_addr: {
78+
identifier: 0x11, # 0x11: MS NetBIOS host name
79+
host_or_addr: [0]
80+
}
81+
)
82+
83+
process_tower = lambda do |tower|
84+
pipe = tower.pipe_or_port.pipe_or_port[...-1].pack('C*')
85+
host = tower.host_or_addr.host_or_addr[...-1].pack('C*')
86+
{
87+
pipe: pipe,
88+
host: host,
89+
# https://learn.microsoft.com/en-us/windows/win32/midl/ncacn-nb-nb
90+
endpoint: "ncacn_np:#{host}[#{pipe}]"
91+
}
92+
end
93+
else
94+
raise NotImplementedError, "Unsupported protocol: #{protocol}"
95+
end
96+
4397
tower = EpmTwrt.new(decoded_tower)
4498
ept_map_request = EpmEptMapRequest.new(
4599
obj: Uuid.new,
@@ -53,21 +107,31 @@ def get_host_port_from_ept_mapper(uuid:, maj_ver:, min_ver:, max_towers: 1)
53107
rescue IOError
54108
raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EptMapResponse'
55109
end
56-
unless ept_map_response.error_status == WindowsError::NTStatus::STATUS_SUCCESS
110+
111+
if ept_map_response.error_status == STATUS_NO_ELEMENTS
112+
raise RubySMB::Dcerpc::Error::EpmError,
113+
"Error returned with ept_map: "\
114+
"(0x16c9a0d6) STATUS_NO_ELEMENTS: There are no elements that satisfy the specified search criteria."
115+
elsif ept_map_response.error_status != WindowsError::NTStatus::STATUS_SUCCESS
57116
raise RubySMB::Dcerpc::Error::EpmError,
58117
"Error returned with ept_map: "\
59118
"#{WindowsError::NTStatus.find_by_retval(ept_map_response.error_status.value).join(',')}"
60119
end
61-
tower_binary = ept_map_response.towers[0].tower_octet_string.to_binary_s
62-
begin
63-
decoded_tower = EpmDecodedTowerOctetString.read(tower_binary)
64-
rescue IOError
65-
raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EpmDecodedTowerOctetString'
120+
121+
ept_map_response.towers.map do |tower|
122+
tower_binary = tower.tower_octet_string.to_binary_s
123+
begin
124+
decoded_tower = EpmDecodedTowerOctetString.read(tower_binary)
125+
rescue IOError
126+
raise RubySMB::Dcerpc::Error::InvalidPacket, 'Error reading EpmDecodedTowerOctetString'
127+
end
128+
129+
process_tower.(decoded_tower)
66130
end
67-
{
68-
port: decoded_tower.pipe_or_port.pipe_or_port.to_i,
69-
host: decoded_tower.host_or_addr.host_or_addr.to_i
70-
}
131+
end
132+
133+
def ept_map_endpoint(endpoint, **kwargs)
134+
ept_map(uuid: endpoint::UUID, maj_ver: endpoint::VER_MAJOR, min_ver: endpoint::VER_MINOR, **kwargs)
71135
end
72136
end
73137
end

0 commit comments

Comments
 (0)