/* Copyright (C) 2017-2020 Tal Aloni . All rights reserved. * * You can redistribute this program and/or modify it under the terms of * the GNU Lesser Public License as published by the Free Software Foundation, * either version 3 of the License, or (at your option) any later version. */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Sockets; using System.Security.Cryptography; using System.Threading; using SMBLibrary.Authentication.NTLM; using SMBLibrary.NetBios; using SMBLibrary.Services; using SMBLibrary.SMB2; using Utilities; namespace SMBLibrary.Client { public class SMB2Client : ISMBClient { public static readonly int NetBiosOverTCPPort = 139; public static readonly int DirectTCPPort = 445; public static readonly uint ClientMaxTransactSize = 1048576; public static readonly uint ClientMaxReadSize = 1048576; public static readonly uint ClientMaxWriteSize = 1048576; private static readonly ushort DesiredCredits = 16; private SMBTransportType m_transport; private bool m_isConnected; private bool m_isLoggedIn; private Socket m_clientSocket; private object m_incomingQueueLock = new object(); private List m_incomingQueue = new List(); private EventWaitHandle m_incomingQueueEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset); private SessionPacket m_sessionResponsePacket; private EventWaitHandle m_sessionResponseEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset); private uint m_messageID = 0; private SMB2Dialect m_dialect; private bool m_signingRequired; private uint m_maxTransactSize; private uint m_maxReadSize; private uint m_maxWriteSize; private ulong m_sessionID; private byte[] m_securityBlob; private byte[] m_sessionKey; private ushort m_availableCredits = 1; public SMB2Client() { } public bool Connect(IPAddress serverAddress, SMBTransportType transport) { m_transport = transport; if (!m_isConnected) { int port; if (transport == SMBTransportType.NetBiosOverTCP) { port = NetBiosOverTCPPort; } else { port = DirectTCPPort; } if (!ConnectSocket(serverAddress, port)) { return false; } if (transport == SMBTransportType.NetBiosOverTCP) { SessionRequestPacket sessionRequest = new SessionRequestPacket(); sessionRequest.CalledName = NetBiosUtils.GetMSNetBiosName("*SMBSERVER", NetBiosSuffix.FileServiceService); sessionRequest.CallingName = NetBiosUtils.GetMSNetBiosName(Environment.MachineName, NetBiosSuffix.WorkstationService); TrySendPacket(m_clientSocket, sessionRequest); SessionPacket sessionResponsePacket = WaitForSessionResponsePacket(); if (!(sessionResponsePacket is PositiveSessionResponsePacket)) { m_clientSocket.Disconnect(false); if (!ConnectSocket(serverAddress, port)) { return false; } NameServiceClient nameServiceClient = new NameServiceClient(serverAddress); string serverName = nameServiceClient.GetServerName(); if (serverName == null) { return false; } sessionRequest.CalledName = serverName; TrySendPacket(m_clientSocket, sessionRequest); sessionResponsePacket = WaitForSessionResponsePacket(); if (!(sessionResponsePacket is PositiveSessionResponsePacket)) { return false; } } } bool supportsDialect = NegotiateDialect(); if (!supportsDialect) { m_clientSocket.Close(); } else { m_isConnected = true; } } return m_isConnected; } private bool ConnectSocket(IPAddress serverAddress, int port) { m_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try { m_clientSocket.Connect(serverAddress, port); } catch (SocketException) { return false; } ConnectionState state = new ConnectionState(m_clientSocket); NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; m_clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); return true; } public void Disconnect() { if (m_isConnected) { m_clientSocket.Disconnect(false); m_isConnected = false; } } private bool NegotiateDialect() { NegotiateRequest request = new NegotiateRequest(); request.SecurityMode = SecurityMode.SigningEnabled; request.ClientGuid = Guid.NewGuid(); request.ClientStartTime = DateTime.Now; request.Dialects.Add(SMB2Dialect.SMB202); request.Dialects.Add(SMB2Dialect.SMB210); TrySendCommand(request); NegotiateResponse response = WaitForCommand(SMB2CommandName.Negotiate) as NegotiateResponse; if (response != null && response.Header.Status == NTStatus.STATUS_SUCCESS) { m_dialect = response.DialectRevision; m_signingRequired = (response.SecurityMode & SecurityMode.SigningRequired) > 0; m_maxTransactSize = Math.Min(response.MaxTransactSize, ClientMaxTransactSize); m_maxReadSize = Math.Min(response.MaxReadSize, ClientMaxReadSize); m_maxWriteSize = Math.Min(response.MaxWriteSize, ClientMaxWriteSize); m_securityBlob = response.SecurityBuffer; return true; } return false; } public NTStatus Login(string domainName, string userName, string password) { return Login(domainName, userName, password, AuthenticationMethod.NTLMv2); } public NTStatus Login(string domainName, string userName, string password, AuthenticationMethod authenticationMethod) { if (!m_isConnected) { throw new InvalidOperationException("A connection must be successfully established before attempting login"); } byte[] negotiateMessage = NTLMAuthenticationHelper.GetNegotiateMessage(m_securityBlob, domainName, authenticationMethod); if (negotiateMessage == null) { return NTStatus.SEC_E_INVALID_TOKEN; } SessionSetupRequest request = new SessionSetupRequest(); request.SecurityMode = SecurityMode.SigningEnabled; request.SecurityBuffer = negotiateMessage; TrySendCommand(request); SMB2Command response = WaitForCommand(SMB2CommandName.SessionSetup); if (response != null) { if (response.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED && response is SessionSetupResponse) { byte[] authenticateMessage = NTLMAuthenticationHelper.GetAuthenticateMessage(((SessionSetupResponse)response).SecurityBuffer, domainName, userName, password, authenticationMethod, out m_sessionKey); if (authenticateMessage == null) { return NTStatus.SEC_E_INVALID_TOKEN; } m_sessionID = response.Header.SessionID; request = new SessionSetupRequest(); request.SecurityMode = SecurityMode.SigningEnabled; request.SecurityBuffer = authenticateMessage; TrySendCommand(request); response = WaitForCommand(SMB2CommandName.SessionSetup); if (response != null) { m_isLoggedIn = (response.Header.Status == NTStatus.STATUS_SUCCESS); return response.Header.Status; } } else { return response.Header.Status; } } return NTStatus.STATUS_INVALID_SMB; } public NTStatus Logoff() { if (!m_isConnected) { throw new InvalidOperationException("A login session must be successfully established before attempting logoff"); } LogoffRequest request = new LogoffRequest(); TrySendCommand(request); SMB2Command response = WaitForCommand(SMB2CommandName.Logoff); if (response != null) { m_isLoggedIn = (response.Header.Status != NTStatus.STATUS_SUCCESS); return response.Header.Status; } return NTStatus.STATUS_INVALID_SMB; } public List ListShares(out NTStatus status) { if (!m_isConnected || !m_isLoggedIn) { throw new InvalidOperationException("A login session must be successfully established before retrieving share list"); } ISMBFileStore namedPipeShare = TreeConnect("IPC$", out status); if (namedPipeShare == null) { return null; } List shares = ServerServiceHelper.ListShares(namedPipeShare, SMBLibrary.Services.ShareType.DiskDrive, out status); namedPipeShare.Disconnect(); return shares; } public ISMBFileStore TreeConnect(string shareName, out NTStatus status) { if (!m_isConnected || !m_isLoggedIn) { throw new InvalidOperationException("A login session must be successfully established before connecting to a share"); } IPAddress serverIPAddress = ((IPEndPoint)m_clientSocket.RemoteEndPoint).Address; string sharePath = String.Format(@"\\{0}\{1}", serverIPAddress.ToString(), shareName); TreeConnectRequest request = new TreeConnectRequest(); request.Path = sharePath; TrySendCommand(request); SMB2Command response = WaitForCommand(SMB2CommandName.TreeConnect); if (response != null) { status = response.Header.Status; if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is TreeConnectResponse) { return new SMB2FileStore(this, response.Header.TreeID); } } else { status = NTStatus.STATUS_INVALID_SMB; } return null; } private void OnClientSocketReceive(IAsyncResult ar) { ConnectionState state = (ConnectionState)ar.AsyncState; Socket clientSocket = state.ClientSocket; if (!clientSocket.Connected) { return; } int numberOfBytesReceived = 0; try { numberOfBytesReceived = clientSocket.EndReceive(ar); } catch (ArgumentException) // The IAsyncResult object was not returned from the corresponding synchronous method on this class. { return; } catch (ObjectDisposedException) { Log("[ReceiveCallback] EndReceive ObjectDisposedException"); return; } catch (SocketException ex) { Log("[ReceiveCallback] EndReceive SocketException: " + ex.Message); return; } if (numberOfBytesReceived == 0) { m_isConnected = false; } else { NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; buffer.SetNumberOfBytesReceived(numberOfBytesReceived); ProcessConnectionBuffer(state); try { clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); } catch (ObjectDisposedException) { m_isConnected = false; Log("[ReceiveCallback] BeginReceive ObjectDisposedException"); } catch (SocketException ex) { m_isConnected = false; Log("[ReceiveCallback] BeginReceive SocketException: " + ex.Message); } } } private void ProcessConnectionBuffer(ConnectionState state) { NBTConnectionReceiveBuffer receiveBuffer = state.ReceiveBuffer; while (receiveBuffer.HasCompletePacket()) { SessionPacket packet = null; try { packet = receiveBuffer.DequeuePacket(); } catch (Exception) { state.ClientSocket.Close(); break; } if (packet != null) { ProcessPacket(packet, state); } } } private void ProcessPacket(SessionPacket packet, ConnectionState state) { if (packet is SessionMessagePacket) { SMB2Command command; try { command = SMB2Command.ReadResponse(packet.Trailer, 0); } catch (Exception ex) { Log("Invalid SMB2 response: " + ex.Message); state.ClientSocket.Close(); m_isConnected = false; return; } m_availableCredits += command.Header.Credits; if (m_transport == SMBTransportType.DirectTCPTransport && command is NegotiateResponse) { NegotiateResponse negotiateResponse = (NegotiateResponse)command; if ((negotiateResponse.Capabilities & Capabilities.LargeMTU) > 0) { // [MS-SMB2] 3.2.5.1 Receiving Any Message - If the message size received exceeds Connection.MaxTransactSize, the client MUST disconnect the connection. // Note: Windows clients do not enforce the MaxTransactSize value, we add 256 bytes. int maxPacketSize = SessionPacket.HeaderLength + (int)Math.Min(negotiateResponse.MaxTransactSize, ClientMaxTransactSize) + 256; if (maxPacketSize > state.ReceiveBuffer.Buffer.Length) { state.ReceiveBuffer.IncreaseBufferSize(maxPacketSize); } } } // [MS-SMB2] 3.2.5.1.2 - If the MessageId is 0xFFFFFFFFFFFFFFFF, this is not a reply to a previous request, // and the client MUST NOT attempt to locate the request, but instead process it as follows: // If the command field in the SMB2 header is SMB2 OPLOCK_BREAK, it MUST be processed as specified in 3.2.5.19. // Otherwise, the response MUST be discarded as invalid. if (command.Header.MessageID != 0xFFFFFFFFFFFFFFFF || command.Header.Command == SMB2CommandName.OplockBreak) { lock (m_incomingQueueLock) { m_incomingQueue.Add(command); m_incomingQueueEventHandle.Set(); } } } else if ((packet is PositiveSessionResponsePacket || packet is NegativeSessionResponsePacket) && m_transport == SMBTransportType.NetBiosOverTCP) { m_sessionResponsePacket = packet; m_sessionResponseEventHandle.Set(); } else if (packet is SessionKeepAlivePacket && m_transport == SMBTransportType.NetBiosOverTCP) { // [RFC 1001] NetBIOS session keep alives do not require a response from the NetBIOS peer } else { Log("Inappropriate NetBIOS session packet"); state.ClientSocket.Close(); } } internal SMB2Command WaitForCommand(SMB2CommandName commandName) { const int TimeOut = 5000; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); while (stopwatch.ElapsedMilliseconds < TimeOut) { lock (m_incomingQueueLock) { for (int index = 0; index < m_incomingQueue.Count; index++) { SMB2Command command = m_incomingQueue[index]; if (command.CommandName == commandName) { m_incomingQueue.RemoveAt(index); return command; } } } m_incomingQueueEventHandle.WaitOne(100); } return null; } internal SessionPacket WaitForSessionResponsePacket() { const int TimeOut = 5000; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); while (stopwatch.ElapsedMilliseconds < TimeOut) { if (m_sessionResponsePacket != null) { SessionPacket result = m_sessionResponsePacket; m_sessionResponsePacket = null; return result; } m_sessionResponseEventHandle.WaitOne(100); } return null; } private void Log(string message) { System.Diagnostics.Debug.Print(message); } internal void TrySendCommand(SMB2Command request) { if (m_dialect == SMB2Dialect.SMB202 || m_transport == SMBTransportType.NetBiosOverTCP) { request.Header.CreditCharge = 0; request.Header.Credits = 1; m_availableCredits -= 1; } else { if (request.Header.CreditCharge == 0) { request.Header.CreditCharge = 1; } if (m_availableCredits < request.Header.CreditCharge) { throw new Exception("Not enough credits"); } m_availableCredits -= request.Header.CreditCharge; if (m_availableCredits < DesiredCredits) { request.Header.Credits += (ushort)(DesiredCredits - m_availableCredits); } } request.Header.MessageID = m_messageID; request.Header.SessionID = m_sessionID; if (m_signingRequired) { request.Header.IsSigned = (m_sessionID != 0 && (request.CommandName == SMB2CommandName.TreeConnect || request.Header.TreeID != 0)); if (request.Header.IsSigned) { request.Header.Signature = new byte[16]; // Request could be reused byte[] buffer = request.GetBytes(); byte[] signature = new HMACSHA256(m_sessionKey).ComputeHash(buffer, 0, buffer.Length); // [MS-SMB2] The first 16 bytes of the hash MUST be copied into the 16-byte signature field of the SMB2 Header. request.Header.Signature = ByteReader.ReadBytes(signature, 0, 16); } } TrySendCommand(m_clientSocket, request); if (m_dialect == SMB2Dialect.SMB202 || m_transport == SMBTransportType.NetBiosOverTCP) { m_messageID++; } else { m_messageID += request.Header.CreditCharge; } } public uint MaxTransactSize { get { return m_maxTransactSize; } } public uint MaxReadSize { get { return m_maxReadSize; } } public uint MaxWriteSize { get { return m_maxWriteSize; } } public static void TrySendCommand(Socket socket, SMB2Command request) { SessionMessagePacket packet = new SessionMessagePacket(); packet.Trailer = request.GetBytes(); TrySendPacket(socket, packet); } public static void TrySendPacket(Socket socket, SessionPacket packet) { try { byte[] packetBytes = packet.GetBytes(); socket.Send(packetBytes); } catch (SocketException) { } catch (ObjectDisposedException) { } } } }