From 55b83add4e9bcbae4f8d4a9a428ef4f6847a11f9 Mon Sep 17 00:00:00 2001 From: Tal Aloni Date: Fri, 20 Jan 2017 22:04:07 +0200 Subject: [PATCH] Added SMB 2.0 / 2.1 server implementation --- SMBLibrary/Enums/NTStatus.cs | 7 + SMBLibrary/Enums/Win32Error.cs | 1 + SMBLibrary/SMBLibrary.csproj | 15 ++ .../Server/ConnectionState/ConnectionState.cs | 2 + .../ConnectionState/SMB2ConnectionState.cs | 71 ++++++ .../Server/ConnectionState/SMB2Session.cs | 165 ++++++++++++++ .../Helpers/NTFileSystemHelper.Query.cs | 191 ++++++++++++++++ .../NTFileSystemHelper.QueryFileSystem.cs | 101 +++++++++ .../Server/Helpers/NTFileSystemHelper.Set.cs | 207 +++++++++++++++++ SMBLibrary/Server/SMB2/CreateHelper.cs | 149 +++++++++++++ SMBLibrary/Server/SMB2/IOCtlHelper.cs | 51 +++++ SMBLibrary/Server/SMB2/NegotiateHelper.cs | 101 +++++++++ .../Server/SMB2/QueryDirectoryHelper.cs | 208 +++++++++++++++++ SMBLibrary/Server/SMB2/QueryInfoHelper.cs | 75 +++++++ .../Server/SMB2/ReadWriteResponseHelper.cs | 57 +++++ SMBLibrary/Server/SMB2/SessionSetupHelper.cs | 101 +++++++++ SMBLibrary/Server/SMB2/SetInfoHelper.cs | 65 ++++++ SMBLibrary/Server/SMB2/TreeConnectHelper.cs | 62 +++++ SMBLibrary/Server/SMBServer.SMB1.cs | 21 +- SMBLibrary/Server/SMBServer.SMB2.cs | 211 ++++++++++++++++++ SMBLibrary/Server/SMBServer.cs | 119 ++++++++-- 21 files changed, 1958 insertions(+), 22 deletions(-) create mode 100644 SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs create mode 100644 SMBLibrary/Server/ConnectionState/SMB2Session.cs create mode 100644 SMBLibrary/Server/Helpers/NTFileSystemHelper.Query.cs create mode 100644 SMBLibrary/Server/Helpers/NTFileSystemHelper.QueryFileSystem.cs create mode 100644 SMBLibrary/Server/Helpers/NTFileSystemHelper.Set.cs create mode 100644 SMBLibrary/Server/SMB2/CreateHelper.cs create mode 100644 SMBLibrary/Server/SMB2/IOCtlHelper.cs create mode 100644 SMBLibrary/Server/SMB2/NegotiateHelper.cs create mode 100644 SMBLibrary/Server/SMB2/QueryDirectoryHelper.cs create mode 100644 SMBLibrary/Server/SMB2/QueryInfoHelper.cs create mode 100644 SMBLibrary/Server/SMB2/ReadWriteResponseHelper.cs create mode 100644 SMBLibrary/Server/SMB2/SessionSetupHelper.cs create mode 100644 SMBLibrary/Server/SMB2/SetInfoHelper.cs create mode 100644 SMBLibrary/Server/SMB2/TreeConnectHelper.cs create mode 100644 SMBLibrary/Server/SMBServer.SMB2.cs diff --git a/SMBLibrary/Enums/NTStatus.cs b/SMBLibrary/Enums/NTStatus.cs index 9c956b5..d041cbc 100644 --- a/SMBLibrary/Enums/NTStatus.cs +++ b/SMBLibrary/Enums/NTStatus.cs @@ -4,7 +4,10 @@ namespace SMBLibrary public enum NTStatus : uint { STATUS_SUCCESS = 0x00000000, + STATUS_OBJECT_NAME_EXISTS = 0x40000000, + STATUS_NO_MORE_FILES = 0x80000006, STATUS_NOT_IMPLEMENTED = 0xC0000002, + STATUS_INVALID_INFO_CLASS = 0xC0000003, STATUS_INVALID_HANDLE = 0xC0000008, STATUS_INVALID_PARAMETER = 0xC000000D, STATUS_NO_SUCH_DEVICE = 0xC000000E, @@ -23,12 +26,16 @@ namespace SMBLibrary STATUS_LOGON_FAILURE = 0xC000006D, // Authentication failure. STATUS_ACCOUNT_RESTRICTION = 0xC000006E, // The user has an empty password, which is not allowed STATUS_DISK_FULL = 0xC000007F, + STATUS_INSUFFICIENT_RESOURCES = 0xC000009A, STATUS_MEDIA_WRITE_PROTECTED = 0xC00000A2, STATUS_FILE_IS_A_DIRECTORY = 0xC00000BA, STATUS_NOT_SUPPORTED = 0xC00000BB, + STATUS_NETWORK_NAME_DELETED = 0xC00000C9, STATUS_TOO_MANY_SESSIONS = 0xC00000CE, STATUS_TOO_MANY_OPENED_FILES = 0xC000011F, STATUS_CANNOT_DELETE = 0xC0000121, + STATUS_FILE_CLOSED = 0xC0000128, + STATUS_FS_DRIVER_REQUIRED = 0xC000019C, STATUS_USER_SESSION_DELETED = 0xC0000203, STATUS_INSUFF_SERVER_RESOURCES = 0xC0000205, diff --git a/SMBLibrary/Enums/Win32Error.cs b/SMBLibrary/Enums/Win32Error.cs index 99fbb4e..7daa5ce 100644 --- a/SMBLibrary/Enums/Win32Error.cs +++ b/SMBLibrary/Enums/Win32Error.cs @@ -8,6 +8,7 @@ namespace SMBLibrary ERROR_ACCESS_DENIED = 0x0005, ERROR_SHARING_VIOLATION = 0x0020, ERROR_DISK_FULL = 0x0070, + ERROR_ALREADY_EXISTS = 0x00B7, ERROR_LOGON_FAILURE = 0x052E, ERROR_ACCOUNT_RESTRICTION = 0x052F, ERROR_ACCOUNT_DISABLED = 0x0533, diff --git a/SMBLibrary/SMBLibrary.csproj b/SMBLibrary/SMBLibrary.csproj index abf35a0..c9413a2 100644 --- a/SMBLibrary/SMBLibrary.csproj +++ b/SMBLibrary/SMBLibrary.csproj @@ -118,12 +118,17 @@ + + + + + @@ -148,8 +153,18 @@ + + + + + + + + + + diff --git a/SMBLibrary/Server/ConnectionState/ConnectionState.cs b/SMBLibrary/Server/ConnectionState/ConnectionState.cs index 1aa4be9..c4b2068 100644 --- a/SMBLibrary/Server/ConnectionState/ConnectionState.cs +++ b/SMBLibrary/Server/ConnectionState/ConnectionState.cs @@ -19,6 +19,8 @@ namespace SMBLibrary.Server { NotSet, NTLM012, // NT LM 0.12 + SMB202, // SMB 2.0.2 + SMB210, // SMB 2.1 } public class ConnectionState diff --git a/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs b/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs new file mode 100644 index 0000000..69901bd --- /dev/null +++ b/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs @@ -0,0 +1,71 @@ +/* Copyright (C) 2014-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.IO; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server +{ + public delegate ulong? AllocatePersistentFileID(); + + public class SMB2ConnectionState : ConnectionState + { + // Key is SessionID + private Dictionary m_sessions = new Dictionary(); + private ulong m_nextSessionID = 1; + public AllocatePersistentFileID AllocatePersistentFileID; + + public SMB2ConnectionState(ConnectionState state, AllocatePersistentFileID allocatePersistentFileID) : base(state) + { + AllocatePersistentFileID = allocatePersistentFileID; + } + + public ulong? AllocateSessionID() + { + for (ulong offset = 0; offset < UInt64.MaxValue; offset++) + { + ulong sessionID = (ulong)(m_nextSessionID + offset); + if (sessionID == 0 || sessionID == 0xFFFFFFFF) + { + continue; + } + if (!m_sessions.ContainsKey(sessionID)) + { + m_nextSessionID = (ulong)(sessionID + 1); + return sessionID; + } + } + return null; + } + + public SMB2Session CreateSession(ulong sessionID, string userName) + { + SMB2Session session = new SMB2Session(this, sessionID, userName); + m_sessions.Add(sessionID, session); + return session; + } + + public SMB2Session GetSession(ulong sessionID) + { + SMB2Session session; + m_sessions.TryGetValue(sessionID, out session); + return session; + } + + public void RemoveSession(ulong sessionID) + { + m_sessions.Remove(sessionID); + } + + public void ClearSessions() + { + m_sessions.Clear(); + } + } +} diff --git a/SMBLibrary/Server/ConnectionState/SMB2Session.cs b/SMBLibrary/Server/ConnectionState/SMB2Session.cs new file mode 100644 index 0000000..7a6d1f8 --- /dev/null +++ b/SMBLibrary/Server/ConnectionState/SMB2Session.cs @@ -0,0 +1,165 @@ +/* Copyright (C) 2014-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.IO; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server +{ + public class SMB2Session + { + private SMB2ConnectionState m_connection; + private ulong m_sessionID; + private string m_userName; + + // Key is TreeID + private Dictionary m_connectedTrees = new Dictionary(); + private uint m_nextTreeID = 1; // TreeID uniquely identifies a tree connect within the scope of the session + + // Key is the persistent portion of the FileID + private Dictionary m_openFiles = new Dictionary(); + + // Key is the persistent portion of the FileID + private Dictionary m_openSearches = new Dictionary(); + + public SMB2Session(SMB2ConnectionState connecton, ulong sessionID, string userName) + { + m_connection = connecton; + m_sessionID = sessionID; + m_userName = userName; + } + + private uint? AllocateTreeID() + { + for (uint offset = 0; offset < UInt32.MaxValue; offset++) + { + uint treeID = (uint)(m_nextTreeID + offset); + if (treeID == 0 || treeID == 0xFFFFFFFF) + { + continue; + } + if (!m_connectedTrees.ContainsKey(treeID)) + { + m_nextTreeID = (uint)(treeID + 1); + return treeID; + } + } + return null; + } + + public uint? AddConnectedTree(ISMBShare share) + { + uint? treeID = AllocateTreeID(); + if (treeID.HasValue) + { + m_connectedTrees.Add(treeID.Value, share); + } + return treeID; + } + + public ISMBShare GetConnectedTree(uint treeID) + { + if (m_connectedTrees.ContainsKey(treeID)) + { + return m_connectedTrees[treeID]; + } + else + { + return null; + } + } + + public void RemoveConnectedTree(uint treeID) + { + m_connectedTrees.Remove(treeID); + } + + public void RemoveConnectedTrees() + { + m_connectedTrees.Clear(); + } + + public bool IsTreeConnected(uint treeID) + { + return m_connectedTrees.ContainsKey(treeID); + } + + /// Should include the path relative to the share + /// The persistent portion of the FileID + public ulong? AddOpenFile(string relativePath) + { + return AddOpenFile(relativePath, null); + } + + public ulong? AddOpenFile(string relativePath, Stream stream) + { + return AddOpenFile(relativePath, stream, false); + } + + public ulong? AddOpenFile(string relativePath, Stream stream, bool deleteOnClose) + { + ulong? persistentID = m_connection.AllocatePersistentFileID(); + if (persistentID.HasValue) + { + m_openFiles.Add(persistentID.Value, new OpenFileObject(relativePath, stream, deleteOnClose)); + } + return persistentID; + } + + public OpenFileObject GetOpenFileObject(ulong fileID) + { + if (m_openFiles.ContainsKey(fileID)) + { + return m_openFiles[fileID]; + } + else + { + return null; + } + } + + public void RemoveOpenFile(ulong fileID) + { + Stream stream = m_openFiles[fileID].Stream; + if (stream != null) + { + stream.Close(); + } + m_openFiles.Remove(fileID); + m_openSearches.Remove(fileID); + } + + public OpenSearch AddOpenSearch(ulong fileID, List entries, int enumerationLocation) + { + OpenSearch openSearch = new OpenSearch(entries, enumerationLocation); + m_openSearches.Add(fileID, openSearch); + return openSearch; + } + + public OpenSearch GetOpenSearch(ulong fileID) + { + OpenSearch openSearch; + m_openSearches.TryGetValue(fileID, out openSearch); + return openSearch; + } + + public void RemoveOpenSearch(ulong fileID) + { + m_openSearches.Remove(fileID); + } + + public string UserName + { + get + { + return m_userName; + } + } + } +} diff --git a/SMBLibrary/Server/Helpers/NTFileSystemHelper.Query.cs b/SMBLibrary/Server/Helpers/NTFileSystemHelper.Query.cs new file mode 100644 index 0000000..19a045a --- /dev/null +++ b/SMBLibrary/Server/Helpers/NTFileSystemHelper.Query.cs @@ -0,0 +1,191 @@ +/* Copyright (C) 2014-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.IO; +using Utilities; + +namespace SMBLibrary.Server +{ + public partial class NTFileSystemHelper + { + public static NTStatus GetNamedPipeInformation(out FileInformation result, FileInformationClass informationClass) + { + switch (informationClass) + { + case FileInformationClass.FileBasicInformation: + { + FileBasicInformation information = new FileBasicInformation(); + information.FileAttributes = FileAttributes.Temporary; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FileStandardInformation: + { + FileStandardInformation information = new FileStandardInformation(); + information.DeletePending = true; + result = information; + return NTStatus.STATUS_SUCCESS; + } + default: + result = null; + return NTStatus.STATUS_INVALID_INFO_CLASS; + } + } + + public static NTStatus GetFileInformation(out FileInformation result, FileSystemEntry entry, bool deletePending, FileInformationClass informationClass) + { + switch (informationClass) + { + case FileInformationClass.FileBasicInformation: + { + FileBasicInformation information = new FileBasicInformation(); + information.CreationTime = entry.CreationTime; + information.LastAccessTime = entry.LastAccessTime; + information.LastWriteTime = entry.LastWriteTime; + information.ChangeTime = entry.LastWriteTime; + information.FileAttributes = GetFileAttributes(entry); + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FileStandardInformation: + { + FileStandardInformation information = new FileStandardInformation(); + information.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + information.EndOfFile = entry.Size; + information.Directory = entry.IsDirectory; + information.DeletePending = deletePending; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FileInternalInformation: + { + FileInternalInformation information = new FileInternalInformation(); + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FileEaInformation: + { + FileEaInformation information = new FileEaInformation(); + information.EaSize = 0; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FilePositionInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileFullEaInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileModeInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileAlignmentInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileAllInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileAlternateNameInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileStreamInformation: + { + // This information class is used to enumerate the data streams of a file or a directory. + // A buffer of FileStreamInformation data elements is returned by the server. + FileStreamInformation information = new FileStreamInformation(); + information.StreamSize = entry.Size; + information.StreamAllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + information.StreamName = "::$DATA"; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FilePipeInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FilePipeLocalInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FilePipeRemoteInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileCompressionInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + case FileInformationClass.FileNetworkOpenInformation: + { + FileNetworkOpenInformation information = new FileNetworkOpenInformation(); + information.CreationTime = entry.CreationTime; + information.LastAccessTime = entry.LastAccessTime; + information.LastWriteTime = entry.LastWriteTime; + information.ChangeTime = entry.LastWriteTime; + information.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + information.EndOfFile = entry.Size; + information.FileAttributes = GetFileAttributes(entry); + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileInformationClass.FileAttributeTagInformation: + { + result = null; + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + default: + result = null; + return NTStatus.STATUS_INVALID_INFO_CLASS; + } + } + + public static FileAttributes GetFileAttributes(FileSystemEntry entry) + { + FileAttributes attributes = 0; + if (entry.IsHidden) + { + attributes |= FileAttributes.Hidden; + } + if (entry.IsReadonly) + { + attributes |= FileAttributes.ReadOnly; + } + if (entry.IsArchived) + { + attributes |= FileAttributes.Archive; + } + if (entry.IsDirectory) + { + attributes |= FileAttributes.Directory; + } + + if (attributes == 0) + { + attributes = FileAttributes.Normal; + } + + return attributes; + } + } +} diff --git a/SMBLibrary/Server/Helpers/NTFileSystemHelper.QueryFileSystem.cs b/SMBLibrary/Server/Helpers/NTFileSystemHelper.QueryFileSystem.cs new file mode 100644 index 0000000..b4ca2c2 --- /dev/null +++ b/SMBLibrary/Server/Helpers/NTFileSystemHelper.QueryFileSystem.cs @@ -0,0 +1,101 @@ +/* Copyright (C) 2014-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.IO; +using Utilities; + +namespace SMBLibrary.Server +{ + public partial class NTFileSystemHelper + { + public static NTStatus GetFileSystemInformation(out FileSystemInformation result, FileSystemInformationClass informationClass, IFileSystem fileSystem) + { + switch (informationClass) + { + case FileSystemInformationClass.FileFsVolumeInformation: + { + FileFsVolumeInformation information = new FileFsVolumeInformation(); + information.SupportsObjects = false; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileSystemInformationClass.FileFsSizeInformation: + { + FileFsSizeInformation information = new FileFsSizeInformation(); + information.TotalAllocationUnits = fileSystem.Size / NTFileSystemHelper.ClusterSize; + information.AvailableAllocationUnits = fileSystem.FreeSpace / NTFileSystemHelper.ClusterSize; + information.SectorsPerAllocationUnit = NTFileSystemHelper.ClusterSize / NTFileSystemHelper.BytesPerSector; + information.BytesPerSector = NTFileSystemHelper.BytesPerSector; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileSystemInformationClass.FileFsDeviceInformation: + { + FileFsDeviceInformation information = new FileFsDeviceInformation(); + information.DeviceType = DeviceType.Disk; + information.Characteristics = DeviceCharacteristics.IsMounted; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileSystemInformationClass.FileFsAttributeInformation: + { + FileFsAttributeInformation information = new FileFsAttributeInformation(); + information.FileSystemAttributes = FileSystemAttributes.UnicodeOnDisk; + information.MaximumComponentNameLength = 255; + information.FileSystemName = fileSystem.Name; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileSystemInformationClass.FileFsControlInformation: + { + FileFsControlInformation information = new FileFsControlInformation(); + information.FileSystemControlFlags = FileSystemControlFlags.ContentIndexingDisabled; + information.DefaultQuotaThreshold = UInt64.MaxValue; + information.DefaultQuotaLimit = UInt64.MaxValue; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileSystemInformationClass.FileFsFullSizeInformation: + { + FileFsFullSizeInformation information = new FileFsFullSizeInformation(); + information.TotalAllocationUnits = fileSystem.Size / NTFileSystemHelper.ClusterSize; + information.CallerAvailableAllocationUnits = fileSystem.FreeSpace / NTFileSystemHelper.ClusterSize; + information.ActualAvailableAllocationUnits = fileSystem.FreeSpace / NTFileSystemHelper.ClusterSize; + information.SectorsPerAllocationUnit = NTFileSystemHelper.ClusterSize / NTFileSystemHelper.BytesPerSector; + information.BytesPerSector = NTFileSystemHelper.BytesPerSector; + result = information; + return NTStatus.STATUS_SUCCESS; + } + case FileSystemInformationClass.FileFsObjectIdInformation: + { + result = null; + // STATUS_INVALID_PARAMETER is returned when the file system does not implement object IDs + // See: https://msdn.microsoft.com/en-us/library/cc232106.aspx + return NTStatus.STATUS_INVALID_PARAMETER; + } + case FileSystemInformationClass.FileFsSectorSizeInformation: + { + FileFsSectorSizeInformation information = new FileFsSectorSizeInformation(); + information.LogicalBytesPerSector = NTFileSystemHelper.BytesPerSector; + information.PhysicalBytesPerSectorForAtomicity = NTFileSystemHelper.BytesPerSector; + information.PhysicalBytesPerSectorForPerformance = NTFileSystemHelper.BytesPerSector; + information.FileSystemEffectivePhysicalBytesPerSectorForAtomicity = NTFileSystemHelper.BytesPerSector; + information.ByteOffsetForSectorAlignment = 0; + information.ByteOffsetForPartitionAlignment = 0; + result = information; + return NTStatus.STATUS_SUCCESS; + } + default: + { + result = null; + return NTStatus.STATUS_INVALID_INFO_CLASS; + } + } + } + } +} diff --git a/SMBLibrary/Server/Helpers/NTFileSystemHelper.Set.cs b/SMBLibrary/Server/Helpers/NTFileSystemHelper.Set.cs new file mode 100644 index 0000000..498b8ab --- /dev/null +++ b/SMBLibrary/Server/Helpers/NTFileSystemHelper.Set.cs @@ -0,0 +1,207 @@ +/* Copyright (C) 2014-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.IO; +using Utilities; + +namespace SMBLibrary.Server +{ + public partial class NTFileSystemHelper + { + public static NTStatus SetFileInformation(IFileSystem fileSystem, OpenFileObject openFile, FileInformation information, ConnectionState state) + { + if (information is FileBasicInformation) + { + FileBasicInformation basicInformation = (FileBasicInformation)information; + bool isHidden = ((basicInformation.FileAttributes & FileAttributes.Hidden) > 0); + bool isReadonly = (basicInformation.FileAttributes & FileAttributes.ReadOnly) > 0; + bool isArchived = (basicInformation.FileAttributes & FileAttributes.Archive) > 0; + try + { + fileSystem.SetAttributes(openFile.Path, isHidden, isReadonly, isArchived); + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Failed to set file attributes on '{0}'. Access Denied.", openFile.Path); + return NTStatus.STATUS_ACCESS_DENIED; + } + + try + { + fileSystem.SetDates(openFile.Path, basicInformation.CreationTime, basicInformation.LastWriteTime, basicInformation.LastAccessTime); + } + catch (IOException ex) + { + ushort errorCode = IOExceptionHelper.GetWin32ErrorCode(ex); + if (errorCode == (ushort)Win32Error.ERROR_SHARING_VIOLATION) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Failed to set file dates on '{0}'. Sharing Violation.", openFile.Path); + return NTStatus.STATUS_SHARING_VIOLATION; + } + else + { + state.LogToServer(Severity.Debug, "SetFileInformation: Failed to set file dates on '{0}'. Data Error.", openFile.Path); + return NTStatus.STATUS_DATA_ERROR; + } + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Failed to set file dates on '{0}'. Access Denied.", openFile.Path); + return NTStatus.STATUS_ACCESS_DENIED; + } + return NTStatus.STATUS_SUCCESS; + } + else if (information is FileRenameInformationType2) + { + FileRenameInformationType2 renameInformation = (FileRenameInformationType2)information; + string destination = renameInformation.FileName; + if (!destination.StartsWith(@"\")) + { + destination = @"\" + destination; + } + + if (openFile.Stream != null) + { + openFile.Stream.Close(); + } + + try + { + if (renameInformation.ReplaceIfExists && (fileSystem.GetEntry(destination) != null )) + { + fileSystem.Delete(destination); + } + fileSystem.Move(openFile.Path, destination); + } + catch (IOException ex) + { + ushort errorCode = IOExceptionHelper.GetWin32ErrorCode(ex); + if (errorCode == (ushort)Win32Error.ERROR_SHARING_VIOLATION) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot rename '{0}'. Sharing Violation.", openFile.Path); + return NTStatus.STATUS_SHARING_VIOLATION; + } + if (errorCode == (ushort)Win32Error.ERROR_ALREADY_EXISTS) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot rename '{0}'. Already Exists.", openFile.Path); + return NTStatus.STATUS_OBJECT_NAME_EXISTS; + } + else + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot rename '{0}'. Data Error.", openFile.Path); + return NTStatus.STATUS_DATA_ERROR; + } + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot rename '{0}'. Access Denied.", openFile.Path); + return NTStatus.STATUS_ACCESS_DENIED; + } + openFile.Path = destination; + return NTStatus.STATUS_SUCCESS; + } + else if (information is FileDispositionInformation) + { + if (((FileDispositionInformation)information).DeletePending) + { + // We're supposed to delete the file on close, but it's too late to report errors at this late stage + if (openFile.Stream != null) + { + openFile.Stream.Close(); + } + + try + { + state.LogToServer(Severity.Information, "SetFileInformation: Deleting file '{0}'", openFile.Path); + fileSystem.Delete(openFile.Path); + } + catch (IOException ex) + { + ushort errorCode = IOExceptionHelper.GetWin32ErrorCode(ex); + if (errorCode == (ushort)Win32Error.ERROR_SHARING_VIOLATION) + { + state.LogToServer(Severity.Information, "SetFileInformation: Error deleting '{0}'. Sharing Violation.", openFile.Path); + return NTStatus.STATUS_SHARING_VIOLATION; + } + else + { + state.LogToServer(Severity.Information, "SetFileInformation: Error deleting '{0}'. Data Error.", openFile.Path); + return NTStatus.STATUS_DATA_ERROR; + } + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Information, "SetFileInformation: Error deleting '{0}', Access Denied.", openFile.Path); + return NTStatus.STATUS_ACCESS_DENIED; + } + } + return NTStatus.STATUS_SUCCESS; + } + else if (information is FileAllocationInformation) + { + ulong allocationSize = ((FileAllocationInformation)information).AllocationSize; + try + { + openFile.Stream.SetLength((long)allocationSize); + } + catch (IOException ex) + { + ushort errorCode = IOExceptionHelper.GetWin32ErrorCode(ex); + if (errorCode == (ushort)Win32Error.ERROR_SHARING_VIOLATION) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot set allocation for '{0}'. Sharing Violation.", openFile.Path); + return NTStatus.STATUS_SHARING_VIOLATION; + } + else + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot set allocation for '{0}'. Data Error.", openFile.Path); + return NTStatus.STATUS_DATA_ERROR; + } + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot set allocation for '{0}'. Access Denied.", openFile.Path); + return NTStatus.STATUS_ACCESS_DENIED; + } + return NTStatus.STATUS_SUCCESS; + } + else if (information is FileEndOfFileInformation) + { + ulong endOfFile = ((FileEndOfFileInformation)information).EndOfFile; + try + { + openFile.Stream.SetLength((long)endOfFile); + } + catch (IOException ex) + { + ushort errorCode = IOExceptionHelper.GetWin32ErrorCode(ex); + if (errorCode == (ushort)Win32Error.ERROR_SHARING_VIOLATION) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot set end of file for '{0}'. Sharing Violation.", openFile.Path); + return NTStatus.STATUS_SHARING_VIOLATION; + } + else + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot set end of file for '{0}'. Data Error.", openFile.Path); + return NTStatus.STATUS_DATA_ERROR; + } + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Debug, "SetFileInformation: Cannot set end of file for '{0}'. Access Denied.", openFile.Path); + return NTStatus.STATUS_ACCESS_DENIED; + } + return NTStatus.STATUS_SUCCESS; + } + else + { + return NTStatus.STATUS_NOT_IMPLEMENTED; + } + } + } +} diff --git a/SMBLibrary/Server/SMB2/CreateHelper.cs b/SMBLibrary/Server/SMB2/CreateHelper.cs new file mode 100644 index 0000000..eb16ab1 --- /dev/null +++ b/SMBLibrary/Server/SMB2/CreateHelper.cs @@ -0,0 +1,149 @@ +/* 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.IO; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class CreateHelper + { + internal static SMB2Command GetCreateResponse(CreateRequest request, ISMBShare share, SMB2ConnectionState state) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + string path = request.Name; + if (!path.StartsWith(@"\")) + { + path = @"\" + path; + } + if (share is NamedPipeShare) + { + Stream pipeStream = ((NamedPipeShare)share).OpenPipe(path); + if (pipeStream != null) + { + ulong? persistentFileID = session.AddOpenFile(path, pipeStream); + if (!persistentFileID.HasValue) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_TOO_MANY_OPENED_FILES); + } + return CreateResponseForNamedPipe(persistentFileID.Value); + } + else + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_OBJECT_PATH_NOT_FOUND); + } + } + else + { + FileSystemShare fileSystemShare = (FileSystemShare)share; + + FileSystemEntry entry; + NTStatus createStatus = NTFileSystemHelper.CreateFile(out entry, (FileSystemShare)share, session.UserName, path, request.CreateDisposition, request.CreateOptions, request.DesiredAccess, state); + if (createStatus != NTStatus.STATUS_SUCCESS) + { + return new ErrorResponse(request.CommandName, createStatus); + } + + IFileSystem fileSystem = fileSystemShare.FileSystem; + FileAccess fileAccess = NTFileSystemHelper.ToFileAccess(request.DesiredAccess.File); + FileShare fileShare = NTFileSystemHelper.ToFileShare(request.ShareAccess); + + Stream stream; + bool deleteOnClose = false; + if (fileAccess == (FileAccess)0 || entry.IsDirectory) + { + stream = null; + } + else + { + // When FILE_OPEN_REPARSE_POINT is specified, the operation should continue normally if the file is not a reparse point. + // FILE_OPEN_REPARSE_POINT is a hint that the caller does not intend to actually read the file, with the exception + // of a file copy operation (where the caller will attempt to simply copy the reparse point). + deleteOnClose = (request.CreateOptions & CreateOptions.FILE_DELETE_ON_CLOSE) > 0; + bool openReparsePoint = (request.CreateOptions & CreateOptions.FILE_OPEN_REPARSE_POINT) > 0; + bool disableBuffering = (request.CreateOptions & CreateOptions.FILE_NO_INTERMEDIATE_BUFFERING) > 0; + bool buffered = (request.CreateOptions & CreateOptions.FILE_SEQUENTIAL_ONLY) > 0 && !disableBuffering && !openReparsePoint; + state.LogToServer(Severity.Verbose, "Create: Opening '{0}', Access={1}, Share={2}, Buffered={3}", path, fileAccess, fileShare, buffered); + try + { + stream = fileSystem.OpenFile(path, FileMode.Open, fileAccess, fileShare); + } + catch (IOException ex) + { + ushort errorCode = IOExceptionHelper.GetWin32ErrorCode(ex); + if (errorCode == (ushort)Win32Error.ERROR_SHARING_VIOLATION) + { + state.LogToServer(Severity.Debug, "NTCreate: Sharing violation opening '{0}'", path); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_SHARING_VIOLATION); + } + else + { + state.LogToServer(Severity.Debug, "NTCreate: Sharing violation opening '{0}', Data Error", path); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_DATA_ERROR); + } + } + catch (UnauthorizedAccessException) + { + state.LogToServer(Severity.Debug, "NTCreate: Sharing violation opening '{0}', Access Denied", path); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_ACCESS_DENIED); + } + + if (buffered) + { + stream = new PrefetchedStream(stream); + } + } + + ulong? persistentFileID = session.AddOpenFile(path, stream, deleteOnClose); + if (!persistentFileID.HasValue) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_TOO_MANY_OPENED_FILES); + } + + CreateResponse response = CreateResponseFromFileSystemEntry(entry, persistentFileID.Value); + if (request.RequestedOplockLevel == OplockLevel.Batch) + { + response.OplockLevel = OplockLevel.Batch; + } + return response; + } + } + + private static CreateResponse CreateResponseForNamedPipe(ulong persistentFileID) + { + CreateResponse response = new CreateResponse(); + response.FileId.Persistent = persistentFileID; + response.CreateAction = CreateAction.FILE_OPENED; + response.FileAttributes = FileAttributes.Normal; + return response; + } + + private static CreateResponse CreateResponseFromFileSystemEntry(FileSystemEntry entry, ulong persistentFileID) + { + CreateResponse response = new CreateResponse(); + if (entry.IsDirectory) + { + response.FileAttributes = FileAttributes.Directory; + } + else + { + response.FileAttributes = FileAttributes.Normal; + } + response.FileId.Persistent = persistentFileID; + response.CreateAction = CreateAction.FILE_OPENED; + response.CreationTime = entry.CreationTime; + response.LastWriteTime = entry.LastWriteTime; + response.ChangeTime = entry.LastWriteTime; + response.LastAccessTime = entry.LastAccessTime; + response.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + response.EndofFile = entry.Size; + return response; + } + } +} diff --git a/SMBLibrary/Server/SMB2/IOCtlHelper.cs b/SMBLibrary/Server/SMB2/IOCtlHelper.cs new file mode 100644 index 0000000..044ed69 --- /dev/null +++ b/SMBLibrary/Server/SMB2/IOCtlHelper.cs @@ -0,0 +1,51 @@ +/* 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.IO; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class IOCtlHelper + { + private const uint FSCTL_DFS_GET_REFERRALS = 0x00060194; + private const uint FSCTL_DFS_GET_REFERRALS_EX = 0x000601B0; + private const uint FSCTL_PIPE_TRANSCEIVE = 0x0011C017; + + internal static SMB2Command GetIOCtlResponse(IOCtlRequest request, ISMBShare share, SMB2ConnectionState state) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + if (request.CtlCode == FSCTL_DFS_GET_REFERRALS || request.CtlCode == FSCTL_DFS_GET_REFERRALS_EX) + { + // [MS-SMB2] 3.3.5.15.2 Handling a DFS Referral Information Request + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FS_DRIVER_REQUIRED); + } + + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + + if (share is NamedPipeShare) + { + if (request.CtlCode == FSCTL_PIPE_TRANSCEIVE) + { + IOCtlResponse response = new IOCtlResponse(); + response.CtlCode = request.CtlCode; + openFile.Stream.Write(request.Input, 0, request.Input.Length); + response.Output = ByteReader.ReadAllBytes(openFile.Stream); + return response; + } + } + + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + } +} diff --git a/SMBLibrary/Server/SMB2/NegotiateHelper.cs b/SMBLibrary/Server/SMB2/NegotiateHelper.cs new file mode 100644 index 0000000..d79a218 --- /dev/null +++ b/SMBLibrary/Server/SMB2/NegotiateHelper.cs @@ -0,0 +1,101 @@ +/* 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 SMBLibrary.Authentication; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + /// + /// Negotiate helper + /// + public class NegotiateHelper + { + public const string SMB2002Dialect = "SMB 2.002"; + public const string SMB2xxxDialect = "SMB 2.???"; + + // Special case - SMB2 client initially connecting using SMB1 + internal static SMB2Command GetNegotiateResponse(List smb2Dialects, ConnectionState state, Guid serverGuid) + { + NegotiateResponse response = new NegotiateResponse(); + response.Header.Credits = 1; + + if (smb2Dialects.Contains(SMB2xxxDialect)) + { + response.DialectRevision = SMB2Dialect.SMB2xx; + } + else if (smb2Dialects.Contains(SMB2002Dialect)) + { + state.ServerDialect = SMBDialect.SMB202; + response.DialectRevision = SMB2Dialect.SMB202; + } + else + { + throw new ArgumentException("SMB2 dialect is not present"); + } + response.ServerGuid = serverGuid; + response.MaxTransactSize = 65536; + response.MaxReadSize = 65536; + response.MaxWriteSize = 65536; + response.SystemTime = DateTime.Now; + response.ServerStartTime = DateTime.Today; + return response; + } + + internal static SMB2Command GetNegotiateResponse(NegotiateRequest request, ConnectionState state, Guid serverGuid) + { + NegotiateResponse response = new NegotiateResponse(); + if (request.Dialects.Contains(SMB2Dialect.SMB210)) + { + state.ServerDialect = SMBDialect.SMB210; + response.DialectRevision = SMB2Dialect.SMB210; + } + else if (request.Dialects.Contains(SMB2Dialect.SMB202)) + { + state.ServerDialect = SMBDialect.SMB202; + response.DialectRevision = SMB2Dialect.SMB202; + } + else + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + response.ServerGuid = serverGuid; + response.MaxTransactSize = 65536; + response.MaxReadSize = 65536; + response.MaxWriteSize = 65536; + response.SystemTime = DateTime.Now; + response.ServerStartTime = DateTime.Today; + return response; + } + + internal static List FindSMB2Dialects(SMBLibrary.SMB1.SMB1Message message) + { + if (message.Commands.Count > 0 && message.Commands[0] is SMBLibrary.SMB1.NegotiateRequest) + { + SMBLibrary.SMB1.NegotiateRequest request = (SMBLibrary.SMB1.NegotiateRequest)message.Commands[0]; + return FindSMB2Dialects(request); + } + return new List(); + } + + internal static List FindSMB2Dialects(SMBLibrary.SMB1.NegotiateRequest request) + { + List result = new List(); + if (request.Dialects.Contains(SMB2002Dialect)) + { + result.Add(SMB2002Dialect); + } + if (request.Dialects.Contains(SMB2xxxDialect)) + { + result.Add(SMB2xxxDialect); + } + return result; + } + } +} diff --git a/SMBLibrary/Server/SMB2/QueryDirectoryHelper.cs b/SMBLibrary/Server/SMB2/QueryDirectoryHelper.cs new file mode 100644 index 0000000..7f8e30f --- /dev/null +++ b/SMBLibrary/Server/SMB2/QueryDirectoryHelper.cs @@ -0,0 +1,208 @@ +/* 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 SMBLibrary.Authentication; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class QueryDirectoryHelper + { + internal static SMB2Command GetQueryDirectoryResponse(QueryDirectoryRequest request, ISMBShare share, SMB2ConnectionState state) + { + if (!(share is FileSystemShare)) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + + SMB2Session session = state.GetSession(request.Header.SessionID); + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + + FileSystemShare fileSystemShare = (FileSystemShare)share; + IFileSystem fileSystem = fileSystemShare.FileSystem; + + if (!fileSystem.GetEntry(openFile.Path).IsDirectory) + { + if ((request.Flags & QueryDirectoryFlags.SMB2_REOPEN) > 0) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + else + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_INVALID_PARAMETER); + } + } + + ulong fileID = request.FileId.Persistent; + OpenSearch openSearch = session.GetOpenSearch(fileID); + if (openSearch == null || request.Reopen) + { + if (request.Reopen) + { + session.RemoveOpenSearch(fileID); + } + List entries; + NTStatus searchStatus = NTFileSystemHelper.FindEntries(out entries, fileSystemShare.FileSystem, openFile.Path, request.FileName); + if (searchStatus != NTStatus.STATUS_SUCCESS) + { + state.LogToServer(Severity.Verbose, "Query Directory: Path: '{0}', Searched for '{1}', NTStatus: {2}", openFile.Path, request.FileName, searchStatus.ToString()); + return new ErrorResponse(request.CommandName, searchStatus); + } + state.LogToServer(Severity.Verbose, "Query Directory: Path: '{0}', Searched for '{1}', found {2} matching entries", openFile.Path, request.FileName, entries.Count); + openSearch = session.AddOpenSearch(fileID, entries, 0); + } + + if (request.Restart || request.Reopen) + { + openSearch.EnumerationLocation = 0; + } + + if (openSearch.Entries.Count == 0) + { + // [MS-SMB2] If there are no entries to return [..] the server MUST fail the request with STATUS_NO_SUCH_FILE. + session.RemoveOpenSearch(fileID); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NO_SUCH_FILE); + } + + if (openSearch.EnumerationLocation == openSearch.Entries.Count) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NO_MORE_FILES); + } + + List page = new List(); + int pageLength = 0; + for (int index = openSearch.EnumerationLocation; index < openSearch.Entries.Count; index++) + { + QueryDirectoryFileInformation fileInformation; + try + { + fileInformation = FromFileSystemEntry(openSearch.Entries[index], request.FileInformationClass); + } + catch (UnsupportedInformationLevelException) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_INVALID_INFO_CLASS); + } + + if (pageLength + fileInformation.Length <= request.OutputBufferLength) + { + page.Add(fileInformation); + pageLength += fileInformation.Length; + openSearch.EnumerationLocation = index + 1; + } + else + { + break; + } + + if (request.ReturnSingleEntry) + { + break; + } + } + + QueryDirectoryResponse response = new QueryDirectoryResponse(); + response.SetFileInformationList(page); + return response; + } + + internal static QueryDirectoryFileInformation FromFileSystemEntry(FileSystemEntry entry, FileInformationClass informationClass) + { + switch (informationClass) + { + case FileInformationClass.FileBothDirectoryInformation: + { + FileBothDirectoryInformation result = new FileBothDirectoryInformation(); + result.CreationTime = entry.CreationTime; + result.LastAccessTime = entry.LastAccessTime; + result.LastWriteTime = entry.LastWriteTime; + result.ChangeTime = entry.LastWriteTime; + result.EndOfFile = entry.Size; + result.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + result.FileAttributes = NTFileSystemHelper.GetFileAttributes(entry); + result.EaSize = 0; + result.ShortName = NTFileSystemHelper.GetShortName(entry.Name); + result.FileName = entry.Name; + return result; + } + case FileInformationClass.FileDirectoryInformation: + { + FileDirectoryInformation result = new FileDirectoryInformation(); + result.CreationTime = entry.CreationTime; + result.LastAccessTime = entry.LastAccessTime; + result.LastWriteTime = entry.LastWriteTime; + result.ChangeTime = entry.LastWriteTime; + result.EndOfFile = entry.Size; + result.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + result.FileAttributes = NTFileSystemHelper.GetFileAttributes(entry); + result.FileName = entry.Name; + return result; + } + case FileInformationClass.FileFullDirectoryInformation: + { + FileFullDirectoryInformation result = new FileFullDirectoryInformation(); + result.CreationTime = entry.CreationTime; + result.LastAccessTime = entry.LastAccessTime; + result.LastWriteTime = entry.LastWriteTime; + result.ChangeTime = entry.LastWriteTime; + result.EndOfFile = entry.Size; + result.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + result.FileAttributes = NTFileSystemHelper.GetFileAttributes(entry); + result.EaSize = 0; + result.FileName = entry.Name; + return result; + } + case FileInformationClass.FileIdBothDirectoryInformation: + { + FileIdBothDirectoryInformation result = new FileIdBothDirectoryInformation(); + result.CreationTime = entry.CreationTime; + result.LastAccessTime = entry.LastAccessTime; + result.LastWriteTime = entry.LastWriteTime; + result.ChangeTime = entry.LastWriteTime; + result.EndOfFile = entry.Size; + result.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + result.FileAttributes = NTFileSystemHelper.GetFileAttributes(entry); + result.EaSize = 0; + result.ShortName = NTFileSystemHelper.GetShortName(entry.Name); + result.FileId = 0; + result.FileName = entry.Name; + return result; + } + case FileInformationClass.FileIdFullDirectoryInformation: + { + FileIdFullDirectoryInformation result = new FileIdFullDirectoryInformation(); + result.CreationTime = entry.CreationTime; + result.LastAccessTime = entry.LastAccessTime; + result.LastWriteTime = entry.LastWriteTime; + result.ChangeTime = entry.LastWriteTime; + result.EndOfFile = entry.Size; + result.AllocationSize = NTFileSystemHelper.GetAllocationSize(entry.Size); + result.FileAttributes = NTFileSystemHelper.GetFileAttributes(entry); + result.EaSize = 0; + result.FileId = 0; + result.FileName = entry.Name; + return result; + } + case FileInformationClass.FileNamesInformation: + { + FileNamesInformation result = new FileNamesInformation(); + result.FileName = entry.Name; + return result; + } + default: + { + throw new UnsupportedInformationLevelException(); + } + } + } + } +} diff --git a/SMBLibrary/Server/SMB2/QueryInfoHelper.cs b/SMBLibrary/Server/SMB2/QueryInfoHelper.cs new file mode 100644 index 0000000..bbd820c --- /dev/null +++ b/SMBLibrary/Server/SMB2/QueryInfoHelper.cs @@ -0,0 +1,75 @@ +/* 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 SMBLibrary.Authentication; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class QueryInfoHelper + { + internal static SMB2Command GetQueryInfoResponse(QueryInfoRequest request, ISMBShare share, SMB2ConnectionState state) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + if (request.InfoType == InfoType.File) + { + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + + FileInformation fileInformation; + NTStatus queryStatus; + if (share is NamedPipeShare) + { + queryStatus = NTFileSystemHelper.GetNamedPipeInformation(out fileInformation, request.FileInformationClass); + } + else // FileSystemShare + { + IFileSystem fileSystem = ((FileSystemShare)share).FileSystem; + FileSystemEntry entry = fileSystem.GetEntry(openFile.Path); + if (entry == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NO_SUCH_FILE); + } + queryStatus = NTFileSystemHelper.GetFileInformation(out fileInformation, entry, openFile.DeleteOnClose, request.FileInformationClass); + } + + if (queryStatus != NTStatus.STATUS_SUCCESS) + { + state.LogToServer(Severity.Verbose, "GetFileInformation on '{0}' failed. Information class: {1}, NTStatus: {2}", openFile.Path, request.FileInformationClass, queryStatus); + return new ErrorResponse(request.CommandName, queryStatus); + } + + QueryInfoResponse response = new QueryInfoResponse(); + response.SetFileInformation(fileInformation); + return response; + } + else if (request.InfoType == InfoType.FileSystem) + { + if (share is FileSystemShare) + { + IFileSystem fileSystem = ((FileSystemShare)share).FileSystem; + FileSystemInformation fileSystemInformation; + NTStatus queryStatus = NTFileSystemHelper.GetFileSystemInformation(out fileSystemInformation, request.FileSystemInformationClass, fileSystem); + if (queryStatus != NTStatus.STATUS_SUCCESS) + { + state.LogToServer(Severity.Verbose, "GetFileSystemInformation failed. Information class: {0}, NTStatus: {1}", request.FileSystemInformationClass, queryStatus); + return new ErrorResponse(request.CommandName, queryStatus); + } + QueryInfoResponse response = new QueryInfoResponse(); + response.SetFileSystemInformation(fileSystemInformation); + return response; + } + } + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + } +} diff --git a/SMBLibrary/Server/SMB2/ReadWriteResponseHelper.cs b/SMBLibrary/Server/SMB2/ReadWriteResponseHelper.cs new file mode 100644 index 0000000..3a8bec0 --- /dev/null +++ b/SMBLibrary/Server/SMB2/ReadWriteResponseHelper.cs @@ -0,0 +1,57 @@ +/* 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 SMBLibrary.Authentication; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class ReadWriteResponseHelper + { + internal static SMB2Command GetReadResponse(ReadRequest request, ISMBShare share, SMB2ConnectionState state) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + + byte[] data; + NTStatus readStatus = NTFileSystemHelper.ReadFile(out data, openFile, (long)request.Offset, (int)request.ReadLength, state); + if (readStatus != NTStatus.STATUS_SUCCESS) + { + return new ErrorResponse(request.CommandName, readStatus); + } + ReadResponse response = new ReadResponse(); + response.Data = data; + return response; + } + + internal static SMB2Command GetWriteResponse(WriteRequest request, ISMBShare share, SMB2ConnectionState state) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + + int numberOfBytesWritten; + NTStatus writeStatus = NTFileSystemHelper.WriteFile(out numberOfBytesWritten, openFile, (long)request.Offset, request.Data, state); + if (writeStatus != NTStatus.STATUS_SUCCESS) + { + return new ErrorResponse(request.CommandName, writeStatus); + } + WriteResponse response = new WriteResponse(); + response.Count = (uint)numberOfBytesWritten; + return response; + } + } +} diff --git a/SMBLibrary/Server/SMB2/SessionSetupHelper.cs b/SMBLibrary/Server/SMB2/SessionSetupHelper.cs new file mode 100644 index 0000000..39880cd --- /dev/null +++ b/SMBLibrary/Server/SMB2/SessionSetupHelper.cs @@ -0,0 +1,101 @@ +/* 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 SMBLibrary.Authentication; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + /// + /// Session Setup helper + /// + public class SessionSetupHelper + { + internal static SMB2Command GetSessionSetupResponse(SessionSetupRequest request, INTLMAuthenticationProvider users, SMB2ConnectionState state) + { + // [MS-SMB2] Windows [..] will also accept raw Kerberos messages and implicit NTLM messages as part of GSS authentication. + SessionSetupResponse response = new SessionSetupResponse(); + byte[] messageBytes = request.SecurityBuffer; + bool isRawMessage = true; + if (!AuthenticationMessageUtils.IsSignatureValid(messageBytes)) + { + messageBytes = GSSAPIHelper.GetNTLMSSPMessage(request.SecurityBuffer); + isRawMessage = false; + } + if (!AuthenticationMessageUtils.IsSignatureValid(messageBytes)) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + + // According to [MS-SMB2] 3.3.5.5.3, response.Header.SessionID must be allocated if the server returns STATUS_MORE_PROCESSING_REQUIRED + if (request.Header.SessionID == 0) + { + ulong? sessionID = state.AllocateSessionID(); + if (!sessionID.HasValue) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_TOO_MANY_SESSIONS); + } + response.Header.SessionID = sessionID.Value; + } + + MessageTypeName messageType = AuthenticationMessageUtils.GetMessageType(messageBytes); + if (messageType == MessageTypeName.Negotiate) + { + NegotiateMessage negotiateMessage = new NegotiateMessage(messageBytes); + ChallengeMessage challengeMessage = users.GetChallengeMessage(negotiateMessage); + if (isRawMessage) + { + response.SecurityBuffer = challengeMessage.GetBytes(); + } + else + { + response.SecurityBuffer = GSSAPIHelper.GetGSSTokenResponseBytesFromNTLMSSPMessage(challengeMessage.GetBytes()); + } + response.Header.Status = NTStatus.STATUS_MORE_PROCESSING_REQUIRED; + } + else // MessageTypeName.Authenticate + { + AuthenticateMessage authenticateMessage = new AuthenticateMessage(messageBytes); + bool loginSuccess; + try + { + loginSuccess = users.Authenticate(authenticateMessage); + } + catch (EmptyPasswordNotAllowedException) + { + state.LogToServer(Severity.Information, "User '{0}' authentication using an empty password was rejected", authenticateMessage.UserName); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_ACCOUNT_RESTRICTION); + } + + if (loginSuccess) + { + state.LogToServer(Severity.Information, "User '{0}' authenticated successfully", authenticateMessage.UserName); + state.CreateSession(request.Header.SessionID, authenticateMessage.UserName); + } + else if (users.FallbackToGuest(authenticateMessage.UserName)) + { + state.LogToServer(Severity.Information, "User '{0}' failed authentication. logged in as guest", authenticateMessage.UserName); + state.CreateSession(request.Header.SessionID, "Guest"); + response.SessionFlags = SessionFlags.IsGuest; + } + else + { + state.LogToServer(Severity.Information, "User '{0}' failed authentication", authenticateMessage.UserName); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_LOGON_FAILURE); + } + + if (!isRawMessage) + { + response.SecurityBuffer = GSSAPIHelper.GetGSSTokenAcceptCompletedResponse(); + } + } + return response; + } + } +} diff --git a/SMBLibrary/Server/SMB2/SetInfoHelper.cs b/SMBLibrary/Server/SMB2/SetInfoHelper.cs new file mode 100644 index 0000000..86120ab --- /dev/null +++ b/SMBLibrary/Server/SMB2/SetInfoHelper.cs @@ -0,0 +1,65 @@ +/* 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 SMBLibrary.Authentication; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class SetInfoHelper + { + internal static SMB2Command GetSetInfoResponse(SetInfoRequest request, ISMBShare share, SMB2ConnectionState state) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + if (request.InfoType == InfoType.File) + { + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + + if (share is FileSystemShare) + { + IFileSystem fileSystem = ((FileSystemShare)share).FileSystem; + if (!((FileSystemShare)share).HasWriteAccess(session.UserName)) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_ACCESS_DENIED); + } + FileInformation information; + try + { + information = FileInformation.GetFileInformation(request.Buffer, 0, request.FileInformationClass); + } + catch (UnsupportedInformationLevelException) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_INVALID_INFO_CLASS); + } + catch (NotImplementedException) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + catch (Exception) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_INVALID_PARAMETER); + } + + NTStatus status = NTFileSystemHelper.SetFileInformation(fileSystem, openFile, information, state); + if (status != NTStatus.STATUS_SUCCESS) + { + state.LogToServer(Severity.Verbose, "SetFileInformation on '{0}' failed. Information class: {1}, NTStatus: {2}", openFile.Path, information.FileInformationClass, status); + return new ErrorResponse(request.CommandName, status); + } + return new SetInfoResponse(); + } + } + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + } +} diff --git a/SMBLibrary/Server/SMB2/TreeConnectHelper.cs b/SMBLibrary/Server/SMB2/TreeConnectHelper.cs new file mode 100644 index 0000000..f32d02a --- /dev/null +++ b/SMBLibrary/Server/SMB2/TreeConnectHelper.cs @@ -0,0 +1,62 @@ +/* Copyright (C) 2014-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.Text; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server.SMB2 +{ + public class TreeConnectHelper + { + internal static SMB2Command GetTreeConnectResponse(TreeConnectRequest request, SMB2ConnectionState state, NamedPipeShare services, ShareCollection shares) + { + SMB2Session session = state.GetSession(request.Header.SessionID); + TreeConnectResponse response = new TreeConnectResponse(); + string shareName = ServerPathUtils.GetShareName(request.Path); + ISMBShare share; + ShareType shareType; + ShareFlags shareFlags = ShareFlags.ManualCaching; + if (String.Equals(shareName, NamedPipeShare.NamedPipeShareName, StringComparison.InvariantCultureIgnoreCase)) + { + share = services; + shareType = ShareType.Pipe; + shareFlags = ShareFlags.NoCaching; + } + else + { + share = shares.GetShareFromName(shareName); + shareType = ShareType.Disk; + if (share == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_OBJECT_PATH_NOT_FOUND); + } + + if (!((FileSystemShare)share).HasReadAccess(session.UserName)) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_ACCESS_DENIED); + } + } + + uint? treeID = session.AddConnectedTree(share); + if (!treeID.HasValue) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_INSUFF_SERVER_RESOURCES); + } + response.Header.TreeID = treeID.Value; + response.ShareType = shareType; + response.ShareFlags = shareFlags; + response.MaximalAccess.File = FileAccessMask.FILE_READ_DATA | FileAccessMask.FILE_WRITE_DATA | FileAccessMask.FILE_APPEND_DATA | + FileAccessMask.FILE_READ_EA | FileAccessMask.FILE_WRITE_EA | + FileAccessMask.FILE_EXECUTE | + FileAccessMask.FILE_READ_ATTRIBUTES | FileAccessMask.FILE_WRITE_ATTRIBUTES | + FileAccessMask.DELETE | FileAccessMask.READ_CONTROL | FileAccessMask.WRITE_DAC | FileAccessMask.WRITE_OWNER | FileAccessMask.SYNCHRONIZE; + return response; + } + } +} diff --git a/SMBLibrary/Server/SMBServer.SMB1.cs b/SMBLibrary/Server/SMBServer.SMB1.cs index e8de18a..1f96885 100644 --- a/SMBLibrary/Server/SMBServer.SMB1.cs +++ b/SMBLibrary/Server/SMBServer.SMB1.cs @@ -15,7 +15,7 @@ namespace SMBLibrary.Server { public partial class SMBServer { - public void ProcessSMB1Message(SMB1Message message, SMB1ConnectionState state) + public void ProcessSMB1Message(SMB1Message message, ref ConnectionState state) { SMB1Message reply = new SMB1Message(); PrepareResponseHeader(reply, message); @@ -23,7 +23,7 @@ namespace SMBLibrary.Server foreach (SMB1Command command in message.Commands) { - SMB1Command response = ProcessSMB1Command(reply.Header, command, state, sendQueue); + SMB1Command response = ProcessSMB1Command(reply.Header, command, ref state, sendQueue); if (response != null) { reply.Commands.Add(response); @@ -51,7 +51,7 @@ namespace SMBLibrary.Server /// /// May return null /// - public SMB1Command ProcessSMB1Command(SMB1Header header, SMB1Command command, SMB1ConnectionState state, List sendQueue) + public SMB1Command ProcessSMB1Command(SMB1Header header, SMB1Command command, ref ConnectionState state, List sendQueue) { if (state.ServerDialect == SMBDialect.NotSet) { @@ -60,6 +60,7 @@ namespace SMBLibrary.Server NegotiateRequest request = (NegotiateRequest)command; if (request.Dialects.Contains(SMBServer.NTLanManagerDialect)) { + state = new SMB1ConnectionState(state); state.ServerDialect = SMBDialect.NTLM012; if (EnableExtendedSecurity && header.ExtendedSecurityFlag) { @@ -89,7 +90,15 @@ namespace SMBLibrary.Server header.Status = NTStatus.STATUS_SMB_BAD_COMMAND; return new ErrorResponse(command.CommandName); } - else if (command is SessionSetupAndXRequest) + else + { + return ProcessSMB1Command(header, command, (SMB1ConnectionState)state, sendQueue); + } + } + + private SMB1Command ProcessSMB1Command(SMB1Header header, SMB1Command command, SMB1ConnectionState state, List sendQueue) + { + if (command is SessionSetupAndXRequest) { SessionSetupAndXRequest request = (SessionSetupAndXRequest)command; state.MaxBufferSize = request.MaxBufferSize; @@ -329,12 +338,12 @@ namespace SMBLibrary.Server return new ErrorResponse(command.CommandName); } - public static void TrySendMessage(SMB1ConnectionState state, SMB1Message response) + public static void TrySendMessage(ConnectionState state, SMB1Message response) { SessionMessagePacket packet = new SessionMessagePacket(); packet.Trailer = response.GetBytes(); TrySendPacket(state, packet); - state.LogToServer(Severity.Verbose, "Response sent: {0} Commands, First Command: {1}, Packet length: {2}", response.Commands.Count, response.Commands[0].CommandName.ToString(), packet.Length); + state.LogToServer(Severity.Verbose, "SMB1 message sent: {0} responses, First response: {1}, Packet length: {2}", response.Commands.Count, response.Commands[0].CommandName.ToString(), packet.Length); } private static void PrepareResponseHeader(SMB1Message response, SMB1Message request) diff --git a/SMBLibrary/Server/SMBServer.SMB2.cs b/SMBLibrary/Server/SMBServer.SMB2.cs new file mode 100644 index 0000000..fc8cd38 --- /dev/null +++ b/SMBLibrary/Server/SMBServer.SMB2.cs @@ -0,0 +1,211 @@ +/* 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 SMBLibrary.NetBios; +using SMBLibrary.Server.SMB2; +using SMBLibrary.SMB2; +using Utilities; + +namespace SMBLibrary.Server +{ + public partial class SMBServer + { + // Key is the persistent portion of the FileID + private Dictionary m_globalOpenFiles = new Dictionary(); + private static ulong m_nextPersistentFileID = 1; // A numeric value that uniquely identifies the open handle to a file or a pipe within the scope of all opens granted by the server + + private ulong? AllocatePersistentFileID() + { + for (ulong offset = 0; offset < UInt64.MaxValue; offset++) + { + ulong persistentID = (ulong)(m_nextPersistentFileID + offset); + if (persistentID == 0 || persistentID == 0xFFFFFFFFFFFFFFFF) + { + continue; + } + if (!m_globalOpenFiles.ContainsKey(persistentID)) + { + m_nextPersistentFileID = (ulong)(persistentID + 1); + return persistentID; + } + } + return null; + } + + /// + /// May return null + /// + public SMB2Command ProcessSMB2Command(SMB2Command command, ref ConnectionState state) + { + if (state.ServerDialect == SMBDialect.NotSet) + { + if (command is NegotiateRequest) + { + NegotiateRequest request = (NegotiateRequest)command; + state = new SMB2ConnectionState(state, AllocatePersistentFileID); + return NegotiateHelper.GetNegotiateResponse(request, state, m_serverGuid); + } + else + { + // [MS-SMB2] If the request being received is not an SMB2 NEGOTIATE Request [..] + // and Connection.NegotiateDialect is 0xFFFF or 0x02FF, the server MUST + // disconnect the connection. + state.LogToServer(Severity.Debug, "Invalid Connection State for command {0}", command.CommandName.ToString()); + state.ClientSocket.Close(); + return null; + } + } + else if (command is NegotiateRequest) + { + // [MS-SMB2] If Connection.NegotiateDialect is 0x0202, 0x0210, 0x0300, 0x0302, or 0x0311, + // the server MUST disconnect the connection. + state.LogToServer(Severity.Debug, "Rejecting NegotiateRequest. NegotiateDialect is already set"); + state.ClientSocket.Close(); + return null; + } + else + { + return ProcessSMB2Command(command, (SMB2ConnectionState)state); + } + } + + public SMB2Command ProcessSMB2Command(SMB2Command command, SMB2ConnectionState state) + { + if (command is SessionSetupRequest) + { + return SessionSetupHelper.GetSessionSetupResponse((SessionSetupRequest)command, m_users, state); + } + else if (command is EchoRequest) + { + return new EchoResponse(); + } + else + { + SMB2Session session = state.GetSession(command.Header.SessionID); + if (session == null) + { + return new ErrorResponse(command.CommandName, NTStatus.STATUS_USER_SESSION_DELETED); + } + + if (command is TreeConnectRequest) + { + return TreeConnectHelper.GetTreeConnectResponse((TreeConnectRequest)command, state, m_services, m_shares); + } + else if (command is LogoffRequest) + { + state.RemoveSession(command.Header.SessionID); + return new LogoffResponse(); + } + else + { + ISMBShare share = session.GetConnectedTree(command.Header.TreeID); + if (share == null) + { + return new ErrorResponse(command.CommandName, NTStatus.STATUS_NETWORK_NAME_DELETED); + } + + if (command is TreeDisconnectRequest) + { + session.RemoveConnectedTree(command.Header.TreeID); + return new TreeDisconnectResponse(); + } + else if (command is CreateRequest) + { + return CreateHelper.GetCreateResponse((CreateRequest)command, share, state); + } + else if (command is QueryInfoRequest) + { + return QueryInfoHelper.GetQueryInfoResponse((QueryInfoRequest)command, share, state); + } + else if (command is SetInfoRequest) + { + return SetInfoHelper.GetSetInfoResponse((SetInfoRequest)command, share, state); + } + else if (command is QueryDirectoryRequest) + { + return QueryDirectoryHelper.GetQueryDirectoryResponse((QueryDirectoryRequest)command, share, state); + } + else if (command is ReadRequest) + { + return ReadWriteResponseHelper.GetReadResponse((ReadRequest)command, share, state); + } + else if (command is WriteRequest) + { + return ReadWriteResponseHelper.GetWriteResponse((WriteRequest)command, share, state); + } + else if (command is FlushRequest) + { + FlushRequest request = (FlushRequest)command; + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + openFile.Stream.Flush(); + return new FlushResponse(); + } + else if (command is CloseRequest) + { + CloseRequest request = (CloseRequest)command; + OpenFileObject openFile = session.GetOpenFileObject(request.FileId.Persistent); + if (openFile == null) + { + return new ErrorResponse(request.CommandName, NTStatus.STATUS_FILE_CLOSED); + } + session.RemoveOpenFile(request.FileId.Persistent); + return new CloseResponse(); + } + else if (command is IOCtlRequest) + { + return IOCtlHelper.GetIOCtlResponse((IOCtlRequest)command, share, state); + } + else if (command is ChangeNotifyRequest) + { + // [MS-SMB2] If the underlying object store does not support change notifications, the server MUST fail this request with STATUS_NOT_SUPPORTED + return new ErrorResponse(command.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + } + } + + return new ErrorResponse(command.CommandName, NTStatus.STATUS_NOT_SUPPORTED); + } + + public static void TrySendResponse(ConnectionState state, SMB2Command response) + { + SessionMessagePacket packet = new SessionMessagePacket(); + packet.Trailer = response.GetBytes(); + TrySendPacket(state, packet); + state.LogToServer(Severity.Verbose, "SMB2 response sent: {0}, Packet length: {1}", response.CommandName.ToString(), packet.Length); + } + + public static void TrySendResponseChain(ConnectionState state, List responseChain) + { + SessionMessagePacket packet = new SessionMessagePacket(); + packet.Trailer = SMB2Command.GetCommandChainBytes(responseChain); + TrySendPacket(state, packet); + state.LogToServer(Severity.Verbose, "SMB2 response chain sent: Response count: {0}, First response: {1}, Packet length: {2}", responseChain.Count, responseChain[0].CommandName.ToString(), packet.Length); + } + + private static void UpdateSMB2Header(SMB2Command response, SMB2Command request) + { + response.Header.MessageID = request.Header.MessageID; + response.Header.CreditCharge = request.Header.CreditCharge; + response.Header.Credits = Math.Max((ushort)1, request.Header.Credits); + response.Header.IsRelatedOperations = request.Header.IsRelatedOperations; + response.Header.Reserved = request.Header.Reserved; + if (response.Header.SessionID == 0) + { + response.Header.SessionID = request.Header.SessionID; + } + if (response.Header.TreeID == 0) + { + response.Header.TreeID = request.Header.TreeID; + } + } + } +} diff --git a/SMBLibrary/Server/SMBServer.cs b/SMBLibrary/Server/SMBServer.cs index b15d7d7..d93f4ef 100644 --- a/SMBLibrary/Server/SMBServer.cs +++ b/SMBLibrary/Server/SMBServer.cs @@ -11,6 +11,7 @@ using System.Net.Sockets; using SMBLibrary.NetBios; using SMBLibrary.Services; using SMBLibrary.SMB1; +using SMBLibrary.SMB2; using Utilities; namespace SMBLibrary.Server @@ -27,6 +28,8 @@ namespace SMBLibrary.Server private NamedPipeShare m_services; // Named pipes private IPAddress m_serverAddress; private SMBTransportType m_transport; + private bool m_enableSMB1; + private bool m_enableSMB2; private Socket m_listenerSocket; private bool m_listening; @@ -34,13 +37,19 @@ namespace SMBLibrary.Server public event EventHandler OnLogEntry; - public SMBServer(ShareCollection shares, INTLMAuthenticationProvider users, IPAddress serverAddress, SMBTransportType transport) + public SMBServer(ShareCollection shares, INTLMAuthenticationProvider users, IPAddress serverAddress, SMBTransportType transport) : this(shares, users, serverAddress, transport, true, true) + { + } + + public SMBServer(ShareCollection shares, INTLMAuthenticationProvider users, IPAddress serverAddress, SMBTransportType transport, bool enableSMB1, bool enableSMB2) { m_shares = shares; m_users = users; m_serverAddress = serverAddress; m_serverGuid = Guid.NewGuid(); m_transport = transport; + m_enableSMB1 = enableSMB1; + m_enableSMB2 = enableSMB2; m_services = new NamedPipeShare(shares.ListShares()); } @@ -93,7 +102,7 @@ namespace SMBLibrary.Server return; } - SMB1ConnectionState state = new SMB1ConnectionState(new ConnectionState(Log)); + ConnectionState state = new ConnectionState(Log); // Disable the Nagle Algorithm for this tcp socket: clientSocket.NoDelay = true; state.ClientSocket = clientSocket; @@ -116,7 +125,7 @@ namespace SMBLibrary.Server private void ReceiveCallback(IAsyncResult result) { - SMB1ConnectionState state = (SMB1ConnectionState)result.AsyncState; + ConnectionState state = (ConnectionState)result.AsyncState; Socket clientSocket = state.ClientSocket; if (!m_listening) @@ -149,7 +158,7 @@ namespace SMBLibrary.Server NBTConnectionReceiveBuffer receiveBuffer = state.ReceiveBuffer; receiveBuffer.SetNumberOfBytesReceived(numberOfBytesReceived); - ProcessConnectionBuffer(state); + ProcessConnectionBuffer(ref state); if (clientSocket.Connected) { @@ -166,7 +175,7 @@ namespace SMBLibrary.Server } } - public void ProcessConnectionBuffer(SMB1ConnectionState state) + public void ProcessConnectionBuffer(ref ConnectionState state) { Socket clientSocket = state.ClientSocket; @@ -185,12 +194,12 @@ namespace SMBLibrary.Server if (packet != null) { - ProcessPacket(packet, state); + ProcessPacket(packet, ref state); } } } - public void ProcessPacket(SessionPacket packet, SMB1ConnectionState state) + public void ProcessPacket(SessionPacket packet, ref ConnectionState state) { if (packet is SessionRequestPacket && m_transport == SMBTransportType.NetBiosOverTCP) { @@ -203,19 +212,97 @@ namespace SMBLibrary.Server } else if (packet is SessionMessagePacket) { - SMB1Message message = null; - try + // Note: To be compatible with SMB2 specifications, we must accept SMB_COM_NEGOTIATE. + // We will disconnect the connection if m_enableSMB1 == false and the client does not support SMB2. + bool acceptSMB1 = (state.ServerDialect == SMBDialect.NotSet || state.ServerDialect == SMBDialect.NTLM012); + bool acceptSMB2 = (m_enableSMB2 && (state.ServerDialect == SMBDialect.NotSet || state.ServerDialect == SMBDialect.SMB202 || state.ServerDialect == SMBDialect.SMB210)); + + if (SMB1Header.IsValidSMB1Header(packet.Trailer)) { - message = SMB1Message.GetSMB1Message(packet.Trailer); + if (!acceptSMB1) + { + state.LogToServer(Severity.Verbose, "Rejected SMB1 message"); + state.ClientSocket.Close(); + return; + } + + SMB1Message message = null; + try + { + message = SMB1Message.GetSMB1Message(packet.Trailer); + } + catch (Exception ex) + { + state.LogToServer(Severity.Warning, "Invalid SMB1 message: " + ex.Message); + state.ClientSocket.Close(); + return; + } + state.LogToServer(Severity.Verbose, "SMB1 message received: {0} requests, First request: {1}, Packet length: {2}", message.Commands.Count, message.Commands[0].CommandName.ToString(), packet.Length); + if (state.ServerDialect == SMBDialect.NotSet && m_enableSMB2) + { + // Check if the client supports SMB 2 + List smb2Dialects = SMB2.NegotiateHelper.FindSMB2Dialects(message); + if (smb2Dialects.Count > 0) + { + SMB2Command response = SMB2.NegotiateHelper.GetNegotiateResponse(smb2Dialects, state, m_serverGuid); + TrySendResponse(state, response); + return; + } + } + + if (m_enableSMB1) + { + ProcessSMB1Message(message, ref state); + } + else + { + // [MS-SMB2] 3.3.5.3.2 If the string is not present in the dialect list and the server does not implement SMB, + // the server MUST disconnect the connection [..] without sending a response. + state.LogToServer(Severity.Verbose, "Rejected SMB1 message"); + state.ClientSocket.Close(); + } } - catch (Exception ex) + else if (SMB2Header.IsValidSMB2Header(packet.Trailer)) { - state.LogToServer(Severity.Warning, "Invalid SMB1 message: " + ex.Message); + if (!acceptSMB2) + { + state.LogToServer(Severity.Verbose, "Rejected SMB2 message"); + state.ClientSocket.Close(); + return; + } + + List requestChain; + try + { + requestChain = SMB2Command.ReadRequestChain(packet.Trailer, 0); + } + catch (Exception ex) + { + state.LogToServer(Severity.Warning, "Invalid SMB2 request chain: " + ex.Message); + state.ClientSocket.Close(); + return; + } + state.LogToServer(Severity.Verbose, "SMB2 request chain received: {0} requests, First request: {1}, Packet length: {2}", requestChain.Count, requestChain[0].CommandName.ToString(), packet.Length); + List responseChain = new List(); + foreach (SMB2Command request in requestChain) + { + SMB2Command response = ProcessSMB2Command(request, ref state); + if (response != null) + { + UpdateSMB2Header(response, request); + responseChain.Add(response); + } + } + if (responseChain.Count > 0) + { + TrySendResponseChain(state, responseChain); + } + } + else + { + state.LogToServer(Severity.Warning, "Invalid SMB message"); state.ClientSocket.Close(); - return; } - state.LogToServer(Severity.Verbose, "Message Received: {0} Commands, First Command: {1}, Packet length: {2}", message.Commands.Count, message.Commands[0].CommandName.ToString(), packet.Length); - ProcessSMB1Message(message, state); } else { @@ -225,7 +312,7 @@ namespace SMBLibrary.Server } } - public static void TrySendPacket(SMB1ConnectionState state, SessionPacket response) + public static void TrySendPacket(ConnectionState state, SessionPacket response) { Socket clientSocket = state.ClientSocket; try