Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion lib/ruby_smb/rap/net_share_enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ module NetShareEnum
# Default server receive-buffer size.
DEFAULT_RECEIVE_BUFFER_SIZE = 0x1000

# Share type codes carried in the low bits of `share_info_1.shi1_type`
# per MS-RAP 2.5.14. The RAP field is 16 bits wide, unlike the 32-bit
# SRVSVC variant in {RubySMB::Dcerpc::Srvsvc::SHARE_TYPES}.
SHARE_TYPES = {
0x0000 => 'DISK',
0x0001 => 'PRINTER',
0x0002 => 'DEVICE',
0x0003 => 'IPC'
}.freeze
STYPE_SPECIAL = 0x8000
STYPE_TEMPORARY = 0x4000

# Single share entry (`share_info_1`) as it appears on the wire.
# MS-RAP 2.5.21. Fixed 20-byte layout.
class ShareInfo1 < BinData::Record
Expand Down Expand Up @@ -134,10 +146,21 @@ def parse_net_share_enum_response(response, raw_response)
entry = ShareInfo1.read(data_bytes[offset, ShareInfo1.new.num_bytes])
{
name: entry.netname.to_s.delete("\x00"),
type: entry.share_type
type: format_share_type(entry.share_type.to_i)
}
end.compact
end

# Format a RAP `share_info_1.shi1_type` value as a pipe-joined string in
# the same style as {RubySMB::Dcerpc::Srvsvc#net_share_enum_all}, so
# callers can consume both APIs uniformly.
def format_share_type(share_type)
base_bits = share_type & ~(STYPE_SPECIAL | STYPE_TEMPORARY)
parts = [SHARE_TYPES[base_bits] || format('UNKNOWN(0x%04x)', base_bits)]
parts << 'SPECIAL' unless (share_type & STYPE_SPECIAL).zero?
parts << 'TEMPORARY' unless (share_type & STYPE_TEMPORARY).zero?
parts.join('|')
end
end
end
end
1 change: 1 addition & 0 deletions lib/ruby_smb/smb1/packet/trans2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module Trans2
require 'ruby_smb/smb1/packet/trans2/query_information_level'
require 'ruby_smb/smb1/packet/trans2/query_fs_information_level'
require 'ruby_smb/smb1/packet/trans2/data_block'
require 'ruby_smb/smb1/packet/trans2/win9x_framing'
require 'ruby_smb/smb1/packet/trans2/subcommands'
require 'ruby_smb/smb1/packet/trans2/request'
require 'ruby_smb/smb1/packet/trans2/request_secondary'
Expand Down
11 changes: 9 additions & 2 deletions lib/ruby_smb/smb1/packet/trans2/find_first2_response.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class FindFirst2ResponseDataBlock < RubySMB::SMB1::Packet::Trans2::DataBlock
# This class represents an SMB1 Trans2 FIND_FIRST2 Response Packet as defined in
# [2.2.6.2.2 Response](https://msdn.microsoft.com/en-us/library/ee441704.aspx)
class FindFirst2Response < RubySMB::GenericPacket
include RubySMB::SMB1::Packet::Trans2::Win9xFraming

COMMAND = RubySMB::SMB1::Commands::SMB_COM_TRANSACTION2

class ParameterBlock < RubySMB::SMB1::Packet::Trans2::Response::ParameterBlock
Expand Down Expand Up @@ -68,10 +70,15 @@ def initialize_instance
# pad byte between entries (see MS-CIFS Appendix A, note <153>).
#
# @param klass [Class] the FileInformationClass class to read the data as
# @param buffer [String, nil] raw trans2_data bytes to parse instead of
# the BinData-parsed buffer. Used by callers that detect a padding
# mismatch between BinData's expected layout and what a Win9x-era
# server actually sent (no 4-byte alignment pad before the data),
# and want to re-feed the bytes from the server-reported data_offset.
# @return [array<BinData::Record>] An array of structs holding the requested information
# @raise [RubySMB::Error::InvalidPacket] if the string buffer is not a valid File Information packet
def results(klass, unicode:)
blob = data_block.trans2_data.buffer.to_binary_s.dup
def results(klass, unicode:, buffer: nil)
blob = (buffer || data_block.trans2_data.buffer.to_binary_s).dup
if klass.new.respond_to?(:next_offset)
read_next_offset_entries(klass, blob, unicode: unicode)
else
Expand Down
68 changes: 68 additions & 0 deletions lib/ruby_smb/smb1/packet/trans2/win9x_framing.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
module RubySMB
module SMB1
module Packet
module Trans2
# Shared workaround for pre-NT / LAN Manager-era servers (observed on
# Windows 9x / ME) that pack `trans2_parameters` directly after
# `byte_count` with no 4-byte-alignment pad, and `trans2_data` with
# whatever padding they feel like — always smaller than the NT-style
# alignment BinData unconditionally assumes via {DataBlock#pad1_length}
# and {DataBlock#pad2_length}. When that happens both sections land in
# the wrong place and `eos`, `sid`, `last_name_offset`, and every
# entry in the data buffer come back garbled.
#
# Fixing this in BinData itself (by having pad1/pad2 consult
# `parameter_block.parameter_offset` / `data_offset`) is the natural
# design, but cross-field lookups during field-read callbacks corrupt
# BinData's registered-class resolution cache, causing unrelated
# Trans2 responses to round-trip their `parameter_block` / `data_block`
# through the base classes instead of the concrete subclasses. So
# instead we surface the raw response bytes at the call site and let
# the response slice both sections from the offsets the server
# reported in its `parameter_block`.
#
# Mix into any {RubySMB::SMB1::Packet::Trans2} response whose caller
# holds on to the raw response bytes. The response itself must have
# the standard {Trans2::Response::ParameterBlock} shape
# (`parameter_offset` / `parameter_count` / `data_offset` /
# `data_count`) and a `data_block` with `trans2_parameters` and
# `trans2_data.buffer` fields — every concrete Trans2 response does.
#
# Same slicing pattern as {RubySMB::Rap::NetShareEnum#parse_net_share_enum_response}
# uses for the sibling Trans (not Trans2) response type.
module Win9xFraming
# Returns `[effective_trans2_parameters, effective_trans2_data_bytes]`
# when the server's layout differs from BinData's, or `[nil, nil]`
# when BinData already read the full buffer (standard NT-era servers).
#
# When a non-nil pair is returned, callers should prefer the override
# values over the BinData-parsed ones:
#
# params_ovr, data_ovr = response.win9x_trans2_overrides(raw)
# params = params_ovr || response.data_block.trans2_parameters
# data = data_ovr || response.data_block.trans2_data.buffer.to_binary_s
#
# @param raw_response [String] the raw bytes the response was read from.
# @return [Array(BinData::Record, String), Array(nil, nil)]
def win9x_trans2_overrides(raw_response)
declared_data = parameter_block.data_count.to_i
parsed_data = data_block.trans2_data.buffer.to_binary_s.bytesize
return [nil, nil] if declared_data.zero? || parsed_data == declared_data

param_offset = parameter_block.parameter_offset.to_i
param_count = parameter_block.parameter_count.to_i
data_offset = parameter_block.data_offset.to_i
return [nil, nil] if raw_response.bytesize < data_offset + declared_data
return [nil, nil] if raw_response.bytesize < param_offset + param_count

params_bytes = raw_response.byteslice(param_offset, param_count)
params_class = data_block.trans2_parameters.class
params = params_class.read(params_bytes)
data_bytes = raw_response.byteslice(data_offset, declared_data)
[params, data_bytes]
end
end
end
end
end
end
21 changes: 15 additions & 6 deletions lib/ruby_smb/smb1/tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,16 @@ def list(directory: '\\', pattern: '*', unicode: true,
raise RubySMB::Error::UnexpectedStatusCode, response.status_code
end

results = response.results(type, unicode: unicode)

eos = response.data_block.trans2_parameters.eos
sid = response.data_block.trans2_parameters.sid
t2p_override, t2d_override = response.win9x_trans2_overrides(raw_response)
results = if t2d_override
response.results(type, unicode: unicode, buffer: t2d_override)
else
response.results(type, unicode: unicode)
end

effective_params = t2p_override || response.data_block.trans2_parameters
eos = effective_params.eos
sid = effective_params.sid
last = results.last&.file_name

while eos.zero? && last
Expand Down Expand Up @@ -310,8 +316,11 @@ def _open_andx(filename:, disposition:, read: true, write: false)
end

request.parameter_block.access_mode = access
request.parameter_block.search_attributes = 0x0016
request.parameter_block.file_attributes = write ? 0x0020 : 0x0000
# search_attributes / file_attributes are SMB_FILE_ATTRIBUTES BitField
# records, not plain uint16s — assign through #read to avoid BinData's
# each_pair-on-Integer NoMethodError when given a literal mask.
request.parameter_block.search_attributes.read([0x0016].pack('v'))
request.parameter_block.file_attributes.read([(write ? 0x0020 : 0x0000)].pack('v'))
request.parameter_block.open_mode = nt_disposition_to_open_mode(disposition)

fname = filename.dup
Expand Down
38 changes: 35 additions & 3 deletions spec/lib/ruby_smb/rap/net_share_enum_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,43 @@ def build_rap_response(status:, entries: [])
]
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
expect(pipe.net_share_enum).to eq([
{ name: 'IPC$', type: 0x0003 },
{ name: 'DATA', type: 0x0000 }
{ name: 'IPC$', type: 'IPC' },
{ name: 'DATA', type: 'DISK' }
])
end

it 'stringifies all MS-RAP 2.5.14 base share types' do
entries = [
{ name: 'DSK', type: 0x0000 },
{ name: 'PRN', type: 0x0001 },
{ name: 'DEV', type: 0x0002 },
{ name: 'IPC$', type: 0x0003 }
]
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
expect(pipe.net_share_enum.map { |s| s[:type] }).to eq(%w[DISK PRINTER DEVICE IPC])
end

it 'appends SPECIAL / TEMPORARY modifiers to the base type string' do
entries = [
{ name: 'HIDDEN$', type: 0x0000 | RubySMB::Rap::NetShareEnum::STYPE_SPECIAL },
{ name: 'TMP', type: 0x0000 | RubySMB::Rap::NetShareEnum::STYPE_TEMPORARY },
{ name: 'IPCH$', type: 0x0003 | RubySMB::Rap::NetShareEnum::STYPE_SPECIAL |
RubySMB::Rap::NetShareEnum::STYPE_TEMPORARY }
]
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
expect(pipe.net_share_enum.map { |s| s[:type] }).to eq([
'DISK|SPECIAL',
'DISK|TEMPORARY',
'IPC|SPECIAL|TEMPORARY'
])
end

it 'formats an unknown base type code as UNKNOWN(0xXXXX)' do
entries = [{ name: 'Q', type: 0x0007 }]
allow(client).to receive(:send_recv).and_return(build_rap_response(status: 0, entries: entries))
expect(pipe.net_share_enum.first[:type]).to eq('UNKNOWN(0x0007)')
end

it 'sends a Trans request targeting \\PIPE\\LANMAN with the tree id' do
allow(client).to receive(:send_recv) do |request|
expect(request).to be_a(RubySMB::SMB1::Packet::Trans::Request)
Expand Down Expand Up @@ -136,7 +168,7 @@ def build_rap_response(status:, entries: [])
shares = pipe.net_share_enum
expect(shares.length).to eq(1)
expect(shares[0][:name]).to eq('ABCDEFGHIJKL')
expect(shares[0][:type]).to eq(0)
expect(shares[0][:type]).to eq('DISK')
end
end

Expand Down
113 changes: 113 additions & 0 deletions spec/lib/ruby_smb/smb1/packet/trans2/win9x_framing_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
require 'spec_helper'

RSpec.describe RubySMB::SMB1::Packet::Trans2::Win9xFraming do
# FindFirst2Response is the first production consumer of the mixin; its
# fixture data already covers every code path in #win9x_trans2_overrides
# (zero-length buffer, on-wire match, server-reported mismatch, truncated
# raw response). Using it here keeps the spec grounded in real field
# layouts without standing up an anonymous host class.
let(:info_std) do
RubySMB::SMB1::Packet::Trans2::FindInformationLevel::FindInfoStandard
end

# Reusable fixtures: NT-style (with pad1=3) and Win9x-style (pad1=0) raw
# FindFirst2Response frames carrying the same single-entry payload so the
# overrides helper sees the same server-declared offsets differ from what
# BinData positionally reads.
let(:single_entry_bytes) do
"\x98\x5c\x38\x70\x98\x5c\x00\x00\x98\x5c\x39\x70".b +
"\x00\x00\x00\x00\x00\x00\x00\x00\x10\x00\x01".b + '.'
end
let(:data_count) { single_entry_bytes.bytesize }

def smb_header
"\xffSMB\x32".b + "\x00".b * 4 + "\x98".b + "\x03\x60".b + ("\x00".b * 20)
end

def build_response(parameter_offset:, data_offset:, word_count:, pad1: 0, pad2: 0)
# The concrete field layout of FindFirst2Response's parameter_block
# changes with word_count: 11 words include a 1-entry setup array;
# 10 words (Win9x style) omit it. trans2_parameters itself is a
# fixed 10-byte struct (sid, search_count, eos, ea_err_off, last_name_off).
trans2_params = [0x0300, 1, 1, 0, 0].pack('v*')
pb_values = [10, data_count, 0, 10, parameter_offset, 0,
data_count, data_offset, 0]
# word_count=11 → setup_count(1) + reserved2(0) + 1-word setup array
# word_count=10 → setup_count(0) + reserved2(0), no setup array
pb_tail = word_count == 11 ? "\x01\x00".b + [1].pack('v') : "\x00\x00".b
param_block = pb_values.pack('v*') + pb_tail
byte_count = pad1 + 10 + pad2 + data_count
smb_header + [word_count].pack('C') + param_block +
[byte_count].pack('v') + ("\x00".b * pad1) + trans2_params +
("\x00".b * pad2) + single_entry_bytes
end

describe '#win9x_trans2_overrides' do
context 'when BinData has already read the full buffer (NT-style server)' do
it 'returns [nil, nil]' do
# NT-era response: word_count=11 with 1-word setup, pad1=3, pad2=2
# → trans2_parameters at offset 60, trans2_data at 72. BinData's
# Trans2::DataBlock positional read lands exactly on the wire data.
raw = build_response(
parameter_offset: 60, data_offset: 72,
word_count: 11, pad1: 3, pad2: 2
)
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
expect(response.win9x_trans2_overrides(raw)).to eq([nil, nil])
end
end

context 'when the server declared no trans2_data (data_count == 0)' do
it 'returns [nil, nil]' do
raw = build_response(
parameter_offset: 60, data_offset: 72,
word_count: 11, pad1: 3, pad2: 2
)
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
response.parameter_block.data_count = 0
expect(response.win9x_trans2_overrides(raw)).to eq([nil, nil])
end
end

context 'when the server used Win9x-era framing (no pad1)' do
it 'returns trans2_parameters re-read at the server-reported offset' do
raw = build_response(
parameter_offset: 55, data_offset: 66,
word_count: 10, pad1: 0, pad2: 1
)
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
params, = response.win9x_trans2_overrides(raw)
expect(params).to be_a(RubySMB::SMB1::Packet::Trans2::FindFirst2ResponseTrans2Parameters)
expect(params.sid).to eq 0x0300
expect(params.search_count).to eq 1
expect(params.eos).to eq 1
end

it 'returns the trans2_data bytes sliced from the server-reported offset' do
raw = build_response(
parameter_offset: 55, data_offset: 66,
word_count: 10, pad1: 0, pad2: 1
)
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
_, data_bytes = response.win9x_trans2_overrides(raw)
expect(data_bytes).to eq single_entry_bytes
# And #results can read entries from it through the buffer: kwarg.
entries = response.results(info_std, unicode: false, buffer: data_bytes)
expect(entries.length).to eq 1
expect(entries.first.file_name.to_s).to eq '.'
end
end

context 'when the raw response is truncated before the server-reported offsets' do
it 'returns [nil, nil] rather than raising' do
raw = build_response(
parameter_offset: 55, data_offset: 66,
word_count: 10, pad1: 0, pad2: 1
)
response = RubySMB::SMB1::Packet::Trans2::FindFirst2Response.read(raw)
truncated = raw.byteslice(0, raw.bytesize - 20)
expect(response.win9x_trans2_overrides(truncated)).to eq([nil, nil])
end
end
end
end
Loading