diff --git a/ClientExamples.md b/ClientExamples.md index 67dc699a..1592e7c4 100644 --- a/ClientExamples.md +++ b/ClientExamples.md @@ -1,6 +1,6 @@ Login and list shares: ====================== -``` +```cs SMB1Client client = new SMB1Client(); // SMB2Client can be used as well bool isConnected = client.Connect(IPAddress.Parse("192.168.1.11"), SMBTransportType.DirectTCPTransport); if (isConnected) @@ -17,7 +17,7 @@ if (isConnected) Connect to share and list files and directories - SMB1: ======================================================= -``` +```cs ISMBFileStore fileStore = client.TreeConnect("Shared", out status); if (status == NTStatus.STATUS_SUCCESS) { @@ -36,7 +36,7 @@ status = fileStore.Disconnect(); Connect to share and list files and directories - SMB2: ======================================================= -``` +```cs ISMBFileStore fileStore = client.TreeConnect("Shared", out status); if (status == NTStatus.STATUS_SUCCESS) { @@ -55,7 +55,7 @@ status = fileStore.Disconnect(); Read large file to its end: =========================== -``` +```cs ISMBFileStore fileStore = client.TreeConnect("Shared", out status); object fileHandle; FileStatus fileStatus; @@ -93,7 +93,7 @@ status = fileStore.Disconnect(); Create a file and write to it: ============================== -``` +```cs ISMBFileStore fileStore = client.TreeConnect("Shared", out status); string filePath = "NewFile.txt"; if (fileStore is SMB1FileStore) @@ -119,7 +119,7 @@ status = fileStore.Disconnect(); Write a large file: =================== -``` +```cs ISMBFileStore fileStore = client.TreeConnect("Shared", out status); if (status != NTStatus.STATUS_SUCCESS) { @@ -137,7 +137,7 @@ FileStatus fileStatus; status = fileStore.CreateFile(out fileHandle, out fileStatus, remoteFilePath, AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.None, CreateDisposition.FILE_CREATE, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); if (status == NTStatus.STATUS_SUCCESS) { - int writeOffset = 0; + long writeOffset = 0; while (localFileStream.Position < localFileStream.Length) { byte[] buffer = new byte[(int)client.MaxWriteSize]; @@ -161,7 +161,7 @@ status = fileStore.Disconnect(); Delete file: ============ -``` +```cs ISMBFileStore fileStore = client.TreeConnect("Shared", out status); string filePath = "DeleteMe.txt"; if (fileStore is SMB1FileStore) @@ -181,4 +181,43 @@ if (status == NTStatus.STATUS_SUCCESS) status = fileStore.CloseFile(fileHandle); } status = fileStore.Disconnect(); +``` + +Cross-platform Kerberos authentication: +======================================= +You can have cross-platform Kerberos login support by creating a class that implements IAuthenticationClient. +[Kerberos.NET](https://github.com/dotnet/Kerberos.NET) can easily be used to implement IAuthenticationClient. +Note that in order for Kerberos.NET to work on non-Windows platforms, you must provide a cross-platform implementation of IKerberosDnsQuery (and register it using DnsQuery.RegisterImplementation) +[DnsClient.NET](https://github.com/MichaCo/DnsClient.NET) can easily be used to implement IKerberosDnsQuery. + +```cs +public class KerberosNetAuthenticationClient : IAuthenticationClient +{ + private readonly KerberosClient m_kerberosClient; + private string m_spn; + private byte[] m_sessionKey; + + public KerberosNetAuthenticationClient(string user, string password, string domain, string host) + { + m_kerberosClient = new KerberosClient(); + m_kerberosClient.Authenticate(new KerberosPasswordCredential(user, password, domain)).Wait(); + m_spn = $"cifs/{host}"; + } + + public byte[] InitializeSecurityContext(byte[] inputToken) + { + KrbApReq ticket = m_kerberosClient.GetServiceTicket(m_spn).GetAwaiter().GetResult(); + KerberosClientCacheEntry cachedItem = (KerberosClientCacheEntry)m_kerberosClient.Cache.GetCacheItem(m_spn); + m_sessionKey = cachedItem.SessionKey.KeyValue.ToArray(); + return ticket.EncodeGssApi().ToArray(); + } + + public byte[] GetSessionKey() => m_sessionKey; + + public void ResetSecurityContext(string spn) + { + m_spn = spn; + m_sessionKey = null; + } +} ``` \ No newline at end of file diff --git a/DiskAccessLibrary.FileSystems.Abstractions/DiskAccessLibrary.FileSystems.Abstractions.VS2005.csproj b/DiskAccessLibrary.FileSystems.Abstractions/DiskAccessLibrary.FileSystems.Abstractions.VS2005.csproj deleted file mode 100644 index 0a41fefe..00000000 --- a/DiskAccessLibrary.FileSystems.Abstractions/DiskAccessLibrary.FileSystems.Abstractions.VS2005.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {9119EC7E-AF78-4814-BF03-F3823A29A471} - Library - Properties - DiskAccessLibrary.FileSystems.Abstractions - DiskAccessLibrary.FileSystems.Abstractions - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - \ No newline at end of file diff --git a/DiskAccessLibrary.FileSystems.Abstractions/DiskAccessLibrary.FileSystems.Abstractions.csproj b/DiskAccessLibrary.FileSystems.Abstractions/DiskAccessLibrary.FileSystems.Abstractions.csproj deleted file mode 100644 index e59ed0d1..00000000 --- a/DiskAccessLibrary.FileSystems.Abstractions/DiskAccessLibrary.FileSystems.Abstractions.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net20;net40;netstandard2.0 - false - DiskAccessLibrary.FileSystems.Abstractions - 1.0.0 - 1573;1591 - DiskAccessLibrary.FileSystems.Abstractions - false - Tal Aloni - DiskAccessLibrary abstractions and interfaces for FileSystem implementations - LGPL-3.0-or-later - https://github.com/TalAloni/DynamicDiskPartitioner - https://github.com/TalAloni/DynamicDiskPartitioner - true - - - diff --git a/DiskAccessLibrary.FileSystems.Abstractions/FileSystem.cs b/DiskAccessLibrary.FileSystems.Abstractions/FileSystem.cs deleted file mode 100644 index 8f29d383..00000000 --- a/DiskAccessLibrary.FileSystems.Abstractions/FileSystem.cs +++ /dev/null @@ -1,154 +0,0 @@ -/* Copyright (C) 2014-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.IO; - -namespace DiskAccessLibrary.FileSystems.Abstractions -{ - public abstract class FileSystem : IFileSystem - { - public abstract FileSystemEntry GetEntry(string path); - public abstract FileSystemEntry CreateFile(string path); - public abstract FileSystemEntry CreateDirectory(string path); - public abstract void Move(string source, string destination); - public abstract void Delete(string path); - public abstract List ListEntriesInDirectory(string path); - public abstract Stream OpenFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options); - public abstract void SetAttributes(string path, bool? isHidden, bool? isReadonly, bool? isArchived); - public abstract void SetDates(string path, DateTime? creationDT, DateTime? lastWriteDT, DateTime? lastAccessDT); - - public List ListEntriesInRootDirectory() - { - return ListEntriesInDirectory(@"\"); - } - - public virtual List> ListDataStreams(string path) - { - FileSystemEntry entry = GetEntry(path); - List> result = new List>(); - if (!entry.IsDirectory) - { - result.Add(new KeyValuePair("::$DATA", entry.Size)); - } - return result; - } - - public Stream OpenFile(string path, FileMode mode, FileAccess access, FileShare share) - { - return OpenFile(path, mode, access, share, FileOptions.None); - } - - public void CopyFile(string sourcePath, string destinationPath) - { - const int bufferLength = 1024 * 1024; - FileSystemEntry sourceFile = GetEntry(sourcePath); - FileSystemEntry destinationFile = GetEntry(destinationPath); - if (sourceFile == null | sourceFile.IsDirectory) - { - throw new FileNotFoundException(); - } - - if (destinationFile != null && destinationFile.IsDirectory) - { - throw new ArgumentException("Destination cannot be a directory"); - } - - if (destinationFile == null) - { - destinationFile = CreateFile(destinationPath); - } - Stream sourceStream = OpenFile(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, FileOptions.SequentialScan); - Stream destinationStream = OpenFile(destinationPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite, FileOptions.None); - while (sourceStream.Position < sourceStream.Length) - { - int readSize = (int)Math.Max(bufferLength, sourceStream.Length - sourceStream.Position); - byte[] buffer = new byte[readSize]; - sourceStream.Read(buffer, 0, buffer.Length); - destinationStream.Write(buffer, 0, buffer.Length); - } - sourceStream.Close(); - destinationStream.Close(); - } - - public virtual bool Exists(string path) - { - try - { - GetEntry(path); - } - catch (FileNotFoundException) - { - return false; - } - catch (DirectoryNotFoundException) - { - return false; - } - - return true; - } - - public abstract string Name - { - get; - } - - public abstract long Size - { - get; - } - - public abstract long FreeSpace - { - get; - } - - public abstract bool SupportsNamedStreams - { - get; - } - - public static string GetParentDirectory(string path) - { - if (path == String.Empty) - { - path = @"\"; - } - - if (!path.StartsWith(@"\")) - { - throw new ArgumentException("Invalid path"); - } - - if (path.Length > 1 && path.EndsWith(@"\")) - { - path = path.Substring(0, path.Length - 1); - } - - int separatorIndex = path.LastIndexOf(@"\"); - return path.Substring(0, separatorIndex + 1); - } - - /// - /// Will append a trailing slash to a directory path if not already present - /// - /// - /// - public static string GetDirectoryPath(string path) - { - if (path.EndsWith(@"\")) - { - return path; - } - else - { - return path + @"\"; - } - } - } -} diff --git a/DiskAccessLibrary.FileSystems.Abstractions/FileSystemEntry.cs b/DiskAccessLibrary.FileSystems.Abstractions/FileSystemEntry.cs deleted file mode 100644 index 56e042db..00000000 --- a/DiskAccessLibrary.FileSystems.Abstractions/FileSystemEntry.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* Copyright (C) 2014-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; - -namespace DiskAccessLibrary.FileSystems.Abstractions -{ - public class FileSystemEntry - { - /// - /// Full Path. Directory path should end with a trailing slash. - /// - public string FullName; - public string Name; - public bool IsDirectory; - public ulong Size; - public DateTime CreationTime; - public DateTime LastWriteTime; - public DateTime LastAccessTime; - public bool IsHidden; - public bool IsReadonly; - public bool IsArchived; - - public FileSystemEntry(string fullName, string name, bool isDirectory, ulong size, DateTime creationTime, DateTime lastWriteTime, DateTime lastAccessTime, bool isHidden, bool isReadonly, bool isArchived) - { - FullName = fullName; - Name = name; - IsDirectory = isDirectory; - Size = size; - CreationTime = creationTime; - LastWriteTime = lastWriteTime; - LastAccessTime = lastAccessTime; - IsHidden = isHidden; - IsReadonly = isHidden; - IsArchived = isHidden; - - if (isDirectory) - { - FullName = FileSystem.GetDirectoryPath(FullName); - } - } - - public FileSystemEntry Clone() - { - FileSystemEntry clone = (FileSystemEntry)MemberwiseClone(); - return clone; - } - } -} diff --git a/DiskAccessLibrary.FileSystems.Abstractions/IFileSystem.cs b/DiskAccessLibrary.FileSystems.Abstractions/IFileSystem.cs deleted file mode 100644 index 14f6e9d6..00000000 --- a/DiskAccessLibrary.FileSystems.Abstractions/IFileSystem.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* Copyright (C) 2014-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.IO; - -namespace DiskAccessLibrary.FileSystems.Abstractions -{ - public interface IFileSystem - { - /// - /// - /// - /// - FileSystemEntry GetEntry(string path); - - /// - /// - /// - FileSystemEntry CreateFile(string path); - - /// - /// - /// - FileSystemEntry CreateDirectory(string path); - - /// - /// - /// - /// - void Move(string source, string destination); - - /// - /// - /// - /// - void Delete(string path); - - /// - /// - /// - List ListEntriesInDirectory(string path); - - /// - /// - /// - /// - List> ListDataStreams(string path); - - /// - /// - /// - /// - Stream OpenFile(string path, FileMode mode, FileAccess access, FileShare share, FileOptions options); - - /// - /// - /// - void SetAttributes(string path, bool? isHidden, bool? isReadonly, bool? isArchived); - - /// - /// - /// - void SetDates(string path, DateTime? creationDT, DateTime? lastWriteDT, DateTime? lastAccessDT); - - string Name - { - get; - } - - /// - long Size - { - get; - } - - /// - long FreeSpace - { - get; - } - - /// - /// Indicates support for opening named streams (alternate data streams). - /// Named streams are opened using the filename:stream syntax. - /// - bool SupportsNamedStreams - { - get; - } - } -} diff --git a/DiskAccessLibrary.FileSystems.Abstractions/Properties/AssemblyInfo.cs b/DiskAccessLibrary.FileSystems.Abstractions/Properties/AssemblyInfo.cs deleted file mode 100644 index e9a83e4f..00000000 --- a/DiskAccessLibrary.FileSystems.Abstractions/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("DiskAccessLibrary.FileSystems.Abstractions")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Tal Aloni")] -[assembly: AssemblyProduct("DiskAccessLibrary.FileSystems.Abstractions")] -[assembly: AssemblyCopyright("Copyright © Tal Aloni 2012-2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("e71e5d6b-84ac-4889-810a-d18c2f6fbcbe")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Readme.md b/Readme.md index 3c4ef8e9..aacf20d2 100644 --- a/Readme.md +++ b/Readme.md @@ -1,9 +1,9 @@ About SMBLibrary: ================= SMBLibrary is an open-source C# SMB 1.0/CIFS, SMB 2.0, SMB 2.1 and SMB 3.0 server and client implementation. -SMBLibrary gives .NET developers an easy way to share a directory / file system / virtual file system, with any operating system that supports the SMB protocol. +SMBLibrary gives .NET developers an easy way to share a directory / file system / virtual file system or to connect to an existing share, with any operating system that supports the SMB protocol. SMBLibrary is modular, you can take advantage of Integrated Windows Authentication and the Windows storage subsystem on a Windows host or use independent implementations that allow for cross-platform compatibility. -SMBLibrary shares can be accessed from any Windows version since Windows NT 4.0. +SMBLibrary can communicate with any Windows version since Windows NT 4.0. Supported SMB / CIFS transport methods: ======================================= @@ -15,45 +15,9 @@ Supported SMB / CIFS transport methods: - A 'keep alive' packet is sent from time to time over NBT connections. - SMB2: Direct TCP hosting supports large MTUs. -Notes: -====== -By default, Windows already use ports 139 and 445. there are several techniques to free / utilize those ports: - -##### Method 1: Disable Windows File and Printer Sharing server completely: -###### Windows XP/2003: -1. For every network adapter: Uncheck 'File and Printer Sharing for Microsoft Networks". -2. Navigate to 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NetBT\Parameters' and set 'SMBDeviceEnabled' to '0' (this will free port 445). -3. Reboot. - -###### Windows 7/8/2008/2012: -Disable the "Server" service (p.s. "TCP\IP NETBIOS Helper" should be enabled). - -##### Method 2: Use Windows File Sharing AND SMBLibrary: -Windows bind port 139 to the first IP addres of every adapter, while port 445 is bound globally. -This means that if you'll disable port 445 (or block it using a firewall), you'll be able to use a different service on port 139 for every IP address. - -###### Additional Notes: -* To free port 139 for a given adapter, go to 'Internet Protocol (TCP/IP) Properties' > Advanced > WINS, and select 'Disable NetBIOS over TCP/IP'. -Uncheck 'File and Printer Sharing for Microsoft Networks' to ensure Windows will not answer to SMB traffic on port 445 for this adapter. - -* It's important to note that disabling NetBIOS over TCP/IP will also disable NetBIOS name service for that adapter (a.k.a. WINS), This service uses UDP port 137. -SMBLibrary offers a name service of its own. - -* You can install a virtual network adapter driver for Windows to be used solely with SMBLibrary: - - You can install the 'Microsoft Loopback adapter' and use it for server-only communication with SMBLibrary. - -###### Windows 7/8/2008/2012: -* It's possible to prevent Windows from using port 445 by removing all of the '\Device\Tcpip_{..}' and '\Device\Tcpip6_{..}' entries from the `Bind' registry key under 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Linkage'. - -* if you want localhost access from Windows explorer to work as expected, you must specify the IP address that you selected (\\\\127.0.0.1 or \\\\localhost will not work as expected), in addition, I have observed that when connecting to the first IP address of a given adapter, Windows will only attempt to connect to port 445. - -##### Method 3: Use an IP address that is invisible to Windows File Sharing: -Using PCap.Net you can programmatically setup a virtual Network adapter and intercept SMB traffic (similar to how a virtual machine operates), You should use the ARP protocol to notify the network about the new IP address, and then process the incoming SMB traffic using SMBLibrary, good luck! - Using SMBLibrary: ================= -Any directory / filesystem / object you wish to share must implement the IFileSystem interface (or the lower-level INTFileStore interface). -You can share anything from actual directories to custom objects, as long as they expose a directory structure. +Server notes can be found [here](ServerNotes.md). Client code examples can be found [here](ClientExamples.md). @@ -63,6 +27,16 @@ NuGet Packages: [SMBLibrary.Win32](https://www.nuget.org/packages/SMBLibrary.Win32/) - Allows utilizing Integrated Windows Authentication and/or the Windows storage subsystem on a Windows host. [SMBLibrary.Adapters](https://www.nuget.org/packages/SMBLibrary.Adapters/) - IFileSystem to INTFileStore adapter for SMBLibrary. +Licensing: +========== +A commercial license of SMBLibrary is available for a fee. +This is intended for companies who are unable to use the LGPL version. +Please contact me for additional details. + +Contributions: +============== +If you choose to make a contribution to this project, you must agree to irrevocably assign to SMBLibrary and/or Tal Aloni all worldwide copyright and intellectual property rights in and to your contribution, effective upon submission. + Contact: ======== If you have any question, feel free to contact me. diff --git a/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryDirectory.cs b/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryDirectory.cs index 3a92023d..349d7937 100644 --- a/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryDirectory.cs +++ b/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryDirectory.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2022 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, @@ -50,9 +50,9 @@ public NTStatus QueryDirectory(out List result, o // The SMB1 / SMB2 specifications mandate that when zero entries are found, the server SHOULD / MUST return STATUS_NO_SUCH_FILE. // For this reason, we MUST include the current directory and/or parent directory when enumerating a directory // in order to diffrentiate between a directory that does not exist and a directory with no entries. - FileSystemEntry currentDirectory = m_fileSystem.GetEntry(path); + FileSystemEntry currentDirectory = m_fileSystem.GetEntry(path).Clone(); currentDirectory.Name = "."; - FileSystemEntry parentDirectory = m_fileSystem.GetEntry(FileSystem.GetParentDirectory(path)); + FileSystemEntry parentDirectory = m_fileSystem.GetEntry(FileSystem.GetParentDirectory(path)).Clone(); parentDirectory.Name = ".."; entries.Insert(0, parentDirectory); entries.Insert(0, currentDirectory); diff --git a/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryFileSystem.cs b/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryFileSystem.cs index ff062746..8c20d99a 100644 --- a/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryFileSystem.cs +++ b/SMBLibrary.Adapters/NTFileSystemAdapter/NTFileSystemAdapter.QueryFileSystem.cs @@ -1,13 +1,10 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2026 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.Adapters { @@ -45,7 +42,7 @@ public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileS case FileSystemInformationClass.FileFsAttributeInformation: { FileFsAttributeInformation information = new FileFsAttributeInformation(); - information.FileSystemAttributes = FileSystemAttributes.CasePreservedNamed | FileSystemAttributes.UnicodeOnDisk; + information.FileSystemAttributes = FileSystemAttributes.CasePreservedNames | FileSystemAttributes.UnicodeOnDisk; information.MaximumComponentNameLength = 255; information.FileSystemName = m_fileSystem.Name; result = information; diff --git a/SMBLibrary.Adapters/Properties/AssemblyInfo.cs b/SMBLibrary.Adapters/Properties/AssemblyInfo.cs deleted file mode 100644 index fc116cfb..00000000 --- a/SMBLibrary.Adapters/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SMBLibrary.Adapters")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("none")] -[assembly: AssemblyProduct("SMBLibrary.Adapters")] -[assembly: AssemblyCopyright("Copyright © none 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("359460ac-a179-40cf-8491-e51a198e438c")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.4.6.0")] -[assembly: AssemblyFileVersion("1.4.6.0")] diff --git a/SMBLibrary.Adapters/SMBLibrary.Adapters.VS2005.csproj b/SMBLibrary.Adapters/SMBLibrary.Adapters.VS2005.csproj deleted file mode 100644 index 9a41e01c..00000000 --- a/SMBLibrary.Adapters/SMBLibrary.Adapters.VS2005.csproj +++ /dev/null @@ -1,60 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {DF51D33B-F030-4B25-803A-3BEBC35E5BEC} - Library - Properties - SMBLibrary.Adapters - SMBLibrary.Adapters - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - {9119EC7E-AF78-4814-BF03-F3823A29A471} - DiskAccessLibrary.FileSystems.Abstractions - - - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7} - SMBLibrary.VS2005 - - - - - \ No newline at end of file diff --git a/SMBLibrary.Adapters/SMBLibrary.Adapters.csproj b/SMBLibrary.Adapters/SMBLibrary.Adapters.csproj index 9956471a..38c65336 100644 --- a/SMBLibrary.Adapters/SMBLibrary.Adapters.csproj +++ b/SMBLibrary.Adapters/SMBLibrary.Adapters.csproj @@ -2,12 +2,12 @@ net20;net40;netstandard2.0 - false SMBLibrary.Adapters - 1.4.6 + 1.5.7 1573;1591 SMBLibrary.Adapters Tal Aloni + Copyright © Tal Aloni 2014-2026 FileSystem adapters for SMBLibrary LGPL-3.0-or-later https://github.com/TalAloni/SMBLibrary @@ -16,8 +16,11 @@ - + + + + diff --git a/SMBLibrary.Tests/AesCcmTests.cs b/SMBLibrary.Tests/AesCcmTests.cs index 5482027b..c9efdfec 100644 --- a/SMBLibrary.Tests/AesCcmTests.cs +++ b/SMBLibrary.Tests/AesCcmTests.cs @@ -4,12 +4,12 @@ * 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 Microsoft.VisualStudio.TestTools.UnitTesting; using Utilities; namespace SMBLibrary.Tests { + [TestClass] public class AesCcmTests { [TestMethod] @@ -121,13 +121,5 @@ public void TestDecryption() byte[] data = AesCcm.DecryptAndAuthenticate(key, nonce, encyrptedData, associatedData, signature); Assert.IsTrue(ByteUtils.AreByteArraysEqual(expectedData, data)); } - - public void TestAll() - { - TestEncryption_Rfc3610_Packet_Vector1(); - TestDecryption_Rfc3610_Packet_Vector1(); - TestEncryption(); - TestDecryption(); - } } } diff --git a/SMBLibrary.Tests/Client/SMB2ClientTests.cs b/SMBLibrary.Tests/Client/SMB2ClientTests.cs new file mode 100644 index 00000000..5396bad4 --- /dev/null +++ b/SMBLibrary.Tests/Client/SMB2ClientTests.cs @@ -0,0 +1,101 @@ +/* Copyright (C) 2024-2026 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.Client; +using System; +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Threading; + +namespace SMBLibrary.Tests.Client +{ + [TestClass] + public class SMB2ClientTests + { + private static readonly int s_minPort = 1025; + private static readonly int s_maxPort = 50000; + private static int s_nextServerPort = s_minPort + new Random().Next(s_maxPort - s_minPort); + + private int m_serverPort; + private TcpListener m_tcpListener; + private bool m_clientConnected; + + [TestInitialize] + public void Initialize() + { + m_serverPort = Interlocked.Increment(ref s_nextServerPort); + m_tcpListener = new TcpListener(IPAddress.Loopback, m_serverPort); + m_tcpListener.Start(); + } + + private void AcceptTcpClient_DoNotReply(IAsyncResult ar) + { + TcpClient client = m_tcpListener.EndAcceptTcpClient(ar); + m_clientConnected = true; + } + + private void AcceptTcpClient_SendNonSmbData(IAsyncResult ar) + { + TcpClient client = m_tcpListener.EndAcceptTcpClient(ar); + m_clientConnected = true; + byte[] buffer = new byte[4]; + client.Client.Send(buffer); + } + + [TestMethod] + public void When_SMB2ClientConnectsAndServerDoesNotReply_ShouldReachTimeout() + { + m_tcpListener.BeginAcceptTcpClient(AcceptTcpClient_DoNotReply, null); + + int timeoutInMilliseconds = 1000; + SMB2Client client = new SMB2Client(timeoutInMilliseconds); + + ManualResetEvent manualResetEvent = new ManualResetEvent(false); + Stopwatch stopwatch = new Stopwatch(); + bool isConnected = false; + new Thread(() => + { + stopwatch.Start(); + isConnected = client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort); + stopwatch.Stop(); + }).Start(); + + while (!m_clientConnected) + { + Thread.Sleep(1); + } + Assert.IsFalse(isConnected); + Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200); + } + + [TestMethod] + public void When_SMB2ClientConnectsAndServerSendNonSmbData_ShouldNotReachTimeout() + { + m_tcpListener.BeginAcceptTcpClient(AcceptTcpClient_SendNonSmbData, null); + int timeoutInMilliseconds = 1000; + SMB2Client client = new SMB2Client(timeoutInMilliseconds); + + ManualResetEvent manualResetEvent = new ManualResetEvent(false); + Stopwatch stopwatch = new Stopwatch(); + bool isConnected = false; + new Thread(() => + { + stopwatch.Start(); + isConnected = client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort); + stopwatch.Stop(); + }).Start(); + + while (!m_clientConnected) + { + Thread.Sleep(1); + } + Assert.IsFalse(isConnected); + Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200); + } + } +} diff --git a/SMBLibrary.Tests/Client/SMB2DfsFileStoreTests.cs b/SMBLibrary.Tests/Client/SMB2DfsFileStoreTests.cs new file mode 100644 index 00000000..e30e1808 --- /dev/null +++ b/SMBLibrary.Tests/Client/SMB2DfsFileStoreTests.cs @@ -0,0 +1,354 @@ +/* Copyright (C) 2026 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.Client; +using SMBLibrary.Client.DFS; +using SMBLibrary.DFS; +using SMBLibrary.SMB2; + +namespace SMBLibrary.Tests.Client +{ + [TestClass] + public class SMB2DfsFileStoreTests + { + [TestMethod] + public void CreateFile_WhenNotCovered_FollowsReferralToTargetAndRoutesHandle() + { + // Arrange: a link referral \SERVER1\DfsRoot\Link -> \SERVER2\Share + byte[] referralBytes = BuildReferral(@"\SERVER1\DfsRoot\Link", @"\SERVER2\Share"); + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_PATH_NOT_COVERED, ReferralResponseBytes = referralBytes }; + FakeFileStore targetStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + Dictionary targets = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { @"SERVER2\Share", targetStore } + }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, targets); + + // Act + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, @"Link\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + // Assert: resolved to the target, remainder path preserved + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + Assert.IsNotNull(handle); + CollectionAssert.Contains(dfsFileStore.ConnectRequests, @"SERVER2\Share"); + Assert.AreEqual("file.txt", targetStore.LastCreateFilePath); + + // The returned handle must route subsequent operations to the target it was opened against. + byte[] data; + dfsFileStore.ReadFile(out data, handle, 0, 3); + Assert.AreEqual(1, targetStore.ReadFileCount); + Assert.AreEqual(0, rootStore.ReadFileCount); + } + + [TestMethod] + public void CreateFile_WhenCovered_UsesDfsRootStoreWithoutReferral() + { + // Arrange + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, new Dictionary()); + + // Act + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, @"folder\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + // Assert + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + Assert.AreEqual(0, dfsFileStore.ConnectRequests.Count); + // [MS-SMB2] 2.2.13 - a request against the namespace root is a DFS operation, so the name is subject to + // DFS name normalization and must be a full path rather than a share-relative one. + Assert.AreEqual(@"SERVER1\DfsRoot\folder\file.txt", rootStore.LastCreateFilePath); + + byte[] data; + dfsFileStore.ReadFile(out data, handle, 0, 3); + Assert.AreEqual(1, rootStore.ReadFileCount); + } + + [TestMethod] + public void CreateFile_WhenPathIsShareRoot_SendsSharePathWithoutTrailingSeparator() + { + // Opening the share root using a single backslash is the idiom used in ClientExamples.md; + // String.Empty is the other spelling. Neither may produce a trailing separator. + foreach (string shareRoot in new string[] { @"\", String.Empty }) + { + string spelling = (shareRoot.Length == 0) ? "String.Empty" : "a single backslash"; + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, new Dictionary()); + + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, shareRoot, (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status, "Share root expressed as " + spelling); + Assert.AreEqual(@"SERVER1\DfsRoot", rootStore.LastCreateFilePath, "Share root expressed as " + spelling); + } + } + + [TestMethod] + public void CreateFile_WhenPathHasLeadingBackslash_RequestsReferralForTheNormalizedPath() + { + // The CREATE name and the referral path are built from the same caller-supplied value. If only the + // former is normalized the server is asked to resolve \\SERVER1\DfsRoot\\Link\file.txt and the + // referral fails, so the link is never followed. + byte[] referralBytes = BuildReferral(@"\SERVER1\DfsRoot\Link", @"\SERVER2\Share"); + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_PATH_NOT_COVERED, ReferralResponseBytes = referralBytes }; + FakeFileStore targetStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + Dictionary targets = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { @"SERVER2\Share", targetStore } + }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, targets); + + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, @"\Link\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + Assert.AreEqual(@"\\SERVER1\DfsRoot\Link\file.txt", rootStore.LastReferralRequestPath); + Assert.AreEqual("file.txt", targetStore.LastCreateFilePath); + } + + [TestMethod] + public void CreateFile_WhenReferralTargetIsItselfADfsRoot_SendsDfsPathToTheTarget() + { + // An interlink: the referral target is a namespace root of its own, so requests against it are DFS + // operations too and must carry a full path rather than a share-relative one. + byte[] referralBytes = BuildReferral(@"\SERVER1\DfsRoot\Link", @"\SERVER2\Nested"); + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_PATH_NOT_COVERED, ReferralResponseBytes = referralBytes }; + FakeFileStore nestedRootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + // TreeConnect returns an SMB2DfsFileStore when the target share is a namespace root. + SMB2DfsFileStore nestedDfsStore = new SMB2DfsFileStore(new SMB2Client(), "SERVER2", "Nested", nestedRootStore); + Dictionary targets = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { @"SERVER2\Nested", nestedDfsStore } + }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, targets); + + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, @"Link\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + Assert.AreEqual(@"SERVER2\Nested\file.txt", nestedRootStore.LastCreateFilePath); + } + + [TestMethod] + public void SMB2FileStore_ByDefault_IsNotADfsOperation() + { + // A share that is not a DFS namespace root is never wrapped, so its store must leave the flag clear + // and keep sending share-relative names. + SMB2FileStore fileStore = new SMB2FileStore(new SMB2Client(), 1, false); + + Assert.IsFalse(fileStore.IsDfsOperation); + } + + [TestMethod] + public void CreateFile_WhenPathHasLeadingBackslash_DoesNotDoubleSeparator() + { + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, new Dictionary()); + + object handle; + FileStatus fileStatus; + dfsFileStore.CreateFile(out handle, out fileStatus, @"\folder\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + Assert.AreEqual(@"SERVER1\DfsRoot\folder\file.txt", rootStore.LastCreateFilePath); + } + + [TestMethod] + public void CreateFile_WhenConnectedByIPAddress_KeepsPathShareRelative() + { + // [MS-DFSC] The server normalizes a DFS path against the namespace name, which an IP address can never + // match. Sending a DFS path here would break callers that connect by address to a share that happens to + // carry SMB2_SHAREFLAG_DFS_ROOT and work today. + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "10.0.0.5", "Namespace", rootStore, new Dictionary()); + + object handle; + FileStatus fileStatus; + dfsFileStore.CreateFile(out handle, out fileStatus, @"folder\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + Assert.AreEqual(@"folder\file.txt", rootStore.LastCreateFilePath); + } + + [TestMethod] + public void Constructor_WhenServerIsName_MarksUnderlyingStoreAsDfsOperation() + { + SMB2Client client = new SMB2Client(); + SMB2FileStore fileStore = new SMB2FileStore(client, 1, false); + + new SMB2DfsFileStore(client, "SERVER1", "DfsRoot", fileStore); + + Assert.IsTrue(fileStore.IsDfsOperation, "Requests against a DFS namespace root must be marked as DFS operations."); + } + + [TestMethod] + public void Constructor_WhenServerIsIPAddress_DoesNotMarkUnderlyingStoreAsDfsOperation() + { + SMB2Client client = new SMB2Client(); + SMB2FileStore fileStore = new SMB2FileStore(client, 1, false); + + new SMB2DfsFileStore(client, "10.0.0.5", "Namespace", fileStore); + + Assert.IsFalse(fileStore.IsDfsOperation); + } + + [TestMethod] + public void CreateFile_WhenReferralTargetUnreachable_ReturnsPathNotCovered() + { + // Arrange: no target registered => ConnectToTarget returns null. + byte[] referralBytes = BuildReferral(@"\SERVER1\DfsRoot\Link", @"\SERVER2\Share"); + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_PATH_NOT_COVERED, ReferralResponseBytes = referralBytes }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, new Dictionary()); + + // Act + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, @"Link\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + // Assert + Assert.AreEqual(NTStatus.STATUS_PATH_NOT_COVERED, status); + Assert.IsNull(handle); + } + + [TestMethod] + public void CreateFile_WhenFirstReferralTargetUnreachable_FailsOverToNextTarget() + { + // Arrange: two referral targets, only the second is reachable + byte[] referralBytes = BuildReferral(@"\SERVER1\DfsRoot\Link", @"\SERVER2\Share", @"\SERVER3\Share"); + FakeFileStore rootStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_PATH_NOT_COVERED, ReferralResponseBytes = referralBytes }; + FakeFileStore targetStore = new FakeFileStore() { CreateFileStatus = NTStatus.STATUS_SUCCESS }; + Dictionary targets = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { @"SERVER3\Share", targetStore } + }; + TestableDfsFileStore dfsFileStore = new TestableDfsFileStore(new SMB2Client(), "SERVER1", "DfsRoot", rootStore, targets); + + // Act + object handle; + FileStatus fileStatus; + NTStatus status = dfsFileStore.CreateFile(out handle, out fileStatus, @"Link\file.txt", (AccessMask)0, (FileAttributes)0, (ShareAccess)0, (CreateDisposition)0, (CreateOptions)0, null); + + // Assert + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + CollectionAssert.Contains(dfsFileStore.ConnectRequests, @"SERVER2\Share"); + CollectionAssert.Contains(dfsFileStore.ConnectRequests, @"SERVER3\Share"); + Assert.AreEqual("file.txt", targetStore.LastCreateFilePath); + } + + private static byte[] BuildReferral(string dfsPath, params string[] networkAddresses) + { + ResponseGetDfsReferral referral = new ResponseGetDfsReferral(); + referral.PathConsumed = (ushort)(dfsPath.Length * 2); + referral.ReferralHeaderFlags = DfsReferralHeaderFlags.StorageServers; + foreach (string networkAddress in networkAddresses) + { + referral.ReferralEntries.Add(new DfsReferralEntryV4() + { + TimeToLive = 300, + ReferralEntryFlags = DfsReferralEntryFlags.None, + DfsPath = dfsPath, + DfsAlternatePath = dfsPath, + NetworkAddress = networkAddress, + ServiceSiteGuid = Guid.Empty + }); + } + return referral.GetBytes(); + } + + /// + /// SMB2DfsFileStore subclass that intercepts target connections so the referral-following logic + /// can be exercised without a live server. + /// + private class TestableDfsFileStore : SMB2DfsFileStore + { + private Dictionary m_targets; + public List ConnectRequests = new List(); + + public TestableDfsFileStore(SMB2Client client, string serverName, string shareName, ISMBFileStore dfsFileStore, Dictionary targets) + : base(client, serverName, shareName, dfsFileStore) + { + m_targets = targets; + } + + protected override ISMBFileStore ConnectToTarget(string serverName, string shareName) + { + string key = serverName + @"\" + shareName; + ConnectRequests.Add(key); + ISMBFileStore fileStore; + if (m_targets.TryGetValue(key, out fileStore)) + { + return fileStore; + } + return null; + } + } + + private class FakeFileStore : ISMBFileStore + { + public NTStatus CreateFileStatus = NTStatus.STATUS_SUCCESS; + public byte[] ReferralResponseBytes; + public string LastCreateFilePath; + public string LastReferralRequestPath; + public int ReadFileCount; + + public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string path, AccessMask desiredAccess, FileAttributes fileAttributes, ShareAccess shareAccess, CreateDisposition createDisposition, CreateOptions createOptions, SecurityContext securityContext) + { + LastCreateFilePath = path; + if (CreateFileStatus == NTStatus.STATUS_SUCCESS) + { + handle = new object(); + fileStatus = FileStatus.FILE_OPENED; + return NTStatus.STATUS_SUCCESS; + } + handle = null; + fileStatus = FileStatus.FILE_DOES_NOT_EXIST; + return CreateFileStatus; + } + + public NTStatus DeviceIOControl(object handle, uint ctlCode, byte[] input, out byte[] output, int maxOutputLength) + { + if (ctlCode == (uint)IoControlCode.FSCTL_DFS_GET_REFERRALS && input != null) + { + LastReferralRequestPath = new RequestGetDfsReferral(input).RequestFileName; + } + output = ReferralResponseBytes; + return (ReferralResponseBytes != null) ? NTStatus.STATUS_SUCCESS : NTStatus.STATUS_NOT_SUPPORTED; + } + + public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCount) + { + ReadFileCount++; + data = new byte[maxCount]; + return NTStatus.STATUS_SUCCESS; + } + + public NTStatus CloseFile(object handle) { return NTStatus.STATUS_SUCCESS; } + public NTStatus Disconnect() { return NTStatus.STATUS_SUCCESS; } + public uint MaxReadSize { get { return 65536; } } + public uint MaxWriteSize { get { return 65536; } } + + public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offset, byte[] data) { throw new NotImplementedException(); } + public NTStatus FlushFileBuffers(object handle) { throw new NotImplementedException(); } + public NTStatus LockFile(object handle, long byteOffset, long length, bool exclusiveLock) { throw new NotImplementedException(); } + public NTStatus UnlockFile(object handle, long byteOffset, long length) { throw new NotImplementedException(); } + public NTStatus QueryDirectory(out List result, object handle, string fileName, FileInformationClass informationClass) { throw new NotImplementedException(); } + public NTStatus GetFileInformation(out FileInformation result, object handle, FileInformationClass informationClass) { throw new NotImplementedException(); } + public NTStatus SetFileInformation(object handle, FileInformation information) { throw new NotImplementedException(); } + public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileSystemInformationClass informationClass) { throw new NotImplementedException(); } + public NTStatus SetFileSystemInformation(FileSystemInformation information) { throw new NotImplementedException(); } + public NTStatus GetSecurityInformation(out SecurityDescriptor result, object handle, SecurityInformation securityInformation) { throw new NotImplementedException(); } + public NTStatus SetSecurityInformation(object handle, SecurityInformation securityInformation, SecurityDescriptor securityDescriptor) { throw new NotImplementedException(); } + public NTStatus NotifyChange(out object ioRequest, object handle, NotifyChangeFilter completionFilter, bool watchTree, int outputBufferSize, OnNotifyChangeCompleted onNotifyChangeCompleted, object context) { throw new NotImplementedException(); } + public NTStatus Cancel(object ioRequest) { throw new NotImplementedException(); } + } + } +} diff --git a/SMBLibrary.Tests/Components/Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll b/SMBLibrary.Tests/Components/Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll deleted file mode 100644 index 387ad78f..00000000 Binary files a/SMBLibrary.Tests/Components/Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll and /dev/null differ diff --git a/SMBLibrary.Tests/DFS/RequestGetDfsReferralExTests.cs b/SMBLibrary.Tests/DFS/RequestGetDfsReferralExTests.cs new file mode 100644 index 00000000..9b51b3ef --- /dev/null +++ b/SMBLibrary.Tests/DFS/RequestGetDfsReferralExTests.cs @@ -0,0 +1,45 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.DFS; + +namespace SMBLibrary.Tests.DFS +{ + [TestClass] + public class RequestGetDfsReferralExTests + { + [TestMethod] + public void ParseRequestGetDfsReferralEx() + { + byte[] buffer = new byte[] + { + 0x04, 0x00, 0x01, 0x00, 0x56, 0x00, 0x00, 0x00, 0x22, 0x00, 0x5c, 0x00, 0x4c, 0x00, 0x41, 0x00, + 0x42, 0x00, 0x2e, 0x00, 0x4c, 0x00, 0x4f, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4c, 0x00, 0x5c, 0x00, + 0x46, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, 0x00, 0x00, 0x30, 0x00, 0x44, 0x00, + 0x65, 0x00, 0x66, 0x00, 0x61, 0x00, 0x75, 0x00, 0x6c, 0x00, 0x74, 0x00, 0x2d, 0x00, 0x46, 0x00, + 0x69, 0x00, 0x72, 0x00, 0x73, 0x00, 0x74, 0x00, 0x2d, 0x00, 0x53, 0x00, 0x69, 0x00, 0x74, 0x00, + 0x65, 0x00, 0x2d, 0x00, 0x4e, 0x00, 0x61, 0x00, 0x6d, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00 + }; + + RequestGetDfsReferralEx request = new RequestGetDfsReferralEx(buffer); + Assert.AreEqual(4, request.MaxReferralLevel); + Assert.AreEqual(RequestGetDfsReferralExFlags.SiteName, request.Flags); + Assert.AreEqual(@"\LAB.LOCAL\Files", request.RequestFileName); + Assert.AreEqual("Default-First-Site-Name", request.SiteName); + } + + [TestMethod] + public void Parse_RequestGetDfsReferralEx_GetBytes() + { + RequestGetDfsReferralEx request = new RequestGetDfsReferralEx(); + request.MaxReferralLevel = 4; + request.Flags = RequestGetDfsReferralExFlags.SiteName; + request.RequestFileName = @"\LAB.LOCAL\Files"; + request.SiteName = @"Default-First-Site-Name"; + + request = new RequestGetDfsReferralEx(request.GetBytes()); + Assert.AreEqual(4, request.MaxReferralLevel); + Assert.AreEqual(RequestGetDfsReferralExFlags.SiteName, request.Flags); + Assert.AreEqual(@"\LAB.LOCAL\Files", request.RequestFileName); + Assert.AreEqual("Default-First-Site-Name", request.SiteName); + } + } +} diff --git a/SMBLibrary.Tests/DFS/ResponseGetDfsReferralTests.cs b/SMBLibrary.Tests/DFS/ResponseGetDfsReferralTests.cs new file mode 100644 index 00000000..0ec6b103 --- /dev/null +++ b/SMBLibrary.Tests/DFS/ResponseGetDfsReferralTests.cs @@ -0,0 +1,150 @@ +/* Copyright (C) 2025 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.DFS; +using System; +using System.Collections.Generic; + +namespace SMBLibrary.Tests.DFS +{ + [TestClass] + public class ResponseGetDfsReferralTests + { + [TestMethod] + public void ParseResponseGetDfsReferralWithSingleDfsReferralEntryV4() + { + // Arrange + // Returned by Windows Server 2008 R2 SP1 + byte[] buffer = new byte[] + { + 0x3e ,0x00 ,0x01 ,0x00 ,0x03 ,0x00 ,0x00 ,0x00 ,0x04 ,0x00 ,0x22 ,0x00 ,0x01 ,0x00 ,0x04 ,0x00, + 0x2c ,0x01 ,0x00 ,0x00 ,0x22 ,0x00 ,0x62 ,0x00 ,0xa2 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00, + 0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x5c ,0x00 ,0x57 ,0x00 ,0x49 ,0x00, + 0x4e ,0x00 ,0x2d ,0x00 ,0x4d ,0x00 ,0x51 ,0x00 ,0x38 ,0x00 ,0x33 ,0x00 ,0x44 ,0x00 ,0x45 ,0x00, + 0x35 ,0x00 ,0x4e ,0x00 ,0x47 ,0x00 ,0x37 ,0x00 ,0x32 ,0x00 ,0x5c ,0x00 ,0x44 ,0x00 ,0x66 ,0x00, + 0x73 ,0x00 ,0x20 ,0x00 ,0x4e ,0x00 ,0x61 ,0x00 ,0x6d ,0x00 ,0x65 ,0x00 ,0x73 ,0x00 ,0x70 ,0x00, + 0x61 ,0x00 ,0x63 ,0x00 ,0x65 ,0x00 ,0x31 ,0x00 ,0x00 ,0x00 ,0x5c ,0x00 ,0x57 ,0x00 ,0x49 ,0x00, + 0x4e ,0x00 ,0x2d ,0x00 ,0x4d ,0x00 ,0x51 ,0x00 ,0x38 ,0x00 ,0x33 ,0x00 ,0x44 ,0x00 ,0x45 ,0x00, + 0x35 ,0x00 ,0x4e ,0x00 ,0x47 ,0x00 ,0x37 ,0x00 ,0x32 ,0x00 ,0x5c ,0x00 ,0x44 ,0x00 ,0x66 ,0x00, + 0x73 ,0x00 ,0x20 ,0x00 ,0x4e ,0x00 ,0x61 ,0x00 ,0x6d ,0x00 ,0x65 ,0x00 ,0x73 ,0x00 ,0x70 ,0x00, + 0x61 ,0x00 ,0x63 ,0x00 ,0x65 ,0x00 ,0x31 ,0x00 ,0x00 ,0x00 ,0x5c ,0x00 ,0x57 ,0x00 ,0x49 ,0x00, + 0x4e ,0x00 ,0x2d ,0x00 ,0x4d ,0x00 ,0x51 ,0x00 ,0x38 ,0x00 ,0x33 ,0x00 ,0x44 ,0x00 ,0x45 ,0x00, + 0x35 ,0x00 ,0x4e ,0x00 ,0x47 ,0x00 ,0x37 ,0x00 ,0x32 ,0x00 ,0x5c ,0x00 ,0x44 ,0x00 ,0x66 ,0x00, + 0x73 ,0x00 ,0x20 ,0x00 ,0x4e ,0x00 ,0x61 ,0x00 ,0x6d ,0x00 ,0x65 ,0x00 ,0x73 ,0x00 ,0x70 ,0x00, + 0x61 ,0x00 ,0x63 ,0x00 ,0x65 ,0x00 ,0x31 ,0x00 ,0x00 ,0x00 + }; + + // Act + ResponseGetDfsReferral response = new ResponseGetDfsReferral(buffer); + + // Assert + Assert.AreEqual(62, response.PathConsumed); + Assert.AreEqual(DfsReferralHeaderFlags.ReferralServers | DfsReferralHeaderFlags.StorageServers , response.ReferralHeaderFlags); + Assert.AreEqual(1, response.ReferralEntries.Count); + Assert.IsInstanceOfType(response.ReferralEntries[0], typeof(DfsReferralEntryV4)); + + DfsReferralEntryV4 entry = (DfsReferralEntryV4)response.ReferralEntries[0]; + Assert.AreEqual((uint)300, entry.TimeToLive); + Assert.AreEqual(DfsReferralEntryFlags.TargetSetBoundary, entry.ReferralEntryFlags); + Assert.AreEqual("\\WIN-MQ83DE5NG72\\Dfs Namespace1", entry.DfsPath); + Assert.AreEqual("\\WIN-MQ83DE5NG72\\Dfs Namespace1", entry.DfsAlternatePath); + Assert.AreEqual("\\WIN-MQ83DE5NG72\\Dfs Namespace1", entry.NetworkAddress); + Assert.AreEqual(Guid.Empty, entry.ServiceSiteGuid); + } + + [TestMethod] + public void ParseResponseGetDfsReferralWithMultipleDfsReferralEntryV4() + { + // Arrange + byte[] buffer = new byte[] + { + 0x3c, 0x00, 0x02, 0x00, 0x02, 0x00, 0x00, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, 0x04, 0x00, + 0x2c, 0x01, 0x00, 0x00, 0x44, 0x00, 0x82, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x22, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x2c, 0x01, 0x00, 0x00, 0x22, 0x00, 0x60, 0x00, 0xbc, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x4c, 0x00, + 0x41, 0x00, 0x42, 0x00, 0x2d, 0x00, 0x44, 0x00, 0x43, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x4c, 0x00, + 0x41, 0x00, 0x42, 0x00, 0x2e, 0x00, 0x4c, 0x00, 0x4f, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4c, 0x00, + 0x5c, 0x00, 0x46, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, 0x5c, 0x00, 0x53, 0x00, + 0x61, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x4c, 0x00, 0x41, 0x00, + 0x42, 0x00, 0x2d, 0x00, 0x44, 0x00, 0x43, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x4c, 0x00, 0x41, 0x00, + 0x42, 0x00, 0x2e, 0x00, 0x4c, 0x00, 0x4f, 0x00, 0x43, 0x00, 0x41, 0x00, 0x4c, 0x00, 0x5c, 0x00, + 0x46, 0x00, 0x69, 0x00, 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, 0x5c, 0x00, 0x53, 0x00, 0x61, 0x00, + 0x6c, 0x00, 0x65, 0x00, 0x73, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x4c, 0x00, 0x41, 0x00, 0x42, 0x00, + 0x2d, 0x00, 0x46, 0x00, 0x53, 0x00, 0x32, 0x00, 0x5c, 0x00, 0x53, 0x00, 0x61, 0x00, 0x6c, 0x00, + 0x65, 0x00, 0x73, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x4c, 0x00, 0x41, 0x00, 0x42, 0x00, 0x2d, 0x00, + 0x46, 0x00, 0x53, 0x00, 0x31, 0x00, 0x5c, 0x00, 0x53, 0x00, 0x61, 0x00, 0x6c, 0x00, 0x65, 0x00, + 0x73, 0x00, 0x00, 0x00 + + }; + + // Act + ResponseGetDfsReferral response = new ResponseGetDfsReferral(buffer); + + // Assert + Assert.AreEqual(60, response.PathConsumed); + Assert.AreEqual(DfsReferralHeaderFlags.StorageServers, response.ReferralHeaderFlags); + Assert.AreEqual(2, response.ReferralEntries.Count); + Assert.IsInstanceOfType(response.ReferralEntries[0], typeof(DfsReferralEntryV4)); + Assert.IsInstanceOfType(response.ReferralEntries[1], typeof(DfsReferralEntryV4)); + + DfsReferralEntryV4 entry1 = (DfsReferralEntryV4)response.ReferralEntries[0]; + Assert.AreEqual((uint)300, entry1.TimeToLive); + Assert.AreEqual(DfsReferralEntryFlags.TargetSetBoundary, entry1.ReferralEntryFlags); + Assert.AreEqual("\\LAB-DC1.LAB.LOCAL\\Files\\Sales", entry1.DfsPath); + Assert.AreEqual("\\LAB-DC1.LAB.LOCAL\\Files\\Sales", entry1.DfsAlternatePath); + Assert.AreEqual("\\LAB-FS2\\Sales", entry1.NetworkAddress); + Assert.AreEqual(Guid.Empty, entry1.ServiceSiteGuid); + + DfsReferralEntryV4 entry2 = (DfsReferralEntryV4)response.ReferralEntries[1]; + Assert.AreEqual((uint)300, entry2.TimeToLive); + Assert.AreEqual(DfsReferralEntryFlags.None, entry2.ReferralEntryFlags); + Assert.AreEqual("\\LAB-DC1.LAB.LOCAL\\Files\\Sales", entry2.DfsPath); + Assert.AreEqual("\\LAB-DC1.LAB.LOCAL\\Files\\Sales", entry2.DfsAlternatePath); + Assert.AreEqual("\\LAB-FS1\\Sales", entry2.NetworkAddress); + Assert.AreEqual(Guid.Empty, entry2.ServiceSiteGuid); + } + + [TestMethod] + public void Parse_ResponseGetDfsReferralWithSingleDfsReferralEntryV4_GetBytes() + { + // Arrange + ResponseGetDfsReferral response = new ResponseGetDfsReferral(); + response.PathConsumed = 62; + response.ReferralHeaderFlags = DfsReferralHeaderFlags.ReferralServers | DfsReferralHeaderFlags.StorageServers; + response.ReferralEntries = new List() + { + new DfsReferralEntryV4() + { + TimeToLive = 300, + ReferralEntryFlags = DfsReferralEntryFlags.TargetSetBoundary, + DfsPath = "\\WIN-MQ83DE5NG72\\Dfs Namespace1", + DfsAlternatePath = "\\WIN-MQ83DE5NG72\\Dfs Namespace1", + NetworkAddress = "\\WIN-MQ83DE5NG72\\Dfs Namespace1", + ServiceSiteGuid = Guid.Empty + } + }; + + // Act + response = new ResponseGetDfsReferral(response.GetBytes()); + + // Assert + Assert.AreEqual(62, response.PathConsumed); + Assert.AreEqual(DfsReferralHeaderFlags.ReferralServers | DfsReferralHeaderFlags.StorageServers, response.ReferralHeaderFlags); + Assert.AreEqual(1, response.ReferralEntries.Count); + Assert.IsInstanceOfType(response.ReferralEntries[0], typeof(DfsReferralEntryV4)); + + DfsReferralEntryV4 entry = (DfsReferralEntryV4)response.ReferralEntries[0]; + Assert.AreEqual((uint)300, entry.TimeToLive); + Assert.AreEqual(DfsReferralEntryFlags.TargetSetBoundary, entry.ReferralEntryFlags); + Assert.AreEqual("\\WIN-MQ83DE5NG72\\Dfs Namespace1", entry.DfsPath); + Assert.AreEqual("\\WIN-MQ83DE5NG72\\Dfs Namespace1", entry.DfsAlternatePath); + Assert.AreEqual("\\WIN-MQ83DE5NG72\\Dfs Namespace1", entry.NetworkAddress); + Assert.AreEqual(Guid.Empty, entry.ServiceSiteGuid); + } + } +} diff --git a/SMBLibrary.Tests/IntegrationTests/LoginTests.cs b/SMBLibrary.Tests/IntegrationTests/LoginTests.cs new file mode 100644 index 00000000..6ea1c7f3 --- /dev/null +++ b/SMBLibrary.Tests/IntegrationTests/LoginTests.cs @@ -0,0 +1,90 @@ +/* Copyright (C) 2024-2025 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.Authentication.GSSAPI; +using SMBLibrary.Authentication.NTLM; +using SMBLibrary.Client; +using SMBLibrary.Server; +using System; +using System.Net; + +namespace SMBLibrary.Tests.IntegrationTests +{ + [TestClass] + public class LoginTests + { + private static Random s_seedGenerator = new Random(); + + private int m_serverPort; + private SMBServer m_server; + + [TestInitialize] + public void Initialize() + { + m_serverPort = 1000 + new Random(s_seedGenerator.Next()).Next(50000); + SMBShareCollection shares = new SMBShareCollection(); + IGSSMechanism gssMechanism = new IndependentNTLMAuthenticationProvider((username) => "password"); + GSSProvider gssProvider = new GSSProvider(gssMechanism); + m_server = new SMBServer(shares, gssProvider); + m_server.Start(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort, false, true, false, null); + } + + [TestCleanup] + public void Cleanup() + { + m_server.Stop(); + } + + [TestMethod] + public void When_ValidCredentialsProvided_LoginSucceed() + { + // Arrange + SMB2Client client = new SMB2Client(); + client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort); + + // Act + NTStatus status = client.Login("", "John", "password"); + + // Assert + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + } + + [TestMethod] + public void When_ClientDisconnectAndReconnect_LoginSucceed() + { + // Arrange + SMB2Client client = new SMB2Client(); + client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort); + + // Act + NTStatus status = client.Login("", "John", "password"); + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + status = client.Logoff(); + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + client.Disconnect(); + client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort); + status = client.Login("", "John", "password"); + + // Assert + Assert.AreEqual(NTStatus.STATUS_SUCCESS, status); + } + + [TestMethod] + public void When_InvalidCredentialsProvided_LoginFails() + { + // Arrange + SMB2Client client = new SMB2Client(); + client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, m_serverPort); + + // Act + NTStatus status = client.Login("", "John", "Password"); + + // Assert + Assert.AreEqual(NTStatus.STATUS_LOGON_FAILURE, status); + } + } +} diff --git a/SMBLibrary.Tests/NTFileStore/NTDirectoryFileSystemTests.cs b/SMBLibrary.Tests/NTFileStore/NTDirectoryFileSystemTests.cs index 316fe4b5..9b5b27b8 100644 --- a/SMBLibrary.Tests/NTFileStore/NTDirectoryFileSystemTests.cs +++ b/SMBLibrary.Tests/NTFileStore/NTDirectoryFileSystemTests.cs @@ -1,8 +1,12 @@ -using System; -using System.Collections.Generic; +/* Copyright (C) 2019-2026 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.IO; +using System.Runtime.InteropServices; using Microsoft.VisualStudio.TestTools.UnitTesting; -using SMBLibrary; using SMBLibrary.Win32; namespace SMBLibrary.Tests @@ -10,7 +14,7 @@ namespace SMBLibrary.Tests [TestClass] public class NTDirectoryFileSystemTests : NTFileStoreTests { - private static readonly string TestDirectoryPath = @"C:\Tests"; + private static readonly string TestDirectoryPath = Path.Combine(Path.GetTempPath(), "SMBLibraryTests"); static NTDirectoryFileSystemTests() { @@ -23,5 +27,16 @@ static NTDirectoryFileSystemTests() public NTDirectoryFileSystemTests() : base(new NTDirectoryFileSystem(TestDirectoryPath)) { } + + [TestMethod] + public override void TestCancel() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + Assert.Inconclusive(); + } + + base.TestCancel(); + } } } diff --git a/SMBLibrary.Tests/NTFileStore/NTFileStoreTests.cs b/SMBLibrary.Tests/NTFileStore/NTFileStoreTests.cs index 2eca2f6c..a99142f7 100644 --- a/SMBLibrary.Tests/NTFileStore/NTFileStoreTests.cs +++ b/SMBLibrary.Tests/NTFileStore/NTFileStoreTests.cs @@ -1,9 +1,11 @@ -using System; -using System.Collections.Generic; +/* Copyright (C) 2019-2025 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.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; -using SMBLibrary; -using SMBLibrary.Win32; namespace SMBLibrary.Tests { @@ -18,18 +20,13 @@ public abstract class NTFileStoreTests public NTFileStoreTests(INTFileStore fileStore) { m_fileStore = fileStore; - - object handle; - FileStatus fileStatus; - NTStatus status = m_fileStore.CreateFile(out handle, out fileStatus, TestDirName, AccessMask.GENERIC_ALL, FileAttributes.Directory, ShareAccess.Read, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE, null); - Assert.IsTrue(status == NTStatus.STATUS_SUCCESS); - status = m_fileStore.CloseFile(handle); - Assert.IsTrue(status == NTStatus.STATUS_SUCCESS); } [TestMethod] - public void TestCancel() + public virtual void TestCancel() { + CreateTestDirectory(); + object handle; FileStatus fileStatus; m_fileStore.CreateFile(out handle, out fileStatus, TestDirName, AccessMask.GENERIC_ALL, FileAttributes.Directory, ShareAccess.Read, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); @@ -53,9 +50,14 @@ private void OnNotifyChangeCompleted(NTStatus status, byte[] buffer, object cont m_notifyChangeStatus = status; } - public void TestAll() + private void CreateTestDirectory() { - TestCancel(); + object handle; + FileStatus fileStatus; + NTStatus status = m_fileStore.CreateFile(out handle, out fileStatus, TestDirName, AccessMask.GENERIC_ALL, FileAttributes.Directory, ShareAccess.Read, CreateDisposition.FILE_OPEN_IF, CreateOptions.FILE_DIRECTORY_FILE, null); + Assert.IsTrue(status == NTStatus.STATUS_SUCCESS); + status = m_fileStore.CloseFile(handle); + Assert.IsTrue(status == NTStatus.STATUS_SUCCESS); } } } diff --git a/SMBLibrary.Tests/NTLM/NTLMAuthenticationTests.cs b/SMBLibrary.Tests/NTLM/NTLMAuthenticationTests.cs index 224da077..e32e767d 100644 --- a/SMBLibrary.Tests/NTLM/NTLMAuthenticationTests.cs +++ b/SMBLibrary.Tests/NTLM/NTLMAuthenticationTests.cs @@ -1,12 +1,10 @@ -/* Copyright (C) 2014-2019 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2023 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 Microsoft.VisualStudio.TestTools.UnitTesting; using SMBLibrary.Authentication.NTLM; using Utilities; @@ -27,6 +25,15 @@ public void LMv1HashTest() Assert.IsTrue(ByteUtils.AreByteArraysEqual(hash, expected)); } + // Will use weak DES key + [TestMethod] + public void LMv1HashTestEmptyPassword() + { + byte[] hash = NTLMCryptography.LMOWFv1(""); + byte[] expected = new byte[] { 0xaa, 0xd3, 0xb4, 0x35, 0xb5, 0x14, 0x04, 0xee, 0xaa, 0xd3, 0xb4, 0x35, 0xb5, 0x14, 0x04, 0xee }; + Assert.IsTrue(ByteUtils.AreByteArraysEqual(hash, expected)); + } + [TestMethod] public void NTv1HashTest() { @@ -100,7 +107,7 @@ public void NTLMv2ChallengeMessageTest() ChallengeMessage message = new ChallengeMessage(); message.ServerChallenge = serverChallenge; message.Version = new NTLMVersion(6, 0, 6000, NTLMVersion.NTLMSSP_REVISION_W2K3); - message.NegotiateFlags = NegotiateFlags.UnicodeEncoding | NegotiateFlags.OEMEncoding | NegotiateFlags.TargetNameSupplied | NegotiateFlags.Sign | NegotiateFlags.Seal | NegotiateFlags.NTLMSessionSecurity | NegotiateFlags.AlwaysSign | NegotiateFlags.TargetTypeServer | NegotiateFlags.ExtendedSessionSecurity | NegotiateFlags.TargetInfo | NegotiateFlags.Version | NegotiateFlags.Use128BitEncryption | NegotiateFlags.KeyExchange | NegotiateFlags.Use56BitEncryption; + message.NegotiateFlags = NegotiateFlags.UnicodeEncoding | NegotiateFlags.OEMEncoding | NegotiateFlags.TargetNameNegotiated | NegotiateFlags.Sign | NegotiateFlags.Seal | NegotiateFlags.NTLMSessionSecurity | NegotiateFlags.AlwaysSign | NegotiateFlags.TargetTypeServer | NegotiateFlags.ExtendedSessionSecurity | NegotiateFlags.TargetInfo | NegotiateFlags.Version | NegotiateFlags.Use128BitEncryption | NegotiateFlags.KeyExchange | NegotiateFlags.Use56BitEncryption; message.TargetName = "Server"; message.TargetInfo = AVPairUtils.GetAVPairSequence("Domain", "Server"); @@ -140,7 +147,7 @@ public void NTLMv2AuthenticateMessageTest() AuthenticateMessage message = new AuthenticateMessage(); message.EncryptedRandomSessionKey = sessionKey; message.Version = new NTLMVersion(5, 1, 2600, NTLMVersion.NTLMSSP_REVISION_W2K3); - message.NegotiateFlags = NegotiateFlags.UnicodeEncoding | NegotiateFlags.TargetNameSupplied | NegotiateFlags.Sign | NegotiateFlags.Seal | NegotiateFlags.NTLMSessionSecurity | NegotiateFlags.AlwaysSign | NegotiateFlags.ExtendedSessionSecurity | NegotiateFlags.TargetInfo | NegotiateFlags.Version | NegotiateFlags.Use128BitEncryption | NegotiateFlags.KeyExchange | NegotiateFlags.Use56BitEncryption; + message.NegotiateFlags = NegotiateFlags.UnicodeEncoding | NegotiateFlags.TargetNameNegotiated | NegotiateFlags.Sign | NegotiateFlags.Seal | NegotiateFlags.NTLMSessionSecurity | NegotiateFlags.AlwaysSign | NegotiateFlags.ExtendedSessionSecurity | NegotiateFlags.TargetInfo | NegotiateFlags.Version | NegotiateFlags.Use128BitEncryption | NegotiateFlags.KeyExchange | NegotiateFlags.Use56BitEncryption; message.DomainName = "Domain"; message.WorkStation = "COMPUTER"; message.UserName = "User"; @@ -151,18 +158,5 @@ public void NTLMv2AuthenticateMessageTest() // The payload entries may be distributed differently so we use cmp.GetBytes() Assert.IsTrue(ByteUtils.AreByteArraysEqual(messageBytes, cmp.GetBytes())); } - - public void TestAll() - { - LMv1HashTest(); - NTv1HashTest(); - NTv2HashTest(); - LMv1ResponseTest(); - NTLMv1ResponseTest(); - LMv2ResponseTest(); - NTLMv2ResponseTest(); - NTLMv2ChallengeMessageTest(); - NTLMv2AuthenticateMessageTest(); - } } } diff --git a/SMBLibrary.Tests/NTLM/NTLMSigningTests.cs b/SMBLibrary.Tests/NTLM/NTLMSigningTests.cs index 50433d0c..12ab8469 100644 --- a/SMBLibrary.Tests/NTLM/NTLMSigningTests.cs +++ b/SMBLibrary.Tests/NTLM/NTLMSigningTests.cs @@ -1,11 +1,9 @@ -/* Copyright (C) 2017-2019 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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.Security.Cryptography; using Microsoft.VisualStudio.TestTools.UnitTesting; using SMBLibrary.Authentication.NTLM; @@ -47,14 +45,8 @@ public void TestLMMIC() byte[] lmowf = NTLMCryptography.LMOWFv1(password); byte[] exportedSessionKey = GetExportedSessionKey(sessionBaseKey, authenticateMessage, serverChallenge, lmowf); - // https://msdn.microsoft.com/en-us/library/cc236695.aspx - const int micFieldOffset = 72; - ByteWriter.WriteBytes(type3, micFieldOffset, new byte[16]); - byte[] temp = ByteUtils.Concatenate(ByteUtils.Concatenate(type1, type2), type3); - byte[] mic = new HMACMD5(exportedSessionKey).ComputeHash(temp); - byte[] expected = new byte[] { 0x4e, 0x65, 0x54, 0xe6, 0xb3, 0xdc, 0xdc, 0x16, 0xef, 0xc4, 0xd0, 0x03, 0x3b, 0x81, 0x61, 0x6f }; - - Assert.IsTrue(ByteUtils.AreByteArraysEqual(mic, expected)); + bool isMicValid = NTLMCryptography.ValidateAuthenticateMessageMIC(exportedSessionKey, type1, type2, type3); + Assert.IsTrue(isMicValid); } [TestMethod] @@ -87,15 +79,9 @@ public void TestNTLMv1MIC() byte[] sessionBaseKey = new MD4().GetByteHashFromBytes(NTLMCryptography.NTOWFv1(password)); byte[] lmowf = NTLMCryptography.LMOWFv1(password); byte[] exportedSessionKey = GetExportedSessionKey(sessionBaseKey, authenticateMessage, serverChallenge, lmowf); - - // https://msdn.microsoft.com/en-us/library/cc236695.aspx - const int micFieldOffset = 72; - ByteWriter.WriteBytes(type3, micFieldOffset, new byte[16]); - byte[] temp = ByteUtils.Concatenate(ByteUtils.Concatenate(type1, type2), type3); - byte[] mic = new HMACMD5(exportedSessionKey).ComputeHash(temp); - byte[] expected = new byte[] { 0xae, 0xa7, 0xba, 0x44, 0x4e, 0x93, 0xa7, 0xdb, 0xb3, 0x0c, 0x85, 0x49, 0xc2, 0x2b, 0xba, 0x9a }; - - Assert.IsTrue(ByteUtils.AreByteArraysEqual(mic, expected)); + + bool isMicValid = NTLMCryptography.ValidateAuthenticateMessageMIC(exportedSessionKey, type1, type2, type3); + Assert.IsTrue(isMicValid); } [TestMethod] @@ -135,14 +121,8 @@ public void TestNTLMv1ExtendedSessionSecurityKeyExchangeMIC() byte[] lmowf = NTLMCryptography.LMOWFv1(password); byte[] exportedSessionKey = GetExportedSessionKey(sessionBaseKey, authenticateMessage, serverChallenge, lmowf); - // https://msdn.microsoft.com/en-us/library/cc236695.aspx - const int micFieldOffset = 72; - ByteWriter.WriteBytes(type3, micFieldOffset, new byte[16]); - byte[] temp = ByteUtils.Concatenate(ByteUtils.Concatenate(type1, type2), type3); - byte[] mic = new HMACMD5(exportedSessionKey).ComputeHash(temp); - byte[] expected = new byte[] { 0xc6, 0x21, 0x82, 0x59, 0x83, 0xda, 0xc7, 0xe7, 0xfa, 0x96, 0x44, 0x67, 0x16, 0xc3, 0xb3, 0x5b }; - - Assert.IsTrue(ByteUtils.AreByteArraysEqual(mic, expected)); + bool isMicValid = NTLMCryptography.ValidateAuthenticateMessageMIC(exportedSessionKey, type1, type2, type3); + Assert.IsTrue(isMicValid); } [TestMethod] @@ -197,22 +177,51 @@ public void TestNTLMv2KeyExchangeMIC() byte[] sessionBaseKey = new HMACMD5(responseKeyNT).ComputeHash(ntProofStr); byte[] exportedSessionKey = GetExportedSessionKey(sessionBaseKey, authenticateMessage, serverChallenge, null); - // https://msdn.microsoft.com/en-us/library/cc236695.aspx - const int micFieldOffset = 72; - ByteWriter.WriteBytes(type3, micFieldOffset, new byte[16]); - byte[] temp = ByteUtils.Concatenate(ByteUtils.Concatenate(type1, type2), type3); - byte[] mic = new HMACMD5(exportedSessionKey).ComputeHash(temp); - byte[] expected = new byte[] { 0x82, 0x3c, 0xff, 0x48, 0xa9, 0x03, 0x13, 0x4c, 0x33, 0x3c, 0x09, 0x87, 0xf3, 0x16, 0x59, 0x89 }; + bool isMicValid = NTLMCryptography.ValidateAuthenticateMessageMIC(exportedSessionKey, type1, type2, type3); + Assert.IsTrue(isMicValid); + } + + [TestMethod] + public void Test_ComputeClientSignKey() + { + // Arrange + byte[] exportedSessionKey = new byte[] { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }; + byte[] expected = new byte[] { 0x47, 0x88, 0xdc, 0x86, 0x1b, 0x47, 0x82, 0xf3, 0x5d, 0x43, 0xfd, 0x98, 0xfe, 0x1a, 0x2d, 0x39 }; + + // Act + byte[] signKey = NTLMCryptography.ComputeClientSignKey(exportedSessionKey); - Assert.IsTrue(ByteUtils.AreByteArraysEqual(mic, expected)); + // Assert + Assert.IsTrue(ByteUtils.AreByteArraysEqual(expected, signKey)); } - public void TestAll() + [TestMethod] + public void Test_ComputeClientSealKey() { - TestLMMIC(); - TestNTLMv1MIC(); - TestNTLMv1ExtendedSessionSecurityKeyExchangeMIC(); - TestNTLMv2KeyExchangeMIC(); + // Arrange + byte[] exportedSessionKey = new byte[] { 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55 }; + byte[] expected = new byte[] { 0x59, 0xf6, 0x00, 0x97, 0x3c, 0xc4, 0x96, 0x0a, 0x25, 0x48, 0x0a, 0x7c, 0x19, 0x6e, 0x4c, 0x58 }; + + // Act + byte[] sealKey = NTLMCryptography.ComputeClientSealKey(exportedSessionKey); + + // Assert + Assert.IsTrue(ByteUtils.AreByteArraysEqual(expected, sealKey)); + } + + [TestMethod] + public void Test_ComputeMechListMIC() + { + // Arrange + byte[] exportedSessionKey = new byte[] { 0xBE, 0xC7, 0x33, 0xF6, 0x23, 0xB1, 0x2B, 0x98, 0xD4, 0x21, 0xFF, 0xCD, 0xD5, 0x42, 0xE3, 0xA2 }; + byte[] mechListMicBytes = new byte[] { 0x30, 0x0c, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a }; + byte[] expectedMic = new byte[] { 0x01, 0x00, 0x00, 0x00, 0xa1, 0xa6, 0x7b, 0x80, 0x35, 0xc1, 0x76, 0xd3, 0x00, 0x00, 0x00, 0x00 }; + + // Act + byte[] mic = NTLMCryptography.ComputeMechListMIC(exportedSessionKey, mechListMicBytes); + + // Assert + Assert.IsTrue(ByteUtils.AreByteArraysEqual(mic, expectedMic)); } private static byte[] GetExportedSessionKey(byte[] sessionBaseKey, AuthenticateMessage message, byte[] serverChallenge, byte[] lmowf) diff --git a/SMBLibrary.Tests/NTLM/RC4Tests.cs b/SMBLibrary.Tests/NTLM/RC4Tests.cs index 4fa43b9d..ddb52514 100644 --- a/SMBLibrary.Tests/NTLM/RC4Tests.cs +++ b/SMBLibrary.Tests/NTLM/RC4Tests.cs @@ -126,11 +126,19 @@ public void Test3() Assert.IsTrue(ByteUtils.AreByteArraysEqual(cipher, expectedCipher)); } - public void TestAll() + [TestMethod] + public void TestStreaming() { - Test1(); - Test2(); - Test3(); + byte[] key = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF }; + byte[] text = new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + byte[] expectedCipher = new byte[] { 0x74, 0x94, 0xC2, 0xE7, 0x10, 0x4B, 0x08, 0x79 }; + + RC4KeyState keyState = RC4.InitializeStateFromKey(key); + byte[] cipherPart1 = RC4.Encrypt(keyState, ByteReader.ReadBytes(text, 0, 2)); + byte[] cipherPart2 = RC4.Encrypt(keyState, ByteReader.ReadBytes(text, 2, text.Length - 2)); + + byte[] cipher = ByteUtils.Concatenate(cipherPart1, cipherPart2); + Assert.IsTrue(ByteUtils.AreByteArraysEqual(cipher, expectedCipher)); } } } diff --git a/SMBLibrary.Tests/NetBiosTests.cs b/SMBLibrary.Tests/NetBiosTests.cs index d3f8fc4b..33dcd29d 100644 --- a/SMBLibrary.Tests/NetBiosTests.cs +++ b/SMBLibrary.Tests/NetBiosTests.cs @@ -5,7 +5,6 @@ * either version 3 of the License, or (at your option) any later version. */ using System; -using System.Collections.Generic; using Microsoft.VisualStudio.TestTools.UnitTesting; using SMBLibrary.NetBios; using Utilities; @@ -34,11 +33,5 @@ public void Test2() byte[] encodedName = NetBiosUtils.EncodeName(name, String.Empty); Assert.IsTrue(ByteUtils.AreByteArraysEqual(buffer, encodedName)); } - - public void TestAll() - { - Test1(); - Test2(); - } } } diff --git a/SMBLibrary.Tests/Program.cs b/SMBLibrary.Tests/Program.cs deleted file mode 100644 index db4b0bc3..00000000 --- a/SMBLibrary.Tests/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace SMBLibrary.Tests -{ - class Program - { - static void Main(string[] args) - { - new NTLMAuthenticationTests().TestAll(); - new NTLMSigningTests().TestAll(); - new AesCcmTests().TestAll(); - new SMB2EncryptionTests().TestAll(); - new RC4Tests().TestAll(); - - new NetBiosTests().TestAll(); - new RPCTests().TestAll(); - new SMB2SigningTests().TestAll(); - - new NTDirectoryFileSystemTests().TestAll(); - } - } -} diff --git a/SMBLibrary.Tests/Properties/AssemblyInfo.cs b/SMBLibrary.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index abd6321f..00000000 --- a/SMBLibrary.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SMBLibrary.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Tal Aloni")] -[assembly: AssemblyProduct("SMBLibrary.Tests")] -[assembly: AssemblyCopyright("Copyright © Tal Aloni 2014-2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8ab9297a-1e84-4d7e-a080-cf2fbaf9af1d")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SMBLibrary.Tests/RPCTests.cs b/SMBLibrary.Tests/RPCTests.cs index bcda8757..53435fe9 100644 --- a/SMBLibrary.Tests/RPCTests.cs +++ b/SMBLibrary.Tests/RPCTests.cs @@ -4,12 +4,8 @@ * 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 Microsoft.VisualStudio.TestTools.UnitTesting; -using SMBLibrary.RPC; using SMBLibrary.Services; -using Utilities; namespace SMBLibrary.Tests { @@ -17,7 +13,7 @@ namespace SMBLibrary.Tests public class RPCTests { [TestMethod] - public void Test1() + public void Decode_NetrWkstaGetInfoResponse() { byte[] buffer = new byte[]{ 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x08, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, @@ -27,22 +23,24 @@ public void Test1() 0x47, 0x00, 0x52, 0x00, 0x4f, 0x00, 0x55, 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; NetrWkstaGetInfoResponse response = new NetrWkstaGetInfoResponse(buffer); - byte[] responseBytes = response.GetBytes(); - //Assert.IsTrue(ByteUtils.AreByteArraysEqual(buffer, responseBytes)); + Assert.AreEqual((uint)100, response.WkstaInfo.Level); + Assert.IsInstanceOfType(response.WkstaInfo.Info, typeof(WorkstationInfo100)); + Assert.AreEqual("TAL2-VM7", ((WorkstationInfo100)response.WkstaInfo.Info).ComputerName.Value); } [TestMethod] - public void Test2() + public void Decode_NetrServerGetInfoResponse() { byte[] buffer = new byte[] { 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0xf4, 0x01, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x05, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x03, 0x90, 0x84, 0x00, 0x08, 0x00, 0x02, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x31, 0x00, 0x39, 0x00, 0x32, 0x00, 0x2e, 0x00, 0x31, 0x00, 0x36, 0x00, 0x38, 0x00, 0x2e, 0x00, 0x31, 0x00, 0x2e, 0x00, 0x35, 0x00, 0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00 }; NetrServerGetInfoResponse response = new NetrServerGetInfoResponse(buffer); - byte[] responseBytes = response.GetBytes(); - //Assert.IsTrue(ByteUtils.AreByteArraysEqual(buffer, responseBytes)); + Assert.AreEqual((uint)101, response.InfoStruct.Level); + Assert.IsInstanceOfType(response.InfoStruct.Info, typeof(ServerInfo101)); + Assert.AreEqual("192.168.1.57", ((ServerInfo101)response.InfoStruct.Info).ServerName.Value); } [TestMethod] - public void Test3() + public void Decode_NetrShareEnumRequest() { byte[] buffer = new byte[] {0x00, 0x00, 0x02, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x5c, 0x00, 0x31, 0x00, 0x39, 0x00, 0x32, 0x00, 0x2e, 0x00, 0x31, 0x00, 0x36, 0x00, @@ -51,12 +49,14 @@ public void Test3() 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00}; NetrShareEnumRequest request = new NetrShareEnumRequest(buffer); - byte[] requestBytes = request.GetBytes(); - //Assert.IsTrue(ByteUtils.AreByteArraysEqual(buffer, requestBytes)); + Assert.AreEqual(@"\\192.168.1.57", request.ServerName); + Assert.AreEqual((uint)1, request.InfoStruct.Level); + Assert.IsInstanceOfType(request.InfoStruct.Info, typeof(ShareInfo1Container)); + Assert.AreEqual(0, ((ShareInfo1Container)request.InfoStruct.Info).Count); } [TestMethod] - public void Test4() + public void Decode_NetrShareEnumResponse() { byte[] buffer = new byte[] {0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x08, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x80, @@ -80,12 +80,14 @@ public void Test4() 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; NetrShareEnumResponse response = new NetrShareEnumResponse(buffer); - byte[] responseBytes = response.GetBytes(); - //Assert.IsTrue(ByteUtils.AreByteArraysEqual(buffer, responseBytes)); + Assert.AreEqual((uint)4, response.TotalEntries); + Assert.AreEqual((uint)1, response.InfoStruct.Level); + Assert.IsInstanceOfType(response.InfoStruct.Info, typeof(ShareInfo1Container)); + Assert.AreEqual(4, ((ShareInfo1Container)response.InfoStruct.Info).Count); } [TestMethod] - public void Test5() + public void Decode_NetrShareGetInfoRequest() { byte[] buffer = new byte[] {0x00, 0x00, 0x02, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00, 0x5c, 0x00, 0x5c, 0x00, 0x31, 0x00, 0x39, 0x00, 0x32, 0x00, 0x2e, 0x00, 0x31, 0x00, 0x36, 0x00, @@ -94,17 +96,9 @@ public void Test5() 0x61, 0x00, 0x72, 0x00, 0x65, 0x00, 0x64, 0x00, 0x00, 0x00, 0xb7, 0x6c, 0x02, 0x00, 0x00, 0x00}; NetrShareGetInfoRequest request = new NetrShareGetInfoRequest(buffer); - byte[] requestBytes = request.GetBytes(); - //Assert.IsTrue(ByteUtils.AreByteArraysEqual(buffer, requestBytes)); - } - - public void TestAll() - { - Test1(); - Test2(); - Test3(); - Test4(); - Test5(); + Assert.AreEqual(@"\\192.168.1.52", request.ServerName); + Assert.AreEqual((uint)2, request.Level); + Assert.AreEqual("Shared", request.NetName); } } } diff --git a/SMBLibrary.Tests/SMB1/NegotiateResponseParsingTests.cs b/SMBLibrary.Tests/SMB1/NegotiateResponseParsingTests.cs new file mode 100644 index 00000000..5b13d1c4 --- /dev/null +++ b/SMBLibrary.Tests/SMB1/NegotiateResponseParsingTests.cs @@ -0,0 +1,29 @@ +/* Copyright (C) 2026 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.SMB1; + +namespace SMBLibrary.Tests.SMB1 +{ + [TestClass] + public class NegotiateResponseParsingTests + { + [TestMethod] + public void ParseSMB1MessageWithNegotiateResponseFromWindowsNT4() + { + // Arrange + byte[] responseBytes = new byte[] { 0xFF, 0x53, 0x4D, 0x42, 0x72, 0x00, 0x00, 0x00, 0x00, 0x80, 0x41, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x07, 0x32, 0x00, 0x01, 0x00, 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD, 0x43, 0x00, 0x00, 0xC0, 0xDB, 0x1E, 0x78, 0x2B, 0x2B, 0xDD, 0x01, 0x88, 0xFF, 0x08, 0x14, 0x00, 0x9D, 0xBE, 0xDA, 0x52, 0x8D, 0xEB, 0xEA, 0xDE, 0x4F, 0x00, 0x50, 0x00, 0x43, 0x00, 0x4F, 0x00, 0x4E, 0x00, 0x00, 0x00 }; + + // Act + SMB1Message smb1Message = SMB1Message.GetSMB1Message(responseBytes); + + // Assert + Assert.AreEqual(1, smb1Message.Commands.Count); + Assert.IsInstanceOfType(smb1Message.Commands[0], typeof(NegotiateResponse)); + } + } +} diff --git a/SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs b/SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs new file mode 100644 index 00000000..a2602235 --- /dev/null +++ b/SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs @@ -0,0 +1,79 @@ +/* Copyright (C) 2024 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.SMB2; +using System; + +namespace SMBLibrary.Tests.SMB2 +{ + [TestClass] + public class NegotiateRequestParsingTests + { + [TestMethod] + public void ParseNegotiateRequestWithNegotiateContextList_WhenOffsetIsZero() + { + byte[] negotiateRequestCommandBytes = GetNegotiateRequestWithNegotiateContextListBytes(); + NegotiateRequest negotiateRequest = new NegotiateRequest(negotiateRequestCommandBytes, 0); + Assert.AreEqual(5, negotiateRequest.Dialects.Count); + Assert.AreEqual(4, negotiateRequest.NegotiateContextList.Count); + Assert.AreEqual(NegotiateContextType.SMB2_PREAUTH_INTEGRITY_CAPABILITIES, negotiateRequest.NegotiateContextList[0].ContextType); + Assert.AreEqual(NegotiateContextType.SMB2_ENCRYPTION_CAPABILITIES, negotiateRequest.NegotiateContextList[1].ContextType); + } + + [TestMethod] + public void ParseNegotiateRequestWithNegotiateContextList_WhenOffsetIsNonZero() + { + // Test non-zero offset + byte[] negotiateRequestCommandBytes = GetNegotiateRequestWithNegotiateContextListBytes(); + byte[] buffer = new byte[negotiateRequestCommandBytes.Length + 2]; + Array.Copy(negotiateRequestCommandBytes, 0, buffer, 2, negotiateRequestCommandBytes.Length); + + NegotiateRequest negotiateRequest = new NegotiateRequest(buffer, 2); + Assert.AreEqual(5, negotiateRequest.Dialects.Count); + Assert.AreEqual(4, negotiateRequest.NegotiateContextList.Count); + Assert.AreEqual(NegotiateContextType.SMB2_PREAUTH_INTEGRITY_CAPABILITIES, negotiateRequest.NegotiateContextList[0].ContextType); + Assert.AreEqual(NegotiateContextType.SMB2_ENCRYPTION_CAPABILITIES, negotiateRequest.NegotiateContextList[1].ContextType); + } + + [TestMethod] + public void ParseRewrittenNegotiateRequestWithNegotiateContextList() + { + byte[] negotiateRequestCommandBytes = GetNegotiateRequestWithNegotiateContextListBytes(); + NegotiateRequest negotiateRequest = new NegotiateRequest(negotiateRequestCommandBytes, 0); + negotiateRequestCommandBytes = negotiateRequest.GetBytes(); + negotiateRequest = new NegotiateRequest(negotiateRequestCommandBytes, 0); + Assert.AreEqual(5, negotiateRequest.Dialects.Count); + Assert.AreEqual(4, negotiateRequest.NegotiateContextList.Count); + Assert.AreEqual(NegotiateContextType.SMB2_PREAUTH_INTEGRITY_CAPABILITIES, negotiateRequest.NegotiateContextList[0].ContextType); + Assert.AreEqual(NegotiateContextType.SMB2_ENCRYPTION_CAPABILITIES, negotiateRequest.NegotiateContextList[1].ContextType); + } + + private static byte[] GetNegotiateRequestWithNegotiateContextListBytes() + { + return new byte[] + { + 0xfe,0x53,0x4d,0x42,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0xff,0xfe,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, + 0x24,0x00,0x05,0x00,0x01,0x00,0x00,0x00,0x7f,0x00,0x00,0x00,0xd2,0xb6,0x2a,0x44, + 0x99,0x4d,0xef,0x11,0xb8,0x9d,0x00,0x22,0x48,0x39,0x02,0x34,0x70,0x00,0x00,0x00, + 0x04,0x00,0x00,0x00,0x02,0x02,0x10,0x02,0x00,0x03,0x02,0x03,0x11,0x03,0x00,0x00, + 0x01,0x00,0x26,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x20,0x00,0x01,0x00,0x6c,0xec, + 0x6e,0x4d,0x8e,0x58,0x93,0xd9,0xb3,0x47,0x24,0x09,0x12,0x7a,0xc8,0x4f,0x9b,0xf6, + 0x1d,0xaa,0xbc,0xab,0x22,0xf5,0xec,0xf6,0x3d,0xb5,0x3e,0xc3,0x76,0x85,0x00,0x00, + 0x02,0x00,0x06,0x00,0x00,0x00,0x00,0x00,0x02,0x00,0x02,0x00,0x01,0x00,0x00,0x00, + 0x03,0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x04,0x00,0x00,0x00,0x01,0x00,0x00,0x00, + 0x04,0x00,0x02,0x00,0x03,0x00,0x01,0x00,0x05,0x00,0x34,0x00,0x00,0x00,0x00,0x00, + 0x6d,0x00,0x69,0x00,0x72,0x00,0x61,0x00,0x67,0x00,0x65,0x00,0x2d,0x00,0x73,0x00, + 0x65,0x00,0x72,0x00,0x76,0x00,0x65,0x00,0x72,0x00,0x34,0x00,0x2e,0x00,0x75,0x00, + 0x73,0x00,0x65,0x00,0x72,0x00,0x73,0x00,0x2e,0x00,0x6c,0x00,0x6f,0x00,0x63,0x00, + 0x61,0x00,0x6c,0x00 + }; + } + } +} diff --git a/SMBLibrary.Tests/SMB2/NegotiateResponseParsingTests.cs b/SMBLibrary.Tests/SMB2/NegotiateResponseParsingTests.cs new file mode 100644 index 00000000..6a4bf99a --- /dev/null +++ b/SMBLibrary.Tests/SMB2/NegotiateResponseParsingTests.cs @@ -0,0 +1,87 @@ +/* Copyright (C) 2024 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.SMB2; +using System; + +namespace SMBLibrary.Tests.SMB2 +{ + [TestClass] + public class NegotiateResponseParsingTests + { + [TestMethod] + public void ParseNegotiateResponseWithNegotiateContextList_WhenOffsetIsZero() + { + byte[] negotiateResponseCommandBytes = GetNegotiateResponseWithNegotiateContextListBytes(); + NegotiateResponse negotiateResponse = new NegotiateResponse(negotiateResponseCommandBytes, 0); + Assert.AreEqual(SMB2Dialect.SMB311, negotiateResponse.DialectRevision); + Assert.AreEqual(2, negotiateResponse.NegotiateContextList.Count); + } + + [TestMethod] + public void ParseNegotiateResponseWithNegotiateContextList_WhenOffsetIsNonZero() + { + byte[] negotiateResponseCommandBytes = GetNegotiateResponseWithNegotiateContextListBytes(); + byte[] buffer = new byte[negotiateResponseCommandBytes.Length + 2]; + Array.Copy(negotiateResponseCommandBytes, 0, buffer, 2, negotiateResponseCommandBytes.Length); + + NegotiateResponse negotiateResponse = new NegotiateResponse(buffer, 2); + Assert.AreEqual(SMB2Dialect.SMB311, negotiateResponse.DialectRevision); + Assert.AreEqual(2, negotiateResponse.NegotiateContextList.Count); + } + + [TestMethod] + public void ParseRewrittenNegotiateResponseWithNegotiateContextList() + { + byte[] negotiateResponseCommandBytes = GetNegotiateResponseWithNegotiateContextListBytes(); + NegotiateResponse negotiateResponse = new NegotiateResponse(negotiateResponseCommandBytes, 0); + negotiateResponseCommandBytes = negotiateResponse.GetBytes(); + negotiateResponse = new NegotiateResponse(negotiateResponseCommandBytes, 0); + Assert.AreEqual(SMB2Dialect.SMB311, negotiateResponse.DialectRevision); + Assert.AreEqual(2, negotiateResponse.NegotiateContextList.Count); + } + + private static byte[] GetNegotiateResponseWithNegotiateContextListBytes() + { + return new byte[] + { + 0xfe, 0x53, 0x4d, 0x42, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x41, 0x00, 0x01, 0x00, 0x11, 0x03, 0x02, 0x00, 0xdc, 0x13, 0x82, 0xb7, 0x61, 0x5d, 0xd1, 0x49, + 0x93, 0x4c, 0xea, 0x51, 0xa2, 0x06, 0x6f, 0x20, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, + 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x33, 0xee, 0xd9, 0xe6, 0xa4, 0xe2, 0xda, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x40, 0x01, 0xc0, 0x01, 0x00, 0x00, + 0x60, 0x82, 0x01, 0x3c, 0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02, 0xa0, 0x82, 0x01, 0x30, + 0x30, 0x82, 0x01, 0x2c, 0xa0, 0x1a, 0x30, 0x18, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, + 0x37, 0x02, 0x02, 0x1e, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 0x02, 0x0a, + 0xa2, 0x82, 0x01, 0x0c, 0x04, 0x82, 0x01, 0x08, 0x4e, 0x45, 0x47, 0x4f, 0x45, 0x58, 0x54, 0x53, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, + 0x43, 0x41, 0x62, 0x87, 0xb8, 0xbb, 0x52, 0xa8, 0xc2, 0x47, 0x80, 0x3f, 0x56, 0xf8, 0x2d, 0x16, + 0xcb, 0x62, 0x1f, 0x91, 0xe1, 0x46, 0xd3, 0x87, 0x1c, 0xec, 0xde, 0x67, 0x34, 0xf3, 0x8d, 0xb7, + 0xbe, 0xaa, 0x12, 0x08, 0x7f, 0x7e, 0xa0, 0xcc, 0xc6, 0xcf, 0x30, 0x7d, 0x85, 0x1b, 0xea, 0x48, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5c, 0x33, 0x53, 0x0d, 0xea, 0xf9, 0x0d, 0x4d, + 0xb2, 0xec, 0x4a, 0xe3, 0x78, 0x6e, 0xc3, 0x08, 0x4e, 0x45, 0x47, 0x4f, 0x45, 0x58, 0x54, 0x53, + 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00, 0x00, + 0x43, 0x41, 0x62, 0x87, 0xb8, 0xbb, 0x52, 0xa8, 0xc2, 0x47, 0x80, 0x3f, 0x56, 0xf8, 0x2d, 0x16, + 0x5c, 0x33, 0x53, 0x0d, 0xea, 0xf9, 0x0d, 0x4d, 0xb2, 0xec, 0x4a, 0xe3, 0x78, 0x6e, 0xc3, 0x08, + 0x40, 0x00, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x30, 0x56, 0xa0, 0x54, 0x30, 0x52, 0x30, 0x27, + 0x80, 0x25, 0x30, 0x23, 0x31, 0x21, 0x30, 0x1f, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x18, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x20, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x50, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x20, 0x4b, 0x65, 0x79, 0x30, 0x27, 0x80, 0x25, 0x30, 0x23, 0x31, 0x21, 0x30, + 0x1f, 0x06, 0x03, 0x55, 0x04, 0x03, 0x13, 0x18, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x20, 0x53, 0x69, + 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x20, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x20, 0x4b, 0x65, 0x79, + 0x01, 0x00, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x20, 0x00, 0x01, 0x00, 0xa6, 0x4b, + 0x12, 0xc0, 0x02, 0xf7, 0xea, 0x63, 0xd9, 0x5a, 0xf1, 0x62, 0x95, 0x16, 0xf9, 0x73, 0xef, 0xa4, + 0xe5, 0x74, 0x26, 0x7b, 0x55, 0xe9, 0x14, 0x7e, 0x96, 0xea, 0x2e, 0x56, 0x41, 0xfe, 0x00, 0x00, + 0x02, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x02, 0x00 + }; + } + } +} diff --git a/SMBLibrary.Tests/SMB2/QueryInfoResponseParsingTests.cs b/SMBLibrary.Tests/SMB2/QueryInfoResponseParsingTests.cs new file mode 100644 index 00000000..12160a5d --- /dev/null +++ b/SMBLibrary.Tests/SMB2/QueryInfoResponseParsingTests.cs @@ -0,0 +1,45 @@ +/* Copyright (C) 2024 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 Microsoft.VisualStudio.TestTools.UnitTesting; +using SMBLibrary.SMB2; + +namespace SMBLibrary.Tests.SMB2 +{ + [TestClass] + public class QueryInfoResponseParsingTests + { + [TestMethod] + public void ParseQueryInfoResponseWithSecurityInfo() + { + byte[] queryInfoResponseCommandBytes = GetQueryInfoResponseWithSecurityInfo(); + QueryInfoResponse queryInfoResponse = new QueryInfoResponse(queryInfoResponseCommandBytes, 0); + SecurityDescriptor securityDescriptor = queryInfoResponse.GetSecurityInformation(); + Assert.AreEqual(2, securityDescriptor.Dacl.Count); + Assert.IsInstanceOfType(securityDescriptor.Dacl[1], typeof(AccessAllowedACE)); + Assert.AreEqual(1, ((AccessAllowedACE)securityDescriptor.Dacl[1]).Sid.Revision); + } + + + private static byte[] GetQueryInfoResponseWithSecurityInfo() + { + return new byte[] + { + 0xfe, 0x53, 0x4d, 0x42, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x82, 0xe7, 0x2d, 0x2c, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x09, 0x00, 0x48, 0x00, 0x5c, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x80, 0x14, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x20, 0x00, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x02, 0x00, 0x38, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0xff, 0x01, 0x1f, 0x00, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x18, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00 + }; + } + } +} diff --git a/SMBLibrary.Tests/SMB2EncryptionTests.cs b/SMBLibrary.Tests/SMB2EncryptionTests.cs index f5a243ad..92d01fee 100644 --- a/SMBLibrary.Tests/SMB2EncryptionTests.cs +++ b/SMBLibrary.Tests/SMB2EncryptionTests.cs @@ -102,13 +102,5 @@ public void TestDecryption() Assert.IsTrue(ByteUtils.AreByteArraysEqual(expectedDecryptedMessage, decryptedMessage)); } - - public void TestAll() - { - TestEncryptionKeyGeneration(); - TestDecryptionKeyGeneration(); - TestEncryption(); - TestDecryption(); - } } } diff --git a/SMBLibrary.Tests/SMB2SigningTests.cs b/SMBLibrary.Tests/SMB2SigningTests.cs index a61c63fd..6fcd4c87 100644 --- a/SMBLibrary.Tests/SMB2SigningTests.cs +++ b/SMBLibrary.Tests/SMB2SigningTests.cs @@ -1,12 +1,9 @@ -/* Copyright (C) 2017-2019 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2026 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.Security.Cryptography; using Microsoft.VisualStudio.TestTools.UnitTesting; using SMBLibrary.SMB2; using Utilities; @@ -30,7 +27,6 @@ public void TestSMB202SignatureCalculation() ByteWriter.WriteBytes(message, 48, new byte[16]); byte[] signature = SMB2Cryptography.CalculateSignature(exportedSessionKey, SMB2Dialect.SMB202, message, 0, message.Length); - signature = ByteReader.ReadBytes(signature, 0, 16); byte[] expected = new byte[] { 0xfb, 0xd2, 0x84, 0x34, 0x03, 0x24, 0xc6, 0x2f, 0xbe, 0xbb, 0x65, 0xdd, 0x10, 0x51, 0xf3, 0xae }; Assert.IsTrue(ByteUtils.AreByteArraysEqual(signature, expected)); } @@ -49,7 +45,6 @@ public void TestSMB210SignatureCalculation() ByteWriter.WriteBytes(message, 48, new byte[16]); byte[] signature = SMB2Cryptography.CalculateSignature(exportedSessionKey, SMB2Dialect.SMB210, message, 0, message.Length); - signature = ByteReader.ReadBytes(signature, 0, 16); byte[] expected = new byte[] { 0xa1, 0x64, 0xff, 0xe5, 0x3d, 0x68, 0x11, 0x98, 0x1f, 0x38, 0x67, 0x72, 0xe3, 0x87, 0xe0, 0x6f }; Assert.IsTrue(ByteUtils.AreByteArraysEqual(signature, expected)); } @@ -69,16 +64,8 @@ public void TestSMB300SignatureCalculation() ByteWriter.WriteBytes(message, 48, new byte[16]); byte[] signature = SMB2Cryptography.CalculateSignature(signingKey, SMB2Dialect.SMB300, message, 0, message.Length); - signature = ByteReader.ReadBytes(signature, 0, 16); byte[] expected = new byte[] { 0x73, 0xF2, 0xCC, 0x56, 0x09, 0x3E, 0xD2, 0xB5, 0xD7, 0x10, 0x66, 0x6C, 0xE4, 0x28, 0x2D, 0xD1 }; Assert.IsTrue(ByteUtils.AreByteArraysEqual(signature, expected)); } - - public void TestAll() - { - TestSMB202SignatureCalculation(); - TestSMB210SignatureCalculation(); - TestSMB300SignatureCalculation(); - } } } diff --git a/SMBLibrary.Tests/SMBLibrary.Tests.VS2005.csproj b/SMBLibrary.Tests/SMBLibrary.Tests.VS2005.csproj deleted file mode 100644 index 68280aca..00000000 --- a/SMBLibrary.Tests/SMBLibrary.Tests.VS2005.csproj +++ /dev/null @@ -1,73 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {C79B06EB-32C1-44CA-B7E1-A891B8135658} - Exe - Properties - SMBLibrary.Tests - SMBLibrary.Tests - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - False - Components\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll - - - - - - - - - - - - - - - - - - - - {8CE25496-A52B-4841-822F-74C469D10EE7} - SMBLibrary.Win32 - - - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7} - SMBLibrary - - - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0} - Utilities - - - - - \ No newline at end of file diff --git a/SMBLibrary.Tests/SMBLibrary.Tests.csproj b/SMBLibrary.Tests/SMBLibrary.Tests.csproj index 6386f32a..d32c24bf 100644 --- a/SMBLibrary.Tests/SMBLibrary.Tests.csproj +++ b/SMBLibrary.Tests/SMBLibrary.Tests.csproj @@ -1,11 +1,10 @@  - net40 - false + net472;net6.0 SMBLibrary.Tests SMBLibrary.Tests - Exe + Library @@ -18,9 +17,9 @@ - - Components\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll - + + + \ No newline at end of file diff --git a/SMBLibrary.Win32/Properties/AssemblyInfo.cs b/SMBLibrary.Win32/Properties/AssemblyInfo.cs deleted file mode 100644 index 75889325..00000000 --- a/SMBLibrary.Win32/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SMBLibrary.Win32")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Tal Aloni")] -[assembly: AssemblyProduct("SMBLibrary.Win32")] -[assembly: AssemblyCopyright("Copyright © Tal Aloni 2014-2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("a2cd5b5c-fb90-412f-9b0d-63967b9cb2ee")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.4.6.0")] -[assembly: AssemblyFileVersion("1.4.6.0")] diff --git a/SMBLibrary.Win32/SMBLibrary.Win32.VS2005.csproj b/SMBLibrary.Win32/SMBLibrary.Win32.VS2005.csproj deleted file mode 100644 index 3c03cc37..00000000 --- a/SMBLibrary.Win32/SMBLibrary.Win32.VS2005.csproj +++ /dev/null @@ -1,64 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {8CE25496-A52B-4841-822F-74C469D10EE7} - Library - Properties - SMBLibrary.Win32 - SMBLibrary.Win32 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7} - SMBLibrary - - - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0} - Utilities - - - - - \ No newline at end of file diff --git a/SMBLibrary.Win32/SMBLibrary.Win32.csproj b/SMBLibrary.Win32/SMBLibrary.Win32.csproj index 2908afbb..ad494218 100644 --- a/SMBLibrary.Win32/SMBLibrary.Win32.csproj +++ b/SMBLibrary.Win32/SMBLibrary.Win32.csproj @@ -2,12 +2,12 @@ net20;net40;netstandard2.0 - false SMBLibrary.Win32 - 1.4.6 + 1.5.7 1573;1591 SMBLibrary.Win32 Tal Aloni + Copyright © Tal Aloni 2014-2026 Windows specific extensions for SMBLibrary LGPL-3.0-or-later https://github.com/TalAloni/SMBLibrary diff --git a/SMBLibrary.Win32/Security/SSPIHelper.Kerberos.cs b/SMBLibrary.Win32/Security/SSPIHelper.Kerberos.cs new file mode 100644 index 00000000..cc75071e --- /dev/null +++ b/SMBLibrary.Win32/Security/SSPIHelper.Kerberos.cs @@ -0,0 +1,55 @@ +/* Copyright (C) 2026 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.Runtime.InteropServices; + +namespace SMBLibrary.Win32.Security +{ + public partial class SSPIHelper + { + public static SecHandle AcquireKerberosCredentialsHandle(string serverPrincipalName) + { + return AcquireKerberosCredentialsHandle(serverPrincipalName, null); + } + + public static SecHandle AcquireKerberosCredentialsHandle(string serverPrincipalName, string domainName, string userName, string password) + { + SEC_WINNT_AUTH_IDENTITY auth = GetWinNTAuthIdentity(domainName, userName, password); + return AcquireKerberosCredentialsHandle(serverPrincipalName, auth); + } + + private static SecHandle AcquireKerberosCredentialsHandle(string serverPrincipalName, SEC_WINNT_AUTH_IDENTITY? auth) + { + SecHandle credential; + SECURITY_INTEGER expiry; + + IntPtr pAuthData; + if (auth.HasValue) + { + pAuthData = Marshal.AllocHGlobal(Marshal.SizeOf(auth.Value)); + Marshal.StructureToPtr(auth.Value, pAuthData, false); + } + else + { + pAuthData = IntPtr.Zero; + } + + uint result = AcquireCredentialsHandle(serverPrincipalName, "Kerberos", SECPKG_CRED_BOTH, IntPtr.Zero, pAuthData, IntPtr.Zero, IntPtr.Zero, out credential, out expiry); + if (pAuthData != IntPtr.Zero) + { + Marshal.FreeHGlobal(pAuthData); + } + + if (result != SEC_E_OK) + { + throw new Exception("AcquireCredentialsHandle failed, Error code 0x" + result.ToString("X8")); + } + + return credential; + } + } +} diff --git a/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs b/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs new file mode 100644 index 00000000..0fc6de32 --- /dev/null +++ b/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs @@ -0,0 +1,203 @@ +/* Copyright (C) 2014-2026 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.Runtime.InteropServices; + +namespace SMBLibrary.Win32.Security +{ + public partial class SSPIHelper + { + public static SecHandle AcquireNTLMCredentialsHandle() + { + return AcquireNTLMCredentialsHandle(null); + } + + public static SecHandle AcquireNTLMCredentialsHandle(string domainName, string userName, string password) + { + SEC_WINNT_AUTH_IDENTITY auth = GetWinNTAuthIdentity(domainName, userName, password); + return AcquireNTLMCredentialsHandle(auth); + } + + private static SecHandle AcquireNTLMCredentialsHandle(SEC_WINNT_AUTH_IDENTITY? auth) + { + SecHandle credential; + SECURITY_INTEGER expiry; + + IntPtr pAuthData; + if (auth.HasValue) + { + pAuthData = Marshal.AllocHGlobal(Marshal.SizeOf(auth.Value)); + Marshal.StructureToPtr(auth.Value, pAuthData, false); + } + else + { + pAuthData = IntPtr.Zero; + } + + uint result = AcquireCredentialsHandle(null, "NTLM", SECPKG_CRED_BOTH, IntPtr.Zero, pAuthData, IntPtr.Zero, IntPtr.Zero, out credential, out expiry); + if (pAuthData != IntPtr.Zero) + { + Marshal.FreeHGlobal(pAuthData); + } + if (result != SEC_E_OK) + { + throw new Exception("AcquireCredentialsHandle failed, Error code 0x" + result.ToString("X8")); + } + + return credential; + } + + public static byte[] GetType1Message(string userName, string password, out SecHandle clientContext) + { + return GetType1Message(String.Empty, userName, password, out clientContext); + } + + public static byte[] GetType1Message(string domainName, string userName, string password, out SecHandle clientContext) + { + SecHandle credentialsHandle = AcquireNTLMCredentialsHandle(domainName, userName, password); + byte[] messageBytes = GetInitialMessage(credentialsHandle, out clientContext); + FreeCredentialsHandle(ref credentialsHandle); + return messageBytes; + } + + public static byte[] GetType3Message(SecHandle clientContext, byte[] type2Message) + { + SecHandle newContext = new SecHandle(); + SecBuffer inputBuffer = new SecBuffer(type2Message); + SecBufferDesc input = new SecBufferDesc(inputBuffer); + SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); + SecBufferDesc output = new SecBufferDesc(outputBuffer); + uint contextAttributes; + SECURITY_INTEGER expiry; + + uint result = InitializeSecurityContext(IntPtr.Zero, ref clientContext, null, ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY, 0, SECURITY_NATIVE_DREP, ref input, 0, ref newContext, ref output, out contextAttributes, out expiry); + if (result != SEC_E_OK) + { + if (result == SEC_E_INVALID_HANDLE) + { + throw new Exception("InitializeSecurityContext failed, Invalid handle"); + } + else if (result == SEC_E_INVALID_TOKEN) + { + throw new Exception("InitializeSecurityContext failed, Invalid token"); + } + else if (result == SEC_E_BUFFER_TOO_SMALL) + { + throw new Exception("InitializeSecurityContext failed, Buffer too small"); + } + else + { + throw new Exception("InitializeSecurityContext failed, Error code 0x" + result.ToString("X8")); + } + } + byte[] messageBytes = output.GetBufferBytes(0); + inputBuffer.Dispose(); + input.Dispose(); + outputBuffer.Dispose(); + output.Dispose(); + return messageBytes; + } + + public static byte[] GetType2Message(byte[] type1MessageBytes, out SecHandle serverContext) + { + SecHandle credentialsHandle = AcquireNTLMCredentialsHandle(); + SecBuffer inputBuffer = new SecBuffer(type1MessageBytes); + SecBufferDesc input = new SecBufferDesc(inputBuffer); + serverContext = new SecHandle(); + SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); + SecBufferDesc output = new SecBufferDesc(outputBuffer); + uint contextAttributes; + SECURITY_INTEGER timestamp; + + uint result = AcceptSecurityContext(ref credentialsHandle, IntPtr.Zero, ref input, ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY, SECURITY_NATIVE_DREP, ref serverContext, ref output, out contextAttributes, out timestamp); + if (result != SEC_E_OK && result != SEC_I_CONTINUE_NEEDED) + { + if (result == SEC_E_INVALID_HANDLE) + { + throw new Exception("AcceptSecurityContext failed, Invalid handle"); + } + else if (result == SEC_E_INVALID_TOKEN) + { + throw new Exception("AcceptSecurityContext failed, Invalid token"); + } + else if (result == SEC_E_BUFFER_TOO_SMALL) + { + throw new Exception("AcceptSecurityContext failed, Buffer too small"); + } + else + { + throw new Exception("AcceptSecurityContext failed, Error code 0x" + result.ToString("X8")); + } + } + FreeCredentialsHandle(ref credentialsHandle); + byte[] messageBytes = output.GetBufferBytes(0); + inputBuffer.Dispose(); + input.Dispose(); + outputBuffer.Dispose(); + output.Dispose(); + return messageBytes; + } + + /// + /// AcceptSecurityContext will return SEC_E_LOGON_DENIED when the password is correct in these cases: + /// 1. The account is listed under the "Deny access to this computer from the network" list. + /// 2. 'limitblankpassworduse' is set to 1, non-guest is attempting to login with an empty password, + /// and the Guest account is disabled, has non-empty pasword set or listed under the "Deny access to this computer from the network" list. + /// + /// Note: "If the Guest account is enabled, SSPI logon may succeed as Guest for user credentials that are not valid". + /// + /// + /// 1. 'limitblankpassworduse' will not affect the Guest account. + /// 2. Listing the user in the "Deny access to this computer from the network" or the "Deny logon locally" lists will not affect AcceptSecurityContext if all of these conditions are met. + /// - 'limitblankpassworduse' is set to 1. + /// - The user has an empty password set. + /// - Guest is NOT listed in the "Deny access to this computer from the network" list. + /// - Guest is enabled and has empty pasword set. + /// + public static bool AuthenticateType3Message(SecHandle serverContext, byte[] type3MessageBytes) + { + SecHandle newContext = new SecHandle(); + SecBuffer inputBuffer = new SecBuffer(type3MessageBytes); + SecBufferDesc input = new SecBufferDesc(inputBuffer); + SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); + SecBufferDesc output = new SecBufferDesc(outputBuffer); + uint contextAttributes; + SECURITY_INTEGER timestamp; + + uint result = AcceptSecurityContext(IntPtr.Zero, ref serverContext, ref input, ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY, SECURITY_NATIVE_DREP, ref newContext, ref output, out contextAttributes, out timestamp); + + inputBuffer.Dispose(); + input.Dispose(); + outputBuffer.Dispose(); + output.Dispose(); + + if (result == SEC_E_OK) + { + return true; + } + else if ((uint)result == SEC_E_LOGON_DENIED) + { + return false; + } + else + { + if (result == SEC_E_INVALID_HANDLE) + { + throw new Exception("AcceptSecurityContext failed, Invalid handle"); + } + else if (result == SEC_E_INVALID_TOKEN) + { + throw new Exception("AcceptSecurityContext failed, Invalid security token"); + } + else + { + throw new Exception("AcceptSecurityContext failed, Error code 0x" + result.ToString("X8")); + } + } + } + } +} diff --git a/SMBLibrary.Win32/Security/SSPIHelper.cs b/SMBLibrary.Win32/Security/SSPIHelper.cs index 87aa1b04..326de570 100644 --- a/SMBLibrary.Win32/Security/SSPIHelper.cs +++ b/SMBLibrary.Win32/Security/SSPIHelper.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2026 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.Runtime.InteropServices; namespace SMBLibrary.Win32.Security @@ -17,7 +16,7 @@ public struct SecHandle public IntPtr dwUpper; }; - public class SSPIHelper + public partial class SSPIHelper { private const int MAX_TOKEN_SIZE = 12000; @@ -50,7 +49,7 @@ public class SSPIHelper private const uint SECPKG_ATTR_NAME = 1; // Username private const uint SECPKG_ATTR_SESSION_KEY = 9; private const uint SECPKG_ATTR_ACCESS_TOKEN = 18; - + [StructLayout(LayoutKind.Sequential)] private struct SECURITY_INTEGER { @@ -179,68 +178,20 @@ public extern static uint DeleteSecurityContext( ref SecHandle phContext ); - public static SecHandle AcquireNTLMCredentialsHandle() - { - return AcquireNTLMCredentialsHandle(null); - } - - public static SecHandle AcquireNTLMCredentialsHandle(string domainName, string userName, string password) - { - SEC_WINNT_AUTH_IDENTITY auth = new SEC_WINNT_AUTH_IDENTITY(); - auth.Domain = domainName; - auth.DomainLength = (uint)domainName.Length; - auth.User = userName; - auth.UserLength = (uint)userName.Length; - auth.Password = password; - auth.PasswordLength = (uint)password.Length; - auth.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; - return AcquireNTLMCredentialsHandle(auth); - } - - private static SecHandle AcquireNTLMCredentialsHandle(SEC_WINNT_AUTH_IDENTITY? auth) - { - SecHandle credential; - SECURITY_INTEGER expiry; - - IntPtr pAuthData; - if (auth.HasValue) - { - pAuthData = Marshal.AllocHGlobal(Marshal.SizeOf(auth.Value)); - Marshal.StructureToPtr(auth.Value, pAuthData, false); - } - else - { - pAuthData = IntPtr.Zero; - } - - uint result = AcquireCredentialsHandle(null, "NTLM", SECPKG_CRED_BOTH, IntPtr.Zero, pAuthData, IntPtr.Zero, IntPtr.Zero, out credential, out expiry); - if (pAuthData != IntPtr.Zero) - { - Marshal.FreeHGlobal(pAuthData); - } - if (result != SEC_E_OK) - { - throw new Exception("AcquireCredentialsHandle failed, Error code 0x" + result.ToString("X8")); - } - - return credential; - } - - public static byte[] GetType1Message(string userName, string password, out SecHandle clientContext) + public static byte[] GetInitialMessage(SecHandle credentialsHandle, out SecHandle clientContext) { - return GetType1Message(String.Empty, userName, password, out clientContext); + return GetInitialMessage(credentialsHandle, null, out clientContext); } - public static byte[] GetType1Message(string domainName, string userName, string password, out SecHandle clientContext) + public static byte[] GetInitialMessage(SecHandle credentialsHandle, string targetName, out SecHandle clientContext) { - SecHandle credentialsHandle = AcquireNTLMCredentialsHandle(domainName, userName, password); clientContext = new SecHandle(); SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); SecBufferDesc output = new SecBufferDesc(outputBuffer); uint contextAttributes; SECURITY_INTEGER expiry; - uint result = InitializeSecurityContext(ref credentialsHandle, IntPtr.Zero, null, ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY, 0, SECURITY_NATIVE_DREP, IntPtr.Zero, 0, ref clientContext, ref output, out contextAttributes, out expiry); + uint result = InitializeSecurityContext(ref credentialsHandle, IntPtr.Zero, targetName, ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY, 0, SECURITY_NATIVE_DREP, IntPtr.Zero, 0, ref clientContext, ref output, out contextAttributes, out expiry); if (result != SEC_E_OK && result != SEC_I_CONTINUE_NEEDED) { if (result == SEC_E_INVALID_HANDLE) @@ -256,149 +207,12 @@ public static byte[] GetType1Message(string domainName, string userName, string throw new Exception("InitializeSecurityContext failed, Error code 0x" + result.ToString("X8")); } } - FreeCredentialsHandle(ref credentialsHandle); - byte[] messageBytes = output.GetBufferBytes(0); - outputBuffer.Dispose(); - output.Dispose(); - return messageBytes; - } - - public static byte[] GetType3Message(SecHandle clientContext, byte[] type2Message) - { - SecHandle newContext = new SecHandle(); - SecBuffer inputBuffer = new SecBuffer(type2Message); - SecBufferDesc input = new SecBufferDesc(inputBuffer); - SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); - SecBufferDesc output = new SecBufferDesc(outputBuffer); - uint contextAttributes; - SECURITY_INTEGER expiry; - - uint result = InitializeSecurityContext(IntPtr.Zero, ref clientContext, null, ISC_REQ_CONFIDENTIALITY | ISC_REQ_INTEGRITY, 0, SECURITY_NATIVE_DREP, ref input, 0, ref newContext, ref output, out contextAttributes, out expiry); - if (result != SEC_E_OK) - { - if (result == SEC_E_INVALID_HANDLE) - { - throw new Exception("InitializeSecurityContext failed, Invalid handle"); - } - else if (result == SEC_E_INVALID_TOKEN) - { - throw new Exception("InitializeSecurityContext failed, Invalid token"); - } - else if (result == SEC_E_BUFFER_TOO_SMALL) - { - throw new Exception("InitializeSecurityContext failed, Buffer too small"); - } - else - { - throw new Exception("InitializeSecurityContext failed, Error code 0x" + result.ToString("X8")); - } - } - byte[] messageBytes = output.GetBufferBytes(0); - inputBuffer.Dispose(); - input.Dispose(); - outputBuffer.Dispose(); - output.Dispose(); - return messageBytes; - } - - public static byte[] GetType2Message(byte[] type1MessageBytes, out SecHandle serverContext) - { - SecHandle credentialsHandle = AcquireNTLMCredentialsHandle(); - SecBuffer inputBuffer = new SecBuffer(type1MessageBytes); - SecBufferDesc input = new SecBufferDesc(inputBuffer); - serverContext = new SecHandle(); - SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); - SecBufferDesc output = new SecBufferDesc(outputBuffer); - uint contextAttributes; - SECURITY_INTEGER timestamp; - - uint result = AcceptSecurityContext(ref credentialsHandle, IntPtr.Zero, ref input, ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY, SECURITY_NATIVE_DREP, ref serverContext, ref output, out contextAttributes, out timestamp); - if (result != SEC_E_OK && result != SEC_I_CONTINUE_NEEDED) - { - if (result == SEC_E_INVALID_HANDLE) - { - throw new Exception("AcceptSecurityContext failed, Invalid handle"); - } - else if (result == SEC_E_INVALID_TOKEN) - { - throw new Exception("AcceptSecurityContext failed, Invalid token"); - } - else if (result == SEC_E_BUFFER_TOO_SMALL) - { - throw new Exception("AcceptSecurityContext failed, Buffer too small"); - } - else - { - throw new Exception("AcceptSecurityContext failed, Error code 0x" + result.ToString("X8")); - } - } - FreeCredentialsHandle(ref credentialsHandle); byte[] messageBytes = output.GetBufferBytes(0); - inputBuffer.Dispose(); - input.Dispose(); outputBuffer.Dispose(); output.Dispose(); return messageBytes; } - /// - /// AcceptSecurityContext will return SEC_E_LOGON_DENIED when the password is correct in these cases: - /// 1. The account is listed under the "Deny access to this computer from the network" list. - /// 2. 'limitblankpassworduse' is set to 1, non-guest is attempting to login with an empty password, - /// and the Guest account is disabled, has non-empty pasword set or listed under the "Deny access to this computer from the network" list. - /// - /// Note: "If the Guest account is enabled, SSPI logon may succeed as Guest for user credentials that are not valid". - /// - /// - /// 1. 'limitblankpassworduse' will not affect the Guest account. - /// 2. Listing the user in the "Deny access to this computer from the network" or the "Deny logon locally" lists will not affect AcceptSecurityContext if all of these conditions are met. - /// - 'limitblankpassworduse' is set to 1. - /// - The user has an empty password set. - /// - Guest is NOT listed in the "Deny access to this computer from the network" list. - /// - Guest is enabled and has empty pasword set. - /// - public static bool AuthenticateType3Message(SecHandle serverContext, byte[] type3MessageBytes) - { - SecHandle newContext = new SecHandle(); - SecBuffer inputBuffer = new SecBuffer(type3MessageBytes); - SecBufferDesc input = new SecBufferDesc(inputBuffer); - SecBuffer outputBuffer = new SecBuffer(MAX_TOKEN_SIZE); - SecBufferDesc output = new SecBufferDesc(outputBuffer); - uint contextAttributes; - SECURITY_INTEGER timestamp; - - uint result = AcceptSecurityContext(IntPtr.Zero, ref serverContext, ref input, ASC_REQ_INTEGRITY | ASC_REQ_CONFIDENTIALITY, SECURITY_NATIVE_DREP, ref newContext, ref output, out contextAttributes, out timestamp); - - inputBuffer.Dispose(); - input.Dispose(); - outputBuffer.Dispose(); - output.Dispose(); - - if (result == SEC_E_OK) - { - return true; - } - else if ((uint)result == SEC_E_LOGON_DENIED) - { - return false; - } - else - { - if (result == SEC_E_INVALID_HANDLE) - { - throw new Exception("AcceptSecurityContext failed, Invalid handle"); - } - else if (result == SEC_E_INVALID_TOKEN) - { - throw new Exception("AcceptSecurityContext failed, Invalid security token"); - } - else - { - throw new Exception("AcceptSecurityContext failed, Error code 0x" + result.ToString("X8")); - } - } - } - public static string GetUserName(SecHandle context) { string userName; @@ -447,5 +261,18 @@ public static IntPtr GetAccessToken(SecHandle serverContext) return IntPtr.Zero; } } + + private static SEC_WINNT_AUTH_IDENTITY GetWinNTAuthIdentity(string domainName, string userName, string password) + { + SEC_WINNT_AUTH_IDENTITY auth = new SEC_WINNT_AUTH_IDENTITY(); + auth.Domain = domainName; + auth.DomainLength = (uint)domainName.Length; + auth.User = userName; + auth.UserLength = (uint)userName.Length; + auth.Password = password; + auth.PasswordLength = (uint)password.Length; + auth.Flags = SEC_WINNT_AUTH_IDENTITY_ANSI; + return auth; + } } } diff --git a/SMBLibrary/Authentication/GSSAPI/SPNEGO/SimpleProtectedNegotiationTokenInit.cs b/SMBLibrary/Authentication/GSSAPI/SPNEGO/SimpleProtectedNegotiationTokenInit.cs index f4e5dfe2..a553ad60 100644 --- a/SMBLibrary/Authentication/GSSAPI/SPNEGO/SimpleProtectedNegotiationTokenInit.cs +++ b/SMBLibrary/Authentication/GSSAPI/SPNEGO/SimpleProtectedNegotiationTokenInit.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -102,16 +102,7 @@ public override byte[] GetBytes() protected virtual int GetTokenFieldsLength() { - int result = 0; - if (MechanismTypeList != null) - { - int typeListSequenceLength = GetMechanismTypeListSequenceLength(MechanismTypeList); - int typeListSequenceLengthFieldSize = DerEncodingHelper.GetLengthFieldSize(typeListSequenceLength); - int typeListConstructionLength = 1 + typeListSequenceLengthFieldSize + typeListSequenceLength; - int typeListConstructionLengthFieldSize = DerEncodingHelper.GetLengthFieldSize(typeListConstructionLength); - int entryLength = 1 + typeListConstructionLengthFieldSize + 1 + typeListSequenceLengthFieldSize + typeListSequenceLength; - result += entryLength; - } + int result = GetEncodedMechanismTypeListLength(MechanismTypeList); if (MechanismToken != null) { int mechanismTokenLengthFieldSize = DerEncodingHelper.GetLengthFieldSize(MechanismToken.Length); @@ -200,6 +191,11 @@ protected static void WriteMechanismTypeList(byte[] buffer, ref int offset, List int constructionLength = 1 + sequenceLengthFieldSize + sequenceLength; ByteWriter.WriteByte(buffer, ref offset, MechanismTypeListTag); DerEncodingHelper.WriteLength(buffer, ref offset, constructionLength); + WriteMechanismTypeListSequence(buffer, ref offset, mechanismTypeList, sequenceLength); + } + + protected static void WriteMechanismTypeListSequence(byte[] buffer, ref int offset, List mechanismTypeList, int sequenceLength) + { ByteWriter.WriteByte(buffer, ref offset, (byte)DerEncodingTag.Sequence); DerEncodingHelper.WriteLength(buffer, ref offset, sequenceLength); foreach (byte[] mechanismType in mechanismTypeList) @@ -229,5 +225,32 @@ protected static void WriteMechanismListMIC(byte[] buffer, ref int offset, byte[ DerEncodingHelper.WriteLength(buffer, ref offset, mechanismListMIC.Length); ByteWriter.WriteBytes(buffer, ref offset, mechanismListMIC); } + + public static byte[] GetMechanismTypeListBytes(List mechanismTypeList) + { + int sequenceLength = GetMechanismTypeListSequenceLength(mechanismTypeList); + int sequenceLengthFieldSize = DerEncodingHelper.GetLengthFieldSize(sequenceLength); + int constructionLength = 1 + sequenceLengthFieldSize + sequenceLength; + byte[] buffer = new byte[constructionLength]; + int offset = 0; + WriteMechanismTypeListSequence(buffer, ref offset, mechanismTypeList, sequenceLength); + return buffer; + } + + private static int GetEncodedMechanismTypeListLength(List mechanismTypeList) + { + if (mechanismTypeList == null) + { + return 0; + } + else + { + int typeListSequenceLength = GetMechanismTypeListSequenceLength(mechanismTypeList); + int typeListSequenceLengthFieldSize = DerEncodingHelper.GetLengthFieldSize(typeListSequenceLength); + int typeListConstructionLength = 1 + typeListSequenceLengthFieldSize + typeListSequenceLength; + int typeListConstructionLengthFieldSize = DerEncodingHelper.GetLengthFieldSize(typeListConstructionLength); + return 1 + typeListConstructionLengthFieldSize + 1 + typeListSequenceLengthFieldSize + typeListSequenceLength; + } + } } } diff --git a/SMBLibrary/Authentication/NTLM/Helpers/NTLMCryptography.cs b/SMBLibrary/Authentication/NTLM/Helpers/NTLMCryptography.cs index 5dcdc2e1..1e1deea7 100644 --- a/SMBLibrary/Authentication/NTLM/Helpers/NTLMCryptography.cs +++ b/SMBLibrary/Authentication/NTLM/Helpers/NTLMCryptography.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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.Globalization; using System.Reflection; using System.Security.Cryptography; @@ -16,6 +15,7 @@ namespace SMBLibrary.Authentication.NTLM { public class NTLMCryptography { +#if ENABLE_NTLMV1 public static byte[] ComputeLMv1Response(byte[] challenge, string password) { byte[] hash = LMOWFv1(password); @@ -37,30 +37,6 @@ public static byte[] ComputeNTLMv1ExtendedSessionSecurityResponse(byte[] serverC return DesLongEncrypt(passwordHash, challengeHashShort); } - public static byte[] ComputeLMv2Response(byte[] serverChallenge, byte[] clientChallenge, string password, string user, string domain) - { - byte[] key = LMOWFv2(password, user, domain); - byte[] bytes = ByteUtils.Concatenate(serverChallenge, clientChallenge); - HMACMD5 hmac = new HMACMD5(key); - byte[] hash = hmac.ComputeHash(bytes, 0, bytes.Length); - - return ByteUtils.Concatenate(hash, clientChallenge); - } - - /// - /// [MS-NLMP] https://msdn.microsoft.com/en-us/library/cc236700.aspx - /// - /// ClientChallengeStructure with 4 zero bytes padding, a.k.a. temp - public static byte[] ComputeNTLMv2Proof(byte[] serverChallenge, byte[] clientChallengeStructurePadded, string password, string user, string domain) - { - byte[] key = NTOWFv2(password, user, domain); - byte[] temp = clientChallengeStructurePadded; - - HMACMD5 hmac = new HMACMD5(key); - byte[] _NTProof = hmac.ComputeHash(ByteUtils.Concatenate(serverChallenge, temp), 0, serverChallenge.Length + temp.Length); - return _NTProof; - } - public static byte[] DesEncrypt(byte[] key, byte[] plainText) { return DesEncrypt(key, plainText, 0, plainText.Length); @@ -78,11 +54,26 @@ public static ICryptoTransform CreateWeakDesEncryptor(CipherMode mode, byte[] rg { DES des = DES.Create(); des.Mode = mode; - DESCryptoServiceProvider sm = des as DESCryptoServiceProvider; - MethodInfo mi = sm.GetType().GetMethod("_NewEncryptor", BindingFlags.NonPublic | BindingFlags.Instance); - object[] Par = { rgbKey, mode, rgbIV, sm.FeedbackSize, 0 }; - ICryptoTransform trans = mi.Invoke(sm, Par) as ICryptoTransform; - return trans; + ICryptoTransform transform; + if (DES.IsWeakKey(rgbKey) || DES.IsSemiWeakKey(rgbKey)) + { +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER + MethodInfo getTransformCoreMethodInfo = des.GetType().GetMethod("CreateTransformCore", BindingFlags.NonPublic | BindingFlags.Static); + object[] getTransformCoreParameters = { mode, des.Padding, rgbKey, rgbIV, des.BlockSize / 8 , des.FeedbackSize / 8, des.BlockSize / 8, true }; + transform = getTransformCoreMethodInfo.Invoke(null, getTransformCoreParameters) as ICryptoTransform; +#else + DESCryptoServiceProvider desServiceProvider = des as DESCryptoServiceProvider; + MethodInfo newEncryptorMethodInfo = desServiceProvider.GetType().GetMethod("_NewEncryptor", BindingFlags.NonPublic | BindingFlags.Instance); + object[] encryptorParameters = { rgbKey, mode, rgbIV, desServiceProvider.FeedbackSize, 0 }; + transform = newEncryptorMethodInfo.Invoke(desServiceProvider, encryptorParameters) as ICryptoTransform; +#endif + } + else + { + transform = des.CreateEncryptor(rgbKey, rgbIV); + } + + return transform; } /// @@ -123,7 +114,11 @@ public static byte[] DesLongEncrypt(byte[] key, byte[] plainText) public static Encoding GetOEMEncoding() { +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER + return ASCIIEncoding.GetEncoding(28591); +#else return Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); +#endif } /// @@ -156,24 +151,6 @@ public static byte[] NTOWFv1(string password) return new MD4().GetByteHashFromBytes(passwordBytes); } - /// - /// LMOWFv2 is identical to NTOWFv2 - /// - public static byte[] LMOWFv2(string password, string user, string domain) - { - return NTOWFv2(password, user, domain); - } - - public static byte[] NTOWFv2(string password, string user, string domain) - { - byte[] passwordBytes = UnicodeEncoding.Unicode.GetBytes(password); - byte[] key = new MD4().GetByteHashFromBytes(passwordBytes); - string text = user.ToUpper() + domain; - byte[] bytes = UnicodeEncoding.Unicode.GetBytes(text); - HMACMD5 hmac = new HMACMD5(key); - return hmac.ComputeHash(bytes, 0, bytes.Length); - } - /// /// Extends a 7-byte key into an 8-byte key. /// Note: The DES key ostensibly consists of 64 bits, however, only 56 of these are actually used by the algorithm. @@ -221,7 +198,7 @@ public static byte[] KXKey(byte[] sessionBaseKey, NegotiateFlags negotiateFlags, } else { - if ((negotiateFlags & NegotiateFlags.RequestLMSessionKey) > 0) + if ((negotiateFlags & NegotiateFlags.RequestNonNTSessionKey) > 0) { byte[] keyExchangeKey = ByteUtils.Concatenate(ByteReader.ReadBytes(lmowf, 0, 8), new byte[8]); return keyExchangeKey; @@ -239,5 +216,158 @@ public static byte[] KXKey(byte[] sessionBaseKey, NegotiateFlags negotiateFlags, return keyExchangeKey; } } +#endif + + public static byte[] ComputeLMv2Response(byte[] serverChallenge, byte[] clientChallenge, string password, string user, string domain) + { + byte[] key = LMOWFv2(password, user, domain); + byte[] bytes = ByteUtils.Concatenate(serverChallenge, clientChallenge); + HMACMD5 hmac = new HMACMD5(key); + byte[] hash = hmac.ComputeHash(bytes, 0, bytes.Length); + + return ByteUtils.Concatenate(hash, clientChallenge); + } + + /// + /// [MS-NLMP] https://msdn.microsoft.com/en-us/library/cc236700.aspx + /// + /// ClientChallengeStructure with 4 zero bytes padding, a.k.a. temp + public static byte[] ComputeNTLMv2Proof(byte[] serverChallenge, byte[] clientChallengeStructurePadded, string password, string user, string domain) + { + byte[] key = NTOWFv2(password, user, domain); + byte[] temp = clientChallengeStructurePadded; + + HMACMD5 hmac = new HMACMD5(key); + byte[] _NTProof = hmac.ComputeHash(ByteUtils.Concatenate(serverChallenge, temp), 0, serverChallenge.Length + temp.Length); + return _NTProof; + } + + /// + /// LMOWFv2 is identical to NTOWFv2 + /// + public static byte[] LMOWFv2(string password, string user, string domain) + { + return NTOWFv2(password, user, domain); + } + + public static byte[] NTOWFv2(string password, string user, string domain) + { + byte[] passwordBytes = UnicodeEncoding.Unicode.GetBytes(password); + byte[] key = new MD4().GetByteHashFromBytes(passwordBytes); + string text = user.ToUpper() + domain; + byte[] bytes = UnicodeEncoding.Unicode.GetBytes(text); + HMACMD5 hmac = new HMACMD5(key); + return hmac.ComputeHash(bytes, 0, bytes.Length); + } + + /// + /// Caller must verify that the authenticate message has MIC before calling this method + /// + public static bool ValidateAuthenticateMessageMIC(byte[] exportedSessionKey, byte[] negotiateMessageBytes, byte[] challengeMessageBytes, byte[] authenticateMessageBytes) + { + // https://msdn.microsoft.com/en-us/library/cc236695.aspx + int micFieldOffset = AuthenticateMessage.GetMicFieldOffset(authenticateMessageBytes); + byte[] expectedMic = ByteReader.ReadBytes(authenticateMessageBytes, micFieldOffset, AuthenticateMessage.MicFieldLenght); + + ByteWriter.WriteBytes(authenticateMessageBytes, micFieldOffset, new byte[AuthenticateMessage.MicFieldLenght]); + byte[] temp = ByteUtils.Concatenate(ByteUtils.Concatenate(negotiateMessageBytes, challengeMessageBytes), authenticateMessageBytes); + byte[] mic = new HMACMD5(exportedSessionKey).ComputeHash(temp); + + return ByteUtils.AreByteArraysEqual(mic, expectedMic); + } + + public static byte[] ComputeClientSignKey(byte[] exportedSessionKey) + { + return ComputeSignKey(exportedSessionKey, true); + } + + public static byte[] ComputeServerSignKey(byte[] exportedSessionKey) + { + return ComputeSignKey(exportedSessionKey, false); + } + + public static byte[] ComputeSignKey(byte[] exportedSessionKey, bool isClient) + { + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/524cdccb-563e-4793-92b0-7bc321fce096 + string str; + if (isClient) + { + str = "session key to client-to-server signing key magic constant"; + } + else + { + str = "session key to server-to-client signing key magic constant"; + } + byte[] encodedString = Encoding.GetEncoding(28591).GetBytes(str); + byte[] nullTerminatedEncodedString = ByteUtils.Concatenate(encodedString, new byte[1]); + byte[] concatenated = ByteUtils.Concatenate(exportedSessionKey, nullTerminatedEncodedString); + return MD5.Create().ComputeHash(concatenated); + } + + public static byte[] ComputeClientSealKey(byte[] exportedSessionKey) + { + return ComputeSealKey(exportedSessionKey, true); + } + + public static byte[] ComputeServerSealKey(byte[] exportedSessionKey) + { + return ComputeSealKey(exportedSessionKey, false); + } + + public static byte[] ComputeSealKey(byte[] exportedSessionKey, bool isClient) + { + // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/524cdccb-563e-4793-92b0-7bc321fce096 + string str; + if (isClient) + { + str = "session key to client-to-server sealing key magic constant"; + } + else + { + str = "session key to server-to-client sealing key magic constant"; + } + byte[] encodedString = Encoding.GetEncoding(28591).GetBytes(str); + byte[] nullTerminatedEncodedString = ByteUtils.Concatenate(encodedString, new byte[1]); + byte[] concatenated = ByteUtils.Concatenate(exportedSessionKey, nullTerminatedEncodedString); + return MD5.Create().ComputeHash(concatenated); + } + + public static byte[] ComputeMechListMIC(byte[] exportedSessionKey, byte[] message) + { + return ComputeMechListMIC(exportedSessionKey, message, 0); + } + + public static byte[] ComputeMechListMIC(byte[] exportedSessionKey, byte[] message, uint seqNum) + { + byte[] signKey = ComputeClientSignKey(exportedSessionKey); + byte[] sealKey = ComputeClientSealKey(exportedSessionKey); + RC4KeyState sealKeyState = RC4.InitializeStateFromKey(sealKey); + return ComputeMessageSignature(signKey, sealKeyState, message, seqNum); + } + + public static byte[] ComputeMessageSignature(byte[] signKey, RC4KeyState sealKeyState, byte[] message, uint seqNum) + { + // [MS-NLMP] 3.4.4.2 + byte[] hash = ComputeMessageHash(signKey, message, seqNum); + byte[] encryptedHash = RC4.Encrypt(sealKeyState, hash); + + byte[] version = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + byte[] sequenceNumberBytes = LittleEndianConverter.GetBytes(seqNum); + return ByteUtils.Concatenate(ByteUtils.Concatenate(version, encryptedHash), sequenceNumberBytes); + } + + public static bool VerifyMessageHash(byte[] signKey, byte[] message, uint seqNum, byte[] expectedHash) + { + byte[] hash = ComputeMessageHash(signKey, message, seqNum); + return ByteUtils.AreByteArraysEqual(hash, expectedHash); + } + + private static byte[] ComputeMessageHash(byte[] signKey, byte[] message, uint seqNum) + { + byte[] sequenceNumberBytes = LittleEndianConverter.GetBytes(seqNum); + byte[] concatenated = ByteUtils.Concatenate(sequenceNumberBytes, message); + byte[] fullHash = new HMACMD5(signKey).ComputeHash(concatenated); + return ByteReader.ReadBytes(fullHash, 0, 8); + } } } diff --git a/SMBLibrary/Authentication/NTLM/Helpers/RC4.cs b/SMBLibrary/Authentication/NTLM/Helpers/RC4.cs index 64c0607a..55c94b3c 100644 --- a/SMBLibrary/Authentication/NTLM/Helpers/RC4.cs +++ b/SMBLibrary/Authentication/NTLM/Helpers/RC4.cs @@ -1,30 +1,26 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2026 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. - * - * Based on: https://bitlush.com/blog/rc4-encryption-in-c-sharp */ -using System; -using System.Collections.Generic; -using System.Text; - namespace System.Security.Cryptography { public class RC4 { public static byte[] Encrypt(byte[] key, byte[] data) { - return EncryptOutput(key, data); + RC4KeyState state = InitializeStateFromKey(key); + return Encrypt(state, data); } public static byte[] Decrypt(byte[] key, byte[] data) { - return EncryptOutput(key, data); + RC4KeyState state = InitializeStateFromKey(key); + return Encrypt(state, data); } - private static byte[] EncryptInitalize(byte[] key) + public static RC4KeyState InitializeStateFromKey(byte[] key) { byte[] s = new byte[256]; for (int index = 0; index < 256; index++) @@ -39,34 +35,48 @@ private static byte[] EncryptInitalize(byte[] key) Swap(s, i, j); } - return s; + return new RC4KeyState(s); } - private static byte[] EncryptOutput(byte[] key, byte[] data) + public static byte[] Encrypt(RC4KeyState state, byte[] data) { - byte[] s = EncryptInitalize(key); - - int i = 0; - int j = 0; + byte[] s = state.S; byte[] output = new byte[data.Length]; for (int index = 0; index < data.Length; index++) { - i = (i + 1) & 255; - j = (j + s[i]) & 255; + state.I = (state.I + 1) & 255; + state.J = (state.J + state.S[state.I]) & 255; - Swap(s, i, j); - output[index] = (byte)(data[index] ^ s[(s[i] + s[j]) & 255]); + Swap(state.S, state.I, state.J); + output[index] = (byte)(data[index] ^ s[(s[state.I] + s[state.J]) & 255]); } return output; } - private static void Swap(byte[] s, int i, int j) + public static byte[] Decrypt(RC4KeyState state, byte[] data) + { + return Encrypt(state, data); + } + + private static void Swap(byte[] state, int i, int j) { - byte c = s[i]; + byte c = state[i]; - s[i] = s[j]; - s[j] = c; + state[i] = state[j]; + state[j] = c; + } + } + + public class RC4KeyState + { + internal byte[] S; + internal int I; + internal int J; + + internal RC4KeyState(byte[] s) + { + S = s; } } } diff --git a/SMBLibrary/Authentication/NTLM/IndependentNTLMAuthenticationProvider.cs b/SMBLibrary/Authentication/NTLM/IndependentNTLMAuthenticationProvider.cs index 194f91ac..5682ea26 100644 --- a/SMBLibrary/Authentication/NTLM/IndependentNTLMAuthenticationProvider.cs +++ b/SMBLibrary/Authentication/NTLM/IndependentNTLMAuthenticationProvider.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2026 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.Security.Cryptography; using SMBLibrary.Authentication.GSSAPI; using Utilities; @@ -78,11 +77,18 @@ public override NTStatus GetChallengeMessage(out object context, byte[] negotiat byte[] serverChallenge = GenerateServerChallenge(); context = new AuthContext(serverChallenge); + ChallengeMessage challengeMessage = CreateChallengeMessage(negotiateMessage, serverChallenge); + challengeMessageBytes = challengeMessage.GetBytes(); + return NTStatus.SEC_I_CONTINUE_NEEDED; + } + + protected virtual ChallengeMessage CreateChallengeMessage(NegotiateMessage negotiateMessage, byte[] serverChallenge) + { ChallengeMessage challengeMessage = new ChallengeMessage(); // https://msdn.microsoft.com/en-us/library/cc236691.aspx challengeMessage.NegotiateFlags = NegotiateFlags.TargetTypeServer | NegotiateFlags.TargetInfo | - NegotiateFlags.TargetNameSupplied | + NegotiateFlags.TargetNameNegotiated | NegotiateFlags.Version; // [MS-NLMP] NTLMSSP_NEGOTIATE_NTLM MUST be set in the [..] CHALLENGE_MESSAGE to the client. challengeMessage.NegotiateFlags |= NegotiateFlags.NTLMSessionSecurity; @@ -143,12 +149,17 @@ public override NTStatus GetChallengeMessage(out object context, byte[] negotiat challengeMessage.NegotiateFlags |= NegotiateFlags.KeyExchange; } - challengeMessage.TargetName = Environment.MachineName; + string serverName = GetServerName(); + challengeMessage.TargetName = serverName; challengeMessage.ServerChallenge = serverChallenge; - challengeMessage.TargetInfo = AVPairUtils.GetAVPairSequence(Environment.MachineName, Environment.MachineName); + challengeMessage.TargetInfo = AVPairUtils.GetAVPairSequence(serverName, serverName); challengeMessage.Version = NTLMVersion.Server2003; - challengeMessageBytes = challengeMessage.GetBytes(); - return NTStatus.SEC_I_CONTINUE_NEEDED; + return challengeMessage; + } + + public virtual string GetServerName() + { + return Environment.MachineName; } public override NTStatus Authenticate(object context, byte[] authenticateMessageBytes) @@ -229,6 +240,7 @@ public override NTStatus Authenticate(object context, byte[] authenticateMessage { if (AuthenticationMessageUtils.IsNTLMv1ExtendedSessionSecurity(message.LmChallengeResponse)) { +#if ENABLE_NTLMV1 // NTLM v1 Extended Session Security: success = AuthenticateV1Extended(password, serverChallenge, message.LmChallengeResponse, message.NtChallengeResponse); if (success) @@ -238,6 +250,9 @@ public override NTStatus Authenticate(object context, byte[] authenticateMessage byte[] lmowf = NTLMCryptography.LMOWFv1(password); keyExchangeKey = NTLMCryptography.KXKey(sessionBaseKey, message.NegotiateFlags, message.LmChallengeResponse, serverChallenge, lmowf); } +#else + success = false; +#endif } else { @@ -255,6 +270,7 @@ public override NTStatus Authenticate(object context, byte[] authenticateMessage } else { +#if ENABLE_NTLMV1 success = AuthenticateV1(password, serverChallenge, message.LmChallengeResponse, message.NtChallengeResponse); if (success) { @@ -263,6 +279,9 @@ public override NTStatus Authenticate(object context, byte[] authenticateMessage byte[] lmowf = NTLMCryptography.LMOWFv1(password); keyExchangeKey = NTLMCryptography.KXKey(sessionBaseKey, message.NegotiateFlags, message.LmChallengeResponse, serverChallenge, lmowf); } +#else + success = false; +#endif } if (success) @@ -331,6 +350,7 @@ private bool EnableGuestLogin } } +#if ENABLE_NTLMV1 /// /// LM v1 / NTLM v1 /// @@ -356,6 +376,7 @@ private static bool AuthenticateV1Extended(string password, byte[] serverChallen return ByteUtils.AreByteArraysEqual(expectedNTLMv1Response, ntResponse); } +#endif /// /// LM v2 / NTLM v2 diff --git a/SMBLibrary/Authentication/NTLM/Structures/AuthenticateMessage.cs b/SMBLibrary/Authentication/NTLM/Structures/AuthenticateMessage.cs index b97866c0..7b2921f7 100644 --- a/SMBLibrary/Authentication/NTLM/Structures/AuthenticateMessage.cs +++ b/SMBLibrary/Authentication/NTLM/Structures/AuthenticateMessage.cs @@ -1,12 +1,11 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2023 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 System.Security.Cryptography; using Utilities; namespace SMBLibrary.Authentication.NTLM @@ -17,6 +16,7 @@ namespace SMBLibrary.Authentication.NTLM public class AuthenticateMessage { public const string ValidSignature = "NTLMSSP\0"; + public const int MicFieldLenght = 16; public string Signature; // 8 bytes public MessageTypeName MessageType; @@ -59,7 +59,7 @@ public AuthenticateMessage(byte[] buffer) } if (HasMicField()) { - MIC = ByteReader.ReadBytes(buffer, offset, 16); + MIC = ByteReader.ReadBytes(buffer, offset, MicFieldLenght); } } @@ -142,5 +142,25 @@ public byte[] GetBytes() return buffer; } + + public void CalculateMIC(byte[] sessionKey, byte[] negotiateMessage, byte[] challengeMessage) + { + MIC = new byte[MicFieldLenght]; + byte[] authenticateMessageBytes = GetBytes(); + byte[] temp = ByteUtils.Concatenate(ByteUtils.Concatenate(negotiateMessage, challengeMessage), authenticateMessageBytes); + MIC = new HMACMD5(sessionKey).ComputeHash(temp); + } + + public static int GetMicFieldOffset(byte[] authenticateMessageBytes) + { + NegotiateFlags negotiateFlags = (NegotiateFlags)LittleEndianConverter.ToUInt32(authenticateMessageBytes, 60); + int offset = 64; + if ((negotiateFlags & NegotiateFlags.Version) > 0) + { + offset += NTLMVersion.Length; + } + + return offset; + } } } diff --git a/SMBLibrary/Authentication/NTLM/Structures/ChallengeMessage.cs b/SMBLibrary/Authentication/NTLM/Structures/ChallengeMessage.cs index e7884a50..f0f58e23 100644 --- a/SMBLibrary/Authentication/NTLM/Structures/ChallengeMessage.cs +++ b/SMBLibrary/Authentication/NTLM/Structures/ChallengeMessage.cs @@ -52,7 +52,7 @@ public ChallengeMessage(byte[] buffer) public byte[] GetBytes() { - if ((NegotiateFlags & NegotiateFlags.TargetNameSupplied) == 0) + if ((NegotiateFlags & NegotiateFlags.TargetNameNegotiated) == 0) { TargetName = String.Empty; } diff --git a/SMBLibrary/Authentication/NTLM/Structures/Enums/NegotiateFlags.cs b/SMBLibrary/Authentication/NTLM/Structures/Enums/NegotiateFlags.cs index 0d09ea50..6e776041 100644 --- a/SMBLibrary/Authentication/NTLM/Structures/Enums/NegotiateFlags.cs +++ b/SMBLibrary/Authentication/NTLM/Structures/Enums/NegotiateFlags.cs @@ -7,7 +7,12 @@ public enum NegotiateFlags : uint { UnicodeEncoding = 0x00000001, // NTLMSSP_NEGOTIATE_UNICODE OEMEncoding = 0x00000002, // NTLM_NEGOTIATE_OEM - TargetNameSupplied = 0x00000004, // NTLMSSP_REQUEST_TARGET + + /// + /// If set in the Negotiate message, the server MUST supply target name. + /// Windows Server 2008 R2 will supply target name even if not requested. + /// + TargetNameNegotiated = 0x00000004, // NTLMSSP_REQUEST_TARGET Sign = 0x00000010, // NTLMSSP_NEGOTIATE_SIGN Seal = 0x00000020, // NTLMSSP_NEGOTIATE_SEAL Datagram = 0x00000040, // NTLMSSP_NEGOTIATE_DATAGRAM @@ -37,7 +42,7 @@ public enum NegotiateFlags : uint /// ExtendedSessionSecurity = 0x00080000, // NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY Identify = 0x00100000, // NTLMSSP_NEGOTIATE_IDENTIFY - RequestLMSessionKey = 0x00400000, // NTLMSSP_REQUEST_NON_NT_SESSION_KEY + RequestNonNTSessionKey = 0x00400000, // NTLMSSP_REQUEST_NON_NT_SESSION_KEY TargetInfo = 0x00800000, // NTLMSSP_NEGOTIATE_TARGET_INFO Version = 0x02000000, // NTLMSSP_NEGOTIATE_VERSION Use128BitEncryption = 0x20000000, // NTLMSSP_NEGOTIATE_128 diff --git a/SMBLibrary/Authentication/NTLM/Structures/NTLMv2ClientChallenge.cs b/SMBLibrary/Authentication/NTLM/Structures/NTLMv2ClientChallenge.cs index b8d5b4fd..91729835 100644 --- a/SMBLibrary/Authentication/NTLM/Structures/NTLMv2ClientChallenge.cs +++ b/SMBLibrary/Authentication/NTLM/Structures/NTLMv2ClientChallenge.cs @@ -45,12 +45,21 @@ public NTLMv2ClientChallenge(DateTime timeStamp, byte[] clientChallenge, string } public NTLMv2ClientChallenge(DateTime timeStamp, byte[] clientChallenge, KeyValuePairList targetInfo) + : this(timeStamp, clientChallenge, targetInfo, null) + { + } + + public NTLMv2ClientChallenge(DateTime timeStamp, byte[] clientChallenge, KeyValuePairList targetInfo, string spn) { CurrentVersion = StructureVersion; MaximumSupportedVersion = StructureVersion; TimeStamp = timeStamp; ClientChallenge = clientChallenge; AVPairs = targetInfo; + if (!string.IsNullOrEmpty(spn)) + { + AVPairs.Add(AVPairKey.TargetName, UnicodeEncoding.Unicode.GetBytes(spn)); + } } public NTLMv2ClientChallenge(byte[] buffer) : this(buffer, 0) diff --git a/SMBLibrary/Client/Authentication/IAuthenticationClient.cs b/SMBLibrary/Client/Authentication/IAuthenticationClient.cs new file mode 100644 index 00000000..89bf8dd4 --- /dev/null +++ b/SMBLibrary/Client/Authentication/IAuthenticationClient.cs @@ -0,0 +1,22 @@ +/* Copyright (C) 2023-2026 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. + */ +namespace SMBLibrary.Client.Authentication +{ + public interface IAuthenticationClient + { + /// Credentials blob or null if security blob is invalid + byte[] InitializeSecurityContext(byte[] securityBlob); + + byte[] GetSessionKey(); + + /// + /// Used when the client needs to perform login to an additional SMB server, + /// Only used when connecting to a DFS Namespace Server (DFS root). + /// + void ResetSecurityContext(string spn); + } +} diff --git a/SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs b/SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs new file mode 100644 index 00000000..0c31127c --- /dev/null +++ b/SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs @@ -0,0 +1,153 @@ +/* Copyright (C) 2017-2026 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 SMBLibrary.Authentication.GSSAPI; +using SMBLibrary.Authentication.NTLM; +using System.Collections.Generic; +using Utilities; + +namespace SMBLibrary.Client.Authentication +{ + public class NTLMAuthenticationClient : IAuthenticationClient + { + private string m_domainName; + private string m_userName; + private string m_password; + private string m_spn; + private byte[] m_sessionKey; + private AuthenticationMethod m_authenticationMethod; + private byte[] m_negotiateMessageBytes; + + private bool m_isNegotiationMessageAcquired = false; + + public NTLMAuthenticationClient(string domainName, string userName, string password, string spn, AuthenticationMethod authenticationMethod) + { + m_domainName = domainName; + m_userName = userName; + m_password = password; + m_spn = spn; + m_authenticationMethod = authenticationMethod; + } + + public byte[] InitializeSecurityContext(byte[] securityBlob) + { + if (!m_isNegotiationMessageAcquired) + { + m_isNegotiationMessageAcquired = true; + return GetNegotiateMessage(securityBlob); + } + else + { + return GetAuthenticateMessage(securityBlob); + } + } + + protected virtual byte[] GetNegotiateMessage(byte[] securityBlob) + { + bool useGSSAPI = false; + if (securityBlob.Length > 0) + { + SimpleProtectedNegotiationTokenInit spnegoToken = null; + try + { + spnegoToken = SimpleProtectedNegotiationToken.ReadToken(securityBlob, 0, true) as SimpleProtectedNegotiationTokenInit; + } + catch + { + } + + if (spnegoToken == null || !ContainsMechanism(spnegoToken, GSSProvider.NTLMSSPIdentifier)) + { + return null; + } + useGSSAPI = true; + } + + m_negotiateMessageBytes = CreateNegotiateMessage(); + if (useGSSAPI) + { + SimpleProtectedNegotiationTokenInit outputToken = new SimpleProtectedNegotiationTokenInit(); + outputToken.MechanismTypeList = new List(); + outputToken.MechanismTypeList.Add(GSSProvider.NTLMSSPIdentifier); + outputToken.MechanismToken = m_negotiateMessageBytes; + return outputToken.GetBytes(true); + } + else + { + return m_negotiateMessageBytes; + } + } + + protected virtual byte[] CreateNegotiateMessage() + { + return NTLMAuthenticationHelper.GetNegotiateMessage(m_userName, m_password, m_authenticationMethod); + } + + protected virtual byte[] GetAuthenticateMessage(byte[] securityBlob) + { + bool useGSSAPI = false; + SimpleProtectedNegotiationTokenResponse spnegoToken = null; + try + { + spnegoToken = SimpleProtectedNegotiationToken.ReadToken(securityBlob, 0, false) as SimpleProtectedNegotiationTokenResponse; + } + catch + { + } + + byte[] challengeMessageBytes; + if (spnegoToken != null) + { + challengeMessageBytes = spnegoToken.ResponseToken; + useGSSAPI = true; + } + else + { + challengeMessageBytes = securityBlob; + } + + byte[] authenticateMessageBytes = NTLMAuthenticationHelper.GetAuthenticateMessage(m_negotiateMessageBytes, challengeMessageBytes, m_domainName, m_userName, m_password, m_spn, m_authenticationMethod, out m_sessionKey); + if (useGSSAPI && authenticateMessageBytes != null) + { + SimpleProtectedNegotiationTokenResponse outputToken = new SimpleProtectedNegotiationTokenResponse(); + outputToken.ResponseToken = authenticateMessageBytes; + List mechanismTypeList = new List() { GSSProvider.NTLMSSPIdentifier }; + byte[] mechListBytes = SimpleProtectedNegotiationTokenInit.GetMechanismTypeListBytes(mechanismTypeList); + outputToken.MechanismListMIC = NTLMCryptography.ComputeMechListMIC(m_sessionKey, mechListBytes); + return outputToken.GetBytes(); + } + else + { + return authenticateMessageBytes; + } + } + + public virtual byte[] GetSessionKey() + { + return m_sessionKey; + } + + public virtual void ResetSecurityContext(string spn) + { + m_spn = spn; + m_isNegotiationMessageAcquired = false; + m_negotiateMessageBytes = null; + m_sessionKey = null; + } + + private static bool ContainsMechanism(SimpleProtectedNegotiationTokenInit token, byte[] mechanismIdentifier) + { + for (int index = 0; index < token.MechanismTypeList.Count; index++) + { + if (ByteUtils.AreByteArraysEqual(token.MechanismTypeList[index], mechanismIdentifier)) + { + return true; + } + } + return false; + } + } +} diff --git a/SMBLibrary/Client/ConnectionState.cs b/SMBLibrary/Client/ConnectionState.cs index dfd3e876..941bb544 100644 --- a/SMBLibrary/Client/ConnectionState.cs +++ b/SMBLibrary/Client/ConnectionState.cs @@ -1,16 +1,12 @@ -/* Copyright (C) 2017-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2023 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.Net; using System.Net.Sockets; using SMBLibrary.NetBios; -using SMBLibrary.SMB1; -using Utilities; namespace SMBLibrary.Client { diff --git a/SMBLibrary/Client/DFS/DfsPath.cs b/SMBLibrary/Client/DFS/DfsPath.cs new file mode 100644 index 00000000..39fc2a7c --- /dev/null +++ b/SMBLibrary/Client/DFS/DfsPath.cs @@ -0,0 +1,151 @@ +/* Copyright (C) 2026 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; + +namespace SMBLibrary.Client.DFS +{ + public class DfsPath + { + private List m_components; + + public DfsPath(string uncPath) + { + if (uncPath == null) + { + throw new ArgumentNullException(nameof(uncPath)); + } + + if (uncPath.Length == 0) + { + throw new ArgumentException("UNC path must not be empty.", nameof(uncPath)); + } + + m_components = new List(); + string[] parts = uncPath.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + foreach (string part in parts) + { + m_components.Add(part); + } + + if (m_components.Count == 0) + { + throw new ArgumentException("UNC path contains no components.", nameof(uncPath)); + } + } + + private DfsPath(List components) + { + m_components = components; + } + + public string ToUncPath() + { + return @"\\" + String.Join(@"\", m_components.ToArray()); + } + + public DfsPath ReplacePrefix(DfsPath oldPrefix, DfsPath newPrefix) + { + List oldComponents = oldPrefix.m_components; + List newComponents = newPrefix.m_components; + + if (oldComponents.Count > m_components.Count) + { + return this; + } + + for (int i = 0; i < oldComponents.Count; i++) + { + if (!String.Equals(m_components[i], oldComponents[i], StringComparison.OrdinalIgnoreCase)) + { + return this; + } + } + + List result = new List(newComponents); + for (int i = oldComponents.Count; i < m_components.Count; i++) + { + result.Add(m_components[i]); + } + + return new DfsPath(result); + } + + public override string ToString() + { + return ToUncPath(); + } + + public string ServerName + { + get + { + return m_components[0]; + } + } + + public string ShareName + { + get + { + if (m_components.Count > 1) + { + return m_components[1]; + } + return null; + } + } + + public string PathWithinShare + { + get + { + if (m_components.Count > 2) + { + return String.Join(@"\", m_components.GetRange(2, m_components.Count - 2).ToArray()); + } + return String.Empty; + } + } + + public bool HasOnlyOneComponent + { + get + { + return m_components.Count == 1; + } + } + + public bool IsSysVolOrNetLogon + { + get + { + if (m_components.Count < 2) + { + return false; + } + + string share = m_components[1]; + return String.Equals(share, "SYSVOL", StringComparison.OrdinalIgnoreCase) || + String.Equals(share, "NETLOGON", StringComparison.OrdinalIgnoreCase); + } + } + + public bool IsIpc + { + get + { + if (m_components.Count < 2) + { + return false; + } + + return String.Equals(m_components[1], "IPC$", StringComparison.OrdinalIgnoreCase); + } + } + } +} diff --git a/SMBLibrary/Client/DFS/DfsReferralHelper.cs b/SMBLibrary/Client/DFS/DfsReferralHelper.cs new file mode 100644 index 00000000..ca574bd0 --- /dev/null +++ b/SMBLibrary/Client/DFS/DfsReferralHelper.cs @@ -0,0 +1,44 @@ +/* Copyright (C) 2026 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 SMBLibrary.DFS; +using SMBLibrary.SMB2; + +namespace SMBLibrary.Client.DFS +{ + public class DfsReferralHelper + { + // MS-SMB2 §2.2.31: sentinel FileId for DFS referral requests + public static readonly FileID DfsReferralFileId = new FileID() + { + Persistent = 0xFFFFFFFFFFFFFFFF, + Volatile = 0xFFFFFFFFFFFFFFFF + }; + + public static readonly int MaxOutputBufferSize = 8192; + + public static NTStatus GetDfsReferral(ISMBFileStore fileStore, string dfsPath, out ResponseGetDfsReferral referralResponse) + { + referralResponse = null; + + // MS-DFSC §2.2.2: request V4 referrals (highest version) + RequestGetDfsReferral request = new RequestGetDfsReferral(); + request.MaxReferralLevel = 4; + request.RequestFileName = dfsPath; + byte[] inputBytes = request.GetBytes(); + + byte[] outputBytes; + NTStatus status = fileStore.DeviceIOControl(DfsReferralFileId, (uint)IoControlCode.FSCTL_DFS_GET_REFERRALS, inputBytes, out outputBytes, MaxOutputBufferSize); + + if (status == NTStatus.STATUS_SUCCESS && outputBytes != null) + { + referralResponse = new ResponseGetDfsReferral(outputBytes); + } + + return status; + } + } +} diff --git a/SMBLibrary/Client/DFS/SMB2DfsFileStore.cs b/SMBLibrary/Client/DFS/SMB2DfsFileStore.cs new file mode 100644 index 00000000..909fe0e4 --- /dev/null +++ b/SMBLibrary/Client/DFS/SMB2DfsFileStore.cs @@ -0,0 +1,471 @@ +/* Copyright (C) 2026 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.Net; +using SMBLibrary.DFS; + +namespace SMBLibrary.Client.DFS +{ + /// + /// ISMBFileStore wrapper for a DFS root share. + /// When a CreateFile is not covered by the DFS root (STATUS_PATH_NOT_COVERED), a DFS referral is + /// requested and the operation is retried against a referral target, connecting to another server + /// when necessary (reusing the authentication client via IAuthenticationClient.ResetSecurityContext). + /// + internal class SMB2DfsFileStore : ISMBFileStore + { + // Guards against referral loops when chaining interlinks. + private const int MaxReferralHopCount = 8; + + private SMB2Client m_client; + private string m_serverName; + private string m_shareName; + private ISMBFileStore m_dfsFileStore; + private bool m_useDfsPaths; + + // Connections and tree connections established while following referrals to other targets. + private Dictionary m_targetClients; + private Dictionary m_targetFileStores; + // Whether each target above is itself a DFS namespace root, and so expects DFS paths too. + private Dictionary m_dfsRootTargets; + + internal SMB2DfsFileStore(SMB2Client client, string serverName, string shareName, ISMBFileStore dfsFileStore) + { + m_client = client; + m_serverName = serverName; + m_shareName = shareName; + m_dfsFileStore = dfsFileStore; + m_targetClients = new Dictionary(StringComparer.OrdinalIgnoreCase); + m_targetFileStores = new Dictionary(StringComparer.OrdinalIgnoreCase); + m_dfsRootTargets = new Dictionary(StringComparer.OrdinalIgnoreCase); + + // [MS-DFSC] The server normalizes a DFS path against the namespace name, which an IP address can never + // match. A caller that connected by address therefore keeps using share-relative paths, so that access + // to a share that happens to carry SMB2_SHAREFLAG_DFS_ROOT keeps working exactly as it did before. + m_useDfsPaths = !IsIPAddress(serverName); + SetDfsOperations(m_dfsFileStore, m_useDfsPaths); + } + + private static bool IsIPAddress(string serverName) + { + IPAddress serverAddress; + return IPAddress.TryParse(serverName, out serverAddress); + } + + private static void SetDfsOperations(ISMBFileStore fileStore, bool isDfsOperation) + { + SMB2FileStore smb2FileStore = fileStore as SMB2FileStore; + if (smb2FileStore != null) + { + smb2FileStore.IsDfsOperation = isDfsOperation; + } + } + + /// + /// A caller may express the share root as an empty string or as a single backslash, and may include a + /// leading or trailing separator on any path. The CREATE name and the DFS referral path are built from + /// the same caller-supplied value, so both go through this to stay consistent with each other. + /// + private static string NormalizePathWithinShare(string pathWithinShare) + { + if (pathWithinShare == null) + { + return String.Empty; + } + return pathWithinShare.Trim('\\'); + } + + /// + /// [MS-SMB2] 2.2.13 - When SMB2_FLAGS_DFS_OPERATIONS is set the name is subject to DFS name normalization + /// and must be a full path in the form 'server\share\path', with no leading separator. + /// + private static string GetDfsName(string serverName, string shareName, string pathWithinShare) + { + string sharePath = serverName + @"\" + shareName; + string relativePath = NormalizePathWithinShare(pathWithinShare); + return (relativePath.Length > 0) ? sharePath + @"\" + relativePath : sharePath; + } + + public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string path, AccessMask desiredAccess, FileAttributes fileAttributes, ShareAccess shareAccess, CreateDisposition createDisposition, CreateOptions createOptions, SecurityContext securityContext) + { + handle = null; + fileStatus = FileStatus.FILE_DOES_NOT_EXIST; + + ISMBFileStore fileStore = m_dfsFileStore; + string currentServer = m_serverName; + string currentShare = m_shareName; + string effectivePath = path; + // This instance only ever wraps a DFS namespace root, so the first hop takes DFS paths unless the + // caller connected by IP address. A referral target only does when it is a namespace root itself. + bool currentIsDfsRoot = m_useDfsPaths; + + for (int hopCount = 0; ; hopCount++) + { + object innerHandle; + string name = currentIsDfsRoot ? GetDfsName(currentServer, currentShare, effectivePath) : effectivePath; + NTStatus status = fileStore.CreateFile(out innerHandle, out fileStatus, name, desiredAccess, fileAttributes, shareAccess, createDisposition, createOptions, securityContext); + if (status != NTStatus.STATUS_PATH_NOT_COVERED) + { + if (status == NTStatus.STATUS_SUCCESS) + { + handle = new DfsHandle(fileStore, innerHandle); + } + return status; + } + + if (hopCount >= MaxReferralHopCount) + { + return status; + } + + // Same normalization as the CREATE name above: a leading separator here would produce + // \\server\share\\path and the server would fail to resolve the referral. + string dfsPath = BuildUncPath(currentServer, currentShare, NormalizePathWithinShare(effectivePath)); + List targets; + if (!TryGetReferralTargets(fileStore, dfsPath, out targets)) + { + return status; + } + + // MS-DFSC 3.1.5.4.3: targets are listed in order of preference, try the next target when a target is unreachable + ISMBFileStore targetFileStore = null; + DfsPath target = null; + bool targetIsDfsRoot = false; + foreach (DfsPath referralTarget in targets) + { + targetFileStore = GetOrConnectFileStore(referralTarget.ServerName, referralTarget.ShareName, out targetIsDfsRoot); + if (targetFileStore != null) + { + target = referralTarget; + break; + } + } + + if (targetFileStore == null) + { + return status; + } + + fileStore = targetFileStore; + currentServer = target.ServerName; + currentShare = target.ShareName; + effectivePath = target.PathWithinShare; + currentIsDfsRoot = targetIsDfsRoot; + } + } + + private static bool TryGetReferralTargets(ISMBFileStore fileStore, string dfsPath, out List targets) + { + targets = new List(); + ResponseGetDfsReferral referralResponse; + try + { + NTStatus status = DfsReferralHelper.GetDfsReferral(fileStore, dfsPath, out referralResponse); + if (status != NTStatus.STATUS_SUCCESS || referralResponse == null) + { + return false; + } + } + catch + { + // e.g. a malformed referral response buffer + return false; + } + + DfsPath requestedPath = new DfsPath(dfsPath); + foreach (DfsReferralEntry referralEntry in referralResponse.ReferralEntries) + { + // A STATUS_PATH_NOT_COVERED referral is always a V1-V4 link/root referral; only V3/V4 are handled. + DfsReferralEntryV3 entry = referralEntry as DfsReferralEntryV3; + if (entry == null || String.IsNullOrEmpty(entry.DfsPath) || String.IsNullOrEmpty(entry.NetworkAddress)) + { + continue; + } + + DfsPath target = requestedPath.ReplacePrefix(new DfsPath(entry.DfsPath), new DfsPath(entry.NetworkAddress)); + // ReplacePrefix returns the path unchanged when the referral does not cover it + if (ReferenceEquals(target, requestedPath) || + String.IsNullOrEmpty(target.ShareName) || + String.Equals(target.ToUncPath(), requestedPath.ToUncPath(), StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + targets.Add(target); + } + + return targets.Count > 0; + } + + private ISMBFileStore GetOrConnectFileStore(string serverName, string shareName, out bool isDfsRoot) + { + string key = BuildUncPath(serverName, shareName, null); + ISMBFileStore fileStore; + if (m_targetFileStores.TryGetValue(key, out fileStore)) + { + isDfsRoot = m_dfsRootTargets[key]; + return fileStore; + } + + fileStore = ConnectToTarget(serverName, shareName); + if (fileStore == null) + { + isDfsRoot = false; + return null; + } + + isDfsRoot = false; + if (fileStore is SMB2DfsFileStore dfsFileStore) + { + // Use the underlying file store, referrals from the target are followed (and hop-limited) by this instance. + // The target is a namespace root of its own, so it expects DFS paths just like the root we started from, + // unless it decided otherwise (a target addressed by IP). + isDfsRoot = dfsFileStore.m_useDfsPaths; + fileStore = dfsFileStore.m_dfsFileStore; + } + + m_targetFileStores.Add(key, fileStore); + m_dfsRootTargets.Add(key, isDfsRoot); + return fileStore; + } + + /// + /// Tree connects to a referral target share, reusing an existing connection to the target server when possible. + /// + protected virtual ISMBFileStore ConnectToTarget(string serverName, string shareName) + { + SMB2Client client = GetOrConnectClient(serverName); + if (client == null) + { + return null; + } + + NTStatus status; + ISMBFileStore fileStore = client.TreeConnect(shareName, out status); + if (status != NTStatus.STATUS_SUCCESS) + { + return null; + } + return fileStore; + } + + private SMB2Client GetOrConnectClient(string serverName) + { + if (String.Equals(serverName, m_serverName, StringComparison.OrdinalIgnoreCase)) + { + return m_client; + } + + SMB2Client client; + if (m_targetClients.TryGetValue(serverName, out client)) + { + return client; + } + + client = m_client.ConnectAndLoginToDfsTarget(serverName); + if (client == null) + { + return null; + } + + m_targetClients.Add(serverName, client); + return client; + } + + private static string BuildUncPath(string serverName, string shareName, string pathWithinShare) + { + string uncPath = @"\\" + serverName + @"\" + shareName; + if (!String.IsNullOrEmpty(pathWithinShare)) + { + uncPath += @"\" + pathWithinShare; + } + return uncPath; + } + + public NTStatus CloseFile(object handle) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.CloseFile(dfsHandle.Handle); + } + + public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCount) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.ReadFile(out data, dfsHandle.Handle, offset, maxCount); + } + + public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offset, byte[] data) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.WriteFile(out numberOfBytesWritten, dfsHandle.Handle, offset, data); + } + + public NTStatus FlushFileBuffers(object handle) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.FlushFileBuffers(dfsHandle.Handle); + } + + public NTStatus LockFile(object handle, long byteOffset, long length, bool exclusiveLock) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.LockFile(dfsHandle.Handle, byteOffset, length, exclusiveLock); + } + + public NTStatus UnlockFile(object handle, long byteOffset, long length) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.UnlockFile(dfsHandle.Handle, byteOffset, length); + } + + public NTStatus QueryDirectory(out List result, object handle, string fileName, FileInformationClass informationClass) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.QueryDirectory(out result, dfsHandle.Handle, fileName, informationClass); + } + + public NTStatus GetFileInformation(out FileInformation result, object handle, FileInformationClass informationClass) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.GetFileInformation(out result, dfsHandle.Handle, informationClass); + } + + public NTStatus SetFileInformation(object handle, FileInformation information) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.SetFileInformation(dfsHandle.Handle, information); + } + + public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileSystemInformationClass informationClass) + { + return m_dfsFileStore.GetFileSystemInformation(out result, informationClass); + } + + public NTStatus SetFileSystemInformation(FileSystemInformation information) + { + return m_dfsFileStore.SetFileSystemInformation(information); + } + + public NTStatus GetSecurityInformation(out SecurityDescriptor result, object handle, SecurityInformation securityInformation) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.GetSecurityInformation(out result, dfsHandle.Handle, securityInformation); + } + + public NTStatus SetSecurityInformation(object handle, SecurityInformation securityInformation, SecurityDescriptor securityDescriptor) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.SetSecurityInformation(dfsHandle.Handle, securityInformation, securityDescriptor); + } + + public NTStatus NotifyChange(out object ioRequest, object handle, NotifyChangeFilter completionFilter, bool watchTree, int outputBufferSize, OnNotifyChangeCompleted onNotifyChangeCompleted, object context) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + object innerIoRequest; + NTStatus status = dfsHandle.FileStore.NotifyChange(out innerIoRequest, dfsHandle.Handle, completionFilter, watchTree, outputBufferSize, onNotifyChangeCompleted, context); + ioRequest = (innerIoRequest != null) ? new DfsHandle(dfsHandle.FileStore, innerIoRequest) : null; + return status; + } + + public NTStatus Cancel(object ioRequest) + { + DfsHandle dfsHandle = GetDfsHandle(ioRequest); + return dfsHandle.FileStore.Cancel(dfsHandle.Handle); + } + + public NTStatus DeviceIOControl(object handle, uint ctlCode, byte[] input, out byte[] output, int maxOutputLength) + { + DfsHandle dfsHandle = GetDfsHandle(handle); + return dfsHandle.FileStore.DeviceIOControl(dfsHandle.Handle, ctlCode, input, out output, maxOutputLength); + } + + public NTStatus Disconnect() + { + foreach (ISMBFileStore fileStore in m_targetFileStores.Values) + { + try + { + fileStore.Disconnect(); + } + catch (InvalidOperationException) + { + // The connection to the target has already been lost + } + } + m_targetFileStores.Clear(); + + foreach (SMB2Client client in m_targetClients.Values) + { + try + { + client.Logoff(); + } + catch (InvalidOperationException) + { + } + client.Disconnect(); + } + m_targetClients.Clear(); + + return m_dfsFileStore.Disconnect(); + } + + private DfsHandle GetDfsHandle(object handle) + { + DfsHandle dfsHandle = handle as DfsHandle; + if (dfsHandle == null) + { + // A handle that did not originate from this instance (e.g. the FileID used for DFS referral requests) is directed to the DFS root file store + return new DfsHandle(m_dfsFileStore, handle); + } + return dfsHandle; + } + + public uint MaxReadSize + { + get + { + uint maxReadSize = m_dfsFileStore.MaxReadSize; + foreach (ISMBFileStore fileStore in m_targetFileStores.Values) + { + maxReadSize = Math.Min(maxReadSize, fileStore.MaxReadSize); + } + return maxReadSize; + } + } + + public uint MaxWriteSize + { + get + { + uint maxWriteSize = m_dfsFileStore.MaxWriteSize; + foreach (ISMBFileStore fileStore in m_targetFileStores.Values) + { + maxWriteSize = Math.Min(maxWriteSize, fileStore.MaxWriteSize); + } + return maxWriteSize; + } + } + + /// + /// Associates a handle (or NotifyChange ioRequest) with the file store that produced it, so that + /// subsequent operations are routed to the referral target the handle was opened against. + /// + private class DfsHandle + { + public readonly ISMBFileStore FileStore; + public readonly object Handle; + + public DfsHandle(ISMBFileStore fileStore, object handle) + { + FileStore = fileStore; + Handle = handle; + } + } + } +} diff --git a/SMBLibrary/Client/Helpers/IPAddressHelper.cs b/SMBLibrary/Client/Helpers/IPAddressHelper.cs new file mode 100644 index 00000000..3a43b91f --- /dev/null +++ b/SMBLibrary/Client/Helpers/IPAddressHelper.cs @@ -0,0 +1,27 @@ +/* Copyright (C) 2023 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.Net; +using System.Net.Sockets; + +namespace SMBLibrary.Client +{ + public class IPAddressHelper + { + public static IPAddress SelectAddressPreferIPv4(IPAddress[] hostAddresses) + { + foreach (IPAddress hostAddress in hostAddresses) + { + if (hostAddress.AddressFamily == AddressFamily.InterNetwork) + { + return hostAddress; + } + } + + return hostAddresses[0]; + } + } +} diff --git a/SMBLibrary/Client/Helpers/NTLMAuthenticationHelper.cs b/SMBLibrary/Client/Helpers/NTLMAuthenticationHelper.cs index 61e29fae..9a805ace 100644 --- a/SMBLibrary/Client/Helpers/NTLMAuthenticationHelper.cs +++ b/SMBLibrary/Client/Helpers/NTLMAuthenticationHelper.cs @@ -1,13 +1,11 @@ -/* Copyright (C) 2017-2018 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2026 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.Security.Cryptography; -using SMBLibrary.Authentication.GSSAPI; using SMBLibrary.Authentication.NTLM; using Utilities; @@ -15,90 +13,53 @@ namespace SMBLibrary.Client { public class NTLMAuthenticationHelper { - public static byte[] GetNegotiateMessage(byte[] securityBlob, string domainName, AuthenticationMethod authenticationMethod) + public static byte[] GetNegotiateMessage(string userName, string password, AuthenticationMethod authenticationMethod) { - bool useGSSAPI = false; - if (securityBlob.Length > 0) - { - SimpleProtectedNegotiationTokenInit inputToken = null; - try - { - inputToken = SimpleProtectedNegotiationToken.ReadToken(securityBlob, 0, true) as SimpleProtectedNegotiationTokenInit; - } - catch - { - } - - if (inputToken == null || !ContainsMechanism(inputToken, GSSProvider.NTLMSSPIdentifier)) - { - return null; - } - useGSSAPI = true; - } + bool isAnonymous = (userName == String.Empty && password == String.Empty); + return GetNegotiateMessage(isAnonymous, authenticationMethod, false); + } + public static byte[] GetNegotiateMessage(bool isAnonymous, AuthenticationMethod authenticationMethod, bool requestSeal) + { NegotiateMessage negotiateMessage = new NegotiateMessage(); negotiateMessage.NegotiateFlags = NegotiateFlags.UnicodeEncoding | NegotiateFlags.OEMEncoding | NegotiateFlags.Sign | NegotiateFlags.NTLMSessionSecurity | - NegotiateFlags.DomainNameSupplied | - NegotiateFlags.WorkstationNameSupplied | + NegotiateFlags.TargetNameNegotiated | NegotiateFlags.AlwaysSign | NegotiateFlags.Version | NegotiateFlags.Use128BitEncryption | - NegotiateFlags.KeyExchange | NegotiateFlags.Use56BitEncryption; - if (authenticationMethod == AuthenticationMethod.NTLMv1) + if (requestSeal) { - negotiateMessage.NegotiateFlags |= NegotiateFlags.LanManagerSessionKey; + negotiateMessage.NegotiateFlags |= NegotiateFlags.Seal; } - else + + if (!isAnonymous) { - negotiateMessage.NegotiateFlags |= NegotiateFlags.ExtendedSessionSecurity; + negotiateMessage.NegotiateFlags |= NegotiateFlags.KeyExchange; } - negotiateMessage.Version = NTLMVersion.Server2003; - negotiateMessage.DomainName = domainName; - negotiateMessage.Workstation = Environment.MachineName; - if (useGSSAPI) + if (authenticationMethod == AuthenticationMethod.NTLMv1) { - SimpleProtectedNegotiationTokenInit outputToken = new SimpleProtectedNegotiationTokenInit(); - outputToken.MechanismTypeList = new List(); - outputToken.MechanismTypeList.Add(GSSProvider.NTLMSSPIdentifier); - outputToken.MechanismToken = negotiateMessage.GetBytes(); - return outputToken.GetBytes(true); + negotiateMessage.NegotiateFlags |= NegotiateFlags.LanManagerSessionKey; } else { - return negotiateMessage.GetBytes(); + negotiateMessage.NegotiateFlags |= NegotiateFlags.ExtendedSessionSecurity; } + + negotiateMessage.Version = NTLMVersion.Server2003; + return negotiateMessage.GetBytes(); } - public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainName, string userName, string password, AuthenticationMethod authenticationMethod, out byte[] sessionKey) + public static byte[] GetAuthenticateMessage(byte[] negotiateMessageBytes, byte[] challengeMessageBytes, string domainName, string userName, string password, string spn, AuthenticationMethod authenticationMethod, out byte[] sessionKey) { sessionKey = null; - bool useGSSAPI = false; - SimpleProtectedNegotiationTokenResponse inputToken = null; - try - { - inputToken = SimpleProtectedNegotiationToken.ReadToken(securityBlob, 0, false) as SimpleProtectedNegotiationTokenResponse; - } - catch - { - } - - ChallengeMessage challengeMessage; - if (inputToken != null) - { - challengeMessage = GetChallengeMessage(inputToken.ResponseToken); - useGSSAPI = true; - } - else - { - challengeMessage = GetChallengeMessage(securityBlob); - } + ChallengeMessage challengeMessage = GetChallengeMessage(challengeMessageBytes); if (challengeMessage == null) { return null; @@ -125,6 +86,11 @@ public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainNa authenticateMessage.NegotiateFlags |= NegotiateFlags.OEMEncoding; } + if ((challengeMessage.NegotiateFlags & NegotiateFlags.Seal) > 0) + { + authenticateMessage.NegotiateFlags |= NegotiateFlags.Seal; + } + if ((challengeMessage.NegotiateFlags & NegotiateFlags.KeyExchange) > 0) { authenticateMessage.NegotiateFlags |= NegotiateFlags.KeyExchange; @@ -139,6 +105,11 @@ public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainNa authenticateMessage.NegotiateFlags |= NegotiateFlags.ExtendedSessionSecurity; } + if (userName == String.Empty && password == String.Empty) + { + authenticateMessage.NegotiateFlags |= NegotiateFlags.Anonymous; + } + authenticateMessage.UserName = userName; authenticateMessage.DomainName = domainName; authenticateMessage.WorkStation = Environment.MachineName; @@ -146,7 +117,14 @@ public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainNa byte[] keyExchangeKey; if (authenticationMethod == AuthenticationMethod.NTLMv1 || authenticationMethod == AuthenticationMethod.NTLMv1ExtendedSessionSecurity) { - if (authenticationMethod == AuthenticationMethod.NTLMv1) +#if ENABLE_NTLMV1 + // https://msdn.microsoft.com/en-us/library/cc236699.aspx + if (userName == String.Empty && password == String.Empty) + { + authenticateMessage.LmChallengeResponse = new byte[1]; + authenticateMessage.NtChallengeResponse = new byte[0]; + } + else if (authenticationMethod == AuthenticationMethod.NTLMv1) { authenticateMessage.LmChallengeResponse = NTLMCryptography.ComputeLMv1Response(challengeMessage.ServerChallenge, password); authenticateMessage.NtChallengeResponse = NTLMCryptography.ComputeNTLMv1Response(challengeMessage.ServerChallenge, password); @@ -156,25 +134,36 @@ public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainNa authenticateMessage.LmChallengeResponse = ByteUtils.Concatenate(clientChallenge, new byte[16]); authenticateMessage.NtChallengeResponse = NTLMCryptography.ComputeNTLMv1ExtendedSessionSecurityResponse(challengeMessage.ServerChallenge, clientChallenge, password); } - // https://msdn.microsoft.com/en-us/library/cc236699.aspx + sessionBaseKey = new MD4().GetByteHashFromBytes(NTLMCryptography.NTOWFv1(password)); byte[] lmowf = NTLMCryptography.LMOWFv1(password); keyExchangeKey = NTLMCryptography.KXKey(sessionBaseKey, authenticateMessage.NegotiateFlags, authenticateMessage.LmChallengeResponse, challengeMessage.ServerChallenge, lmowf); +#else + throw new NotSupportedException("NTLM v1 support has not been enabled"); +#endif } else // NTLMv2 { - NTLMv2ClientChallenge clientChallengeStructure = new NTLMv2ClientChallenge(time, clientChallenge, challengeMessage.TargetInfo); + // https://msdn.microsoft.com/en-us/library/cc236700.aspx + NTLMv2ClientChallenge clientChallengeStructure = new NTLMv2ClientChallenge(time, clientChallenge, challengeMessage.TargetInfo, spn); byte[] clientChallengeStructurePadded = clientChallengeStructure.GetBytesPadded(); byte[] ntProofStr = NTLMCryptography.ComputeNTLMv2Proof(challengeMessage.ServerChallenge, clientChallengeStructurePadded, password, userName, domainName); - - authenticateMessage.LmChallengeResponse = NTLMCryptography.ComputeLMv2Response(challengeMessage.ServerChallenge, clientChallenge, password, userName, challengeMessage.TargetName); - authenticateMessage.NtChallengeResponse = ByteUtils.Concatenate(ntProofStr, clientChallengeStructurePadded); - - // https://msdn.microsoft.com/en-us/library/cc236700.aspx + if (userName == String.Empty && password == String.Empty) + { + authenticateMessage.LmChallengeResponse = new byte[1]; + authenticateMessage.NtChallengeResponse = new byte[0]; + } + else + { + authenticateMessage.LmChallengeResponse = NTLMCryptography.ComputeLMv2Response(challengeMessage.ServerChallenge, clientChallenge, password, userName, challengeMessage.TargetName); + authenticateMessage.NtChallengeResponse = ByteUtils.Concatenate(ntProofStr, clientChallengeStructurePadded); + } + byte[] responseKeyNT = NTLMCryptography.NTOWFv2(password, userName, domainName); sessionBaseKey = new HMACMD5(responseKeyNT).ComputeHash(ntProofStr); keyExchangeKey = sessionBaseKey; } + authenticateMessage.Version = NTLMVersion.Server2003; // https://msdn.microsoft.com/en-us/library/cc236676.aspx @@ -189,16 +178,8 @@ public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainNa sessionKey = keyExchangeKey; } - if (useGSSAPI) - { - SimpleProtectedNegotiationTokenResponse outputToken = new SimpleProtectedNegotiationTokenResponse(); - outputToken.ResponseToken = authenticateMessage.GetBytes(); - return outputToken.GetBytes(); - } - else - { - return authenticateMessage.GetBytes(); - } + authenticateMessage.CalculateMIC(sessionKey, negotiateMessageBytes, challengeMessageBytes); + return authenticateMessage.GetBytes(); } private static ChallengeMessage GetChallengeMessage(byte[] messageBytes) @@ -220,17 +201,5 @@ private static ChallengeMessage GetChallengeMessage(byte[] messageBytes) } return null; } - - private static bool ContainsMechanism(SimpleProtectedNegotiationTokenInit token, byte[] mechanismIdentifier) - { - for (int index = 0; index < token.MechanismTypeList.Count; index++) - { - if (ByteUtils.AreByteArraysEqual(token.MechanismTypeList[index], GSSProvider.NTLMSSPIdentifier)) - { - return true; - } - } - return false; - } } } diff --git a/SMBLibrary/Client/Helpers/ServerServiceHelper.cs b/SMBLibrary/Client/Helpers/ServerServiceHelper.cs index fdc0921c..39d06458 100644 --- a/SMBLibrary/Client/Helpers/ServerServiceHelper.cs +++ b/SMBLibrary/Client/Helpers/ServerServiceHelper.cs @@ -20,7 +20,7 @@ public static List ListShares(INTFileStore namedPipeShare, ShareType? sh } /// - /// When a Windows Server host is using Failover Cluster & Cluster Shared Volumes, each of those CSV file shares is associated + /// When a Windows Server host is using Failover Cluster and Cluster Shared Volumes, each of those CSV file shares is associated /// with a specific host name associated with the cluster and is not accessible using the node IP address or node host name. /// public static List ListShares(INTFileStore namedPipeShare, string serverName, ShareType? shareType, out NTStatus status) @@ -38,7 +38,7 @@ public static List ListShares(INTFileStore namedPipeShare, string server shareEnumRequest.InfoStruct.Level = 1; shareEnumRequest.InfoStruct.Info = new ShareInfo1Container(); shareEnumRequest.PreferedMaximumLength = UInt32.MaxValue; - shareEnumRequest.ServerName = serverName; + shareEnumRequest.ServerName = @"\\" + serverName; RequestPDU requestPDU = new RequestPDU(); requestPDU.Flags = PacketFlags.FirstFragment | PacketFlags.LastFragment; requestPDU.DataRepresentation.CharacterFormat = CharacterFormat.ASCII; diff --git a/SMBLibrary/Client/ISMBClient.cs b/SMBLibrary/Client/ISMBClient.cs index 7ff3c85b..52119a42 100644 --- a/SMBLibrary/Client/ISMBClient.cs +++ b/SMBLibrary/Client/ISMBClient.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2021 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2025 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, @@ -28,6 +28,8 @@ public interface ISMBClient ISMBFileStore TreeConnect(string shareName, out NTStatus status); + NTStatus Echo(); + uint MaxReadSize { get; @@ -37,5 +39,10 @@ uint MaxWriteSize { get; } + + bool IsConnected + { + get; + } } } diff --git a/SMBLibrary/Client/NameServiceClient.cs b/SMBLibrary/Client/NameServiceClient.cs index e67ecd0d..e4e6c47c 100644 --- a/SMBLibrary/Client/NameServiceClient.cs +++ b/SMBLibrary/Client/NameServiceClient.cs @@ -32,7 +32,7 @@ public string GetServerName() foreach (KeyValuePair entry in response.Names) { NetBiosSuffix suffix = NetBiosUtils.GetSuffixFromMSNetBiosName(entry.Key); - if (suffix == NetBiosSuffix.FileServiceService) + if (suffix == NetBiosSuffix.FileServerService) { return entry.Key; } diff --git a/SMBLibrary/Client/SMB1Client.cs b/SMBLibrary/Client/SMB1Client.cs index c3328fa6..500a2a3e 100644 --- a/SMBLibrary/Client/SMB1Client.cs +++ b/SMBLibrary/Client/SMB1Client.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2021 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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, @@ -9,8 +9,10 @@ using System.Diagnostics; using System.Net; using System.Net.Sockets; +using System.Security.Cryptography; using System.Threading; using SMBLibrary.Authentication.NTLM; +using SMBLibrary.Client.Authentication; using SMBLibrary.NetBios; using SMBLibrary.Services; using SMBLibrary.SMB1; @@ -21,18 +23,19 @@ namespace SMBLibrary.Client public class SMB1Client : ISMBClient { private const string NTLanManagerDialect = "NT LM 0.12"; - + public static readonly int NetBiosOverTCPPort = 139; public static readonly int DirectTCPPort = 445; private static readonly ushort ClientMaxBufferSize = 65535; // Valid range: 512 - 65535 private static readonly ushort ClientMaxMpxCount = 1; - private static readonly int ResponseTimeoutInMilliseconds = 5000; + private static readonly int DefaultResponseTimeoutInMilliseconds = 5000; private SMBTransportType m_transport; private bool m_isConnected; private bool m_isLoggedIn; private Socket m_clientSocket; + private ConnectionState m_connectionState; private bool m_forceExtendedSecurity; private bool m_unicode; private bool m_largeFiles; @@ -41,6 +44,7 @@ public class SMB1Client : ISMBClient private bool m_largeWrite; private uint m_serverMaxBufferSize; private ushort m_maxMpxCount; + private int m_responseTimeoutInMilliseconds; private object m_incomingQueueLock = new object(); private List m_incomingQueue = new List(); @@ -54,18 +58,23 @@ public class SMB1Client : ISMBClient private byte[] m_securityBlob; private byte[] m_sessionKey; - public SMB1Client() + public SMB1Client() : this(DefaultResponseTimeoutInMilliseconds) + { + } + + public SMB1Client(int responseTimeoutInMilliseconds) { + m_responseTimeoutInMilliseconds = responseTimeoutInMilliseconds; } public bool Connect(string serverName, SMBTransportType transport) { - IPHostEntry hostEntry = Dns.GetHostEntry(serverName); - if (hostEntry.AddressList.Length == 0) + IPAddress[] hostAddresses = Dns.GetHostAddresses(serverName); + if (hostAddresses.Length == 0) { throw new Exception(String.Format("Cannot resolve host name {0} to an IP address", serverName)); } - IPAddress serverAddress = hostEntry.AddressList[0]; + IPAddress serverAddress = IPAddressHelper.SelectAddressPreferIPv4(hostAddresses); return Connect(serverAddress, transport); } @@ -75,30 +84,26 @@ public bool Connect(IPAddress serverAddress, SMBTransportType transport) } public bool Connect(IPAddress serverAddress, SMBTransportType transport, bool forceExtendedSecurity) + { + int port = (transport == SMBTransportType.DirectTCPTransport ? DirectTCPPort : NetBiosOverTCPPort); + return Connect(serverAddress, transport, port, forceExtendedSecurity); + } + + protected internal bool Connect(IPAddress serverAddress, SMBTransportType transport, int port, bool forceExtendedSecurity) { m_transport = transport; if (!m_isConnected) { m_forceExtendedSecurity = forceExtendedSecurity; - 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.CalledName = NetBiosUtils.GetMSNetBiosName("*SMBSERVER", NetBiosSuffix.FileServerService); sessionRequest.CallingName = NetBiosUtils.GetMSNetBiosName(Environment.MachineName, NetBiosSuffix.WorkstationService); TrySendPacket(m_clientSocket, sessionRequest); @@ -111,8 +116,7 @@ public bool Connect(IPAddress serverAddress, SMBTransportType transport, bool fo return false; } - NameServiceClient nameServiceClient = new NameServiceClient(serverAddress); - string serverName = nameServiceClient.GetServerName(); + string serverName = GetNetBiosServerName(serverAddress); if (serverName == null) { return false; @@ -142,10 +146,16 @@ public bool Connect(IPAddress serverAddress, SMBTransportType transport, bool fo return m_isConnected; } + protected virtual string GetNetBiosServerName(IPAddress serverAddress) + { + NameServiceClient nameServiceClient = new NameServiceClient(serverAddress); + return nameServiceClient.GetServerName(); + } + private bool ConnectSocket(IPAddress serverAddress, int port) { - m_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); - + m_clientSocket = new Socket(serverAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); + try { m_clientSocket.Connect(serverAddress, port); @@ -155,9 +165,9 @@ private bool ConnectSocket(IPAddress serverAddress, int port) 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); + m_connectionState = new ConnectionState(m_clientSocket); + NBTConnectionReceiveBuffer buffer = m_connectionState.ReceiveBuffer; + m_clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), m_connectionState); return true; } @@ -165,8 +175,14 @@ public void Disconnect() { if (m_isConnected) { - m_clientSocket.Disconnect(false); + lock (m_connectionState.ReceiveBuffer) + { + m_clientSocket.Disconnect(false); + m_clientSocket.Close(); + m_connectionState.ReceiveBuffer.Dispose(); + } m_isConnected = false; + m_userID = 0; } } @@ -256,10 +272,15 @@ public NTStatus Login(string domainName, string userName, string password, Authe request.PrimaryDomain = domainName; byte[] clientChallenge = new byte[8]; new Random().NextBytes(clientChallenge); + byte[] proofStr = null; if (authenticationMethod == AuthenticationMethod.NTLMv1) { +#if ENABLE_NTLMV1 request.OEMPassword = NTLMCryptography.ComputeLMv1Response(m_serverChallenge, password); request.UnicodePassword = NTLMCryptography.ComputeNTLMv1Response(m_serverChallenge, password); +#else + throw new NotSupportedException("NTLM v1 support has not been enabled"); +#endif } else if (authenticationMethod == AuthenticationMethod.NTLMv1ExtendedSessionSecurity) { @@ -273,24 +294,34 @@ public NTStatus Login(string domainName, string userName, string password, Authe // https://msdn.microsoft.com/en-us/library/cc236700.aspx request.OEMPassword = NTLMCryptography.ComputeLMv2Response(m_serverChallenge, clientChallenge, password, userName, domainName); NTLMv2ClientChallenge clientChallengeStructure = new NTLMv2ClientChallenge(DateTime.UtcNow, clientChallenge, AVPairUtils.GetAVPairSequence(domainName, Environment.MachineName)); - byte[] temp = clientChallengeStructure.GetBytesPadded(); - byte[] proofStr = NTLMCryptography.ComputeNTLMv2Proof(m_serverChallenge, temp, password, userName, domainName); - request.UnicodePassword = ByteUtils.Concatenate(proofStr, temp); + byte[] clientChallengeStructurePadded = clientChallengeStructure.GetBytesPadded(); + proofStr = NTLMCryptography.ComputeNTLMv2Proof(m_serverChallenge, clientChallengeStructurePadded, password, userName, domainName); + request.UnicodePassword = ByteUtils.Concatenate(proofStr, clientChallengeStructurePadded); } - + TrySendMessage(request); SMB1Message reply = WaitForMessage(CommandName.SMB_COM_SESSION_SETUP_ANDX); if (reply != null) { m_isLoggedIn = (reply.Header.Status == NTStatus.STATUS_SUCCESS); + if (m_isLoggedIn) + { + m_userID = reply.Header.UID; + m_sessionKey = +#if ENABLE_NTLMV1 + (authenticationMethod == AuthenticationMethod.NTLMv1) ? new MD4().GetByteHashFromBytes(NTLMCryptography.NTOWFv1(password)) : +#endif + new HMACMD5(NTLMCryptography.NTOWFv2(password, userName, domainName)).ComputeHash(proofStr); + } return reply.Header.Status; } return NTStatus.STATUS_INVALID_SMB; } else // m_securityBlob != null { - byte[] negotiateMessage = NTLMAuthenticationHelper.GetNegotiateMessage(m_securityBlob, domainName, authenticationMethod); + NTLMAuthenticationClient authenticationClient = new NTLMAuthenticationClient(domainName, userName, password, null, authenticationMethod); + byte[] negotiateMessage = authenticationClient.InitializeSecurityContext(m_securityBlob); if (negotiateMessage == null) { return NTStatus.SEC_E_INVALID_TOKEN; @@ -304,38 +335,43 @@ public NTStatus Login(string domainName, string userName, string password, Authe TrySendMessage(request); SMB1Message reply = WaitForMessage(CommandName.SMB_COM_SESSION_SETUP_ANDX); - if (reply != null) + while (reply != null && reply.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED && reply.Commands[0] is SessionSetupAndXResponseExtended) { - if (reply.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED && reply.Commands[0] is SessionSetupAndXResponseExtended) + SessionSetupAndXResponseExtended response = (SessionSetupAndXResponseExtended)reply.Commands[0]; + byte[] authenticateMessage = authenticationClient.InitializeSecurityContext(response.SecurityBlob); + if (authenticateMessage == null) { - SessionSetupAndXResponseExtended response = (SessionSetupAndXResponseExtended)reply.Commands[0]; - byte[] authenticateMessage = NTLMAuthenticationHelper.GetAuthenticateMessage(response.SecurityBlob, domainName, userName, password, authenticationMethod, out m_sessionKey); - if (authenticateMessage == null) - { - return NTStatus.SEC_E_INVALID_TOKEN; - } - - m_userID = reply.Header.UID; - request = new SessionSetupAndXRequestExtended(); - request.MaxBufferSize = ClientMaxBufferSize; - request.MaxMpxCount = m_maxMpxCount; - request.Capabilities = clientCapabilities; - request.SecurityBlob = authenticateMessage; - TrySendMessage(request); - - reply = WaitForMessage(CommandName.SMB_COM_SESSION_SETUP_ANDX); - if (reply != null) - { - m_isLoggedIn = (reply.Header.Status == NTStatus.STATUS_SUCCESS); - return reply.Header.Status; - } + return NTStatus.SEC_E_INVALID_TOKEN; } - else + + m_userID = reply.Header.UID; + request = new SessionSetupAndXRequestExtended(); + request.MaxBufferSize = ClientMaxBufferSize; + request.MaxMpxCount = m_maxMpxCount; + request.Capabilities = clientCapabilities; + request.SecurityBlob = authenticateMessage; + TrySendMessage(request); + + reply = WaitForMessage(CommandName.SMB_COM_SESSION_SETUP_ANDX); + } + + if (reply != null && reply.Commands[0] is ErrorResponse) + { + return reply.Header.Status; + } + else if (reply != null && reply.Commands[0] is SessionSetupAndXResponseExtended) + { + m_isLoggedIn = (reply.Header.Status == NTStatus.STATUS_SUCCESS); + if (m_isLoggedIn) { - return reply.Header.Status; + m_sessionKey = authenticationClient.GetSessionKey(); } + return reply.Header.Status; + } + else + { + return NTStatus.STATUS_INVALID_SMB; } - return NTStatus.STATUS_INVALID_SMB; } } @@ -409,59 +445,85 @@ public SMB1FileStore TreeConnect(string shareName, ServiceName serviceName, out return null; } - private void OnClientSocketReceive(IAsyncResult ar) + public NTStatus Echo() { - 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) + EchoRequest request = new EchoRequest(); + request.EchoCount = 1; + TrySendMessage(request); + SMB1Message reply = WaitForMessage(CommandName.SMB_COM_ECHO); + if (reply != null) { - Log("[ReceiveCallback] EndReceive ObjectDisposedException"); - return; + return reply.Header.Status; } - catch (SocketException ex) + else { - Log("[ReceiveCallback] EndReceive SocketException: " + ex.Message); - return; + return NTStatus.STATUS_INVALID_SMB; } + } - if (numberOfBytesReceived == 0) - { - m_isConnected = false; - } - else - { - NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; - buffer.SetNumberOfBytesReceived(numberOfBytesReceived); - ProcessConnectionBuffer(state); + private void OnClientSocketReceive(IAsyncResult ar) + { + ConnectionState state = (ConnectionState)ar.AsyncState; + Socket clientSocket = state.ClientSocket; + lock (state.ReceiveBuffer) + { + int numberOfBytesReceived = 0; try { - clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); + numberOfBytesReceived = clientSocket.EndReceive(ar); + } + catch (ArgumentException) // The IAsyncResult object was not returned from the corresponding synchronous method on this class. + { + m_isConnected = false; + state.ReceiveBuffer.Dispose(); + return; } catch (ObjectDisposedException) { m_isConnected = false; - Log("[ReceiveCallback] BeginReceive ObjectDisposedException"); + Log("[ReceiveCallback] EndReceive ObjectDisposedException"); + state.ReceiveBuffer.Dispose(); + return; } catch (SocketException ex) { m_isConnected = false; - Log("[ReceiveCallback] BeginReceive SocketException: " + ex.Message); + Log("[ReceiveCallback] EndReceive SocketException: " + ex.Message); + state.ReceiveBuffer.Dispose(); + return; + } + + if (numberOfBytesReceived == 0) + { + m_isConnected = false; + state.ReceiveBuffer.Dispose(); + } + else if (clientSocket.Connected) + { + NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; + buffer.SetNumberOfBytesReceived(numberOfBytesReceived); + ProcessConnectionBuffer(state); + + if (clientSocket.Connected) + { + try + { + clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); + } + catch (ObjectDisposedException) + { + m_isConnected = false; + buffer.Dispose(); + Log("[ReceiveCallback] BeginReceive ObjectDisposedException"); + } + catch (SocketException ex) + { + m_isConnected = false; + buffer.Dispose(); + Log("[ReceiveCallback] BeginReceive SocketException: " + ex.Message); + } + } } } } @@ -478,7 +540,9 @@ private void ProcessConnectionBuffer(ConnectionState state) } catch (Exception) { + Log("[ProcessConnectionBuffer] Invalid packet"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); break; } @@ -502,6 +566,7 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) { Log("Invalid SMB1 message: " + ex.Message); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); m_isConnected = false; return; } @@ -532,14 +597,21 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) { Log("Inappropriate NetBIOS session packet"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); } } internal SMB1Message WaitForMessage(CommandName commandName) { + return WaitForMessage(commandName, out bool _); + } + + internal SMB1Message WaitForMessage(CommandName commandName, out bool connectionTerminated) + { + connectionTerminated = false; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - while (stopwatch.ElapsedMilliseconds < ResponseTimeoutInMilliseconds) + while (stopwatch.ElapsedMilliseconds < m_responseTimeoutInMilliseconds && !(connectionTerminated = !m_clientSocket.Connected)) { lock (m_incomingQueueLock) { @@ -561,10 +633,9 @@ internal SMB1Message WaitForMessage(CommandName commandName) internal SessionPacket WaitForSessionResponsePacket() { - const int TimeOut = 5000; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - while (stopwatch.ElapsedMilliseconds < TimeOut) + while (stopwatch.ElapsedMilliseconds < m_responseTimeoutInMilliseconds) { if (m_sessionResponsePacket != null) { @@ -678,14 +749,30 @@ public uint MaxWriteSize } } - public static void TrySendMessage(Socket socket, SMB1Message message) + public SMBTransportType Transport + { + get + { + return m_transport; + } + } + + public bool IsConnected + { + get + { + return m_isConnected; + } + } + + private void TrySendMessage(Socket socket, SMB1Message message) { SessionMessagePacket packet = new SessionMessagePacket(); packet.Trailer = message.GetBytes(); TrySendPacket(socket, packet); } - public static void TrySendPacket(Socket socket, SessionPacket packet) + private void TrySendPacket(Socket socket, SessionPacket packet) { try { @@ -694,9 +781,11 @@ public static void TrySendPacket(Socket socket, SessionPacket packet) } catch (SocketException) { + m_isConnected = false; } catch (ObjectDisposedException) { + m_isConnected = false; } } } diff --git a/SMBLibrary/Client/SMB1FileStore.cs b/SMBLibrary/Client/SMB1FileStore.cs index c0a41301..ae05e57f 100644 --- a/SMBLibrary/Client/SMB1FileStore.cs +++ b/SMBLibrary/Client/SMB1FileStore.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2019 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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, @@ -36,7 +36,7 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string request.ImpersonationLevel = ImpersonationLevel.Impersonation; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_NT_CREATE_ANDX); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_NT_CREATE_ANDX, out bool connectionTerminated); if (reply != null) { if (reply.Commands[0] is NTCreateAndXResponse) @@ -51,7 +51,7 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string return reply.Header.Status; } } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus CloseFile(object handle) @@ -59,12 +59,12 @@ public NTStatus CloseFile(object handle) CloseRequest request = new CloseRequest(); request.FID = (ushort)handle; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_CLOSE); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_CLOSE, out bool connectionTerminated); if (reply != null) { return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCount) @@ -76,7 +76,7 @@ public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCou request.MaxCountLarge = (uint)maxCount; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_READ_ANDX); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_READ_ANDX, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is ReadAndXResponse) @@ -85,7 +85,7 @@ public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCou } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offset, byte[] data) @@ -97,7 +97,7 @@ public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offs request.Data = data; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_WRITE_ANDX); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_WRITE_ANDX, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is WriteAndXResponse) @@ -106,7 +106,7 @@ public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offs } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus FlushFileBuffers(object handle) @@ -150,7 +150,7 @@ public NTStatus QueryDirectory(out List result, string fileName request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) @@ -180,8 +180,12 @@ public NTStatus QueryDirectory(out List result, string fileName request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); - if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) + reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out connectionTerminated); + if (reply == null) + { + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; + } + else if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) { response = (Transaction2Response)reply.Commands[0]; Transaction2FindNext2Response nextSubcommandResponse = new Transaction2FindNext2Response(response.TransParameters, response.TransData, reply.Header.UnicodeFlag); @@ -197,7 +201,7 @@ public NTStatus QueryDirectory(out List result, string fileName } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus GetFileInformation(out FileInformation result, object handle, FileInformationClass informationClass) @@ -220,7 +224,7 @@ public NTStatus GetFileInformation(out FileInformation result, object handle, Fi request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) @@ -240,7 +244,7 @@ public NTStatus GetFileInformation(out FileInformation result, object handle, Fi } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } else { @@ -273,7 +277,7 @@ public NTStatus GetFileInformation(out QueryInformation result, object handle, Q request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) @@ -284,7 +288,7 @@ public NTStatus GetFileInformation(out QueryInformation result, object handle, Q } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus SetFileInformation(object handle, FileInformation information) @@ -315,12 +319,12 @@ public NTStatus SetFileInformation(object handle, FileInformation information) request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } else { @@ -345,12 +349,12 @@ public NTStatus SetFileInformation(object handle, SetInformation information) request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileSystemInformationClass informationClass) @@ -372,7 +376,7 @@ public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileS request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) @@ -383,7 +387,7 @@ public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileS } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } else { @@ -408,7 +412,7 @@ public NTStatus GetFileSystemInformation(out QueryFSInformation result, QueryFSI request.MaxDataCount = (ushort)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION2, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is Transaction2Response) @@ -419,7 +423,7 @@ public NTStatus GetFileSystemInformation(out QueryFSInformation result, QueryFSI } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus SetFileSystemInformation(FileSystemInformation information) @@ -446,7 +450,7 @@ public NTStatus GetSecurityInformation(out SecurityDescriptor result, object han request.MaxDataCount = (uint)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_NT_TRANSACT); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_NT_TRANSACT, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is NTTransactResponse) @@ -457,7 +461,7 @@ public NTStatus GetSecurityInformation(out SecurityDescriptor result, object han } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus SetSecurityInformation(object handle, SecurityInformation securityInformation, SecurityDescriptor securityDescriptor) @@ -500,7 +504,7 @@ public NTStatus DeviceIOControl(object handle, uint ctlCode, byte[] input, out b request.MaxDataCount = (uint)maxOutputLength; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_NT_TRANSACT); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_NT_TRANSACT, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is NTTransactResponse) @@ -511,7 +515,7 @@ public NTStatus DeviceIOControl(object handle, uint ctlCode, byte[] input, out b } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus FsCtlPipeTranscieve(object handle, byte[] input, out byte[] output, int maxOutputLength) @@ -532,7 +536,7 @@ public NTStatus FsCtlPipeTranscieve(object handle, byte[] input, out byte[] outp request.Name = @"\PIPE\"; TrySendMessage(request); - SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION); + SMB1Message reply = m_client.WaitForMessage(CommandName.SMB_COM_TRANSACTION, out bool connectionTerminated); if (reply != null) { if (reply.Header.Status == NTStatus.STATUS_SUCCESS && reply.Commands[0] is TransactionResponse) @@ -543,7 +547,7 @@ public NTStatus FsCtlPipeTranscieve(object handle, byte[] input, out byte[] outp } return reply.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus Disconnect() @@ -560,6 +564,10 @@ public NTStatus Disconnect() private void TrySendMessage(SMB1Command request) { + if (!m_client.IsConnected) + { + throw new InvalidOperationException("The client is no longer connected"); + } m_client.TrySendMessage(request, m_treeID); } diff --git a/SMBLibrary/Client/SMB2Client.cs b/SMBLibrary/Client/SMB2Client.cs index f8c7dea9..05cd549d 100644 --- a/SMBLibrary/Client/SMB2Client.cs +++ b/SMBLibrary/Client/SMB2Client.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2021 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2026 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, @@ -10,6 +10,8 @@ using System.Net; using System.Net.Sockets; using System.Threading; +using SMBLibrary.Client.Authentication; +using SMBLibrary.Client.DFS; using SMBLibrary.NetBios; using SMBLibrary.SMB2; using Utilities; @@ -25,13 +27,16 @@ public class SMB2Client : ISMBClient public static readonly uint ClientMaxReadSize = 1048576; public static readonly uint ClientMaxWriteSize = 1048576; private static readonly ushort DesiredCredits = 16; - public static readonly int ResponseTimeoutInMilliseconds = 5000; + public static readonly int DefaultResponseTimeoutInMilliseconds = 5000; private string m_serverName; private SMBTransportType m_transport; private bool m_isConnected; private bool m_isLoggedIn; private Socket m_clientSocket; + private ConnectionState m_connectionState; + private int m_responseTimeoutInMilliseconds; + private bool m_enableSMB311Support = false; private object m_incomingQueueLock = new object(); private List m_incomingQueue = new List(); @@ -53,29 +58,52 @@ public class SMB2Client : ISMBClient private ulong m_sessionID; private byte[] m_securityBlob; private byte[] m_sessionKey; + private byte[] m_preauthIntegrityHashValue; // SMB 3.1.1 private ushort m_availableCredits = 1; + private bool m_connectionSupportsMultiCredit = false; + private IAuthenticationClient m_authenticationClient; - public SMB2Client() + public SMB2Client() : this(DefaultResponseTimeoutInMilliseconds) { } + public SMB2Client(int responseTimeoutInMilliseconds) : this(responseTimeoutInMilliseconds, false) + { + } + + public SMB2Client(bool enableSMB311Support) : this(DefaultResponseTimeoutInMilliseconds, enableSMB311Support) + { + } + + public SMB2Client(int responseTimeoutInMilliseconds, bool enableSMB311Support) + { + m_responseTimeoutInMilliseconds = responseTimeoutInMilliseconds; + m_enableSMB311Support = enableSMB311Support; + } + /// - /// When a Windows Server host is using Failover Cluster & Cluster Shared Volumes, each of those CSV file shares is associated + /// When a Windows Server host is using Failover Cluster and Cluster Shared Volumes, each of those CSV file shares is associated /// with a specific host name associated with the cluster and is not accessible using the node IP address or node host name. /// public bool Connect(string serverName, SMBTransportType transport) { m_serverName = serverName; - IPHostEntry hostEntry = Dns.GetHostEntry(serverName); - if (hostEntry.AddressList.Length == 0) + IPAddress[] hostAddresses = Dns.GetHostAddresses(serverName); + if (hostAddresses.Length == 0) { throw new Exception(String.Format("Cannot resolve host name {0} to an IP address", serverName)); } - IPAddress serverAddress = hostEntry.AddressList[0]; + IPAddress serverAddress = IPAddressHelper.SelectAddressPreferIPv4(hostAddresses); return Connect(serverAddress, transport); } public bool Connect(IPAddress serverAddress, SMBTransportType transport) + { + int port = (transport == SMBTransportType.DirectTCPTransport ? DirectTCPPort : NetBiosOverTCPPort); + return Connect(serverAddress, transport, port); + } + + protected internal bool Connect(IPAddress serverAddress, SMBTransportType transport, int port) { if (m_serverName == null) { @@ -85,16 +113,6 @@ 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; @@ -103,7 +121,7 @@ public bool Connect(IPAddress serverAddress, SMBTransportType transport) if (transport == SMBTransportType.NetBiosOverTCP) { SessionRequestPacket sessionRequest = new SessionRequestPacket(); - sessionRequest.CalledName = NetBiosUtils.GetMSNetBiosName("*SMBSERVER", NetBiosSuffix.FileServiceService); + sessionRequest.CalledName = NetBiosUtils.GetMSNetBiosName("*SMBSERVER", NetBiosSuffix.FileServerService); sessionRequest.CallingName = NetBiosUtils.GetMSNetBiosName(Environment.MachineName, NetBiosSuffix.WorkstationService); TrySendPacket(m_clientSocket, sessionRequest); @@ -116,8 +134,7 @@ public bool Connect(IPAddress serverAddress, SMBTransportType transport) return false; } - NameServiceClient nameServiceClient = new NameServiceClient(serverAddress); - string serverName = nameServiceClient.GetServerName(); + string serverName = GetNetBiosServerName(serverAddress); if (serverName == null) { return false; @@ -147,9 +164,15 @@ public bool Connect(IPAddress serverAddress, SMBTransportType transport) return m_isConnected; } + protected virtual string GetNetBiosServerName(IPAddress serverAddress) + { + NameServiceClient nameServiceClient = new NameServiceClient(serverAddress); + return nameServiceClient.GetServerName(); + } + private bool ConnectSocket(IPAddress serverAddress, int port) { - m_clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + m_clientSocket = new Socket(serverAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); try { @@ -160,9 +183,9 @@ private bool ConnectSocket(IPAddress serverAddress, int port) 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); + m_connectionState = new ConnectionState(m_clientSocket); + NBTConnectionReceiveBuffer buffer = m_connectionState.ReceiveBuffer; + m_clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), m_connectionState); return true; } @@ -170,8 +193,17 @@ public void Disconnect() { if (m_isConnected) { - m_clientSocket.Disconnect(false); + lock (m_connectionState.ReceiveBuffer) + { + m_clientSocket.Disconnect(false); + m_clientSocket.Close(); + m_connectionState.ReceiveBuffer.Dispose(); + } m_isConnected = false; + m_messageID = 0; + m_sessionID = 0; + m_availableCredits = 1; + m_connectionSupportsMultiCredit = false; } } @@ -185,13 +217,23 @@ private bool NegotiateDialect() request.Dialects.Add(SMB2Dialect.SMB202); request.Dialects.Add(SMB2Dialect.SMB210); request.Dialects.Add(SMB2Dialect.SMB300); + request.Dialects.Add(SMB2Dialect.SMB302); + if (m_enableSMB311Support) + { + request.Dialects.Add(SMB2Dialect.SMB311); + request.NegotiateContextList = GetNegotiateContextList(); + m_preauthIntegrityHashValue = new byte[64]; + } TrySendCommand(request); NegotiateResponse response = WaitForCommand(request.MessageID) as NegotiateResponse; if (response != null && response.Header.Status == NTStatus.STATUS_SUCCESS) { m_dialect = response.DialectRevision; - m_signingRequired = (response.SecurityMode & SecurityMode.SigningRequired) > 0; + // [MS-SMB2] 3.3.5.7 If Connection.Dialect is "3.1.1" and Session.IsAnonymous and Session.IsGuest + // are set to FALSE and the request is not signed or not encrypted, then the server MUST disconnect the connection. + m_signingRequired = (response.SecurityMode & SecurityMode.SigningRequired) > 0 || + response.DialectRevision == SMB2Dialect.SMB311; m_maxTransactSize = Math.Min(response.MaxTransactSize, ClientMaxTransactSize); m_maxReadSize = Math.Min(response.MaxReadSize, ClientMaxReadSize); m_maxWriteSize = Math.Min(response.MaxWriteSize, ClientMaxWriteSize); @@ -207,13 +249,20 @@ public NTStatus Login(string domainName, string userName, string password) } public NTStatus Login(string domainName, string userName, string password, AuthenticationMethod authenticationMethod) + { + string spn = CreateSpn(m_serverName); + NTLMAuthenticationClient authenticationClient = new NTLMAuthenticationClient(domainName, userName, password, spn, authenticationMethod); + return Login(authenticationClient); + } + + public NTStatus Login(IAuthenticationClient authenticationClient) { if (!m_isConnected) { throw new InvalidOperationException("A connection must be successfully established before attempting login"); } - byte[] negotiateMessage = NTLMAuthenticationHelper.GetNegotiateMessage(m_securityBlob, domainName, authenticationMethod); + byte[] negotiateMessage = authenticationClient.InitializeSecurityContext(m_securityBlob); if (negotiateMessage == null) { return NTStatus.SEC_E_INVALID_TOKEN; @@ -224,44 +273,59 @@ public NTStatus Login(string domainName, string userName, string password, Authe request.SecurityBuffer = negotiateMessage; TrySendCommand(request); SMB2Command response = WaitForCommand(request.MessageID); - if (response != null) + while (response is SessionSetupResponse sessionSetupResponse && response.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED) { - if (response.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED && response is SessionSetupResponse) + byte[] authenticateMessage = authenticationClient.InitializeSecurityContext(sessionSetupResponse.SecurityBuffer); + if (authenticateMessage == null) { - 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(request.MessageID); + } + + if (response is ErrorResponse) + { + return response.Header.Status; + } + else if (response is SessionSetupResponse finalSessionSetupResponse) + { + m_isLoggedIn = (response.Header.Status == NTStatus.STATUS_SUCCESS); + if (m_isLoggedIn) + { + m_sessionID = response.Header.SessionID; + m_sessionKey = authenticationClient.GetSessionKey(); + m_authenticationClient = authenticationClient; + SessionFlags sessionFlags = finalSessionSetupResponse.SessionFlags; + if ((sessionFlags & SessionFlags.IsGuest) > 0) { - return NTStatus.SEC_E_INVALID_TOKEN; + // [MS-SMB2] 3.2.5.3.1 If the SMB2_SESSION_FLAG_IS_GUEST bit is set in the SessionFlags field of the SMB2 + // SESSION_SETUP Response and if RequireMessageSigning is FALSE, Session.SigningRequired MUST be set to FALSE. + m_signingRequired = false; + } + else + { + m_signingKey = SMB2Cryptography.GenerateSigningKey(m_sessionKey, m_dialect, m_preauthIntegrityHashValue); } - m_sessionID = response.Header.SessionID; - request = new SessionSetupRequest(); - request.SecurityMode = SecurityMode.SigningEnabled; - request.SecurityBuffer = authenticateMessage; - TrySendCommand(request); - response = WaitForCommand(request.MessageID); - if (response != null) + if (m_dialect >= SMB2Dialect.SMB300) { - m_isLoggedIn = (response.Header.Status == NTStatus.STATUS_SUCCESS); - if (m_isLoggedIn) - { - m_signingKey = SMB2Cryptography.GenerateSigningKey(m_sessionKey, m_dialect, null); - if (m_dialect == SMB2Dialect.SMB300) - { - m_encryptSessionData = (((SessionSetupResponse)response).SessionFlags & SessionFlags.EncryptData) > 0; - m_encryptionKey = SMB2Cryptography.GenerateClientEncryptionKey(m_sessionKey, SMB2Dialect.SMB300, null); - m_decryptionKey = SMB2Cryptography.GenerateClientDecryptionKey(m_sessionKey, SMB2Dialect.SMB300, null); - } - } - return response.Header.Status; + m_encryptSessionData = (sessionFlags & SessionFlags.EncryptData) > 0; + m_encryptionKey = SMB2Cryptography.GenerateClientEncryptionKey(m_sessionKey, m_dialect, m_preauthIntegrityHashValue); + m_decryptionKey = SMB2Cryptography.GenerateClientDecryptionKey(m_sessionKey, m_dialect, m_preauthIntegrityHashValue); } } - else - { - return response.Header.Status; - } + return response.Header.Status; + } + else + { + return NTStatus.STATUS_INVALID_SMB; } - return NTStatus.STATUS_INVALID_SMB; } public NTStatus Logoff() @@ -316,10 +380,16 @@ public ISMBFileStore TreeConnect(string shareName, out NTStatus status) if (response != null) { status = response.Header.Status; - if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is TreeConnectResponse) + if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is TreeConnectResponse treeConnectResponse) { - bool encryptShareData = (((TreeConnectResponse)response).ShareFlags & ShareFlags.EncryptData) > 0; - return new SMB2FileStore(this, response.Header.TreeID, m_encryptSessionData || encryptShareData); + bool encryptShareData = (treeConnectResponse.ShareFlags & ShareFlags.EncryptData) > 0; + SMB2FileStore fileStore = new SMB2FileStore(this, response.Header.TreeID, m_encryptSessionData || encryptShareData); + if ((treeConnectResponse.ShareFlags & ShareFlags.DfsRoot) > 0) + { + // [MS-DFSC] The share is a DFS namespace root; wrap the file store so that DFS referrals are followed transparently. + return new SMB2DfsFileStore(this, m_serverName, shareName, fileStore); + } + return fileStore; } } else @@ -329,59 +399,84 @@ public ISMBFileStore TreeConnect(string shareName, out NTStatus status) return null; } - private void OnClientSocketReceive(IAsyncResult ar) + public NTStatus Echo() { - 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) + EchoRequest request = new EchoRequest(); + TrySendCommand(request); + SMB2Command response = WaitForCommand(request.MessageID); + if (response != null) { - Log("[ReceiveCallback] EndReceive ObjectDisposedException"); - return; + return response.Header.Status; } - catch (SocketException ex) + else { - Log("[ReceiveCallback] EndReceive SocketException: " + ex.Message); - return; + return NTStatus.STATUS_INVALID_SMB; } + } - if (numberOfBytesReceived == 0) - { - m_isConnected = false; - } - else - { - NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; - buffer.SetNumberOfBytesReceived(numberOfBytesReceived); - ProcessConnectionBuffer(state); + private void OnClientSocketReceive(IAsyncResult ar) + { + ConnectionState state = (ConnectionState)ar.AsyncState; + Socket clientSocket = state.ClientSocket; + lock (state.ReceiveBuffer) + { + int numberOfBytesReceived = 0; try { - clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); + numberOfBytesReceived = clientSocket.EndReceive(ar); + } + catch (ArgumentException) // The IAsyncResult object was not returned from the corresponding synchronous method on this class. + { + m_isConnected = false; + state.ReceiveBuffer.Dispose(); + return; } catch (ObjectDisposedException) { m_isConnected = false; - Log("[ReceiveCallback] BeginReceive ObjectDisposedException"); + Log("[ReceiveCallback] EndReceive ObjectDisposedException"); + state.ReceiveBuffer.Dispose(); + return; } catch (SocketException ex) { m_isConnected = false; - Log("[ReceiveCallback] BeginReceive SocketException: " + ex.Message); + Log("[ReceiveCallback] EndReceive SocketException: " + ex.Message); + state.ReceiveBuffer.Dispose(); + return; + } + + if (numberOfBytesReceived == 0) + { + m_isConnected = false; + state.ReceiveBuffer.Dispose(); + } + else if (clientSocket.Connected) + { + NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; + buffer.SetNumberOfBytesReceived(numberOfBytesReceived); + ProcessConnectionBuffer(state); + + if (clientSocket.Connected) + { + try + { + clientSocket.BeginReceive(buffer.Buffer, buffer.WriteOffset, buffer.AvailableLength, SocketFlags.None, new AsyncCallback(OnClientSocketReceive), state); + } + catch (ObjectDisposedException) + { + m_isConnected = false; + Log("[ReceiveCallback] BeginReceive ObjectDisposedException"); + buffer.Dispose(); + } + catch (SocketException ex) + { + m_isConnected = false; + Log("[ReceiveCallback] BeginReceive SocketException: " + ex.Message); + buffer.Dispose(); + } + } } } } @@ -398,7 +493,9 @@ private void ProcessConnectionBuffer(ConnectionState state) } catch (Exception) { + Log("[ProcessConnectionBuffer] Invalid packet"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); break; } @@ -414,7 +511,8 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) if (packet is SessionMessagePacket) { byte[] messageBytes; - if (m_dialect == SMB2Dialect.SMB300 && SMB2TransformHeader.IsTransformHeader(packet.Trailer, 0)) + bool isEncrypted = m_dialect >= SMB2Dialect.SMB300 && SMB2TransformHeader.IsTransformHeader(packet.Trailer, 0); + if (isEncrypted) { SMB2TransformHeader transformHeader = new SMB2TransformHeader(packet.Trailer, 0); byte[] encryptedMessage = ByteReader.ReadBytes(packet.Trailer, SMB2TransformHeader.Length, (int)transformHeader.OriginalMessageSize); @@ -435,19 +533,28 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) Log("Invalid SMB2 response: " + ex.Message); state.ClientSocket.Close(); m_isConnected = false; + state.ReceiveBuffer.Dispose(); return; } + if (m_preauthIntegrityHashValue != null && (command is NegotiateResponse || (command is SessionSetupResponse sessionSetupResponse && sessionSetupResponse.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED))) + { + m_preauthIntegrityHashValue = SMB2Cryptography.ComputeHash(HashAlgorithm.SHA512, ByteUtils.Concatenate(m_preauthIntegrityHashValue, messageBytes)); + } + m_availableCredits += command.Header.Credits; - if (m_transport == SMBTransportType.DirectTCPTransport && command is NegotiateResponse) + if (m_transport == SMBTransportType.DirectTCPTransport && command is NegotiateResponse negotiateResponse) { - NegotiateResponse negotiateResponse = (NegotiateResponse)command; - if ((negotiateResponse.Capabilities & Capabilities.LargeMTU) > 0) + m_connectionSupportsMultiCredit = (negotiateResponse.Capabilities & Capabilities.LargeMTU) > 0; + if (m_connectionSupportsMultiCredit) { - // [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; + // [MS-SMB2] 3.2.5.1 Receiving Any Message - If the message size received exceeds Connection.MaxTransactSize, the client SHOULD disconnect the connection. + // Note: Windows clients do not enforce the MaxTransactSize value. + // We use a value that we have observed to work well with both Microsoft and non-Microsoft servers. + // see https://github.com/TalAloni/SMBLibrary/issues/239 + int serverMaxTransactSize = (int)Math.Max(negotiateResponse.MaxTransactSize, negotiateResponse.MaxReadSize); + int maxPacketSize = SessionPacket.HeaderLength + (int)Math.Min(serverMaxTransactSize, ClientMaxTransactSize) + 256; if (maxPacketSize > state.ReceiveBuffer.Buffer.Length) { state.ReceiveBuffer.IncreaseBufferSize(maxPacketSize); @@ -461,6 +568,16 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) // Otherwise, the response MUST be discarded as invalid. if (command.Header.MessageID != 0xFFFFFFFFFFFFFFFF || command.Header.Command == SMB2CommandName.OplockBreak) { + bool isInterimResponse = ((command.Header.Flags & SMB2PacketHeaderFlags.AsyncCommand) != 0) && command.Header.Status == NTStatus.STATUS_PENDING; + bool shouldBeSigned = m_isLoggedIn && m_signingRequired && !isEncrypted && !isInterimResponse; + + // [MS-SMB2] 3.2.5.1.3 If signature verification fails, the client MUST discard the received message. The client MAY also choose to disconnect the connection + if (shouldBeSigned && !SMB2Cryptography.VerifySignature(messageBytes, m_dialect, m_signingKey)) + { + Log("Invalid SMB2 response signature"); + return; + } + lock (m_incomingQueueLock) { m_incomingQueue.Add(command); @@ -481,14 +598,21 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) { Log("Inappropriate NetBIOS session packet"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); } } internal SMB2Command WaitForCommand(ulong messageID) { + return WaitForCommand(messageID, out bool _); + } + + internal SMB2Command WaitForCommand(ulong messageID, out bool connectionTerminated) + { + connectionTerminated = false; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - while (stopwatch.ElapsedMilliseconds < ResponseTimeoutInMilliseconds) + while (stopwatch.ElapsedMilliseconds < m_responseTimeoutInMilliseconds && !(connectionTerminated = !m_clientSocket.Connected)) { lock (m_incomingQueueLock) { @@ -515,10 +639,9 @@ internal SMB2Command WaitForCommand(ulong messageID) internal SessionPacket WaitForSessionResponsePacket() { - const int TimeOut = 5000; Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - while (stopwatch.ElapsedMilliseconds < TimeOut) + while (stopwatch.ElapsedMilliseconds < m_responseTimeoutInMilliseconds) { if (m_sessionResponsePacket != null) { @@ -545,8 +668,14 @@ internal void TrySendCommand(SMB2Command request) internal void TrySendCommand(SMB2Command request, bool encryptData) { - if (m_dialect == SMB2Dialect.SMB202 || m_transport == SMBTransportType.NetBiosOverTCP) + if (!m_connectionSupportsMultiCredit && request.Header.CreditCharge > 1) + { + throw new Exception("Attempted to read or write more data than allowed for this connection"); + } + + if (!m_connectionSupportsMultiCredit) { + // [MS-SMB2] 3.2.4.1.5 If [..] Connection.SupportsMultiCredit is FALSE, CreditCharge SHOULD be set to 0. request.Header.CreditCharge = 0; request.Header.Credits = 1; m_availableCredits -= 1; @@ -555,6 +684,9 @@ internal void TrySendCommand(SMB2Command request, bool encryptData) { if (request.Header.CreditCharge == 0) { + // [MS-SMB2] 3.2.4.1.5 If Connection.SupportsMultiCredit is TRUE: + // For READ, WRITE, IOCTL, and QUERY_DIRECTORY requests, CreditCharge field in the SMB2 header SHOULD be set to [..] the value computed. + // For all other requests, the client MUST set CreditCharge to 1. request.Header.CreditCharge = 1; } @@ -577,7 +709,7 @@ internal void TrySendCommand(SMB2Command request, bool encryptData) if (m_signingRequired && !encryptData) { request.Header.IsSigned = (m_sessionID != 0 && ((request.CommandName == SMB2CommandName.TreeConnect || request.Header.TreeID != 0) || - (m_dialect == SMB2Dialect.SMB300 && request.CommandName == SMB2CommandName.Logoff))); + (m_dialect >= SMB2Dialect.SMB300 && request.CommandName == SMB2CommandName.Logoff))); if (request.Header.IsSigned) { request.Header.Signature = new byte[16]; // Request could be reused @@ -588,7 +720,7 @@ internal void TrySendCommand(SMB2Command request, bool encryptData) } } TrySendCommand(m_clientSocket, request, encryptData ? m_encryptionKey : null); - if (m_dialect == SMB2Dialect.SMB202 || m_transport == SMBTransportType.NetBiosOverTCP) + if (!m_connectionSupportsMultiCredit) { m_messageID++; } @@ -598,6 +730,24 @@ internal void TrySendCommand(SMB2Command request, bool encryptData) } } + /// SMB 3.1.1 only + private List GetNegotiateContextList() + { + PreAuthIntegrityCapabilities preAuthIntegrityCapabilities = new PreAuthIntegrityCapabilities(); + preAuthIntegrityCapabilities.HashAlgorithms.Add(HashAlgorithm.SHA512); + preAuthIntegrityCapabilities.Salt = new byte[32]; + new Random().NextBytes(preAuthIntegrityCapabilities.Salt); + + EncryptionCapabilities encryptionCapabilities = new EncryptionCapabilities(); + encryptionCapabilities.Ciphers.Add(CipherAlgorithm.Aes128Ccm); + + return new List() + { + preAuthIntegrityCapabilities, + encryptionCapabilities + }; + } + public uint MaxTransactSize { get @@ -622,7 +772,23 @@ public uint MaxWriteSize } } - public static void TrySendCommand(Socket socket, SMB2Command request, byte[] encryptionKey) + public SMBTransportType Transport + { + get + { + return m_transport; + } + } + + public bool IsConnected + { + get + { + return m_isConnected; + } + } + + private void TrySendCommand(Socket socket, SMB2Command request, byte[] encryptionKey) { SessionMessagePacket packet = new SessionMessagePacket(); if (encryptionKey != null) @@ -633,11 +799,15 @@ public static void TrySendCommand(Socket socket, SMB2Command request, byte[] enc else { packet.Trailer = request.GetBytes(); + if (m_preauthIntegrityHashValue != null && (request is NegotiateRequest || request is SessionSetupRequest)) + { + m_preauthIntegrityHashValue = SMB2Cryptography.ComputeHash(HashAlgorithm.SHA512, ByteUtils.Concatenate(m_preauthIntegrityHashValue, packet.Trailer)); + } } TrySendPacket(socket, packet); } - public static void TrySendPacket(Socket socket, SessionPacket packet) + private void TrySendPacket(Socket socket, SessionPacket packet) { try { @@ -646,10 +816,53 @@ public static void TrySendPacket(Socket socket, SessionPacket packet) } catch (SocketException) { + m_isConnected = false; } catch (ObjectDisposedException) { + m_isConnected = false; } } + + private static string CreateSpn(string serverAddress) + { + return $"cifs/{serverAddress}"; + } + + /// + /// Connects to a DFS referral target server and logs in by reusing this client's authentication client, + /// rebinding its security context to the target server (see IAuthenticationClient.ResetSecurityContext). + /// + internal SMB2Client ConnectAndLoginToDfsTarget(string serverName) + { + if (m_authenticationClient == null) + { + return null; + } + + SMB2Client targetClient = new SMB2Client(m_responseTimeoutInMilliseconds, m_enableSMB311Support); + try + { + if (!targetClient.Connect(serverName, m_transport)) + { + return null; + } + } + catch + { + // Connect throws when the server name cannot be resolved + return null; + } + + m_authenticationClient.ResetSecurityContext(CreateSpn(serverName)); + NTStatus loginStatus = targetClient.Login(m_authenticationClient); + if (loginStatus != NTStatus.STATUS_SUCCESS) + { + targetClient.Disconnect(); + return null; + } + + return targetClient; + } } } diff --git a/SMBLibrary/Client/SMB2FileStore.cs b/SMBLibrary/Client/SMB2FileStore.cs index 64607d79..a6bc4218 100644 --- a/SMBLibrary/Client/SMB2FileStore.cs +++ b/SMBLibrary/Client/SMB2FileStore.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2021 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -18,6 +18,7 @@ public class SMB2FileStore : ISMBFileStore private SMB2Client m_client; private uint m_treeID; private bool m_encryptShareData; + private bool m_isDfsOperation; public SMB2FileStore(SMB2Client client, uint treeID, bool encryptShareData) { @@ -31,6 +32,12 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string handle = null; fileStatus = FileStatus.FILE_DOES_NOT_EXIST; CreateRequest request = new CreateRequest(); + if (m_isDfsOperation) + { + // [MS-SMB2] 3.2.4.1.4 - The client MUST set SMB2_FLAGS_DFS_OPERATIONS when sending a DFS operation, + // otherwise the server will not return STATUS_PATH_NOT_COVERED and no DFS referral will be requested. + request.Header.Flags |= SMB2PacketHeaderFlags.DfsOperations; + } request.Name = path; request.DesiredAccess = desiredAccess; request.FileAttributes = fileAttributes; @@ -40,7 +47,7 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string request.ImpersonationLevel = ImpersonationLevel.Impersonation; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is CreateResponse) @@ -52,7 +59,7 @@ public NTStatus CreateFile(out object handle, out FileStatus fileStatus, string return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus CloseFile(object handle) @@ -60,13 +67,13 @@ public NTStatus CloseFile(object handle) CloseRequest request = new CloseRequest(); request.FileId = (FileID)handle; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCount) @@ -79,7 +86,7 @@ public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCou request.ReadLength = (uint)maxCount; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is ReadResponse) @@ -89,7 +96,7 @@ public NTStatus ReadFile(out byte[] data, object handle, long offset, int maxCou return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offset, byte[] data) @@ -102,7 +109,7 @@ public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offs request.Data = data; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is WriteResponse) @@ -112,7 +119,7 @@ public NTStatus WriteFile(out int numberOfBytesWritten, object handle, long offs return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus FlushFileBuffers(object handle) @@ -121,7 +128,7 @@ public NTStatus FlushFileBuffers(object handle) request.FileId = (FileID) handle; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is FlushResponse) @@ -130,7 +137,7 @@ public NTStatus FlushFileBuffers(object handle) } } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus LockFile(object handle, long byteOffset, long length, bool exclusiveLock) @@ -155,7 +162,7 @@ public NTStatus QueryDirectory(out List result, o request.FileName = fileName; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { while (response.Header.Status == NTStatus.STATUS_SUCCESS && response is QueryDirectoryResponse) @@ -164,12 +171,16 @@ public NTStatus QueryDirectory(out List result, o result.AddRange(page); request.Reopen = false; TrySendCommand(request); - response = m_client.WaitForCommand(request.MessageID); + response = m_client.WaitForCommand(request.MessageID, out connectionTerminated); + if (response == null) + { + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; + } } return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus GetFileInformation(out FileInformation result, object handle, FileInformationClass informationClass) @@ -182,7 +193,7 @@ public NTStatus GetFileInformation(out FileInformation result, object handle, Fi request.FileId = (FileID)handle; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is QueryInfoResponse) @@ -192,7 +203,7 @@ public NTStatus GetFileInformation(out FileInformation result, object handle, Fi return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus SetFileInformation(object handle, FileInformation information) @@ -204,13 +215,13 @@ public NTStatus SetFileInformation(object handle, FileInformation information) request.SetFileInformation(information); TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus GetFileSystemInformation(out FileSystemInformation result, FileSystemInformationClass informationClass) @@ -239,7 +250,7 @@ public NTStatus GetFileSystemInformation(out FileSystemInformation result, objec request.FileId = (FileID)handle; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is QueryInfoResponse) @@ -249,7 +260,7 @@ public NTStatus GetFileSystemInformation(out FileSystemInformation result, objec return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus SetFileSystemInformation(FileSystemInformation information) @@ -267,7 +278,7 @@ public NTStatus GetSecurityInformation(out SecurityDescriptor result, object han request.FileId = (FileID)handle; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if (response.Header.Status == NTStatus.STATUS_SUCCESS && response is QueryInfoResponse) @@ -277,12 +288,25 @@ public NTStatus GetSecurityInformation(out SecurityDescriptor result, object han return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus SetSecurityInformation(object handle, SecurityInformation securityInformation, SecurityDescriptor securityDescriptor) { - return NTStatus.STATUS_NOT_SUPPORTED; + SetInfoRequest request = new SetInfoRequest(); + request.InfoType = InfoType.Security; + request.SecurityInformation = securityInformation; + request.FileId = (FileID)handle; + request.SetSecurityInformation(securityDescriptor); + + TrySendCommand(request); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); + if (response != null) + { + return response.Header.Status; + } + + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus NotifyChange(out object ioRequest, object handle, NotifyChangeFilter completionFilter, bool watchTree, int outputBufferSize, OnNotifyChangeCompleted onNotifyChangeCompleted, object context) @@ -306,7 +330,7 @@ public NTStatus DeviceIOControl(object handle, uint ctlCode, byte[] input, out b request.Input = input; request.MaxOutputResponse = (uint)maxOutputLength; TrySendCommand(request); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { if ((response.Header.Status == NTStatus.STATUS_SUCCESS || response.Header.Status == NTStatus.STATUS_BUFFER_OVERFLOW) && response is IOCtlResponse) @@ -316,7 +340,7 @@ public NTStatus DeviceIOControl(object handle, uint ctlCode, byte[] input, out b return response.Header.Status; } - return NTStatus.STATUS_INVALID_SMB; + return connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT; } public NTStatus Disconnect() @@ -335,6 +359,10 @@ public NTStatus Disconnect() private void TrySendCommand(SMB2Command request) { request.Header.TreeID = m_treeID; + if (!m_client.IsConnected) + { + throw new InvalidOperationException("The client is no longer connected"); + } m_client.TrySendCommand(request, m_encryptShareData); } @@ -354,6 +382,23 @@ public uint MaxWriteSize } } + /// + /// When set, requests are marked with SMB2_FLAGS_DFS_OPERATIONS. + /// Set by SMB2DfsFileStore for a DFS namespace root; the name passed to CreateFile is then expected + /// to already be in the form required by [MS-SMB2] 2.2.13. + /// + internal bool IsDfsOperation + { + get + { + return m_isDfsOperation; + } + set + { + m_isDfsOperation = value; + } + } + private static FileStatus ToFileStatus(CreateAction createAction) { switch (createAction) diff --git a/SMBLibrary/DFS/DfsReferralEntry.cs b/SMBLibrary/DFS/DfsReferralEntry.cs index 2751d1f0..40a2206e 100644 --- a/SMBLibrary/DFS/DfsReferralEntry.cs +++ b/SMBLibrary/DFS/DfsReferralEntry.cs @@ -1,17 +1,51 @@ -/* Copyright (C) 2014 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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 System.IO; using Utilities; -namespace SMBLibrary +namespace SMBLibrary.DFS { public abstract class DfsReferralEntry { + /// + /// [MS-DFSC] 2.2.5 - Referral Entry Types + /// The strings referenced from the fields of a referral entry MUST follow the last referral entry in the RESP_GET_DFS_REFERRAL message. + /// + public abstract byte[] WriteBytes(byte[] buffer, int offset, int stringsOffset); + + public abstract int Length + { + get; + } + + /// + /// Length of referenced strings + /// + public abstract int StringsLength + { + get; + } + + public static DfsReferralEntry ReadEntry(byte[] buffer, ref int offset) + { + ushort versionNumber = LittleEndianConverter.ToUInt16(buffer, offset + 0); + switch (versionNumber) + { + case 1: + return new DfsReferralEntryV1(buffer, ref offset); + case 2: + return new DfsReferralEntryV2(buffer, ref offset); + case 3: + return new DfsReferralEntryV3(buffer, ref offset); + case 4: + return new DfsReferralEntryV4(buffer, ref offset); + default: + throw new InvalidDataException($"DfsReferralEntry version {versionNumber} is invalid"); + } + } } } diff --git a/SMBLibrary/DFS/DfsReferralEntryV1.cs b/SMBLibrary/DFS/DfsReferralEntryV1.cs new file mode 100644 index 00000000..b0137d7e --- /dev/null +++ b/SMBLibrary/DFS/DfsReferralEntryV1.cs @@ -0,0 +1,66 @@ +/* Copyright (C) 2025 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 Utilities; + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.5.1 DFS_REFERRAL_V1 + /// + public class DfsReferralEntryV1 : DfsReferralEntry + { + public const int FixedLength = 8; + + public ushort VersionNumber; + public ushort Size; + public DfsServerType ServerType; + public DfsReferralEntryFlags ReferralEntryFlags; + public string ShareName; + + public DfsReferralEntryV1() + { + VersionNumber = 1; + } + + public DfsReferralEntryV1(byte[] buffer, ref int offset) + { + VersionNumber = LittleEndianConverter.ToUInt16(buffer, offset + 0); + Size = LittleEndianConverter.ToUInt16(buffer, offset + 2); + ServerType = (DfsServerType)LittleEndianConverter.ToUInt16(buffer, offset + 4); + ReferralEntryFlags = (DfsReferralEntryFlags)LittleEndianConverter.ToUInt16(buffer, offset + 6); + ShareName = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + 8); + + offset += Size; + } + + public override byte[] WriteBytes(byte[] buffer, int offset, int stringsOffset) + { + LittleEndianWriter.WriteUInt16(buffer, offset + 0, VersionNumber); + LittleEndianWriter.WriteUInt16(buffer, offset + 2, (ushort)this.Length); + LittleEndianWriter.WriteUInt16(buffer, offset + 4, (ushort)ServerType); + LittleEndianWriter.WriteUInt16(buffer, offset + 6, (ushort)ReferralEntryFlags); + ByteWriter.WriteNullTerminatedUTF16String(buffer, offset + 8, ShareName); + return buffer; + } + + public override int Length + { + get + { + return FixedLength + (ShareName.Length + 1) * 2; + } + } + + public override int StringsLength + { + get + { + return 0; + } + } + } +} diff --git a/SMBLibrary/DFS/DfsReferralEntryV2.cs b/SMBLibrary/DFS/DfsReferralEntryV2.cs new file mode 100644 index 00000000..c29474d0 --- /dev/null +++ b/SMBLibrary/DFS/DfsReferralEntryV2.cs @@ -0,0 +1,94 @@ +/* Copyright (C) 2025 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 Utilities; + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.5.2 DFS_REFERRAL_V2 + /// + public class DfsReferralEntryV2 : DfsReferralEntry + { + public const int FixedLength = 22; + + public ushort VersionNumber; + public ushort Size; + public DfsServerType ServerType; + public DfsReferralEntryFlags ReferralEntryFlags; + public uint Proximity; + public uint TimeToLive; + public string DfsPath; + public string DfsAlternatePath; + public string NetworkAddress; + + public DfsReferralEntryV2() + { + VersionNumber = 2; + } + + public DfsReferralEntryV2(byte[] buffer, ref int offset) + { + VersionNumber = LittleEndianConverter.ToUInt16(buffer, offset + 0); + Size = LittleEndianConverter.ToUInt16(buffer, offset + 2); + ServerType = (DfsServerType)LittleEndianConverter.ToUInt16(buffer, offset + 4); + ReferralEntryFlags = (DfsReferralEntryFlags)LittleEndianConverter.ToUInt16(buffer, offset + 6); + + Proximity = LittleEndianConverter.ToUInt32(buffer, offset + 8); + TimeToLive = LittleEndianConverter.ToUInt32(buffer, offset + 12); + + ushort dfsPathOffset = LittleEndianConverter.ToUInt16(buffer, offset + 16); + ushort dfsAlternatePathOffset = LittleEndianConverter.ToUInt16(buffer, offset + 18); + ushort networkAddressOffset = LittleEndianConverter.ToUInt16(buffer, offset + 20); + + DfsPath = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + dfsPathOffset); + DfsAlternatePath = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + dfsAlternatePathOffset); + NetworkAddress = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + networkAddressOffset); + + offset += Size; + } + + public override byte[] WriteBytes(byte[] buffer, int offset, int stringsOffset) + { + LittleEndianWriter.WriteUInt16(buffer, offset + 0, VersionNumber); + LittleEndianWriter.WriteUInt16(buffer, offset + 2, (ushort)this.Length); + LittleEndianWriter.WriteUInt16(buffer, offset + 4, (ushort)ServerType); + LittleEndianWriter.WriteUInt16(buffer, offset + 6, (ushort)ReferralEntryFlags); + LittleEndianWriter.WriteUInt32(buffer, offset + 8, Proximity); + LittleEndianWriter.WriteUInt32(buffer, offset + 12, TimeToLive); + + int dfsPathOffset = stringsOffset; + int dfsAlternatePathOffset = (ushort)(dfsPathOffset + (DfsPath.Length + 1) * 2); + int networkAddressOffset = (ushort)(dfsAlternatePathOffset + (DfsAlternatePath.Length + 1) * 2); + // offsets are relative to the start of the referral entry + LittleEndianWriter.WriteUInt16(buffer, offset + 16, (ushort)(dfsPathOffset - offset)); + LittleEndianWriter.WriteUInt16(buffer, offset + 18, (ushort)(dfsAlternatePathOffset - offset)); + LittleEndianWriter.WriteUInt16(buffer, offset + 20, (ushort)(networkAddressOffset - offset)); + + ByteWriter.WriteNullTerminatedUTF16String(buffer, dfsPathOffset, DfsPath); + ByteWriter.WriteNullTerminatedUTF16String(buffer, dfsAlternatePathOffset, DfsAlternatePath); + ByteWriter.WriteNullTerminatedUTF16String(buffer, networkAddressOffset, NetworkAddress); + + return buffer; + } + + public override int Length + { + get + { + return FixedLength; + } + } + + public override int StringsLength + { + get + { + return (DfsPath.Length + 1 + DfsAlternatePath.Length + 1 + NetworkAddress.Length + 1) * 2; + } + } + } +} diff --git a/SMBLibrary/DFS/DfsReferralEntryV3.cs b/SMBLibrary/DFS/DfsReferralEntryV3.cs new file mode 100644 index 00000000..9127d1d3 --- /dev/null +++ b/SMBLibrary/DFS/DfsReferralEntryV3.cs @@ -0,0 +1,185 @@ +/* Copyright (C) 2025 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 Utilities; + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.5.3 DFS_REFERRAL_V3 + /// V3 supports both normal referrals and NameListReferrals (for SYSVOL/NETLOGON). + /// + public class DfsReferralEntryV3 : DfsReferralEntry + { + private const int FixedLength = 12; + + public ushort VersionNumber; + public ushort Size; + public DfsServerType ServerType; + public DfsReferralEntryFlags ReferralEntryFlags; + public uint TimeToLive; + + // Normal referral fields (when IsNameListReferral is false) + public string DfsPath; + public string DfsAlternatePath; + public string NetworkAddress; + public Guid ServiceSiteGuid; + + // NameListReferral fields (when IsNameListReferral is true) + public string SpecialName; + public List ExpandedNames; + + public DfsReferralEntryV3() + { + VersionNumber = 3; + } + + public DfsReferralEntryV3(byte[] buffer, ref int offset) + { + VersionNumber = LittleEndianConverter.ToUInt16(buffer, offset + 0); + Size = LittleEndianConverter.ToUInt16(buffer, offset + 2); + ServerType = (DfsServerType)LittleEndianConverter.ToUInt16(buffer, offset + 4); + ReferralEntryFlags = (DfsReferralEntryFlags)LittleEndianConverter.ToUInt16(buffer, offset + 6); + + TimeToLive = LittleEndianConverter.ToUInt32(buffer, offset + 8); + + bool isNameListReferral = (ReferralEntryFlags & DfsReferralEntryFlags.NameListReferral) != 0; + if (!isNameListReferral) + { + ushort dfsPathOffset = LittleEndianConverter.ToUInt16(buffer, offset + 12); + ushort dfsAlternatePathOffset = LittleEndianConverter.ToUInt16(buffer, offset + 14); + ushort networkAddressOffset = LittleEndianConverter.ToUInt16(buffer, offset + 16); + ServiceSiteGuid = LittleEndianConverter.ToGuid(buffer, offset + 18); + + DfsPath = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + dfsPathOffset); + DfsAlternatePath = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + dfsAlternatePathOffset); + NetworkAddress = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + networkAddressOffset); + } + else + { + ushort specialNameOffset = LittleEndianConverter.ToUInt16(buffer, offset + 12); + ushort numberOfExpandedNames = LittleEndianConverter.ToUInt16(buffer, offset + 14); + ushort expandedNameOffset = LittleEndianConverter.ToUInt16(buffer, offset + 16); + + SpecialName = ByteReader.ReadNullTerminatedUTF16String(buffer, offset + specialNameOffset); + ExpandedNames = new List(); + int currentOffset = offset + expandedNameOffset; + for (int nameIndex = 0; nameIndex < numberOfExpandedNames; nameIndex++) + { + if (currentOffset >= buffer.Length) + { + break; + } + + string expandedName = ByteReader.ReadNullTerminatedUTF16String(buffer, currentOffset); + if (expandedName != null) + { + ExpandedNames.Add(expandedName); + currentOffset += (expandedName.Length + 1) * 2; + } + else + { + currentOffset += 2; + } + } + } + + offset += Size; + } + + public override byte[] WriteBytes(byte[] buffer, int offset, int stringsOffset) + { + LittleEndianWriter.WriteUInt16(buffer, offset + 0, VersionNumber); + LittleEndianWriter.WriteUInt16(buffer, offset + 2, (ushort)this.Length); + LittleEndianWriter.WriteUInt16(buffer, offset + 4, (ushort)ServerType); + LittleEndianWriter.WriteUInt16(buffer, offset + 6, (ushort)ReferralEntryFlags); + LittleEndianWriter.WriteUInt32(buffer, offset + 8, TimeToLive); + + if (!IsNameListReferral) + { + int dfsPathOffset = stringsOffset; + int dfsAlternatePathOffset = dfsPathOffset + (DfsPath.Length + 1) * 2; + int networkAddressOffset = dfsAlternatePathOffset + (DfsAlternatePath.Length + 1) * 2; + // offsets are relative to the start of the referral entry + LittleEndianWriter.WriteUInt16(buffer, offset + 12, (ushort)(dfsPathOffset - offset)); + LittleEndianWriter.WriteUInt16(buffer, offset + 14, (ushort)(dfsAlternatePathOffset - offset)); + LittleEndianWriter.WriteUInt16(buffer, offset + 16, (ushort)(networkAddressOffset - offset)); + LittleEndianWriter.WriteGuid(buffer, offset + 18, ServiceSiteGuid); + + ByteWriter.WriteNullTerminatedUTF16String(buffer, dfsPathOffset, DfsPath); + ByteWriter.WriteNullTerminatedUTF16String(buffer, dfsAlternatePathOffset, DfsAlternatePath); + ByteWriter.WriteNullTerminatedUTF16String(buffer, networkAddressOffset, NetworkAddress); + } + else + { + int specialNameOffset = stringsOffset; + int expandedNameOffset = specialNameOffset + (SpecialName.Length + 1) * 2; + // offsets are relative to the start of the referral entry + LittleEndianWriter.WriteUInt16(buffer, offset + 12, (ushort)(specialNameOffset - offset)); + LittleEndianWriter.WriteUInt16(buffer, offset + 14, (ushort)ExpandedNames.Count); + LittleEndianWriter.WriteUInt16(buffer, offset + 16, (ushort)(expandedNameOffset - offset)); + + ByteWriter.WriteNullTerminatedUTF16String(buffer, specialNameOffset, SpecialName); + int currentOffset = expandedNameOffset; + for (int nameIndex = 0; nameIndex < ExpandedNames.Count; nameIndex++) + { + ByteWriter.WriteNullTerminatedUTF16String(buffer, currentOffset, ExpandedNames[nameIndex]); + currentOffset += (ExpandedNames[nameIndex].Length + 1) * 2; + } + } + + return buffer; + } + + public override int Length + { + get + { + if (!IsNameListReferral) + { + return FixedLength + 6 + 16; + } + else + { + return FixedLength + 6; + } + } + } + + public override int StringsLength + { + get + { + if (!IsNameListReferral) + { + return (DfsPath.Length + 1 + DfsAlternatePath.Length + 1 + NetworkAddress.Length + 1) * 2; + } + else + { + int length = (SpecialName.Length + 1) * 2; + for (int nameIndex = 0; nameIndex < ExpandedNames.Count; nameIndex++) + { + length += (ExpandedNames[nameIndex].Length + 1) * 2; + } + return length; + } + } + } + + /// + /// Returns true if this is a NameListReferral (used for SYSVOL/NETLOGON DC lists). + /// + public bool IsNameListReferral + { + get + { + return (ReferralEntryFlags & DfsReferralEntryFlags.NameListReferral) != 0; + } + } + } +} \ No newline at end of file diff --git a/SMBLibrary/DFS/DfsReferralEntryV4.cs b/SMBLibrary/DFS/DfsReferralEntryV4.cs new file mode 100644 index 00000000..c9a9b696 --- /dev/null +++ b/SMBLibrary/DFS/DfsReferralEntryV4.cs @@ -0,0 +1,36 @@ +/* Copyright (C) 2025 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. + */ + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.5.4 DFS_REFERRAL_V4 + /// V4 is structurally identical to V3 but adds the TargetSetBoundary flag semantics. + /// The TargetSetBoundary flag (bit 0x0004) indicates the first target in a target set, + /// allowing clients to group targets for failover purposes. + /// + public class DfsReferralEntryV4 : DfsReferralEntryV3 + { + public DfsReferralEntryV4() + { + VersionNumber = 4; + } + + public DfsReferralEntryV4(byte[] buffer, ref int offset) : base(buffer, ref offset) + { + } + + /// + /// Returns true if this entry marks the boundary of a target set (V4 only). + /// When true, this is the first target in a new target set. + /// + public bool IsTargetSetBoundary + { + get { return (ReferralEntryFlags & DfsReferralEntryFlags.TargetSetBoundary) != 0; } + } + } +} diff --git a/SMBLibrary/DFS/Enums/DfsReferralEntryFlags.cs b/SMBLibrary/DFS/Enums/DfsReferralEntryFlags.cs new file mode 100644 index 00000000..8ae76ae4 --- /dev/null +++ b/SMBLibrary/DFS/Enums/DfsReferralEntryFlags.cs @@ -0,0 +1,28 @@ +using System; + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.4.x DFS_REFERRAL_Vx - ReferralEntryFlags + /// + [Flags] + public enum DfsReferralEntryFlags : ushort + { + /// + /// No flags set. + /// + None = 0x0000, + + /// + /// NameListReferral bit - The referral entry is a NameListReferral containing + /// a list of target names (e.g., domain controller list for SYSVOL/NETLOGON). + /// + NameListReferral = 0x0002, + + /// + /// TargetSetBoundary bit (V4 only) - The first target in a target set. + /// Used for target set grouping in V4 referrals. + /// + TargetSetBoundary = 0x0004, + } +} diff --git a/SMBLibrary/DFS/Enums/DfsReferralHeaderFlags.cs b/SMBLibrary/DFS/Enums/DfsReferralHeaderFlags.cs new file mode 100644 index 00000000..fd9184d7 --- /dev/null +++ b/SMBLibrary/DFS/Enums/DfsReferralHeaderFlags.cs @@ -0,0 +1,12 @@ +using System; + +namespace SMBLibrary.DFS +{ + [Flags] + public enum DfsReferralHeaderFlags : uint + { + ReferralServers = 0x00000001, + StorageServers = 0x00000002, + TargetFailback = 0x00000004, + } +} diff --git a/SMBLibrary/DFS/Enums/DfsServerType.cs b/SMBLibrary/DFS/Enums/DfsServerType.cs new file mode 100644 index 00000000..0c21a4db --- /dev/null +++ b/SMBLibrary/DFS/Enums/DfsServerType.cs @@ -0,0 +1,19 @@ + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.4.x DFS_REFERRAL_Vx - ServerType + /// + public enum DfsServerType : ushort + { + /// + /// The target is a non-root DFS server (link target or storage server). + /// + NonRoot = 0x0000, + + /// + /// The target is a root DFS server (namespace server). + /// + Root = 0x0001, + } +} diff --git a/SMBLibrary/DFS/Enums/RequestGetDfsReferralExFlags.cs b/SMBLibrary/DFS/Enums/RequestGetDfsReferralExFlags.cs new file mode 100644 index 00000000..d3259150 --- /dev/null +++ b/SMBLibrary/DFS/Enums/RequestGetDfsReferralExFlags.cs @@ -0,0 +1,10 @@ +using System; + +namespace SMBLibrary.DFS +{ + [Flags] + public enum RequestGetDfsReferralExFlags : ushort + { + SiteName = 0x0001, + } +} diff --git a/SMBLibrary/DFS/RequestGetDfsReferral.cs b/SMBLibrary/DFS/RequestGetDfsReferral.cs index 1f3e7760..d46ad515 100644 --- a/SMBLibrary/DFS/RequestGetDfsReferral.cs +++ b/SMBLibrary/DFS/RequestGetDfsReferral.cs @@ -4,12 +4,9 @@ * 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 Utilities; -namespace SMBLibrary +namespace SMBLibrary.DFS { /// /// [MS-DFSC] REQ_GET_DFS_REFERRAL diff --git a/SMBLibrary/DFS/RequestGetDfsReferralEx.cs b/SMBLibrary/DFS/RequestGetDfsReferralEx.cs new file mode 100644 index 00000000..01527774 --- /dev/null +++ b/SMBLibrary/DFS/RequestGetDfsReferralEx.cs @@ -0,0 +1,65 @@ +/* Copyright (C) 2025 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 Utilities; + +namespace SMBLibrary.DFS +{ + /// + /// [MS-DFSC] 2.2.3 REQ_GET_DFS_REFERRAL_EX + /// Extended DFS referral request that supports site-aware referrals. + /// + public class RequestGetDfsReferralEx + { + public ushort MaxReferralLevel; + public RequestGetDfsReferralExFlags Flags; + public string RequestFileName; // Unicode + public string SiteName; // Optional, Unicode, null-terminated (when flag is set) + + public RequestGetDfsReferralEx() + { + } + + public RequestGetDfsReferralEx(byte[] buffer) + { + MaxReferralLevel = LittleEndianConverter.ToUInt16(buffer, 0); + Flags = (RequestGetDfsReferralExFlags)LittleEndianConverter.ToUInt16(buffer, 2); + uint requestDataLength = LittleEndianConverter.ToUInt32(buffer, 4); + ushort requestFileNameLength = LittleEndianConverter.ToUInt16(buffer, 8); + int dataOffset = 10; + RequestFileName = ByteReader.ReadNullTerminatedUTF16String(buffer, ref dataOffset); + if ((Flags & RequestGetDfsReferralExFlags.SiteName) != 0) + { + ushort siteNameLength = LittleEndianReader.ReadUInt16(buffer, ref dataOffset); + SiteName = ByteReader.ReadNullTerminatedUTF16String(buffer, ref dataOffset); + } + } + + public byte[] GetBytes() + { + int length = 10 + RequestFileName.Length * 2 + 2; + if ((Flags & RequestGetDfsReferralExFlags.SiteName) != 0) + { + length += 2 + SiteName.Length * 2 + 2; + } + + byte[] buffer = new byte[length]; + LittleEndianWriter.WriteUInt16(buffer, 0, MaxReferralLevel); + LittleEndianWriter.WriteUInt16(buffer, 2, (ushort)Flags); + LittleEndianWriter.WriteUInt32(buffer, 4, (uint)(length - 8)); + LittleEndianWriter.WriteUInt16(buffer, 8, (ushort)RequestFileName.Length); + int dataOffset = 10; + ByteWriter.WriteNullTerminatedUTF16String(buffer, ref dataOffset, RequestFileName); + if ((Flags & RequestGetDfsReferralExFlags.SiteName) != 0) + { + LittleEndianWriter.WriteUInt16(buffer, ref dataOffset, (ushort)SiteName.Length); + ByteWriter.WriteNullTerminatedUTF16String(buffer, ref dataOffset, SiteName); + } + + return buffer; + } + } +} diff --git a/SMBLibrary/DFS/ResponseGetDfsReferral.cs b/SMBLibrary/DFS/ResponseGetDfsReferral.cs index 09cbec7c..25405644 100644 --- a/SMBLibrary/DFS/ResponseGetDfsReferral.cs +++ b/SMBLibrary/DFS/ResponseGetDfsReferral.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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, @@ -6,36 +6,92 @@ */ using System; using System.Collections.Generic; -using System.Text; using Utilities; -namespace SMBLibrary +namespace SMBLibrary.DFS { /// /// [MS-DFSC] RESP_GET_DFS_REFERRAL /// public class ResponseGetDfsReferral { + private const int HeaderSize = 8; + private const int MinReferralEntryHeaderSize = 16; + public ushort PathConsumed; - public ushort NumberOfReferrals; - public uint ReferralHeaderFlags; + // ushort NumberOfReferrals; + public DfsReferralHeaderFlags ReferralHeaderFlags; public List ReferralEntries; - public List StringBuffer; + // StringBuffer; // Padding public ResponseGetDfsReferral() { - throw new NotImplementedException(); + ReferralEntries = new List(); } public ResponseGetDfsReferral(byte[] buffer) { - throw new NotImplementedException(); + if (buffer.Length < HeaderSize) + { + throw new ArgumentException("Buffer too small for DFS referral response header", nameof(buffer)); + } + + PathConsumed = LittleEndianConverter.ToUInt16(buffer, 0); + ushort numberOfReferrals = LittleEndianConverter.ToUInt16(buffer, 2); + ReferralHeaderFlags = (DfsReferralHeaderFlags)LittleEndianConverter.ToUInt32(buffer, 4); + + if (numberOfReferrals > 0 && buffer.Length == HeaderSize) + { + throw new ArgumentException("Buffer too small for DFS referral entries", nameof(buffer)); + } + + ReferralEntries = new List(); + int entryOffset = HeaderSize; + for (int index = 0; index < numberOfReferrals; index++) + { + if (buffer.Length < entryOffset + MinReferralEntryHeaderSize) + { + throw new ArgumentException("Buffer too small for DFS referral entry header", nameof(buffer)); + } + + DfsReferralEntry entry = DfsReferralEntry.ReadEntry(buffer, ref entryOffset); + ReferralEntries.Add(entry); + + if (entryOffset > buffer.Length) + { + throw new ArgumentException("Buffer too small for next DFS referral", nameof(buffer)); + } + } } public byte[] GetBytes() { - throw new NotImplementedException(); + int length = HeaderSize; + foreach (DfsReferralEntry entry in ReferralEntries) + { + length += entry.Length; + } + + int stringsOffset = length; + foreach (DfsReferralEntry entry in ReferralEntries) + { + length += entry.StringsLength; + } + + byte[] buffer = new byte[length]; + LittleEndianWriter.WriteUInt16(buffer, 0, PathConsumed); + LittleEndianWriter.WriteUInt16(buffer, 2, (ushort)ReferralEntries.Count); + LittleEndianWriter.WriteUInt32(buffer, 4, (uint)ReferralHeaderFlags); + + int offset = HeaderSize; + foreach (DfsReferralEntry entry in ReferralEntries) + { + entry.WriteBytes(buffer, offset, stringsOffset); + offset += entry.Length; + stringsOffset += entry.StringsLength; + } + return buffer; } } } diff --git a/SMBLibrary/Enums/NTStatus.cs b/SMBLibrary/Enums/NTStatus.cs index 98ffc8f7..113cd2fd 100644 --- a/SMBLibrary/Enums/NTStatus.cs +++ b/SMBLibrary/Enums/NTStatus.cs @@ -36,6 +36,7 @@ public enum NTStatus : uint STATUS_FILE_LOCK_CONFLICT = 0xC0000054, STATUS_LOCK_NOT_GRANTED = 0xC0000055, STATUS_DELETE_PENDING = 0xC0000056, + STATUS_IO_TIMEOUT = 0xC00000B5, STATUS_PRIVILEGE_NOT_HELD = 0xC0000061, STATUS_WRONG_PASSWORD = 0xC000006A, STATUS_LOGON_FAILURE = 0xC000006D, // Authentication failure. @@ -54,6 +55,7 @@ public enum NTStatus : uint STATUS_BAD_DEVICE_TYPE = 0xC00000CB, STATUS_BAD_NETWORK_NAME = 0xC00000CC, STATUS_TOO_MANY_SESSIONS = 0xC00000CE, + STATUS_REQUEST_NOT_ACCEPTED = 0xC00000D0, STATUS_DIRECTORY_NOT_EMPTY = 0xC0000101, STATUS_NOT_A_DIRECTORY = 0xC0000103, STATUS_TOO_MANY_OPENED_FILES = 0xC000011F, @@ -65,9 +67,10 @@ public enum NTStatus : uint STATUS_FS_DRIVER_REQUIRED = 0xC000019C, STATUS_USER_SESSION_DELETED = 0xC0000203, STATUS_INSUFF_SERVER_RESOURCES = 0xC0000205, + STATUS_PASSWORD_MUST_CHANGE = 0xC0000224, STATUS_NOT_FOUND = 0xC0000225, STATUS_ACCOUNT_LOCKED_OUT = 0xC0000234, - STATUS_PASSWORD_MUST_CHANGE = 0xC0000224, + STATUS_PATH_NOT_COVERED = 0xC0000257, STATUS_NOT_A_REPARSE_POINT = 0xC0000275, STATUS_INVALID_SMB = 0x00010002, // SMB1/CIFS: A corrupt or invalid SMB request was received diff --git a/SMBLibrary/Helpers/FileTimeHelper.cs b/SMBLibrary/Helpers/FileTimeHelper.cs index dc0307ae..f609d44a 100644 --- a/SMBLibrary/Helpers/FileTimeHelper.cs +++ b/SMBLibrary/Helpers/FileTimeHelper.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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 Utilities; namespace SMBLibrary @@ -14,6 +13,21 @@ public class FileTimeHelper { public static readonly DateTime MinFileTimeValue = new DateTime(1601, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private static readonly long MaxFileTimeIntegerValue = DateTime.MaxValue.ToFileTimeUtc(); + + public static DateTime ReadFileTimeSafe(byte[] buffer, int offset) + { + long span = LittleEndianConverter.ToInt64(buffer, offset); + if (span >= 0 && span <= MaxFileTimeIntegerValue) + { + return DateTime.FromFileTimeUtc(span); + } + else + { + return DateTime.MaxValue; + } + } + public static DateTime ReadFileTime(byte[] buffer, int offset) { long span = LittleEndianConverter.ToInt64(buffer, offset); diff --git a/SMBLibrary/Helpers/SP800_1008.cs b/SMBLibrary/Helpers/SP800_1008.cs index 0ac76849..1c785128 100644 --- a/SMBLibrary/Helpers/SP800_1008.cs +++ b/SMBLibrary/Helpers/SP800_1008.cs @@ -1,4 +1,4 @@ -/// Adapted from https://referencesource.microsoft.com/#system.web/Security/Cryptography/SP800_108.cs +// Adapted from https://referencesource.microsoft.com/#system.web/Security/Cryptography/SP800_108.cs using System; using System.Security.Cryptography; using Utilities; diff --git a/SMBLibrary/NTFileStore/Enums/AccessMask/FileAccessMask.cs b/SMBLibrary/NTFileStore/Enums/AccessMask/FileAccessMask.cs index 7c032650..99795bf5 100644 --- a/SMBLibrary/NTFileStore/Enums/AccessMask/FileAccessMask.cs +++ b/SMBLibrary/NTFileStore/Enums/AccessMask/FileAccessMask.cs @@ -15,6 +15,7 @@ public enum FileAccessMask : uint FILE_READ_EA = 0x00000008, FILE_WRITE_EA = 0x00000010, FILE_EXECUTE = 0x00000020, + FILE_DELETE_CHILD = 0x00000040, FILE_READ_ATTRIBUTES = 0x00000080, FILE_WRITE_ATTRIBUTES = 0x00000100, DELETE = 0x00010000, diff --git a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/DeviceCharacteristics.cs b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/DeviceCharacteristics.cs index 4cc1dddc..8b139783 100644 --- a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/DeviceCharacteristics.cs +++ b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/DeviceCharacteristics.cs @@ -2,15 +2,23 @@ namespace SMBLibrary { + /// + /// [MS-FSCC] 2.5.10 - FileFsDeviceInformation + /// [Flags] public enum DeviceCharacteristics : uint { - RemovableMedia = 0x0001, // FILE_REMOVABLE_MEDIA - ReadOnlyDevice = 0x0002, // FILE_READ_ONLY_DEVICE - FloppyDiskette = 0x0004, // FILE_FLOPPY_DISKETTE - WriteOnceMedia = 0x0008, // FILE_WRITE_ONCE_MEDIA - RemoteDevice = 0x0010, // FILE_REMOTE_DEVICE - IsMounted = 0x0020, // FILE_DEVICE_IS_MOUNTED - VirtualVolume = 0x0040, // FILE_VIRTUAL_VOLUME + RemovableMedia = 0x0001, // FILE_REMOVABLE_MEDIA + ReadOnlyDevice = 0x0002, // FILE_READ_ONLY_DEVICE + FloppyDiskette = 0x0004, // FILE_FLOPPY_DISKETTE + WriteOnceMedia = 0x0008, // FILE_WRITE_ONCE_MEDIA + RemoteDevice = 0x0010, // FILE_REMOTE_DEVICE + IsMounted = 0x0020, // FILE_DEVICE_IS_MOUNTED + VirtualVolume = 0x0040, // FILE_VIRTUAL_VOLUME + SecureOpen = 0x0100, // FILE_DEVICE_SECURE_OPEN + TerminalServicesDevice = 0x1000, // FILE_CHARACTERISTIC_TS_DEVICE + WebDAVDevice = 0x2000, // FILE_CHARACTERISTIC_WEBDAV_DEVICE + PortableDevice = 0x4000, // FILE_PORTABLE_DEVICE + AllowAppContainerTraversal = 0x20000, // FILE_DEVICE_ALLOW_APPCONTAINER_TRAVERSAL } } diff --git a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemAttributes.cs b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemAttributes.cs index ee69af46..f62908be 100644 --- a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemAttributes.cs +++ b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemAttributes.cs @@ -2,11 +2,14 @@ namespace SMBLibrary { + /// + /// [MS-FSCC] 2.5.1 - FileFsAttributeInformation + /// [Flags] public enum FileSystemAttributes : uint { CaseSensitiveSearch = 0x0001, // FILE_CASE_SENSITIVE_SEARCH - CasePreservedNamed = 0x0002, // FILE_CASE_PRESERVED_NAMES + CasePreservedNames = 0x0002, // FILE_CASE_PRESERVED_NAMES UnicodeOnDisk = 0x0004, // FILE_UNICODE_ON_DISK PersistentACLs = 0x0008, // FILE_PERSISTENT_ACLS FileCompression = 0x0010, // FILE_FILE_COMPRESSION @@ -14,6 +17,8 @@ public enum FileSystemAttributes : uint SupportsSparseFiles = 0x0040, // FILE_SUPPORTS_SPARSE_FILES SupportsReparsePoints = 0x0080, // FILE_SUPPORTS_REPARSE_POINTS SupportsRemoteStorage = 0x0100, // FILE_SUPPORTS_REMOTE_STORAGE + ReturnsCleanupResultInfo = 0x0200, // FILE_RETURNS_CLEANUP_RESULT_INFO + SupportsPOSIXUnlinkRename = 0x0400, // FILE_SUPPORTS_POSIX_UNLINK_RENAME VolumeIsCompressed = 0x8000, // FILE_VOLUME_IS_COMPRESSED SupportsObjectIDs = 0x00010000, // FILE_SUPPORTS_OBJECT_IDS SupportsEncryption = 0x00020000, // FILE_SUPPORTS_ENCRYPTION @@ -25,5 +30,8 @@ public enum FileSystemAttributes : uint SupportsExtendedAttributes = 0x00800000, // FILE_SUPPORTS_EXTENDED_ATTRIBUTES SupportsOpenByFileID = 0x01000000, // FILE_SUPPORTS_OPEN_BY_FILE_ID SupportsUSNJournal = 0x02000000, // FILE_SUPPORTS_USN_JOURNAL + SupportsIntegrityStreams = 0x04000000, // FILE_SUPPORT_INTEGRITY_STREAMS + SupportsBlockRefCounting = 0x08000000, // FILE_SUPPORTS_BLOCK_REFCOUNTING + SupportsSparseVDL = 0x10000000, // FILE_SUPPORTS_SPARSE_VDL } } diff --git a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemControlFlags.cs b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemControlFlags.cs index 9e97e064..6f985102 100644 --- a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemControlFlags.cs +++ b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/FileSystemControlFlags.cs @@ -1,6 +1,9 @@ namespace SMBLibrary { + /// + /// [MS-FSCC] 2.5.2 - FileFsControlInformation + /// public enum FileSystemControlFlags : uint { QuotaTrack = 0x00000001, // FILE_VC_QUOTA_TRACK diff --git a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/SectorSizeInformationFlags.cs b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/SectorSizeInformationFlags.cs index 7720a44b..1080f614 100644 --- a/SMBLibrary/NTFileStore/Enums/FileSystemInformation/SectorSizeInformationFlags.cs +++ b/SMBLibrary/NTFileStore/Enums/FileSystemInformation/SectorSizeInformationFlags.cs @@ -2,6 +2,9 @@ namespace SMBLibrary { + /// + /// [MS-FSCC] 2.5.7 - FileFsSectorSizeInformation + /// [Flags] public enum SectorSizeInformationFlags : uint { diff --git a/SMBLibrary/NTFileStore/Enums/NotifyChangeFilter.cs b/SMBLibrary/NTFileStore/Enums/NotifyChangeFilter.cs index 4ef3b42f..82edad51 100644 --- a/SMBLibrary/NTFileStore/Enums/NotifyChangeFilter.cs +++ b/SMBLibrary/NTFileStore/Enums/NotifyChangeFilter.cs @@ -2,6 +2,10 @@ namespace SMBLibrary { + /// + /// [MS-CIFS] 2.2.7.4.1 CompletionFilter + /// [MS-SMB2] 2.2.35 CompletionFilter + /// [Flags] public enum NotifyChangeFilter : uint { diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/FileInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/FileInformation.cs index b0b3843d..cc46f0a3 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/FileInformation.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/FileInformation.cs @@ -82,7 +82,7 @@ public static FileInformation GetFileInformation(byte[] buffer, int offset, File case FileInformationClass.FileNetworkOpenInformation: return new FileNetworkOpenInformation(buffer, offset); case FileInformationClass.FileAttributeTagInformation: - throw new NotImplementedException(); + return new FileAttributeTagInformation(buffer, offset); case FileInformationClass.FileValidDataLengthInformation: return new FileValidDataLengthInformation(buffer, offset); case FileInformationClass.FileShortNameInformation: diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/Query/FileAttributeTagInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/Query/FileAttributeTagInformation.cs new file mode 100644 index 00000000..bcd1fba3 --- /dev/null +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/Query/FileAttributeTagInformation.cs @@ -0,0 +1,55 @@ +/* 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 Utilities; + +namespace SMBLibrary +{ + /// + /// [MS-FSCC] 2.4.6 - FileAttributeTagInformation + /// + public class FileAttributeTagInformation : FileInformation + { + public const int FixedLength = 8; + + public FileAttributes FileAttributes; + public uint ReparsePointTag; + + public FileAttributeTagInformation() + { + } + + public FileAttributeTagInformation(byte[] buffer, int offset) + { + FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + 0); + ReparsePointTag = LittleEndianConverter.ToUInt32(buffer, offset + 4); + } + + public override void WriteBytes(byte[] buffer, int offset) + { + LittleEndianWriter.WriteUInt32(buffer, offset + 0, (uint)FileAttributes); + LittleEndianWriter.WriteUInt32(buffer, offset + 4, ReparsePointTag); + } + + public override FileInformationClass FileInformationClass + { + get + { + return FileInformationClass.FileAttributeTagInformation; + } + } + + public override int Length + { + get + { + return FixedLength; + } + } + } +} diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileBothDirectoryInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileBothDirectoryInformation.cs index 6012be52..51b2a66d 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileBothDirectoryInformation.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileBothDirectoryInformation.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2025 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 Utilities; namespace SMBLibrary @@ -37,10 +36,10 @@ public FileBothDirectoryInformation() public FileBothDirectoryInformation(byte[] buffer, int offset) : base(buffer, offset) { - CreationTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 8)); - LastAccessTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 16)); - LastWriteTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 24)); - ChangeTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 32)); + CreationTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 8); + LastAccessTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 16); + LastWriteTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 24); + ChangeTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 32); EndOfFile = LittleEndianConverter.ToInt64(buffer, offset + 40); AllocationSize = LittleEndianConverter.ToInt64(buffer, offset + 48); FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + 56); diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileDirectoryInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileDirectoryInformation.cs index e5acaf89..8bb18449 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileDirectoryInformation.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileDirectoryInformation.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2025 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 Utilities; namespace SMBLibrary @@ -33,10 +32,10 @@ public FileDirectoryInformation() public FileDirectoryInformation(byte[] buffer, int offset) : base(buffer, offset) { - CreationTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 8)); - LastAccessTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 16)); - LastWriteTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 24)); - ChangeTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 32)); + CreationTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 8); + LastAccessTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 16); + LastWriteTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 24); + ChangeTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 32); EndOfFile = LittleEndianConverter.ToInt64(buffer, offset + 40); AllocationSize = LittleEndianConverter.ToInt64(buffer, offset + 48); FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + 56); diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileFullDirectoryInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileFullDirectoryInformation.cs index d8b01c9e..602f06b2 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileFullDirectoryInformation.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileFullDirectoryInformation.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2025 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 Utilities; namespace SMBLibrary @@ -34,10 +33,10 @@ public FileFullDirectoryInformation() public FileFullDirectoryInformation(byte[] buffer, int offset) : base(buffer, offset) { - CreationTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 8)); - LastAccessTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 16)); - LastWriteTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 24)); - ChangeTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 32)); + CreationTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 8); + LastAccessTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 16); + LastWriteTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 24); + ChangeTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 32); EndOfFile = LittleEndianConverter.ToInt64(buffer, offset + 40); AllocationSize = LittleEndianConverter.ToInt64(buffer, offset + 48); FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + 56); diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdBothDirectoryInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdBothDirectoryInformation.cs index 9dbc1eff..cee51ead 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdBothDirectoryInformation.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdBothDirectoryInformation.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2025 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 Utilities; namespace SMBLibrary @@ -39,10 +38,10 @@ public FileIdBothDirectoryInformation() public FileIdBothDirectoryInformation(byte[] buffer, int offset) : base(buffer, offset) { - CreationTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 8)); - LastAccessTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 16)); - LastWriteTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 24)); - ChangeTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 32)); + CreationTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 8); + LastAccessTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 16); + LastWriteTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 24); + ChangeTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 32); EndOfFile = LittleEndianConverter.ToInt64(buffer, offset + 40); AllocationSize = LittleEndianConverter.ToInt64(buffer, offset + 48); FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + 56); diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdFullDirectoryInformation.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdFullDirectoryInformation.cs index 6edb7f8b..b09e22eb 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdFullDirectoryInformation.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/QueryDirectory/FileIdFullDirectoryInformation.cs @@ -1,11 +1,10 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2025 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 Utilities; namespace SMBLibrary @@ -36,10 +35,10 @@ public FileIdFullDirectoryInformation() public FileIdFullDirectoryInformation(byte[] buffer, int offset) : base(buffer, offset) { - CreationTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 8)); - LastAccessTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 16)); - LastWriteTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 24)); - ChangeTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + 32)); + CreationTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 8); + LastAccessTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 16); + LastWriteTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 24); + ChangeTime = FileTimeHelper.ReadFileTimeSafe(buffer, offset + 32); EndOfFile = LittleEndianConverter.ToInt64(buffer, offset + 40); AllocationSize = LittleEndianConverter.ToInt64(buffer, offset + 48); FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + 56); diff --git a/SMBLibrary/NTFileStore/Structures/FileInformation/Set/FileRenameInformationType2.cs b/SMBLibrary/NTFileStore/Structures/FileInformation/Set/FileRenameInformationType2.cs index 8af19219..9b1c5344 100644 --- a/SMBLibrary/NTFileStore/Structures/FileInformation/Set/FileRenameInformationType2.cs +++ b/SMBLibrary/NTFileStore/Structures/FileInformation/Set/FileRenameInformationType2.cs @@ -1,18 +1,16 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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 Utilities; namespace SMBLibrary { /// - /// [MS-FSCC] 2.4.34.2 - FileRenameInformation Type 2 + /// [MS-FSCC] 2.4.37.2 - FileRenameInformation Type 2 /// /// /// [MS-FSA] 2.1.5.14.11 @@ -28,6 +26,7 @@ public class FileRenameInformationType2 : FileInformation public ulong RootDirectory; private uint FileNameLength; public string FileName = String.Empty; + // Padding - the number of bytes required to make the size of this structure at least 24. public FileRenameInformationType2() { @@ -62,7 +61,7 @@ public override int Length { get { - return FixedLength + FileName.Length * 2; + return Math.Max(FixedLength + FileName.Length * 2, 24); } } } diff --git a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/ACE.cs b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/ACE.cs index d27f0c88..49866f80 100644 --- a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/ACE.cs +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/ACE.cs @@ -1,12 +1,10 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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 Utilities; namespace SMBLibrary @@ -16,6 +14,8 @@ namespace SMBLibrary /// public abstract class ACE { + public AceHeader Header; + public abstract void WriteBytes(byte[] buffer, ref int offset); public abstract int Length @@ -30,6 +30,8 @@ public static ACE GetAce(byte[] buffer, int offset) { case AceType.ACCESS_ALLOWED_ACE_TYPE: return new AccessAllowedACE(buffer, offset); + case AceType.ACCESS_DENIED_ACE_TYPE: + return new AccessDeniedACE(buffer, offset); default: throw new NotImplementedException(); } diff --git a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessAllowedACE.cs b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessAllowedACE.cs index 0b934210..f4e77125 100644 --- a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessAllowedACE.cs +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessAllowedACE.cs @@ -1,11 +1,9 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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 Utilities; namespace SMBLibrary @@ -17,7 +15,6 @@ public class AccessAllowedACE : ACE { public const int FixedLength = 8; - public AceHeader Header; public AccessMask Mask; public SID Sid; diff --git a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessDeniedACE.cs b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessDeniedACE.cs new file mode 100644 index 00000000..9a82a9e3 --- /dev/null +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessDeniedACE.cs @@ -0,0 +1,47 @@ +/* Copyright (C) 2014-2024 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 Utilities; + +namespace SMBLibrary +{ + public class AccessDeniedACE : ACE + { + public const int FixedLength = 8; + + public AccessMask Mask; + public SID Sid; + + public AccessDeniedACE() + { + Header = new AceHeader(); + Header.AceType = AceType.ACCESS_DENIED_ACE_TYPE; + } + + public AccessDeniedACE(byte[] buffer, int offset) + { + Header = new AceHeader(buffer, offset + 0); + Mask = (AccessMask)LittleEndianConverter.ToUInt32(buffer, offset + 4); + Sid = new SID(buffer, offset + 8); + } + + public override void WriteBytes(byte[] buffer, ref int offset) + { + Header.AceSize = (ushort)this.Length; + Header.WriteBytes(buffer, ref offset); + LittleEndianWriter.WriteUInt32(buffer, ref offset, (uint)Mask); + Sid.WriteBytes(buffer, ref offset); + } + + public override int Length + { + get + { + return FixedLength + Sid.Length; + } + } + } +} diff --git a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACL.cs b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACL.cs index f7b71cb1..712f88a2 100644 --- a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACL.cs +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACL.cs @@ -1,10 +1,9 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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 Utilities; @@ -41,7 +40,7 @@ public ACL(byte[] buffer, int offset) { ACE ace = ACE.GetAce(buffer, offset); this.Add(ace); - offset += ace.Length; + offset += ace.Header.AceSize; } } diff --git a/SMBLibrary/NTFileStore/Structures/SecurityInformation/SecurityDescriptor.cs b/SMBLibrary/NTFileStore/Structures/SecurityInformation/SecurityDescriptor.cs index 4657e10a..666303a9 100644 --- a/SMBLibrary/NTFileStore/Structures/SecurityInformation/SecurityDescriptor.cs +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/SecurityDescriptor.cs @@ -1,11 +1,9 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2026 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 Utilities; namespace SMBLibrary @@ -32,6 +30,7 @@ public class SecurityDescriptor public SecurityDescriptor() { Revision = 0x01; + Control = SecurityDescriptorControl.SelfRelative; } public SecurityDescriptor(byte[] buffer, int offset) @@ -99,7 +98,7 @@ public byte[] GetBytes() offset = 0; ByteWriter.WriteByte(buffer, ref offset, Revision); ByteWriter.WriteByte(buffer, ref offset, Sbz1); - LittleEndianWriter.WriteUInt16(buffer, ref offset, (ushort)Control); + LittleEndianWriter.WriteUInt16(buffer, ref offset, (ushort)(Control | SecurityDescriptorControl.SelfRelative)); LittleEndianWriter.WriteUInt32(buffer, ref offset, offsetOwner); LittleEndianWriter.WriteUInt32(buffer, ref offset, offsetGroup); LittleEndianWriter.WriteUInt32(buffer, ref offset, offsetSacl); diff --git a/SMBLibrary/NetBios/NBTConnectionReceiveBuffer.cs b/SMBLibrary/NetBios/NBTConnectionReceiveBuffer.cs index 47148d1e..53ca9ab7 100644 --- a/SMBLibrary/NetBios/NBTConnectionReceiveBuffer.cs +++ b/SMBLibrary/NetBios/NBTConnectionReceiveBuffer.cs @@ -1,17 +1,22 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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; +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER +using System.Buffers; +#endif using System.IO; using Utilities; namespace SMBLibrary.NetBios { - public class NBTConnectionReceiveBuffer + /// + /// NBTConnectionReceiveBuffer is not thread-safe. + /// + public class NBTConnectionReceiveBuffer : IDisposable { private byte[] m_buffer; private int m_readOffset = 0; @@ -29,17 +34,30 @@ public NBTConnectionReceiveBuffer(int bufferLength) { throw new ArgumentException("bufferLength must be large enough to hold the largest possible NBT packet"); } + +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER + m_buffer = ArrayPool.Shared.Rent(bufferLength); +#else m_buffer = new byte[bufferLength]; +#endif } public void IncreaseBufferSize(int bufferLength) { +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER + byte[] buffer = ArrayPool.Shared.Rent(bufferLength); +#else byte[] buffer = new byte[bufferLength]; +#endif if (m_bytesInBuffer > 0) { Array.Copy(m_buffer, m_readOffset, buffer, 0, m_bytesInBuffer); m_readOffset = 0; } + +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER + ArrayPool.Shared.Return(m_buffer); +#endif m_buffer = buffer; } @@ -110,6 +128,19 @@ private void RemovePacketBytes() } } + public void Dispose() + { +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER + if (m_buffer != null) + { + ArrayPool.Shared.Return(m_buffer); + m_buffer = null; + } +#else + m_buffer = null; +#endif + } + public byte[] Buffer { get diff --git a/SMBLibrary/NetBios/NameServicePackets/Enums/NetBiosSuffix.cs b/SMBLibrary/NetBios/NameServicePackets/Enums/NetBiosSuffix.cs index a7126fcc..9e11352e 100644 --- a/SMBLibrary/NetBios/NameServicePackets/Enums/NetBiosSuffix.cs +++ b/SMBLibrary/NetBios/NameServicePackets/Enums/NetBiosSuffix.cs @@ -1,12 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Text; - namespace SMBLibrary.NetBios { /// /// 16th character suffix for netbios name. - /// see http://support.microsoft.com/kb/163409/en-us + /// see https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nbte/6dbf0972-bb15-4f29-afeb-baaae98416ed /// public enum NetBiosSuffix : byte { @@ -15,6 +11,6 @@ public enum NetBiosSuffix : byte DomainMasterBrowser = 0x1B, MasterBrowser = 0x1D, BrowserServiceElections = 0x1E, - FileServiceService = 0x20, + FileServerService = 0x20, } } diff --git a/SMBLibrary/NetBios/NetBiosUtils.cs b/SMBLibrary/NetBios/NetBiosUtils.cs index 67fb9b1d..16612223 100644 --- a/SMBLibrary/NetBios/NetBiosUtils.cs +++ b/SMBLibrary/NetBios/NetBiosUtils.cs @@ -60,7 +60,7 @@ public static byte[] EncodeName(string name, NetBiosSuffix suffix, string scopeI return EncodeName(netBiosName, scopeID); } - /// NetBIOS name + /// NetBIOS name /// dot-separated labels, formatted per DNS naming rules public static byte[] EncodeName(string netBiosName, string scopeID) { @@ -75,7 +75,7 @@ public static byte[] EncodeName(string netBiosName, string scopeID) // into two nibbles and then adding the value of 'A' (0x41). // Thus, the '&' character (0x26) would be encoded as "CG". // NetBIOS names are usually padded with spaces before being encoded. - /// NetBIOS name + /// NetBIOS name /// dot-separated labels, formatted per DNS naming rules public static string FirstLevelEncoding(string netBiosName, string scopeID) { diff --git a/SMBLibrary/Properties/AssemblyInfo.cs b/SMBLibrary/Properties/AssemblyInfo.cs deleted file mode 100644 index 91f1f85f..00000000 --- a/SMBLibrary/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SMBLibrary")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Tal Aloni")] -[assembly: AssemblyProduct("SMBLibrary")] -[assembly: AssemblyCopyright("Copyright © Tal Aloni 2014-2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("301890e4-fc53-448e-8070-79d17086f922")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.4.6.2")] -[assembly: AssemblyFileVersion("1.4.6.2")] -[assembly: InternalsVisibleTo("SMBLibrary.Tests")] \ No newline at end of file diff --git a/SMBLibrary/Readme.md b/SMBLibrary/Readme.md index 7f98fbee..9f3ed60d 100644 --- a/SMBLibrary/Readme.md +++ b/SMBLibrary/Readme.md @@ -1,9 +1,9 @@ About SMBLibrary: ================= SMBLibrary is an open-source C# SMB 1.0/CIFS, SMB 2.0, SMB 2.1 and SMB 3.0 server and client implementation. -SMBLibrary gives .NET developers an easy way to share a directory / file system / virtual file system, with any operating system that supports the SMB protocol. +SMBLibrary gives .NET developers an easy way to share a directory / file system / virtual file system or to connect to an existing share, with any operating system that supports the SMB protocol. SMBLibrary is modular, you can take advantage of Integrated Windows Authentication and the Windows storage subsystem on a Windows host or use independent implementations that allow for cross-platform compatibility. -SMBLibrary shares can be accessed from any Windows version since Windows NT 4.0. +SMBLibrary can communicate with any Windows version since Windows NT 4.0. Supported SMB / CIFS transport methods: ======================================= @@ -15,47 +15,27 @@ Supported SMB / CIFS transport methods: - A 'keep alive' packet is sent from time to time over NBT connections. - SMB2: Direct TCP hosting supports large MTUs. -Notes: -====== -By default, Windows already use ports 139 and 445. there are several techniques to free / utilize those ports: - -##### Method 1: Disable Windows File and Printer Sharing server completely: -###### Windows XP/2003: -1. For every network adapter: Uncheck 'File and Printer Sharing for Microsoft Networks". -2. Navigate to 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NetBT\Parameters' and set 'SMBDeviceEnabled' to '0' (this will free port 445). -3. Reboot. - -###### Windows 7/8/2008/2012: -Disable the "Server" service (p.s. "TCP\IP NETBIOS Helper" should be enabled). - -##### Method 2: Use Windows File Sharing AND SMBLibrary: -Windows bind port 139 to the first IP addres of every adapter, while port 445 is bound globally. -This means that if you'll disable port 445 (or block it using a firewall), you'll be able to use a different service on port 139 for every IP address. - -###### Additional Notes: -* To free port 139 for a given adapter, go to 'Internet Protocol (TCP/IP) Properties' > Advanced > WINS, and select 'Disable NetBIOS over TCP/IP'. -Uncheck 'File and Printer Sharing for Microsoft Networks' to ensure Windows will not answer to SMB traffic on port 445 for this adapter. - -* It's important to note that disabling NetBIOS over TCP/IP will also disable NetBIOS name service for that adapter (a.k.a. WINS), This service uses UDP port 137. -SMBLibrary offers a name service of its own. - -* You can install a virtual network adapter driver for Windows to be used solely with SMBLibrary: - - You can install the 'Microsoft Loopback adapter' and use it for server-only communication with SMBLibrary. - -###### Windows 7/8/2008/2012: -* It's possible to prevent Windows from using port 445 by removing all of the '\Device\Tcpip_{..}' and '\Device\Tcpip6_{..}' entries from the `Bind' registry key under 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Linkage'. +Using SMBLibrary: +================= +Server notes can be found [here](../ServerNotes.md). -* if you want localhost access from Windows explorer to work as expected, you must specify the IP address that you selected (\\\\127.0.0.1 or \\\\localhost will not work as expected), in addition, I have observed that when connecting to the first IP address of a given adapter, Windows will only attempt to connect to port 445. +Client code examples can be found [here](../ClientExamples.md). -##### Method 3: Use an IP address that is invisible to Windows File Sharing: -Using PCap.Net you can programmatically setup a virtual Network adapter and intercept SMB traffic (similar to how a virtual machine operates), You should use the ARP protocol to notify the network about the new IP address, and then process the incoming SMB traffic using SMBLibrary, good luck! +NuGet Packages: +=============== +[SMBLibrary](https://www.nuget.org/packages/SMBLibrary/) - Cross-platform server and client implementation. +[SMBLibrary.Win32](https://www.nuget.org/packages/SMBLibrary.Win32/) - Allows utilizing Integrated Windows Authentication and/or the Windows storage subsystem on a Windows host. +[SMBLibrary.Adapters](https://www.nuget.org/packages/SMBLibrary.Adapters/) - IFileSystem to INTFileStore adapter for SMBLibrary. -Using SMBLibrary: -================= -Any directory / filesystem / object you wish to share must implement the IFileSystem interface (or the lower-level INTFileStore interface). -You can share anything from actual directories to custom objects, as long as they expose a directory structure. +Licensing: +========== +A commercial license of SMBLibrary is available for a fee. +This is intended for companies who are unable to use the LGPL version. +Please contact me for additional details. -Client code examples can be found [here](ClientExamples.md). +Contributions: +============== +If you choose to make a contribution to this project, you must agree to irrevocably assign to SMBLibrary and/or Tal Aloni all worldwide copyright and intellectual property rights in and to your contribution, effective upon submission. Contact: ======== diff --git a/SMBLibrary/RevisionHistory.txt b/SMBLibrary/RevisionHistory.txt index d03156b1..11e1d4c8 100644 --- a/SMBLibrary/RevisionHistory.txt +++ b/SMBLibrary/RevisionHistory.txt @@ -459,3 +459,117 @@ Revision History: NTLM: Bugfix: IndependentNTLMAuthenticationProvider login failed to to modification of message byte arrays. 1.4.6 - SMB2Client: Fixed InvalidCastException on failed login to SMB 3.0 server. + +1.4.7 - SMBServer: Added private Start overload allowing to specify the listening port. + Client: Added private Connect overload allowing to specify the server port. + SMB2Client: Correctly handle async responses. + SMB2Client: WaitForCommand: Compare MessageID instead of CommandName. + Client: SMB2FileStore: Implement Flush. + Client: Added support for accessing Cluster Shared Volumes file shares. + SMB2Command: Add MessageID property. + NTStatus: Added STATUS_WRONG_PASSWORD. + NTStatus: Correct STATUS_PASSWORD_MUST_CHANGE value. + +1.4.8 - SMBServer - Start method bugfix. + +1.4.9 - Server: SessionSetupHelper: Bugfix: Use correct sessionID for session creation in which token is accepted immediately. + Server: SessionSetupHelper: Bugfix: Trim session key if longer than 16 bytes. + Server: SMB2: Correctly handle invalid SessionSetup request containing a sessionID already in use. + Client: Fixed bug when trying to use local ip address as host name. + Client: Provide SPN with NTLMv2 token. + NTFileSystemAdapter: Avoid modifications of entries returned from IFileSystem. + +1.5.0 - Server: Fix issue when GSSAPI SessionKey is null. + Client: Added IsConnected property. + Client: Fixed IPv6 related issue. + Client: Add private overload to set response timeout in Connect method. + Client: Allow reusing client instance. + Client: Prefer IPv4 when resolving DNS hostname. + NTLMCryptography: Add .NET 5.0 \ 6.0 support. + +1.5.1 - Client: Support anonymous login. + Client: Added API to provide custom authentication. + Client: Improve disconnection detection. + Client: Add ability to control response timeout. + Client: Calculate Authentication message MIC. + NBTConnectionReceiveBuffer: Use ArrayPool rent buffers to reduce RAM usage. + Added access denied ace support. + +1.5.2 - Server: ServerService: correctly handle unsupported ShareEnum levels. + Server: Use CancellationToken for send keep-alive thread if available. + Server: ConnectionState: Fix thread-safety issue. + Client: Disconnect: Invoke Socket.Close. + Client: NetBIOS over TCP: Apply timeout set by the client instead of hardcoded value. + Client: Removed unneeded connectivity check before invoking EndReceive. + SMB2Client: Support non-Microsoft servers returning MaxReadSize > MaxTransactSize + NBTConnectionReceiveBuffer: Fix thread-safety issues. + FileRenameInformationType2: Bugfix: Ensure length is at least 24 bytes. + +1.5.3 - Server: SMBServer: Mark overloaded Start method as protected internal. + Client: Mark overloaded Start method as protected internal. + Client: Improve client response time when server disconnects or return invalid data. + Client: Fix possible NullReferenceException when disconnection occur during directory enumeration. + Client: SMB1FileStore, SMB2FileStore: Return STATUS_IO_TIMEOUT instead of STATUS_INVALID_SMB when server does not reply and there is no protocol violation. + +1.5.4 - Server: SMB2: QueryInfoHelper: Correctly handle UnsupportedInformationLevelException and NotImplementedException + Client: Update IsConnected on EndReceive socket exception. + Client: NTLMAuthenticationClient: Set MechanismListMIC when applicable. + SMB2Client: Correctly handle signing when authenticated as guest. + SMB2Client: Updated encryption and signing logic to support SMB 3.0.2. + SMB2Client: Added support for single-stage and triple-stage session setup. + SMB2: CreateResponse: Bugfix: Write CreateContexts offset and Length. + SMB2: CreateContext: Bugfix: Read and Write name as ANSI instead of UTF16. + SMB2: CreateContext: Bugfix: Correct Name and Data offset write location. + SMB2: CreateContext: Bugfix: Write DataLength. + SMB2: NegotiateContext: Fix parsing bugs. + SMB2: NegotiateRequest: Parse NegotiateContextList (SMB 3.1.1) + SMB2: NegotiateRequest: Do not pad request if NegotiateContextList is empty. + SMB2: NegotiateResponse: Bugfix: NegotiateContext was not read from the correct position for non-zero offset. + ACL: Bugfix: Use AceSize when parsing ACL. + +1.5.5 - SMB1Client: Fix UserID being ignored when extended security is set to false. + SMB2Client: Always sign outgoing messages when dialect is 3.1.1 + SMB2Client: Correctly report login error status instead of STATUS_INVALID_SMB. + SMB2Client: Enabled SMB 3.0.2 support by default. + SMB2Client: Added constructor argument to enable SMB 3.1.1 + SMB2Client: Throw exception if attempting to read or write more data than allowed for the connection. + SMB2Client: Correctly handle the uncommon case of a server not supporting large MTU over applicable connection. + Client: Improved handling of invalid FILETIME. + Client: Disconnect: Ensure ongoing reading operation is complete before closing the socket. + Client: Move responseTimeoutInMilliseconds from Connect method to constructor. + Client: Added virtual GetNetBiosServerName method. + Client: Added Echo command support. + SMB2: Added FileAttributeTagInformation support. + Server: SMB2: NegotiateResponse: Corrected CommandLength calculation. + +1.5.6 - Client: NTLM: Set NTLMSSP_REQUEST_TARGET bit in negotiate message. + SMB1Client: Correctly report login error status instead of STATUS_INVALID_SMB. + SMB1Client: Compute session key when extended security is set to false. + SMB2Client: Simplified enabling SMB 3.1.1 + SMB2Client: Implemented SetSecurityInformation. + NTLM: NegotiateFlags: Renamed TargetNameSupplied to TargetNameNegotiated. + NTLM: NegotiateFlags: Renamed RequestLMSessionKey to RequestNonNTSessionKey. + DFS: Implemented ResponseGetDfsReferral. + DFS: Implemented RequestGetDfsReferralEx. + SMB1: Implemented SMB_COM_QUERY_INFORMATION_DISK request and response. + Server: SMB1: Add support for SMB_COM_QUERY_INFORMATION_DISK used by Windows 98. + Server: SessionSetupHelper: Correctly handle NTLM v1 non-extended-session-security with LmChallengeResponse set to 0 bytes. + NTFileStore: SecurityDescriptor: Set SelfRelative bit when applicable. + NTFileStore: FileSystemAttributes: Fixed typo, Renamed CasePreservedNamed to CasePreservedNames. + NTFileStore: FileAccessMask: Added missing FILE_DELETE_CHILD. + NTFileStore: FileSystemAttributes: Added missing enum values. + NTFileStore: DeviceCharacteristics: Added additional enum values. + +1.5.7 - Client: NTLMAuthenticationClient: Added virtual CreateNegotiateMessage method. + Client: NTLMAuthenticationHelper: Added GetNegotiateMessage overload. + Client: NTLMAuthenticationHelper: GetNegotiateMessage: Added ability to set NTLMSSP_NEGOTIATE_SEAL. + Client: NTLMAuthenticationClient: Bugfix: DomainName and Workstation must be empty when Version is set. + RC4: Added Encrypt/Decrypt overload accepting RC4KeyState to allow encrypting streams. + NTLMCryptography: ComputeMechListMIC: Update seqNum type to uint. + NTLMCryptography: Extracted ComputeMessageHash method. + NTLMCryptography: Extracted ComputeMessageSignature. + NTLMCryptography: Added VerifyMessageHash method. + NTLMCryptography: Marked ComputeSignKey and ComputeSealKey as public. + IndependentNTLMAuthenticationProvider: Extracted virtual CreateChallengeMessage method. + IndependentNTLMAuthenticationProvider: Extract GetServerName method. + Client: Fix rare racing condition when Disconnect is called before response has been received. diff --git a/SMBLibrary/SMB1/Commands/NegotiateResponse.cs b/SMBLibrary/SMB1/Commands/NegotiateResponse.cs index f80b73b8..21500cde 100644 --- a/SMBLibrary/SMB1/Commands/NegotiateResponse.cs +++ b/SMBLibrary/SMB1/Commands/NegotiateResponse.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2026 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, @@ -60,7 +60,13 @@ public NegotiateResponse(byte[] buffer, int offset, bool isUnicode) : base(buffe // [MS-CIFS] <90> Padding is not added before DomainName // DomainName and ServerName are always in Unicode DomainName = SMB1Helper.ReadSMBString(this.SMBData, ref dataOffset, true); - ServerName = SMB1Helper.ReadSMBString(this.SMBData, ref dataOffset, true); + // [MS-SMB] 2.2.4.5.2.2 In order to determine whether the SMB_Data.Bytes.ServerName field is present, + // the client MUST check the SMB_Data.ByteCount field to determine whether additional data is present + // beyond the NULL terminator of the SMB_Data.Bytes.DomainName string. + if (dataOffset < this.SMBData.Length) + { + ServerName = SMB1Helper.ReadSMBString(this.SMBData, ref dataOffset, true); + } } public override byte[] GetBytes(bool isUnicode) diff --git a/SMBLibrary/SMB1/Commands/QueryInformationDiskRequest.cs b/SMBLibrary/SMB1/Commands/QueryInformationDiskRequest.cs new file mode 100644 index 00000000..ebfaec61 --- /dev/null +++ b/SMBLibrary/SMB1/Commands/QueryInformationDiskRequest.cs @@ -0,0 +1,32 @@ +/* Copyright (C) 2025 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. + */ +namespace SMBLibrary.SMB1 +{ + /// + /// SMB_COM_QUERY_INFORMATION_DISK Request. + /// This command is deprecated. + /// This command is used by Windows 98 SE. + /// + public class QueryInformationDiskRequest : SMB1Command + { + public QueryInformationDiskRequest() + { + } + + public QueryInformationDiskRequest(byte[] buffer, int offset) : base(buffer, offset, false) + { + } + + public override CommandName CommandName + { + get + { + return CommandName.SMB_COM_QUERY_INFORMATION_DISK; + } + } + } +} diff --git a/SMBLibrary/SMB1/Commands/QueryInformationDiskResponse.cs b/SMBLibrary/SMB1/Commands/QueryInformationDiskResponse.cs new file mode 100644 index 00000000..e443a438 --- /dev/null +++ b/SMBLibrary/SMB1/Commands/QueryInformationDiskResponse.cs @@ -0,0 +1,59 @@ +/* Copyright (C) 2025 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 Utilities; + +namespace SMBLibrary.SMB1 +{ + /// + /// SMB_COM_QUERY_INFORMATION_DISK Response. + /// This command is deprecated. + /// This command is used by Windows 98 SE. + /// + public class QueryInformationDiskResponse : SMB1Command + { + public const int ParameterLength = 10; + // Parameters: + public ushort TotalUnits; + public ushort BlocksPerUnit; + public ushort BlockSize; + public ushort FreeUnits; + public ushort Reserved; + + public QueryInformationDiskResponse() + { + } + + public QueryInformationDiskResponse(byte[] buffer, int offset) : base(buffer, offset, false) + { + TotalUnits = LittleEndianConverter.ToUInt16(this.SMBParameters, 0); + BlocksPerUnit = LittleEndianConverter.ToUInt16(this.SMBParameters, 2); + BlockSize = LittleEndianConverter.ToUInt16(this.SMBParameters, 4); + FreeUnits = LittleEndianConverter.ToUInt16(this.SMBParameters, 6); + Reserved = LittleEndianConverter.ToUInt16(this.SMBParameters, 8); + } + + public override byte[] GetBytes(bool isUnicode) + { + this.SMBParameters = new byte[ParameterLength]; + LittleEndianWriter.WriteUInt16(this.SMBParameters, 0, TotalUnits); + LittleEndianWriter.WriteUInt16(this.SMBParameters, 2, BlocksPerUnit); + LittleEndianWriter.WriteUInt16(this.SMBParameters, 4, BlockSize); + LittleEndianWriter.WriteUInt16(this.SMBParameters, 6, FreeUnits); + LittleEndianWriter.WriteUInt16(this.SMBParameters, 8, Reserved); + + return base.GetBytes(isUnicode); + } + + public override CommandName CommandName + { + get + { + return CommandName.SMB_COM_QUERY_INFORMATION_DISK; + } + } + } +} diff --git a/SMBLibrary/SMB1/Commands/SMB1Command.cs b/SMBLibrary/SMB1/Commands/SMB1Command.cs index 6b06cefc..fccc9f02 100644 --- a/SMBLibrary/SMB1/Commands/SMB1Command.cs +++ b/SMBLibrary/SMB1/Commands/SMB1Command.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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, @@ -154,6 +154,8 @@ public static SMB1Command ReadCommandRequest(byte[] buffer, int offset, CommandN return new LogoffAndXRequest(buffer, offset); case CommandName.SMB_COM_TREE_CONNECT_ANDX: return new TreeConnectAndXRequest(buffer, offset, isUnicode); + case CommandName.SMB_COM_QUERY_INFORMATION_DISK: + return new QueryInformationDiskRequest(buffer, offset); case CommandName.SMB_COM_NT_TRANSACT: return new NTTransactRequest(buffer, offset); case CommandName.SMB_COM_NT_TRANSACT_SECONDARY: @@ -443,6 +445,21 @@ public static SMB1Command ReadCommandResponse(byte[] buffer, int offset, Command throw new InvalidDataException(); } } + case CommandName.SMB_COM_QUERY_INFORMATION_DISK: + { + if (wordCount * 2 == QueryInformationResponse.ParameterLength) + { + return new QueryInformationDiskResponse(buffer, offset); + } + else if (wordCount == 0) + { + return new ErrorResponse(commandName); + } + else + { + throw new InvalidDataException(); + } + } case CommandName.SMB_COM_NT_TRANSACT: { if (wordCount * 2 == NTTransactInterimResponse.ParametersLength) diff --git a/SMBLibrary/SMB1/Enums/CommandName.cs b/SMBLibrary/SMB1/Enums/CommandName.cs index 591b5860..41af975b 100644 --- a/SMBLibrary/SMB1/Enums/CommandName.cs +++ b/SMBLibrary/SMB1/Enums/CommandName.cs @@ -32,6 +32,7 @@ public enum CommandName : byte SMB_COM_SESSION_SETUP_ANDX = 0x73, SMB_COM_LOGOFF_ANDX = 0x74, SMB_COM_TREE_CONNECT_ANDX = 0x75, + SMB_COM_QUERY_INFORMATION_DISK = 0x80, SMB_COM_NT_TRANSACT = 0xA0, SMB_COM_NT_TRANSACT_SECONDARY = 0xA1, SMB_COM_NT_CREATE_ANDX = 0xA2, diff --git a/SMBLibrary/SMB1/SMB1Cryptography.cs b/SMBLibrary/SMB1/SMB1Cryptography.cs new file mode 100644 index 00000000..c6c08bee --- /dev/null +++ b/SMBLibrary/SMB1/SMB1Cryptography.cs @@ -0,0 +1,29 @@ +/* Copyright (C) 2025 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.Security.Cryptography; +using Utilities; + +namespace SMBLibrary.SMB1 +{ + internal class SMB1Cryptography + { + public static ulong CalculateSignature(byte[] signingKey, byte[] challengeResponse, byte[] buffer, int offset, int paddedLength) + { + byte[] temp; + if (challengeResponse != null) + { + temp = ByteUtils.Concatenate(ByteUtils.Concatenate(signingKey, challengeResponse), ByteReader.ReadBytes(buffer, offset, paddedLength)); + } + else + { + temp = ByteUtils.Concatenate(signingKey, ByteReader.ReadBytes(buffer, offset, paddedLength)); + } + byte[] hash = MD5.Create().ComputeHash(temp); + return LittleEndianConverter.ToUInt64(hash, 0); + } + } +} diff --git a/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralRequest.cs b/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralRequest.cs index fb4146e0..4f2c31bd 100644 --- a/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralRequest.cs +++ b/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralRequest.cs @@ -4,8 +4,7 @@ * 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.DFS; using Utilities; namespace SMBLibrary.SMB1 diff --git a/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralResponse.cs b/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralResponse.cs index c01dcfdf..32d05fc7 100644 --- a/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralResponse.cs +++ b/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2GetDfsReferralResponse.cs @@ -4,9 +4,7 @@ * 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 Utilities; +using SMBLibrary.DFS; namespace SMBLibrary.SMB1 { diff --git a/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2QueryFileInformationResponse.cs b/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2QueryFileInformationResponse.cs index 75b156b9..bc750c1a 100644 --- a/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2QueryFileInformationResponse.cs +++ b/SMBLibrary/SMB1/Transaction2Subcommands/Transaction2QueryFileInformationResponse.cs @@ -12,7 +12,7 @@ namespace SMBLibrary.SMB1 { /// /// TRANS2_QUERY_FILE_INFORMATION Response - /// public class Transaction2QueryFileInformationResponse : Transaction2Subcommand { public const int ParametersLength = 2; diff --git a/SMBLibrary/SMB2/Commands/CreateResponse.cs b/SMBLibrary/SMB2/Commands/CreateResponse.cs index 6ec9e2a4..5bdff92f 100644 --- a/SMBLibrary/SMB2/Commands/CreateResponse.cs +++ b/SMBLibrary/SMB2/Commands/CreateResponse.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -15,6 +15,7 @@ namespace SMBLibrary.SMB2 /// public class CreateResponse : SMB2Command { + public const int FixedLength = 88; public const int DeclaredSize = 89; private ushort StructureSize; @@ -30,7 +31,7 @@ public class CreateResponse : SMB2Command public FileAttributes FileAttributes; public uint Reserved2; public FileID FileId; - private uint CreateContextsOffsets; + private uint CreateContextsOffset; private uint CreateContextsLength; public List CreateContexts = new List(); @@ -55,11 +56,11 @@ public CreateResponse(byte[] buffer, int offset) : base(buffer, offset) FileAttributes = (FileAttributes)LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 56); Reserved2 = LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 60); FileId = new FileID(buffer, offset + SMB2Header.Length + 64); - CreateContextsOffsets = LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 80); + CreateContextsOffset = LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 80); CreateContextsLength = LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 84); if (CreateContextsLength > 0) { - CreateContexts = CreateContext.ReadCreateContextList(buffer, offset + (int)CreateContextsOffsets); + CreateContexts = CreateContext.ReadCreateContextList(buffer, offset + (int)CreateContextsOffset); } } @@ -78,20 +79,18 @@ public override void WriteCommandBytes(byte[] buffer, int offset) LittleEndianWriter.WriteUInt32(buffer, offset + 56, (uint)FileAttributes); LittleEndianWriter.WriteUInt32(buffer, offset + 60, Reserved2); FileId.WriteBytes(buffer, offset + 64); - CreateContextsOffsets = 0; + CreateContextsOffset = CreateContexts.Count > 0 ? (uint)(SMB2Header.Length + FixedLength) : 0; CreateContextsLength = (uint)CreateContext.GetCreateContextListLength(CreateContexts); - if (CreateContexts.Count > 0) - { - CreateContextsOffsets = SMB2Header.Length + 88; - CreateContext.WriteCreateContextList(buffer, 88, CreateContexts); - } + LittleEndianWriter.WriteUInt32(buffer, offset + 80, CreateContextsOffset); + LittleEndianWriter.WriteUInt32(buffer, offset + 84, CreateContextsLength); + CreateContext.WriteCreateContextList(buffer, SMB2Header.Length + FixedLength, CreateContexts); } public override int CommandLength { get { - return 88 + CreateContext.GetCreateContextListLength(CreateContexts); + return FixedLength + CreateContext.GetCreateContextListLength(CreateContexts); } } } diff --git a/SMBLibrary/SMB2/Commands/NegotiateRequest.cs b/SMBLibrary/SMB2/Commands/NegotiateRequest.cs index 58e0f5de..eed46547 100644 --- a/SMBLibrary/SMB2/Commands/NegotiateRequest.cs +++ b/SMBLibrary/SMB2/Commands/NegotiateRequest.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -23,8 +23,9 @@ public class NegotiateRequest : SMB2Command public ushort Reserved; public Capabilities Capabilities; // If the client does not implements the SMB 3.x dialect family, this field MUST be set to 0. public Guid ClientGuid; - public DateTime ClientStartTime; + public DateTime ClientStartTime; // If Dialects does not contain SMB311 public List Dialects = new List(); + public List NegotiateContextList = new List(); // If Dialects contains SMB311 public NegotiateRequest() : base(SMB2CommandName.Negotiate) { @@ -39,12 +40,28 @@ public NegotiateRequest(byte[] buffer, int offset) : base(buffer, offset) Reserved = LittleEndianConverter.ToUInt16(buffer, offset + SMB2Header.Length + 6); Capabilities = (Capabilities)LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 8); ClientGuid = LittleEndianConverter.ToGuid(buffer, offset + SMB2Header.Length + 12); - ClientStartTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + SMB2Header.Length + 28)); + bool containsNegotiateContextList = false; for (int index = 0; index < dialectCount; index++) { SMB2Dialect dialect = (SMB2Dialect)LittleEndianConverter.ToUInt16(buffer, offset + SMB2Header.Length + 36 + index * 2); Dialects.Add(dialect); + + if (dialect == SMB2Dialect.SMB311) + { + containsNegotiateContextList = true; + } + } + + if (containsNegotiateContextList) + { + uint negotiateContextOffset = LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 28); + ushort NegotiateContextCount = LittleEndianConverter.ToUInt16(buffer, offset + SMB2Header.Length + 32); + NegotiateContextList = NegotiateContext.ReadNegotiateContextList(buffer, offset + (int)negotiateContextOffset, NegotiateContextCount); + } + else + { + ClientStartTime = DateTime.FromFileTimeUtc(LittleEndianConverter.ToInt64(buffer, offset + SMB2Header.Length + 28)); } } @@ -56,12 +73,31 @@ public override void WriteCommandBytes(byte[] buffer, int offset) LittleEndianWriter.WriteUInt16(buffer, offset + 6, Reserved); LittleEndianWriter.WriteUInt32(buffer, offset + 8, (uint)Capabilities); LittleEndianWriter.WriteGuid(buffer, offset + 12, ClientGuid); - LittleEndianWriter.WriteInt64(buffer, offset + 28, ClientStartTime.ToFileTimeUtc()); - + + bool containsSMB311Dialect = false; for (int index = 0; index < Dialects.Count; index++) { SMB2Dialect dialect = Dialects[index]; LittleEndianWriter.WriteUInt16(buffer, offset + 36 + index * 2, (ushort)dialect); + + if (dialect == SMB2Dialect.SMB311) + { + containsSMB311Dialect = true; + } + } + + if (containsSMB311Dialect) + { + int paddingLength = (8 - ((36 + Dialects.Count * 2) % 8)) % 8; + uint negotiateContextOffset = (uint)(SMB2Header.Length + 36 + Dialects.Count * 2 + paddingLength); + ushort negotiateContextCount = (ushort)NegotiateContextList.Count; + LittleEndianWriter.WriteUInt32(buffer, offset + 28, negotiateContextOffset); + LittleEndianWriter.WriteUInt16(buffer, offset + 32, negotiateContextCount); + NegotiateContext.WriteNegotiateContextList(buffer, offset - SMB2Header.Length + (int)negotiateContextOffset, NegotiateContextList); + } + else + { + LittleEndianWriter.WriteInt64(buffer, offset + 28, ClientStartTime.ToFileTimeUtc()); } } @@ -69,7 +105,17 @@ public override int CommandLength { get { - return 36 + Dialects.Count * 2; + bool containsSMB311Dialect = Dialects.Contains(SMB2Dialect.SMB311); + if (containsSMB311Dialect && NegotiateContextList.Count > 0) + { + int paddingLength = (8 - ((36 + Dialects.Count * 2) % 8)) % 8; + int negotiateContextListLength = NegotiateContext.GetNegotiateContextListLength(NegotiateContextList); + return 36 + Dialects.Count * 2 + paddingLength + negotiateContextListLength; + } + else + { + return 36 + Dialects.Count * 2; + } } } } diff --git a/SMBLibrary/SMB2/Commands/NegotiateResponse.cs b/SMBLibrary/SMB2/Commands/NegotiateResponse.cs index af4edb32..3b0bb769 100644 --- a/SMBLibrary/SMB2/Commands/NegotiateResponse.cs +++ b/SMBLibrary/SMB2/Commands/NegotiateResponse.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -58,7 +58,7 @@ public NegotiateResponse(byte[] buffer, int offset) : base(buffer, offset) SecurityBufferLength = LittleEndianConverter.ToUInt16(buffer, offset + SMB2Header.Length + 58); NegotiateContextOffset = LittleEndianConverter.ToUInt32(buffer, offset + SMB2Header.Length + 60); SecurityBuffer = ByteReader.ReadBytes(buffer, offset + SecurityBufferOffset, SecurityBufferLength); - NegotiateContextList = NegotiateContext.ReadNegotiateContextList(buffer, (int)NegotiateContextOffset, NegotiateContextCount); + NegotiateContextList = NegotiateContext.ReadNegotiateContextList(buffer, offset + (int)NegotiateContextOffset, NegotiateContextCount); } public override void WriteCommandBytes(byte[] buffer, int offset) @@ -105,7 +105,7 @@ public override int CommandLength } else { - int paddedSecurityBufferLength = (int)Math.Ceiling((double)SecurityBufferLength / 8) * 8; + int paddedSecurityBufferLength = (int)Math.Ceiling((double)SecurityBuffer.Length / 8) * 8; return FixedSize + paddedSecurityBufferLength + NegotiateContext.GetNegotiateContextListLength(NegotiateContextList); } } diff --git a/SMBLibrary/SMB2/Commands/SMB2Command.cs b/SMBLibrary/SMB2/Commands/SMB2Command.cs index 0738cbf7..fd2b7988 100644 --- a/SMBLibrary/SMB2/Commands/SMB2Command.cs +++ b/SMBLibrary/SMB2/Commands/SMB2Command.cs @@ -135,7 +135,7 @@ public static byte[] GetCommandChainBytes(List commands) return GetCommandChainBytes(commands, null, SMB2Dialect.SMB2xx); } - /// + /// /// Message will be signed using this key if (not null and) SMB2_FLAGS_SIGNED is set. /// /// @@ -179,8 +179,7 @@ public static byte[] GetCommandChainBytes(List commands, byte[] sig { // [MS-SMB2] Any padding at the end of the message MUST be used in the hash computation. byte[] signature = SMB2Cryptography.CalculateSignature(signingKey, dialect, buffer, offset, paddedLength); - // [MS-SMB2] The first 16 bytes of the hash MUST be copied into the 16-byte signature field of the SMB2 Header. - ByteWriter.WriteBytes(buffer, offset + SMB2Header.SignatureOffset, signature, 16); + ByteWriter.WriteBytes(buffer, offset + SMB2Header.SignatureOffset, signature, SMB2Header.SignatureLength); } offset += paddedLength; } diff --git a/SMBLibrary/SMB2/Enums/Negotiate/Capabilities.cs b/SMBLibrary/SMB2/Enums/Negotiate/Capabilities.cs index e7b6c66c..630b2b63 100644 --- a/SMBLibrary/SMB2/Enums/Negotiate/Capabilities.cs +++ b/SMBLibrary/SMB2/Enums/Negotiate/Capabilities.cs @@ -6,11 +6,12 @@ namespace SMBLibrary.SMB2 public enum Capabilities : uint { DFS = 0x00000001, // SMB2_GLOBAL_CAP_DFS - Leasing = 0x00000002, // SMB2_GLOBAL_CAP_LEASING - LargeMTU = 0x0000004, // SMB2_GLOBAL_CAP_LARGE_MTU - MultiChannel = 0x0000008, // SMB2_GLOBAL_CAP_MULTI_CHANNEL - PersistentHandles = 0x00000010, // SMB2_GLOBAL_CAP_PERSISTENT_HANDLES - DirectoryLeasing = 0x00000020, // SMB2_GLOBAL_CAP_DIRECTORY_LEASING - Encryption = 0x00000040, // SMB2_GLOBAL_CAP_ENCRYPTION (SMB 3.x) + Leasing = 0x00000002, // SMB2_GLOBAL_CAP_LEASING (SMB 2.1+) + LargeMTU = 0x0000004, // SMB2_GLOBAL_CAP_LARGE_MTU (SMB 2.1+) + MultiChannel = 0x0000008, // SMB2_GLOBAL_CAP_MULTI_CHANNEL (SMB 3.0+) + PersistentHandles = 0x00000010, // SMB2_GLOBAL_CAP_PERSISTENT_HANDLES (SMB 3.0+) + DirectoryLeasing = 0x00000020, // SMB2_GLOBAL_CAP_DIRECTORY_LEASING (SMB 3.0+) + Encryption = 0x00000040, // SMB2_GLOBAL_CAP_ENCRYPTION (SMB 3.0 / SMB 3.0.2) + Notifications = 0x00000080, // SMB2_GLOBAL_CAP_NOTIFICATIONS (SMB 3.1.1) } } diff --git a/SMBLibrary/SMB2/Enums/Negotiate/NegotiateContextType.cs b/SMBLibrary/SMB2/Enums/Negotiate/NegotiateContextType.cs index f71cbde7..913b9ca8 100644 --- a/SMBLibrary/SMB2/Enums/Negotiate/NegotiateContextType.cs +++ b/SMBLibrary/SMB2/Enums/Negotiate/NegotiateContextType.cs @@ -5,5 +5,10 @@ public enum NegotiateContextType : ushort { SMB2_PREAUTH_INTEGRITY_CAPABILITIES = 0x0001, SMB2_ENCRYPTION_CAPABILITIES = 0x0002, + SMB2_COMPRESSION_CAPABILITIES = 0x0003, + SMB2_NETNAME_NEGOTIATE_CONTEXT_ID = 0x0005, + SMB2_TRANSPORT_CAPABILITIES = 0x0006, + SMB2_RDMA_TRANSFORM_CAPABILITIES = 0x0007, + SMB2_SIGNING_CAPABILITIES = 0x0008, } } diff --git a/SMBLibrary/SMB2/SMB2Cryptography.cs b/SMBLibrary/SMB2/SMB2Cryptography.cs index b88d8557..ba89d1e7 100644 --- a/SMBLibrary/SMB2/SMB2Cryptography.cs +++ b/SMBLibrary/SMB2/SMB2Cryptography.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2020-2026 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, @@ -16,14 +16,26 @@ internal class SMB2Cryptography public static byte[] CalculateSignature(byte[] signingKey, SMB2Dialect dialect, byte[] buffer, int offset, int paddedLength) { + byte[] hash; if (dialect == SMB2Dialect.SMB202 || dialect == SMB2Dialect.SMB210) { - return new HMACSHA256(signingKey).ComputeHash(buffer, offset, paddedLength); + hash = new HMACSHA256(signingKey).ComputeHash(buffer, offset, paddedLength); } else { - return AesCmac.CalculateAesCmac(signingKey, buffer, offset, paddedLength); + hash = AesCmac.CalculateAesCmac(signingKey, buffer, offset, paddedLength); } + + // [MS-SMB2] The first 16 bytes of the hash MUST be copied into the 16-byte signature field of the SMB2 Header. + return ByteReader.ReadBytes(hash, 0, SMB2Header.SignatureLength); + } + + public static bool VerifySignature(byte[] messageBytes, SMB2Dialect dialect, byte[] signingKey) + { + byte[] signature = ByteReader.ReadBytes(messageBytes, SMB2Header.SignatureOffset, SMB2Header.SignatureLength); + Array.Clear(messageBytes, SMB2Header.SignatureOffset, SMB2Header.SignatureLength); + byte[] expectedSignature = CalculateSignature(signingKey, dialect, messageBytes, 0, messageBytes.Length); + return ByteUtils.AreByteArraysEqual(signature, expectedSignature); } public static byte[] GenerateSigningKey(byte[] sessionKey, SMB2Dialect dialect, byte[] preauthIntegrityHashValue) @@ -107,6 +119,18 @@ public static byte[] DecryptMessage(byte[] key, SMB2TransformHeader transformHea return AesCcm.DecryptAndAuthenticate(key, aesCcmNonce, encryptedMessage, associatedData, transformHeader.Signature); } + public static byte[] ComputeHash(HashAlgorithm hashAlgorithm, byte[] buffer) + { + if (hashAlgorithm == HashAlgorithm.SHA512) + { + return SHA512.Create().ComputeHash(buffer); + } + else + { + throw new NotSupportedException($"Hash algorithm {hashAlgorithm} is not supported"); + } + } + private static SMB2TransformHeader CreateTransformHeader(byte[] nonce, int originalMessageLength, ulong sessionID) { byte[] nonceWithPadding = new byte[SMB2TransformHeader.NonceLength]; diff --git a/SMBLibrary/SMB2/SMB2Header.cs b/SMBLibrary/SMB2/SMB2Header.cs index bcb1b847..4ab8c588 100644 --- a/SMBLibrary/SMB2/SMB2Header.cs +++ b/SMBLibrary/SMB2/SMB2Header.cs @@ -4,7 +4,6 @@ * 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 Utilities; namespace SMBLibrary.SMB2 @@ -13,6 +12,7 @@ public class SMB2Header { public const int Length = 64; public const int SignatureOffset = 48; + public const int SignatureLength = 16; public static readonly byte[] ProtocolSignature = new byte[] { 0xFE, 0x53, 0x4D, 0x42 }; @@ -36,7 +36,7 @@ public SMB2Header(SMB2CommandName commandName) ProtocolId = ProtocolSignature; StructureSize = Length; Command = commandName; - Signature = new byte[16]; + Signature = new byte[SignatureLength]; } public SMB2Header(byte[] buffer, int offset) @@ -62,7 +62,7 @@ public SMB2Header(byte[] buffer, int offset) SessionID = LittleEndianConverter.ToUInt64(buffer, offset + 40); if ((Flags & SMB2PacketHeaderFlags.Signed) > 0) { - Signature = ByteReader.ReadBytes(buffer, offset + 48, 16); + Signature = ByteReader.ReadBytes(buffer, offset + 48, SignatureLength); } } diff --git a/SMBLibrary/SMB2/Structures/CreateContext.cs b/SMBLibrary/SMB2/Structures/CreateContext.cs index b537a42c..cd5d5b74 100644 --- a/SMBLibrary/SMB2/Structures/CreateContext.cs +++ b/SMBLibrary/SMB2/Structures/CreateContext.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -44,7 +44,7 @@ public CreateContext(byte[] buffer, int offset) DataLength = LittleEndianConverter.ToUInt32(buffer, offset + 12); if (NameLength > 0) { - Name = ByteReader.ReadUTF16String(buffer, offset + NameOffset, NameLength / 2); + Name = ByteReader.ReadAnsiString(buffer, offset + NameOffset, NameLength); } if (DataLength > 0) { @@ -56,24 +56,25 @@ private void WriteBytes(byte[] buffer, int offset) { LittleEndianWriter.WriteUInt32(buffer, offset + 0, Next); NameOffset = 0; - NameLength = (ushort)(Name.Length * 2); + NameLength = (ushort)Name.Length; if (Name.Length > 0) { - NameOffset = (ushort)FixedLength; + NameOffset = FixedLength; } LittleEndianWriter.WriteUInt16(buffer, offset + 4, NameOffset); LittleEndianWriter.WriteUInt16(buffer, offset + 6, NameLength); LittleEndianWriter.WriteUInt16(buffer, offset + 8, Reserved); - DataOffset = 0; + DataOffset = (ushort)(FixedLength + NameLength); DataLength = (uint)Data.Length; if (Data.Length > 0) { - int paddedNameLength = (int)Math.Ceiling((double)(Name.Length * 2) / 8) * 8; + int paddedNameLength = (int)Math.Ceiling((double)Name.Length / 8) * 8; DataOffset = (ushort)(FixedLength + paddedNameLength); } LittleEndianWriter.WriteUInt16(buffer, offset + 10, DataOffset); - ByteWriter.WriteUTF16String(buffer, NameOffset, Name); - ByteWriter.WriteBytes(buffer, DataOffset, Data); + LittleEndianWriter.WriteUInt32(buffer, offset + 12, DataLength); + ByteWriter.WriteAnsiString(buffer, offset + NameOffset, Name); + ByteWriter.WriteBytes(buffer, offset + DataOffset, Data); } public int Length diff --git a/SMBLibrary/SMB2/Structures/Enums/CipherAlgorithm.cs b/SMBLibrary/SMB2/Structures/Enums/CipherAlgorithm.cs new file mode 100644 index 00000000..a9da60fa --- /dev/null +++ b/SMBLibrary/SMB2/Structures/Enums/CipherAlgorithm.cs @@ -0,0 +1,10 @@ +namespace SMBLibrary.SMB2 +{ + public enum CipherAlgorithm : ushort + { + Aes128Ccm = 0x0001, + Aes128Gcm = 0x0002, + Aes256Ccm = 0x0003, + Aes256Gcm = 0x0004, + } +} diff --git a/SMBLibrary/SMB2/Structures/Enums/HashAlgorithm.cs b/SMBLibrary/SMB2/Structures/Enums/HashAlgorithm.cs new file mode 100644 index 00000000..93e30f2d --- /dev/null +++ b/SMBLibrary/SMB2/Structures/Enums/HashAlgorithm.cs @@ -0,0 +1,7 @@ +namespace SMBLibrary.SMB2 +{ + public enum HashAlgorithm : ushort + { + SHA512 = 1 + } +} diff --git a/SMBLibrary/SMB2/Structures/NegotiateContext/EncryptionCapabilities.cs b/SMBLibrary/SMB2/Structures/NegotiateContext/EncryptionCapabilities.cs new file mode 100644 index 00000000..ca8d7dc9 --- /dev/null +++ b/SMBLibrary/SMB2/Structures/NegotiateContext/EncryptionCapabilities.cs @@ -0,0 +1,53 @@ +/* Copyright (C) 2024 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.Collections.Generic; +using Utilities; + +namespace SMBLibrary.SMB2 +{ + /// + /// [MS-SMB2] 2.2.3.1.2 SMB2_ENCRYPTION_CAPABILITIES + /// + public class EncryptionCapabilities : NegotiateContext + { + // ushort CipherCount; + public List Ciphers = new List(); + + public EncryptionCapabilities() + { + } + + public EncryptionCapabilities(byte[] buffer, int offset) : base(buffer, offset) + { + ushort cipherCount = LittleEndianConverter.ToUInt16(Data, 0); + for (int index = 0; index < cipherCount; index++) + { + Ciphers.Add((CipherAlgorithm)LittleEndianConverter.ToUInt16(Data, index * 2)); + } + } + + public override void WriteData() + { + Data = new byte[DataLength]; + LittleEndianWriter.WriteUInt16(Data, 0, (ushort)Ciphers.Count); + for (int index = 0; index < Ciphers.Count; index++) + { + LittleEndianWriter.WriteUInt16(Data, 2 + index * 2, (ushort)Ciphers[index]); + } + } + + public override int DataLength + { + get + { + return 2 + Ciphers.Count * 2; + } + } + + public override NegotiateContextType ContextType => NegotiateContextType.SMB2_ENCRYPTION_CAPABILITIES; + } +} diff --git a/SMBLibrary/SMB2/Structures/NegotiateContext.cs b/SMBLibrary/SMB2/Structures/NegotiateContext/NegotiateContext.cs similarity index 55% rename from SMBLibrary/SMB2/Structures/NegotiateContext.cs rename to SMBLibrary/SMB2/Structures/NegotiateContext/NegotiateContext.cs index c36f64d3..922eb710 100644 --- a/SMBLibrary/SMB2/Structures/NegotiateContext.cs +++ b/SMBLibrary/SMB2/Structures/NegotiateContext/NegotiateContext.cs @@ -1,10 +1,9 @@ -/* Copyright (C) 2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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 Utilities; @@ -17,8 +16,8 @@ public class NegotiateContext { public const int FixedLength = 8; - public NegotiateContextType ContextType; - private ushort DataLength; + private NegotiateContextType m_contextType; + // ushort DataLength; public uint Reserved; public byte[] Data = new byte[0]; @@ -28,17 +27,21 @@ public NegotiateContext() public NegotiateContext(byte[] buffer, int offset) { - ContextType = (NegotiateContextType)LittleEndianConverter.ToUInt16(buffer, offset + 0); - DataLength = LittleEndianConverter.ToUInt16(buffer, offset + 2); + m_contextType = (NegotiateContextType)LittleEndianConverter.ToUInt16(buffer, offset + 0); + int dataLength = LittleEndianConverter.ToUInt16(buffer, offset + 2); Reserved = LittleEndianConverter.ToUInt32(buffer, offset + 4); - ByteReader.ReadBytes(buffer, offset + 8, DataLength); + Data = ByteReader.ReadBytes(buffer, offset + 8, dataLength); + } + + public virtual void WriteData() + { } public void WriteBytes(byte[] buffer, int offset) { - DataLength = (ushort)Data.Length; + WriteData(); LittleEndianWriter.WriteUInt16(buffer, offset + 0, (ushort)ContextType); - LittleEndianWriter.WriteUInt16(buffer, offset + 2, DataLength); + LittleEndianWriter.WriteUInt16(buffer, offset + 2, (ushort)DataLength); LittleEndianWriter.WriteUInt32(buffer, offset + 4, Reserved); ByteWriter.WriteBytes(buffer, offset + 8, Data); } @@ -47,7 +50,30 @@ public int Length { get { - return FixedLength + Data.Length; + return FixedLength + DataLength; + } + } + + public int PaddedLength + { + get + { + int paddingLength = (8 - (DataLength % 8)) % 8; + return this.Length + paddingLength; + } + } + + public static NegotiateContext ReadNegotiateContext(byte[] buffer, int offset) + { + NegotiateContextType contextType = (NegotiateContextType)LittleEndianConverter.ToUInt16(buffer, offset + 0); + switch (contextType) + { + case NegotiateContextType.SMB2_PREAUTH_INTEGRITY_CAPABILITIES: + return new PreAuthIntegrityCapabilities(buffer, offset); + case NegotiateContextType.SMB2_ENCRYPTION_CAPABILITIES: + return new EncryptionCapabilities(buffer, offset); + default: + return new NegotiateContext(buffer, offset); } } @@ -56,9 +82,9 @@ public static List ReadNegotiateContextList(byte[] buffer, int List result = new List(); for (int index = 0; index < count; index++) { - NegotiateContext context = new NegotiateContext(buffer, offset); + NegotiateContext context = ReadNegotiateContext(buffer, offset); result.Add(context); - offset += context.Length; + offset += context.PaddedLength; } return result; } @@ -69,10 +95,8 @@ public static void WriteNegotiateContextList(byte[] buffer, int offset, List negotiate for (int index = 0; index < negotiateContextList.Count; index++) { NegotiateContext context = negotiateContextList[index]; - int length = context.Length; if (index < negotiateContextList.Count - 1) { - int paddedLength = (int)Math.Ceiling((double)length / 8) * 8; - result += paddedLength; + result += context.PaddedLength; } else { - result += length; + result += context.Length; } } return result; } + + public virtual int DataLength => Data.Length; + + public virtual NegotiateContextType ContextType => m_contextType; } } diff --git a/SMBLibrary/SMB2/Structures/NegotiateContext/PreAuthIntegrityCapabilities.cs b/SMBLibrary/SMB2/Structures/NegotiateContext/PreAuthIntegrityCapabilities.cs new file mode 100644 index 00000000..ca877a46 --- /dev/null +++ b/SMBLibrary/SMB2/Structures/NegotiateContext/PreAuthIntegrityCapabilities.cs @@ -0,0 +1,59 @@ +/* Copyright (C) 2024 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.Collections.Generic; +using Utilities; + +namespace SMBLibrary.SMB2 +{ + /// + /// [MS-SMB2] 2.2.3.1.1 - SMB2_PREAUTH_INTEGRITY_CAPABILITIES + /// + public class PreAuthIntegrityCapabilities : NegotiateContext + { + // ushort HashAlgorithmCount; + // ushort SaltLength; + public List HashAlgorithms = new List(); + public byte[] Salt; + + public PreAuthIntegrityCapabilities() + { + } + + public PreAuthIntegrityCapabilities(byte[] buffer, int offset) : base(buffer, offset) + { + ushort hashAlgorithmCount = LittleEndianConverter.ToUInt16(Data, 0); + ushort saltLength = LittleEndianConverter.ToUInt16(Data, 2); + for (int index = 0; index < hashAlgorithmCount; index++) + { + HashAlgorithms.Add((HashAlgorithm)LittleEndianConverter.ToUInt16(Data, 4 + index * 2)); + } + Salt = ByteReader.ReadBytes(Data, 4 + hashAlgorithmCount * 2, saltLength); + } + + public override void WriteData() + { + Data = new byte[DataLength]; + LittleEndianWriter.WriteUInt16(Data, 0, (ushort)HashAlgorithms.Count); + LittleEndianWriter.WriteUInt16(Data, 2, (ushort)Salt.Length); + for (int index = 0; index < HashAlgorithms.Count; index++) + { + LittleEndianWriter.WriteUInt16(Data, 4 + index * 2, (ushort)HashAlgorithms[index]); + } + ByteWriter.WriteBytes(Data, 4 + HashAlgorithms.Count * 2, Salt); + } + + public override int DataLength + { + get + { + return 4 + HashAlgorithms.Count * 2 + Salt.Length; + } + } + + public override NegotiateContextType ContextType => NegotiateContextType.SMB2_PREAUTH_INTEGRITY_CAPABILITIES; + } +} diff --git a/SMBLibrary/SMBLibrary.VS2005.csproj b/SMBLibrary/SMBLibrary.VS2005.csproj deleted file mode 100644 index d8707598..00000000 --- a/SMBLibrary/SMBLibrary.VS2005.csproj +++ /dev/null @@ -1,608 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7} - Library - Properties - SMBLibrary - SMBLibrary - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0} - Utilities - - - - - \ No newline at end of file diff --git a/SMBLibrary/SMBLibrary.csproj b/SMBLibrary/SMBLibrary.csproj index dfda563f..4570a37b 100644 --- a/SMBLibrary/SMBLibrary.csproj +++ b/SMBLibrary/SMBLibrary.csproj @@ -2,24 +2,37 @@ net20;net40;netstandard2.0 - false SMBLibrary - 1.4.6.2 + 1.5.7.1 1573;1591 SMBLibrary false Tal Aloni + Copyright © Tal Aloni 2014-2026 SMBLibrary is an open-source C# SMB 1.0/CIFS, SMB 2.0, SMB 2.1 and SMB 3.0 server and client implementation LGPL-3.0-or-later https://github.com/TalAloni/SMBLibrary https://github.com/TalAloni/SMBLibrary true + ENABLE_NTLMV1 + + + 4.5.1 + + + + + + <_Parameter1>SMBLibrary.Tests + + + diff --git a/SMBLibrary/Server/ConnectionManager.cs b/SMBLibrary/Server/ConnectionManager.cs index 383308ab..131b6f27 100644 --- a/SMBLibrary/Server/ConnectionManager.cs +++ b/SMBLibrary/Server/ConnectionManager.cs @@ -39,9 +39,13 @@ public bool RemoveConnection(ConnectionState connection) public void ReleaseConnection(ConnectionState connection) { - connection.SendQueue.Stop(); + connection.SendQueue.Abort(); SocketUtils.ReleaseSocket(connection.ClientSocket); connection.CloseSessions(); + lock (connection.ReceiveBuffer) + { + connection.ReceiveBuffer.Dispose(); + } RemoveConnection(connection); } diff --git a/SMBLibrary/Server/ConnectionRequestEventArgs.cs b/SMBLibrary/Server/ConnectionRequestEventArgs.cs index 16ba04b0..253df6c9 100644 --- a/SMBLibrary/Server/ConnectionRequestEventArgs.cs +++ b/SMBLibrary/Server/ConnectionRequestEventArgs.cs @@ -5,7 +5,6 @@ * either version 3 of the License, or (at your option) any later version. */ using System; -using System.Collections.Generic; using System.Net; namespace SMBLibrary.Server diff --git a/SMBLibrary/Server/ConnectionState/SMB1ConnectionState.cs b/SMBLibrary/Server/ConnectionState/SMB1ConnectionState.cs index 6c9903d3..f49adeed 100644 --- a/SMBLibrary/Server/ConnectionState/SMB1ConnectionState.cs +++ b/SMBLibrary/Server/ConnectionState/SMB1ConnectionState.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2019 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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, @@ -78,14 +78,20 @@ public SMB1Session CreateSession(string userName, string machineName, byte[] ses public SMB1Session GetSession(ushort userID) { SMB1Session session; - m_sessions.TryGetValue(userID, out session); + lock (m_sessions) + { + m_sessions.TryGetValue(userID, out session); + } return session; } public void RemoveSession(ushort userID) { SMB1Session session; - m_sessions.TryGetValue(userID, out session); + lock (m_sessions) + { + m_sessions.TryGetValue(userID, out session); + } if (session != null) { session.Close(); diff --git a/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs b/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs index 553a9ea2..724dd192 100644 --- a/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs +++ b/SMBLibrary/Server/ConnectionState/SMB2ConnectionState.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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, @@ -56,14 +56,20 @@ public SMB2Session CreateSession(ulong sessionID, string userName, string machin public SMB2Session GetSession(ulong sessionID) { SMB2Session session; - m_sessions.TryGetValue(sessionID, out session); + lock (m_sessions) + { + m_sessions.TryGetValue(sessionID, out session); + } return session; } public void RemoveSession(ulong sessionID) { SMB2Session session; - m_sessions.TryGetValue(sessionID, out session); + lock (m_sessions) + { + m_sessions.TryGetValue(sessionID, out session); + } if (session != null) { session.Close(); diff --git a/SMBLibrary/Server/NameServer.cs b/SMBLibrary/Server/NameServer.cs index db42cf9c..86149ffd 100644 --- a/SMBLibrary/Server/NameServer.cs +++ b/SMBLibrary/Server/NameServer.cs @@ -111,7 +111,7 @@ private void ReceiveCallback(IAsyncResult result) bool nameMatch = String.Equals(name, Environment.MachineName, StringComparison.OrdinalIgnoreCase); - if (nameMatch && ((suffix == NetBiosSuffix.WorkstationService) || (suffix == NetBiosSuffix.FileServiceService))) + if (nameMatch && ((suffix == NetBiosSuffix.WorkstationService) || (suffix == NetBiosSuffix.FileServerService))) { PositiveNameQueryResponse response = new PositiveNameQueryResponse(); response.Header.TransactionID = request.Header.TransactionID; @@ -129,7 +129,7 @@ private void ReceiveCallback(IAsyncResult result) response.Resource.Name = request.Question.Name; NameFlags nameFlags = new NameFlags(); string name1 = NetBiosUtils.GetMSNetBiosName(Environment.MachineName, NetBiosSuffix.WorkstationService); - string name2 = NetBiosUtils.GetMSNetBiosName(Environment.MachineName, NetBiosSuffix.FileServiceService); + string name2 = NetBiosUtils.GetMSNetBiosName(Environment.MachineName, NetBiosSuffix.FileServerService); NameFlags nameFlags3 = new NameFlags(); nameFlags3.WorkGroup = true; string name3 = NetBiosUtils.GetMSNetBiosName(WorkgroupName, NetBiosSuffix.WorkstationService); @@ -164,7 +164,7 @@ private void ReceiveCallback(IAsyncResult result) private void RegisterNetBIOSName() { NameRegistrationRequest request1 = new NameRegistrationRequest(Environment.MachineName, NetBiosSuffix.WorkstationService, m_serverAddress); - NameRegistrationRequest request2 = new NameRegistrationRequest(Environment.MachineName, NetBiosSuffix.FileServiceService, m_serverAddress); + NameRegistrationRequest request2 = new NameRegistrationRequest(Environment.MachineName, NetBiosSuffix.FileServerService, m_serverAddress); NameRegistrationRequest request3 = new NameRegistrationRequest(WorkgroupName, NetBiosSuffix.WorkstationService, m_serverAddress); request3.NameFlags.WorkGroup = true; RegisterName(request1); diff --git a/SMBLibrary/Server/SMB1/FileStoreResponseHelper.cs b/SMBLibrary/Server/SMB1/FileStoreResponseHelper.cs index 324b9ca7..c4da3ff4 100644 --- a/SMBLibrary/Server/SMB1/FileStoreResponseHelper.cs +++ b/SMBLibrary/Server/SMB1/FileStoreResponseHelper.cs @@ -1,13 +1,10 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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 System.Text; using SMBLibrary.SMB1; using Utilities; @@ -231,5 +228,24 @@ internal static SMB1Command GetSetInformation2Response(SMB1Header header, SetInf state.LogToServer(Severity.Verbose, "Set Information 2 on '{0}{1}' succeeded.", share.Name, openFile.Path); return new SetInformation2Response(); } + + internal static SMB1Command GetQueryInformationDiskResponse(SMB1Header header, QueryInformationDiskRequest request, ISMBShare share, SMB1ConnectionState state) + { + header.Status = share.FileStore.GetFileSystemInformation(out FileSystemInformation fileSystemInfo, FileSystemInformationClass.FileFsSizeInformation); + if (header.Status != NTStatus.STATUS_SUCCESS) + { + state.LogToServer(Severity.Verbose, "GetFileSystemInformation on '{0}' failed. NTStatus: {1}", share.Name, header.Status); + return new ErrorResponse(request.CommandName); + } + + FileFsSizeInformation sizeInformation = (FileFsSizeInformation)fileSystemInfo; + + QueryInformationDiskResponse response = new QueryInformationDiskResponse(); + response.TotalUnits = (ushort)Math.Min(sizeInformation.TotalAllocationUnits, UInt16.MaxValue); + response.BlocksPerUnit = (ushort)sizeInformation.SectorsPerAllocationUnit; + response.BlockSize = (ushort)sizeInformation.BytesPerSector; + response.FreeUnits = (ushort)Math.Min(sizeInformation.AvailableAllocationUnits, UInt16.MaxValue); + return response; + } } } diff --git a/SMBLibrary/Server/SMB1/SessionSetupHelper.cs b/SMBLibrary/Server/SMB1/SessionSetupHelper.cs index e790b974..5e860821 100644 --- a/SMBLibrary/Server/SMB1/SessionSetupHelper.cs +++ b/SMBLibrary/Server/SMB1/SessionSetupHelper.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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, @@ -37,6 +37,13 @@ internal static SMB1Command GetSessionSetupResponse(SMB1Header header, SessionSe byte[] sessionKey = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.SessionKey) as byte[]; object accessToken = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.AccessToken); bool? isGuest = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.IsGuest) as bool?; + + if (sessionKey != null && sessionKey.Length > 16) + { + // [MS-CIFS] 3.3.5.43 If the session key is equal to or longer than 16 bytes, only the least significant 16 bytes MUST be stored in Server.Session.SessionKey + sessionKey = ByteReader.ReadBytes(sessionKey, 0, 16); + } + SMB1Session session; if (!isGuest.HasValue || !isGuest.Value) { @@ -120,6 +127,13 @@ internal static SMB1Command GetSessionSetupResponseExtended(SMB1Header header, S byte[] sessionKey = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.SessionKey) as byte[]; object accessToken = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.AccessToken); bool? isGuest = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.IsGuest) as bool?; + + if (sessionKey != null && sessionKey.Length > 16) + { + // [MS-CIFS] 3.3.5.43 If the session key is equal to or longer than 16 bytes, only the least significant 16 bytes MUST be stored in Server.Session.SessionKey + sessionKey = ByteReader.ReadBytes(sessionKey, 0, 16); + } + if (!isGuest.HasValue || !isGuest.Value) { state.LogToServer(Severity.Information, "Session Setup: User '{0}' authenticated successfully (Domain: '{1}', Workstation: '{2}', OS version: '{3}').", userName, domainName, machineName, osVersion); @@ -154,7 +168,7 @@ private static AuthenticateMessage CreateAuthenticateMessage(string accountNameT { authenticateMessage.NegotiateFlags |= NegotiateFlags.ExtendedSessionSecurity; } - else + else if (lmChallengeResponse.Length == 24) // NTLM v1 non-extended-session-security: if LmChallengeResponse is set to 0 bytes then LanManagerSessionKey is not applicable. { authenticateMessage.NegotiateFlags |= NegotiateFlags.LanManagerSessionKey; } diff --git a/SMBLibrary/Server/SMB2/QueryInfoHelper.cs b/SMBLibrary/Server/SMB2/QueryInfoHelper.cs index 92b721ae..f62a5ff4 100644 --- a/SMBLibrary/Server/SMB2/QueryInfoHelper.cs +++ b/SMBLibrary/Server/SMB2/QueryInfoHelper.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2019 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2024 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, @@ -36,7 +36,22 @@ internal static SMB2Command GetQueryInfoResponse(QueryInfoRequest request, ISMBS } FileInformation fileInformation; - NTStatus queryStatus = share.FileStore.GetFileInformation(out fileInformation, openFile.Handle, request.FileInformationClass); + NTStatus queryStatus; + try + { + queryStatus = share.FileStore.GetFileInformation(out fileInformation, openFile.Handle, request.FileInformationClass); + } + catch (UnsupportedInformationLevelException) + { + state.LogToServer(Severity.Verbose, $"GetFileInformation on '{share.Name}{openFile.Path}' failed. Information class: {request.FileInformationClass}, NTStatus: STATUS_INVALID_INFO_CLASS. (FileId: {request.FileId.Volatile})"); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_INVALID_INFO_CLASS); + } + catch (NotImplementedException) + { + state.LogToServer(Severity.Verbose, $"GetFileInformation on '{share.Name}{openFile.Path}' failed. Information class: {request.FileInformationClass}, NTStatus: STATUS_NOT_IMPLEMENTED. (FileId: {request.FileId.Volatile})"); + return new ErrorResponse(request.CommandName, NTStatus.STATUS_NOT_IMPLEMENTED); + } + if (queryStatus != NTStatus.STATUS_SUCCESS) { state.LogToServer(Severity.Verbose, "GetFileInformation on '{0}{1}' failed. Information class: {2}, NTStatus: {3}. (FileId: {4})", share.Name, openFile.Path, request.FileInformationClass, queryStatus, request.FileId.Volatile); diff --git a/SMBLibrary/Server/SMB2/SessionSetupHelper.cs b/SMBLibrary/Server/SMB2/SessionSetupHelper.cs index aa28f673..8e393370 100644 --- a/SMBLibrary/Server/SMB2/SessionSetupHelper.cs +++ b/SMBLibrary/Server/SMB2/SessionSetupHelper.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2017-2022 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, @@ -40,14 +40,21 @@ internal static SMB2Command GetSessionSetupResponse(SessionSetupRequest request, } // 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 = request.Header.SessionID; + if (sessionID == 0) { - ulong? sessionID = state.AllocateSessionID(); - if (!sessionID.HasValue) + ulong? allocatedSessionID = state.AllocateSessionID(); + if (!allocatedSessionID.HasValue) { return new ErrorResponse(request.CommandName, NTStatus.STATUS_TOO_MANY_SESSIONS); } - response.Header.SessionID = sessionID.Value; + sessionID = allocatedSessionID.Value; + response.Header.SessionID = allocatedSessionID.Value; + } + else if (state.GetSession(sessionID) != null) + { + // We already have an established session associated with this sessionID, the client is in violation + return new ErrorResponse(request.CommandName, NTStatus.STATUS_REQUEST_NOT_ACCEPTED); } if (status == NTStatus.SEC_I_CONTINUE_NEEDED) @@ -63,18 +70,25 @@ internal static SMB2Command GetSessionSetupResponse(SessionSetupRequest request, byte[] sessionKey = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.SessionKey) as byte[]; object accessToken = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.AccessToken); bool? isGuest = securityProvider.GetContextAttribute(state.AuthenticationContext, GSSAttributeName.IsGuest) as bool?; + + if (sessionKey != null && sessionKey.Length > 16) + { + // [MS-SMB2] 3.3.1.8 SessionKey MUST be set to the first 16 bytes of the cryptographic key queried from the GSS protocol for this authenticated context. + sessionKey = ByteReader.ReadBytes(sessionKey, 0, 16); + } + if (!isGuest.HasValue || !isGuest.Value) { state.LogToServer(Severity.Information, "Session Setup: User '{0}' authenticated successfully (Domain: '{1}', Workstation: '{2}', OS version: '{3}').", userName, domainName, machineName, osVersion); bool signingRequired = (request.SecurityMode & SecurityMode.SigningRequired) > 0; SMB2Dialect smb2Dialect = SMBServer.ToSMB2Dialect(state.Dialect); byte[] signingKey = SMB2Cryptography.GenerateSigningKey(sessionKey, smb2Dialect, null); - state.CreateSession(request.Header.SessionID, userName, machineName, sessionKey, accessToken, signingRequired, signingKey); + state.CreateSession(sessionID, userName, machineName, sessionKey, accessToken, signingRequired, signingKey); } else { state.LogToServer(Severity.Information, "Session Setup: User '{0}' failed authentication (Domain: '{1}', Workstation: '{2}', OS version: '{3}'), logged in as guest.", userName, domainName, machineName, osVersion); - state.CreateSession(request.Header.SessionID, "Guest", machineName, sessionKey, accessToken, false, null); + state.CreateSession(sessionID, "Guest", machineName, sessionKey, accessToken, false, null); response.SessionFlags = SessionFlags.IsGuest; } } diff --git a/SMBLibrary/Server/SMBServer.SMB1.cs b/SMBLibrary/Server/SMBServer.SMB1.cs index 0d76dd33..b23d22e4 100644 --- a/SMBLibrary/Server/SMBServer.SMB1.cs +++ b/SMBLibrary/Server/SMBServer.SMB1.cs @@ -1,10 +1,9 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2025 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.SMB1; @@ -244,6 +243,10 @@ private List ProcessSMB1Command(SMB1Header header, SMB1Command comm { return TreeConnectHelper.GetTreeDisconnectResponse(header, (TreeDisconnectRequest)command, share, state); } + else if (command is QueryInformationDiskRequest) + { + return FileStoreResponseHelper.GetQueryInformationDiskResponse(header, (QueryInformationDiskRequest)command, share, state); + } else if (command is TransactionRequest) // Both TransactionRequest and Transaction2Request { return TransactionHelper.GetTransactionResponse(header, (TransactionRequest)command, share, state); diff --git a/SMBLibrary/Server/SMBServer.cs b/SMBLibrary/Server/SMBServer.cs index c85a2660..f8653ea4 100644 --- a/SMBLibrary/Server/SMBServer.cs +++ b/SMBLibrary/Server/SMBServer.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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, @@ -11,7 +11,6 @@ using System.Threading; using SMBLibrary.Authentication.GSSAPI; using SMBLibrary.NetBios; -using SMBLibrary.Services; using SMBLibrary.SMB1; using SMBLibrary.SMB2; using Utilities; @@ -33,6 +32,9 @@ public partial class SMBServer private ConnectionManager m_connectionManager; private Thread m_sendSMBKeepAliveThread; +#if !NET20 + private CancellationTokenSource m_sendSMBKeepAliveCancellationTokenSource; +#endif private IPAddress m_serverAddress; private SMBTransportType m_transport; @@ -77,6 +79,12 @@ public void Start(IPAddress serverAddress, SMBTransportType transport, bool enab /// /// public void Start(IPAddress serverAddress, SMBTransportType transport, bool enableSMB1, bool enableSMB2, bool enableSMB3, TimeSpan? connectionInactivityTimeout) + { + int port = (transport == SMBTransportType.DirectTCPTransport ? DirectTCPPort : NetBiosOverTCPPort); + Start(serverAddress, transport, port, enableSMB1, enableSMB2, enableSMB3, connectionInactivityTimeout); + } + + protected internal void Start(IPAddress serverAddress, SMBTransportType transport, int port, bool enableSMB1, bool enableSMB2, bool enableSMB3, TimeSpan? connectionInactivityTimeout) { if (!m_listening) { @@ -95,18 +103,28 @@ public void Start(IPAddress serverAddress, SMBTransportType transport, bool enab m_serverStartTime = DateTime.Now; m_listenerSocket = new Socket(m_serverAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - int port = (m_transport == SMBTransportType.DirectTCPTransport ? DirectTCPPort : NetBiosOverTCPPort); m_listenerSocket.Bind(new IPEndPoint(m_serverAddress, port)); m_listenerSocket.Listen((int)SocketOptionName.MaxConnections); m_listenerSocket.BeginAccept(ConnectRequestCallback, m_listenerSocket); if (connectionInactivityTimeout.HasValue) { +#if !NET20 + m_sendSMBKeepAliveCancellationTokenSource = new CancellationTokenSource(); +#endif m_sendSMBKeepAliveThread = new Thread(delegate() { while (m_listening) { +#if NET20 Thread.Sleep(InactivityMonitoringInterval); +#else + bool cancelled = m_sendSMBKeepAliveCancellationTokenSource.Token.WaitHandle.WaitOne(InactivityMonitoringInterval); + if (cancelled) + { + return; + } +#endif m_connectionManager.SendSMBKeepAlive(connectionInactivityTimeout.Value); } }); @@ -122,7 +140,11 @@ public void Stop() m_listening = false; if (m_sendSMBKeepAliveThread != null) { +#if NET20 m_sendSMBKeepAliveThread.Abort(); +#else + m_sendSMBKeepAliveCancellationTokenSource?.Cancel(); +#endif } SocketUtils.ReleaseSocket(m_listenerSocket); m_connectionManager.ReleaseAllConnections(); @@ -208,71 +230,72 @@ private void ReceiveCallback(IAsyncResult result) ConnectionState state = (ConnectionState)result.AsyncState; Socket clientSocket = state.ClientSocket; - if (!m_listening) - { - clientSocket.Close(); - return; - } - - int numberOfBytesReceived; - try - { - numberOfBytesReceived = clientSocket.EndReceive(result); - } - catch (ObjectDisposedException) + lock (state.ReceiveBuffer) { - state.LogToServer(Severity.Debug, "The connection was terminated"); - m_connectionManager.ReleaseConnection(state); - return; - } - catch (SocketException ex) - { - const int WSAECONNRESET = 10054; - if (ex.ErrorCode == WSAECONNRESET) + if (!m_listening) { - state.LogToServer(Severity.Debug, "The connection was forcibly closed by the remote host"); - } - else - { - state.LogToServer(Severity.Debug, "The connection was terminated, Socket error code: {0}", ex.ErrorCode); + clientSocket.Close(); + return; } - m_connectionManager.ReleaseConnection(state); - return; - } - - if (numberOfBytesReceived == 0) - { - state.LogToServer(Severity.Debug, "The client closed the connection"); - m_connectionManager.ReleaseConnection(state); - return; - } - - state.UpdateLastReceiveDT(); - NBTConnectionReceiveBuffer receiveBuffer = state.ReceiveBuffer; - receiveBuffer.SetNumberOfBytesReceived(numberOfBytesReceived); - ProcessConnectionBuffer(ref state); - if (clientSocket.Connected) - { + int numberOfBytesReceived; try { - clientSocket.BeginReceive(state.ReceiveBuffer.Buffer, state.ReceiveBuffer.WriteOffset, state.ReceiveBuffer.AvailableLength, 0, ReceiveCallback, state); + numberOfBytesReceived = clientSocket.EndReceive(result); } catch (ObjectDisposedException) { + state.LogToServer(Severity.Debug, "The connection was terminated"); m_connectionManager.ReleaseConnection(state); + return; } - catch (SocketException) + catch (SocketException ex) + { + const int WSAECONNRESET = 10054; + if (ex.ErrorCode == WSAECONNRESET) + { + state.LogToServer(Severity.Debug, "The connection was forcibly closed by the remote host"); + } + else + { + state.LogToServer(Severity.Debug, "The connection was terminated, Socket error code: {0}", ex.ErrorCode); + } + m_connectionManager.ReleaseConnection(state); + return; + } + + if (numberOfBytesReceived == 0) { + state.LogToServer(Severity.Debug, "The client closed the connection"); m_connectionManager.ReleaseConnection(state); + return; + } + + state.UpdateLastReceiveDT(); + NBTConnectionReceiveBuffer receiveBuffer = state.ReceiveBuffer; + receiveBuffer.SetNumberOfBytesReceived(numberOfBytesReceived); + ProcessConnectionBuffer(ref state); + + if (clientSocket.Connected) + { + try + { + clientSocket.BeginReceive(state.ReceiveBuffer.Buffer, state.ReceiveBuffer.WriteOffset, state.ReceiveBuffer.AvailableLength, 0, ReceiveCallback, state); + } + catch (ObjectDisposedException) + { + m_connectionManager.ReleaseConnection(state); + } + catch (SocketException) + { + m_connectionManager.ReleaseConnection(state); + } } } } private void ProcessConnectionBuffer(ref ConnectionState state) { - Socket clientSocket = state.ClientSocket; - NBTConnectionReceiveBuffer receiveBuffer = state.ReceiveBuffer; while (receiveBuffer.HasCompletePacket()) { @@ -284,6 +307,7 @@ private void ProcessConnectionBuffer(ref ConnectionState state) catch (Exception ex) { state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); state.LogToServer(Severity.Warning, "Rejected Invalid NetBIOS session packet: {0}", ex.Message); break; } @@ -310,6 +334,7 @@ private void ProcessPacket(SessionPacket packet, ref ConnectionState state) { state.LogToServer(Severity.Verbose, "Rejected SMB1 message"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); return; } @@ -322,6 +347,7 @@ private void ProcessPacket(SessionPacket packet, ref ConnectionState state) { state.LogToServer(Severity.Warning, "Invalid SMB1 message: " + ex.Message); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); 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); @@ -360,6 +386,7 @@ private void ProcessPacket(SessionPacket packet, ref ConnectionState state) { state.LogToServer(Severity.Verbose, "Rejected SMB2 message"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); return; } @@ -372,6 +399,7 @@ private void ProcessPacket(SessionPacket packet, ref ConnectionState state) { state.LogToServer(Severity.Warning, "Invalid SMB2 request chain: " + ex.Message); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); 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); @@ -396,6 +424,7 @@ private void ProcessPacket(SessionPacket packet, ref ConnectionState state) { state.LogToServer(Severity.Warning, "Inappropriate NetBIOS session packet"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); return; } } diff --git a/SMBLibrary/Server/Shares/SMBShareCollection.cs b/SMBLibrary/Server/Shares/SMBShareCollection.cs index 4cd61a18..03216745 100644 --- a/SMBLibrary/Server/Shares/SMBShareCollection.cs +++ b/SMBLibrary/Server/Shares/SMBShareCollection.cs @@ -39,7 +39,7 @@ public List ListShares() return result; } - /// e.g. \Shared + /// e.g. \Shared public FileSystemShare GetShareFromName(string shareName) { int index = IndexOf(shareName, StringComparison.OrdinalIgnoreCase); diff --git a/SMBLibrary/Services/Exceptions/InvalidLevelException.cs b/SMBLibrary/Services/Exceptions/InvalidLevelException.cs new file mode 100644 index 00000000..03476acc --- /dev/null +++ b/SMBLibrary/Services/Exceptions/InvalidLevelException.cs @@ -0,0 +1,28 @@ +/* Copyright (C) 2024 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; + +namespace SMBLibrary.Services +{ + public class InvalidLevelException : Exception + { + private uint m_level; + + public InvalidLevelException(uint level) + { + m_level = level; + } + + public uint Level + { + get + { + return m_level; + } + } + } +} diff --git a/SMBLibrary/Services/Exceptions/UnsupportedLevelException.cs b/SMBLibrary/Services/Exceptions/UnsupportedLevelException.cs new file mode 100644 index 00000000..234a8fbf --- /dev/null +++ b/SMBLibrary/Services/Exceptions/UnsupportedLevelException.cs @@ -0,0 +1,28 @@ +/* Copyright (C) 2024 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; + +namespace SMBLibrary.Services +{ + public class UnsupportedLevelException : Exception + { + private uint m_level; + + public UnsupportedLevelException(uint level) + { + m_level = level; + } + + public uint Level + { + get + { + return m_level; + } + } + } +} diff --git a/SMBLibrary/Services/ServerService/ServerService.cs b/SMBLibrary/Services/ServerService/ServerService.cs index 5b843180..6ad5d8f9 100644 --- a/SMBLibrary/Services/ServerService/ServerService.cs +++ b/SMBLibrary/Services/ServerService/ServerService.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2014-2018 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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, @@ -6,8 +6,6 @@ */ using System; using System.Collections.Generic; -using System.Text; -using Utilities; namespace SMBLibrary.Services { @@ -47,8 +45,7 @@ public override byte[] GetResponseBytes(ushort opNum, byte[] requestBytes) { case ServerServiceOpName.NetrShareEnum: { - NetrShareEnumRequest request = new NetrShareEnumRequest(requestBytes); - NetrShareEnumResponse response = GetNetrShareEnumResponse(request); + NetrShareEnumResponse response = GetNetrShareEnumResponse(requestBytes); return response.GetBytes(); } case ServerServiceOpName.NetrShareGetInfo: @@ -68,9 +65,27 @@ public override byte[] GetResponseBytes(ushort opNum, byte[] requestBytes) } } - public NetrShareEnumResponse GetNetrShareEnumResponse(NetrShareEnumRequest request) + public NetrShareEnumResponse GetNetrShareEnumResponse(byte[] requestBytes) { + NetrShareEnumRequest request; NetrShareEnumResponse response = new NetrShareEnumResponse(); + try + { + request = new NetrShareEnumRequest(requestBytes); + } + catch (UnsupportedLevelException ex) + { + response.InfoStruct = new ShareEnum(ex.Level); + response.Result = Win32Error.ERROR_NOT_SUPPORTED; + return response; + } + catch (InvalidLevelException ex) + { + response.InfoStruct = new ShareEnum(ex.Level); + response.Result = Win32Error.ERROR_INVALID_LEVEL; + return response; + } + switch (request.InfoStruct.Level) { case 0: diff --git a/SMBLibrary/Services/ServerService/Structures/ServerInfo/ServerInfo.cs b/SMBLibrary/Services/ServerService/Structures/ServerInfo/ServerInfo.cs index 501783f9..7daef0ed 100644 --- a/SMBLibrary/Services/ServerService/Structures/ServerInfo/ServerInfo.cs +++ b/SMBLibrary/Services/ServerService/Structures/ServerInfo/ServerInfo.cs @@ -1,13 +1,10 @@ -/* Copyright (C) 2014-2018 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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.RPC; -using Utilities; namespace SMBLibrary.Services { @@ -56,9 +53,8 @@ public void Read(NDRParser parser) Info = info101; break; default: - throw new NotImplementedException(); + throw new InvalidLevelException(Level); } - ; parser.EndStructure(); // SERVER_INFO Union } diff --git a/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareEnum.cs b/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareEnum.cs index 5347839e..3cca6323 100644 --- a/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareEnum.cs +++ b/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareEnum.cs @@ -1,13 +1,11 @@ -/* Copyright (C) 2014-2018 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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.RPC; -using Utilities; namespace SMBLibrary.Services { @@ -64,9 +62,9 @@ public void Read(NDRParser parser) case 501: case 502: case 503: - throw new NotImplementedException(); + throw new UnsupportedLevelException(level); default: - break; + throw new InvalidLevelException(level); } parser.EndStructure(); // SHARE_ENUM_UNION parser.EndStructure(); // SHARE_ENUM_STRUCT diff --git a/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareInfo.cs b/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareInfo.cs index 22eb9d3d..3d9d27ca 100644 --- a/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareInfo.cs +++ b/SMBLibrary/Services/ServerService/Structures/ShareInfo/ShareInfo.cs @@ -1,13 +1,10 @@ -/* Copyright (C) 2014 Tal Aloni . All rights reserved. +/* Copyright (C) 2014-2024 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.RPC; -using Utilities; namespace SMBLibrary.Services { @@ -56,7 +53,7 @@ public void Read(NDRParser parser) Info = info1; break; default: - throw new NotImplementedException(); + throw new InvalidLevelException(Level); } parser.EndStructure(); // SHARE_INFO Union } diff --git a/SMBLibrary/Utilities/SocketUtils.cs b/SMBLibrary/Utilities/SocketUtils.cs index 4e7d5542..57766205 100644 --- a/SMBLibrary/Utilities/SocketUtils.cs +++ b/SMBLibrary/Utilities/SocketUtils.cs @@ -6,7 +6,7 @@ */ using System; using System.Net.Sockets; -#if NETSTANDARD2_0 +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER using System.Runtime.InteropServices; #endif @@ -14,7 +14,7 @@ namespace Utilities { public class SocketUtils { -#if NETSTANDARD2_0 +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER private static bool IsDotNetFramework() { const string DotnetFrameworkDescription = ".NET Framework"; @@ -34,7 +34,7 @@ public static void SetKeepAlive(Socket socket, TimeSpan timeout) public static void SetKeepAlive(Socket socket, bool enable, TimeSpan timeout, TimeSpan interval) { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); -#if NETSTANDARD2_0 +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER if (IsDotNetFramework()) { #endif @@ -44,7 +44,7 @@ public static void SetKeepAlive(Socket socket, bool enable, TimeSpan timeout, Ti LittleEndianWriter.WriteUInt32(tcp_keepalive, 4, (uint)timeout.TotalMilliseconds); LittleEndianWriter.WriteUInt32(tcp_keepalive, 8, (uint)interval.TotalMilliseconds); socket.IOControl(IOControlCode.KeepAliveValues, tcp_keepalive, null); -#if NETSTANDARD2_0 +#if NETSTANDARD2_0_OR_GREATER || NET5_0_OR_GREATER } else { diff --git a/SMBServer.VS2005.sln b/SMBServer.VS2005.sln deleted file mode 100644 index 587ab082..00000000 --- a/SMBServer.VS2005.sln +++ /dev/null @@ -1,56 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 9.00 -# Visual Studio 2005 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiskAccessLibrary.FileSystems.Abstractions.VS2005", "DiskAccessLibrary.FileSystems.Abstractions\DiskAccessLibrary.FileSystems.Abstractions.VS2005.csproj", "{9119EC7E-AF78-4814-BF03-F3823A29A471}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Utilities.VS2005", "Utilities\Utilities.VS2005.csproj", "{6E0F2D1E-6167-4032-BA90-DEE3A99207D0}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMBLibrary.VS2005", "SMBLibrary\SMBLibrary.VS2005.csproj", "{8D9E8F5D-FD13-4E4C-9723-A333DA2034A7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMBLibrary.Win32.VS2005", "SMBLibrary.Win32\SMBLibrary.Win32.VS2005.csproj", "{8CE25496-A52B-4841-822F-74C469D10EE7}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMBLibrary.Adapters.VS2005", "SMBLibrary.Adapters\SMBLibrary.Adapters.VS2005.csproj", "{DF51D33B-F030-4B25-803A-3BEBC35E5BEC}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMBServer.VS2005", "SMBServer\SMBServer.VS2005.csproj", "{70D43E2A-26A2-4046-A472-5BA8C9437612}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SMBLibrary.Tests.VS2005", "SMBLibrary.Tests\SMBLibrary.Tests.VS2005.csproj", "{C79B06EB-32C1-44CA-B7E1-A891B8135658}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Release|Any CPU.Build.0 = Release|Any CPU - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Release|Any CPU.Build.0 = Release|Any CPU - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7}.Release|Any CPU.Build.0 = Release|Any CPU - {8CE25496-A52B-4841-822F-74C469D10EE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8CE25496-A52B-4841-822F-74C469D10EE7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8CE25496-A52B-4841-822F-74C469D10EE7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8CE25496-A52B-4841-822F-74C469D10EE7}.Release|Any CPU.Build.0 = Release|Any CPU - {DF51D33B-F030-4B25-803A-3BEBC35E5BEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DF51D33B-F030-4B25-803A-3BEBC35E5BEC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DF51D33B-F030-4B25-803A-3BEBC35E5BEC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DF51D33B-F030-4B25-803A-3BEBC35E5BEC}.Release|Any CPU.Build.0 = Release|Any CPU - {70D43E2A-26A2-4046-A472-5BA8C9437612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {70D43E2A-26A2-4046-A472-5BA8C9437612}.Debug|Any CPU.Build.0 = Debug|Any CPU - {70D43E2A-26A2-4046-A472-5BA8C9437612}.Release|Any CPU.ActiveCfg = Release|Any CPU - {70D43E2A-26A2-4046-A472-5BA8C9437612}.Release|Any CPU.Build.0 = Release|Any CPU - {C79B06EB-32C1-44CA-B7E1-A891B8135658}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C79B06EB-32C1-44CA-B7E1-A891B8135658}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C79B06EB-32C1-44CA-B7E1-A891B8135658}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C79B06EB-32C1-44CA-B7E1-A891B8135658}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/SMBServer.VS2019.sln b/SMBServer.sln similarity index 84% rename from SMBServer.VS2019.sln rename to SMBServer.sln index f13b65c5..416bcca4 100644 --- a/SMBServer.VS2019.sln +++ b/SMBServer.sln @@ -3,8 +3,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29728.190 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiskAccessLibrary.FileSystems.Abstractions", "DiskAccessLibrary.FileSystems.Abstractions\DiskAccessLibrary.FileSystems.Abstractions.csproj", "{9119EC7E-AF78-4814-BF03-F3823A29A471}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Utilities", "Utilities\Utilities.csproj", "{6E0F2D1E-6167-4032-BA90-DEE3A99207D0}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SMBLibrary", "SMBLibrary\SMBLibrary.csproj", "{8D9E8F5D-FD13-4E4C-9723-A333DA2034A7}" @@ -23,10 +21,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9119EC7E-AF78-4814-BF03-F3823A29A471}.Release|Any CPU.Build.0 = Release|Any CPU {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Debug|Any CPU.Build.0 = Debug|Any CPU {6E0F2D1E-6167-4032-BA90-DEE3A99207D0}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/SMBServer/Properties/AssemblyInfo.cs b/SMBServer/Properties/AssemblyInfo.cs deleted file mode 100644 index 3a6ce023..00000000 --- a/SMBServer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("SMBServer")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Tal Aloni")] -[assembly: AssemblyProduct("SMBServer")] -[assembly: AssemblyCopyright("Copyright © Tal Aloni 2014-2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("6efa6101-f82c-4798-99a6-11e4ee2f2588")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -[assembly: AssemblyVersion("1.4.6.0")] -[assembly: AssemblyFileVersion("1.4.6.0")] diff --git a/SMBServer/SMBServer.VS2005.csproj b/SMBServer/SMBServer.VS2005.csproj deleted file mode 100644 index ac421616..00000000 --- a/SMBServer/SMBServer.VS2005.csproj +++ /dev/null @@ -1,85 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {70D43E2A-26A2-4046-A472-5BA8C9437612} - WinExe - Properties - SMBServer - SMBServer - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - Form - - - ServerUI.cs - - - - - Designer - ServerUI.cs - - - - - - - - - {8CE25496-A52B-4841-822F-74C469D10EE7} - SMBLibrary.Win32 - - - {8D9E8F5D-FD13-4E4C-9723-A333DA2034A7} - SMBLibrary - - - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0} - Utilities - - - - - Always - - - - - \ No newline at end of file diff --git a/SMBServer/SMBServer.csproj b/SMBServer/SMBServer.csproj index 68a1edec..2803f63d 100644 --- a/SMBServer/SMBServer.csproj +++ b/SMBServer/SMBServer.csproj @@ -3,8 +3,9 @@ WinExe net20;net40;netcoreapp3.1 - false + Copyright © Tal Aloni 2014-2026 SMBServer + 1.5.7 SMBServer true diff --git a/ServerNotes.md b/ServerNotes.md new file mode 100644 index 00000000..34ccc847 --- /dev/null +++ b/ServerNotes.md @@ -0,0 +1,39 @@ +SMBLibrary Server Notes: +======================== +By default, Windows already use ports 139 and 445. there are several techniques to free / utilize those ports: + +##### Method 1: Disable Windows File and Printer Sharing server completely: +###### Windows XP/2003: +1. For every network adapter: Uncheck 'File and Printer Sharing for Microsoft Networks". +2. Navigate to 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NetBT\Parameters' and set 'SMBDeviceEnabled' to '0' (this will free port 445). +3. Reboot. + +###### Windows 7/8/2008/2012: +Disable the "Server" service (p.s. "TCP\IP NETBIOS Helper" should be enabled). + +##### Method 2: Use Windows File Sharing AND SMBLibrary: +Windows bind port 139 to the first IP addres of every adapter, while port 445 is bound globally. +This means that if you'll disable port 445 (or block it using a firewall), you'll be able to use a different service on port 139 for every IP address. + +###### Additional Notes: +* To free port 139 for a given adapter, go to 'Internet Protocol (TCP/IP) Properties' > Advanced > WINS, and select 'Disable NetBIOS over TCP/IP'. +Uncheck 'File and Printer Sharing for Microsoft Networks' to ensure Windows will not answer to SMB traffic on port 445 for this adapter. + +* It's important to note that disabling NetBIOS over TCP/IP will also disable NetBIOS name service for that adapter (a.k.a. WINS), This service uses UDP port 137. +SMBLibrary offers a name service of its own. + +* You can install a virtual network adapter driver for Windows to be used solely with SMBLibrary: + - You can install the 'Microsoft Loopback adapter' and use it for server-only communication with SMBLibrary. + +###### Windows 7/8/2008/2012: +* It's possible to prevent Windows from using port 445 by removing all of the '\Device\Tcpip_{..}' and '\Device\Tcpip6_{..}' entries from the `Bind' registry key under 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanmanServer\Linkage'. + +* if you want localhost access from Windows explorer to work as expected, you must specify the IP address that you selected (\\\\127.0.0.1 or \\\\localhost will not work as expected), in addition, I have observed that when connecting to the first IP address of a given adapter, Windows will only attempt to connect to port 445. + +##### Method 3: Use an IP address that is invisible to Windows File Sharing: +Using PCap.Net you can programmatically setup a virtual Network adapter and intercept SMB traffic (similar to how a virtual machine operates), You should use the ARP protocol to notify the network about the new IP address, and then process the incoming SMB traffic using SMBLibrary, good luck! + +Using SMBLibrary Server implementation: +======================================= +Any directory / filesystem / object you wish to share must implement the IFileSystem interface (or the lower-level INTFileStore interface). +You can share anything from actual directories to custom objects, as long as they expose a directory structure. diff --git a/Utilities/ByteUtils/LittleEndianWriter.cs b/Utilities/ByteUtils/LittleEndianWriter.cs index 4816e78b..2fa75e71 100644 --- a/Utilities/ByteUtils/LittleEndianWriter.cs +++ b/Utilities/ByteUtils/LittleEndianWriter.cs @@ -11,75 +11,75 @@ namespace Utilities { public class LittleEndianWriter { - public static void WriteUInt16(byte[] buffer, int offset, ushort value) + public static void WriteInt16(byte[] buffer, int offset, short value) { byte[] bytes = LittleEndianConverter.GetBytes(value); Array.Copy(bytes, 0, buffer, offset, bytes.Length); } - public static void WriteUInt16(byte[] buffer, ref int offset, ushort value) + public static void WriteInt16(byte[] buffer, ref int offset, short value) { - WriteUInt16(buffer, offset, value); + WriteInt16(buffer, offset, value); offset += 2; } - public static void WriteInt16(byte[] buffer, int offset, short value) + public static void WriteUInt16(byte[] buffer, int offset, ushort value) { byte[] bytes = LittleEndianConverter.GetBytes(value); Array.Copy(bytes, 0, buffer, offset, bytes.Length); } - public static void WriteInt16(byte[] buffer, ref int offset, short value) + public static void WriteUInt16(byte[] buffer, ref int offset, ushort value) { - WriteInt16(buffer, offset, value); + WriteUInt16(buffer, offset, value); offset += 2; } - public static void WriteUInt32(byte[] buffer, int offset, uint value) + public static void WriteInt32(byte[] buffer, int offset, int value) { byte[] bytes = LittleEndianConverter.GetBytes(value); Array.Copy(bytes, 0, buffer, offset, bytes.Length); } - public static void WriteUInt32(byte[] buffer, ref int offset, uint value) + public static void WriteInt32(byte[] buffer, ref int offset, int value) { - WriteUInt32(buffer, offset, value); + WriteInt32(buffer, offset, value); offset += 4; } - public static void WriteInt32(byte[] buffer, int offset, int value) + public static void WriteUInt32(byte[] buffer, int offset, uint value) { byte[] bytes = LittleEndianConverter.GetBytes(value); Array.Copy(bytes, 0, buffer, offset, bytes.Length); } - public static void WriteInt32(byte[] buffer, ref int offset, int value) + public static void WriteUInt32(byte[] buffer, ref int offset, uint value) { - WriteInt32(buffer, offset, value); + WriteUInt32(buffer, offset, value); offset += 4; } - public static void WriteUInt64(byte[] buffer, int offset, ulong value) + public static void WriteInt64(byte[] buffer, int offset, long value) { byte[] bytes = LittleEndianConverter.GetBytes(value); Array.Copy(bytes, 0, buffer, offset, bytes.Length); } - public static void WriteUInt64(byte[] buffer, ref int offset, ulong value) + public static void WriteInt64(byte[] buffer, ref int offset, long value) { - WriteUInt64(buffer, offset, value); + WriteInt64(buffer, offset, value); offset += 8; } - public static void WriteInt64(byte[] buffer, int offset, long value) + public static void WriteUInt64(byte[] buffer, int offset, ulong value) { byte[] bytes = LittleEndianConverter.GetBytes(value); Array.Copy(bytes, 0, buffer, offset, bytes.Length); } - public static void WriteInt64(byte[] buffer, ref int offset, long value) + public static void WriteUInt64(byte[] buffer, ref int offset, ulong value) { - WriteInt64(buffer, offset, value); + WriteUInt64(buffer, offset, value); offset += 8; } @@ -95,6 +95,12 @@ public static void WriteGuid(byte[] buffer, ref int offset, Guid value) offset += 16; } + public static void WriteInt16(Stream stream, short value) + { + byte[] bytes = LittleEndianConverter.GetBytes(value); + stream.Write(bytes, 0, bytes.Length); + } + public static void WriteUInt16(Stream stream, ushort value) { byte[] bytes = LittleEndianConverter.GetBytes(value); @@ -112,5 +118,17 @@ public static void WriteUInt32(Stream stream, uint value) byte[] bytes = LittleEndianConverter.GetBytes(value); stream.Write(bytes, 0, bytes.Length); } + + public static void WriteInt64(Stream stream, long value) + { + byte[] bytes = LittleEndianConverter.GetBytes(value); + stream.Write(bytes, 0, bytes.Length); + } + + public static void WriteUInt64(Stream stream, ulong value) + { + byte[] bytes = LittleEndianConverter.GetBytes(value); + stream.Write(bytes, 0, bytes.Length); + } } } diff --git a/Utilities/Conversion/LittleEndianConverter.cs b/Utilities/Conversion/LittleEndianConverter.cs index bb494dee..4ebb6a78 100644 --- a/Utilities/Conversion/LittleEndianConverter.cs +++ b/Utilities/Conversion/LittleEndianConverter.cs @@ -5,7 +5,6 @@ * either version 3 of the License, or (at your option) any later version. */ using System; -using System.Collections.Generic; namespace Utilities { diff --git a/Utilities/Generics/BlockingQueue.cs b/Utilities/Generics/BlockingQueue.cs index 93291145..fffd1e4f 100644 --- a/Utilities/Generics/BlockingQueue.cs +++ b/Utilities/Generics/BlockingQueue.cs @@ -1,13 +1,11 @@ -/* Copyright (C) 2016-2020 Tal Aloni . All rights reserved. +/* Copyright (C) 2016-2025 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.Threading; -using System.Text; namespace Utilities { @@ -19,6 +17,11 @@ public class BlockingQueue public void Enqueue(T item) { + if (m_stopping) + { + return; + } + lock (m_queue) { m_queue.Enqueue(item); @@ -32,10 +35,11 @@ public void Enqueue(T item) public void Enqueue(List items) { - if (items.Count == 0) + if (m_stopping || items.Count == 0) { return; } + lock (m_queue) { foreach (T item in items) @@ -57,8 +61,12 @@ public bool TryDequeue(out T item) { while (m_queue.Count == 0) { - Monitor.Wait(m_queue); - if (m_stopping) + if (!m_stopping) + { + Monitor.Wait(m_queue); + } + + if (m_stopping && m_queue.Count == 0) { item = default(T); return false; @@ -80,6 +88,15 @@ public void Stop() } } + public void Abort() + { + lock (m_queue) + { + m_queue.Clear(); + Stop(); + } + } + public int Count { get diff --git a/Utilities/Properties/AssemblyInfo.cs b/Utilities/Properties/AssemblyInfo.cs deleted file mode 100644 index a0d0338d..00000000 --- a/Utilities/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Utilities")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("Tal Aloni")] -[assembly: AssemblyProduct("Utilities")] -[assembly: AssemblyCopyright("Copyright © Tal Aloni 2005-2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("a4607d70-de29-4ae9-8b5a-d0b9cb405727")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Revision and Build Numbers -// by using the '*' as shown below: -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Utilities/Utilities.VS2005.csproj b/Utilities/Utilities.VS2005.csproj deleted file mode 100644 index 8fe889d9..00000000 --- a/Utilities/Utilities.VS2005.csproj +++ /dev/null @@ -1,67 +0,0 @@ - - - Debug - AnyCPU - 8.0.50727 - 2.0 - {6E0F2D1E-6167-4032-BA90-DEE3A99207D0} - Library - Properties - Utilities - Utilities - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Utilities/Utilities.csproj b/Utilities/Utilities.csproj index d915a62b..2cac2acf 100644 --- a/Utilities/Utilities.csproj +++ b/Utilities/Utilities.csproj @@ -2,9 +2,9 @@ net20;net40;netstandard2.0 - false Utilities Utilities + Copyright © Tal Aloni 2005-2023