From 4043bfb5f23bb858ec9c9abea5517c755e8cd44d Mon Sep 17 00:00:00 2001 From: Tal Aloni Date: Thu, 21 Sep 2017 12:27:04 +0300 Subject: [PATCH] Client: Preliminary SMB2 client implementation --- SMBLibrary/Client/SMB2Client.cs | 356 ++++++++++++++++++++++++++++++++ SMBLibrary/SMBLibrary.csproj | 1 + 2 files changed, 357 insertions(+) create mode 100644 SMBLibrary/Client/SMB2Client.cs diff --git a/SMBLibrary/Client/SMB2Client.cs b/SMBLibrary/Client/SMB2Client.cs new file mode 100644 index 0000000..2da33a6 --- /dev/null +++ b/SMBLibrary/Client/SMB2Client.cs @@ -0,0 +1,356 @@ +/* Copyright (C) 2017 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.Threading; +using SMBLibrary.Authentication.NTLM; +using SMBLibrary.NetBios; +using SMBLibrary.Services; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Client +{ + public class SMB2Client + { + public const int NetBiosOverTCPPort = 139; + public const int DirectTCPPort = 445; + + private SMBTransportType m_transport; + private bool m_isConnected; + private bool m_isLoggedIn; + private Socket m_clientSocket; + private IAsyncResult m_currentAsyncResult; + + private object m_incomingQueueLock = new object(); + private List m_incomingQueue = new List(); + private EventWaitHandle m_incomingQueueEventHandle = new EventWaitHandle(false, EventResetMode.AutoReset); + + private uint m_messageID = 0; + private SMB2Dialect m_dialect; + private uint m_maxTransactSize; + private uint m_maxReadSize; + private uint m_maxWriteSize; + private ulong m_sessionID; + private byte[] m_securityBlob; + private byte[] m_sessionKey; + + public SMB2Client() + { + } + + public bool Connect(IPAddress serverAddress, SMBTransportType transport) + { + m_transport = transport; + if (!m_isConnected) + { + m_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + int port; + if (transport == SMBTransportType.DirectTCPTransport) + { + port = DirectTCPPort; + } + else + { + port = NetBiosOverTCPPort; + } + + try + { + m_clientSocket.Connect(serverAddress, port); + } + catch (SocketException) + { + return false; + } + + ConnectionState state = new ConnectionState(); + NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; + m_currentAsyncResult = m_clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); + bool supportsSMB2 = NegotiateDialect(); + if (!supportsSMB2) + { + m_clientSocket.Close(); + } + else + { + m_isConnected = true; + } + } + return m_isConnected; + } + + 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_maxTransactSize = response.MaxTransactSize; + m_maxReadSize = response.MaxReadSize; + m_maxWriteSize = response.MaxWriteSize; + 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"); + } + + SessionSetupRequest request = new SessionSetupRequest(); + request.SecurityMode = SecurityMode.SigningEnabled; + request.SecurityBuffer = NTLMAuthenticationHelper.GetNegotiateMessage(m_securityBlob, domainName, authenticationMethod); + TrySendCommand(request); + SMB2Command response = WaitForCommand(SMB2CommandName.SessionSetup); + if (response != null) + { + if (response.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED && response is SessionSetupResponse) + { + m_sessionID = response.Header.SessionID; + request = new SessionSetupRequest(); + request.SecurityMode = SecurityMode.SigningEnabled; + request.SecurityBuffer = NTLMAuthenticationHelper.GetAuthenticateMessage(((SessionSetupResponse)response).SecurityBuffer, domainName, userName, password, authenticationMethod, out m_sessionKey); + 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() + { + 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; + } + + private void OnClientSocketReceive(IAsyncResult ar) + { + if (ar != m_currentAsyncResult) + { + // We ignore calls for old sockets which we no longer use + // See: http://rajputyh.blogspot.co.il/2010/04/solve-exception-message-iasyncresult.html + return; + } + + ConnectionState state = (ConnectionState)ar.AsyncState; + + if (!m_clientSocket.Connected) + { + return; + } + + int numberOfBytesReceived = 0; + try + { + numberOfBytesReceived = m_clientSocket.EndReceive(ar); + } + 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 + { + m_currentAsyncResult = m_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) + { + m_clientSocket.Close(); + break; + } + + if (packet != null) + { + ProcessPacket(packet, state); + } + } + } + + private void ProcessPacket(SessionPacket packet, ConnectionState state) + { + if (packet is SessionKeepAlivePacket && m_transport == SMBTransportType.NetBiosOverTCP) + { + // [RFC 1001] NetBIOS session keep alives do not require a response from the NetBIOS peer + } + else if (packet is PositiveSessionResponsePacket && m_transport == SMBTransportType.NetBiosOverTCP) + { + } + else if (packet is NegativeSessionResponsePacket && m_transport == SMBTransportType.NetBiosOverTCP) + { + m_clientSocket.Close(); + m_isConnected = false; + } + else if (packet is SessionMessagePacket) + { + SMB2Command command; + try + { + command = SMB2Command.ReadResponse(packet.Trailer, 0); + } + catch (Exception ex) + { + Log("Invalid SMB2 response: " + ex.Message); + m_clientSocket.Close(); + m_isConnected = false; + return; + } + + lock (m_incomingQueueLock) + { + m_incomingQueue.Add(command); + m_incomingQueueEventHandle.Set(); + } + } + } + + 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; + } + + private void Log(string message) + { + System.Diagnostics.Debug.Print(message); + } + + internal void TrySendCommand(SMB2Command request) + { + request.Header.Credits = 1; + request.Header.MessageID = m_messageID; + request.Header.SessionID = m_sessionID; + TrySendCommand(m_clientSocket, request); + m_messageID++; + } + + 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 + { + socket.Send(packet.GetBytes()); + } + catch (SocketException) + { + } + catch (ObjectDisposedException) + { + } + } + } +} diff --git a/SMBLibrary/SMBLibrary.csproj b/SMBLibrary/SMBLibrary.csproj index d003125..8b71c24 100644 --- a/SMBLibrary/SMBLibrary.csproj +++ b/SMBLibrary/SMBLibrary.csproj @@ -60,6 +60,7 @@ +