From 8c45d1f823960c68017d12dcedb7975af799b726 Mon Sep 17 00:00:00 2001 From: Nadim El Makhoud Date: Mon, 26 Aug 2024 15:42:52 +0200 Subject: [PATCH 1/3] Update from original repo --- ClientExamples.md | 16 +- Readme.md | 6 + SMBLibrary.Tests/Client/SMB2ClientTests.cs | 97 +++++++ .../IntegrationTests/LoginTests.cs | 88 ++++++ .../SMB2/NegotiateRequestParsingTests.cs | 79 ++++++ .../SMB2/NegotiateResponseParsingTests.cs | 87 ++++++ SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs | 227 +++++++++++++++ .../SimpleProtectedNegotiationTokenInit.cs | 45 ++- .../NTLM/Helpers/NTLMCryptography.cs | 125 +++++++- .../NTLM/Structures/AuthenticateMessage.cs | 28 +- .../NTLM/Structures/NTLMv2ClientChallenge.cs | 9 + .../Authentication/IAuthenticationClient.cs | 16 ++ .../NTLMAuthenticationClient.cs | 140 +++++++++ SMBLibrary/Client/ConnectionState.cs | 6 +- SMBLibrary/Client/Helpers/IPAddressHelper.cs | 27 ++ .../Helpers/NTLMAuthenticationHelper.cs | 125 +++----- .../Client/Helpers/ServerServiceHelper.cs | 4 +- SMBLibrary/Client/ISMBClient.cs | 7 +- SMBLibrary/Client/NameServiceClient.cs | 2 +- SMBLibrary/Client/SMB2Client.cs | 267 +++++++++++++----- SMBLibrary/Client/SMB2FileStore.cs | 60 ++-- SMBLibrary/Enums/NTStatus.cs | 5 +- SMBLibrary/Helpers/SP800_1008.cs | 2 +- .../Set/FileRenameInformationType2.cs | 9 +- .../Structures/SecurityInformation/ACE/ACE.cs | 2 + .../ACE/AccessDeniedACE.cs | 48 ++++ .../NetBios/NBTConnectionReceiveBuffer.cs | 37 ++- .../NameServicePackets/Enums/NetBiosSuffix.cs | 8 +- SMBLibrary/NetBios/NetBiosUtils.cs | 4 +- SMBLibrary/Readme.md | 2 +- SMBLibrary/RevisionHistory.txt | 51 ++++ ...ransaction2QueryFileInformationResponse.cs | 2 +- SMBLibrary/SMB2/Commands/CreateResponse.cs | 21 +- SMBLibrary/SMB2/Commands/NegotiateRequest.cs | 58 +++- SMBLibrary/SMB2/Commands/NegotiateResponse.cs | 4 +- SMBLibrary/SMB2/Commands/SMB2Command.cs | 2 +- .../Enums/Negotiate/NegotiateContextType.cs | 5 + SMBLibrary/SMB2/SMB2Cryptography.cs | 14 +- SMBLibrary/SMB2/Structures/CreateContext.cs | 17 +- .../SMB2/Structures/Enums/CipherAlgorithm.cs | 10 + .../SMB2/Structures/Enums/HashAlgorithm.cs | 7 + .../EncryptionCapabilities.cs | 53 ++++ .../NegotiateContext.cs | 64 +++-- .../PreAuthIntegrityCapabilities.cs | 59 ++++ SMBLibrary/SMBLibrary.csproj | 23 ++ .../Exceptions/InvalidLevelException.cs | 28 ++ .../Exceptions/UnsupportedLevelException.cs | 28 ++ .../Services/ServerService/ServerService.cs | 27 +- .../Structures/ServerInfo/ServerInfo.cs | 8 +- .../Structures/ShareInfo/ShareEnum.cs | 8 +- .../Structures/ShareInfo/ShareInfo.cs | 7 +- SMBServer.VS2019.sln => SMBServer.sln | 0 Utilities/ByteUtils/LittleEndianWriter.cs | 54 ++-- Utilities/Conversion/LittleEndianConverter.cs | 1 - Utilities/Utilities.csproj | 4 + 55 files changed, 1796 insertions(+), 337 deletions(-) create mode 100644 SMBLibrary.Tests/Client/SMB2ClientTests.cs create mode 100644 SMBLibrary.Tests/IntegrationTests/LoginTests.cs create mode 100644 SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs create mode 100644 SMBLibrary.Tests/SMB2/NegotiateResponseParsingTests.cs create mode 100644 SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs create mode 100644 SMBLibrary/Client/Authentication/IAuthenticationClient.cs create mode 100644 SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs create mode 100644 SMBLibrary/Client/Helpers/IPAddressHelper.cs create mode 100644 SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessDeniedACE.cs create mode 100644 SMBLibrary/SMB2/Structures/Enums/CipherAlgorithm.cs create mode 100644 SMBLibrary/SMB2/Structures/Enums/HashAlgorithm.cs create mode 100644 SMBLibrary/SMB2/Structures/NegotiateContext/EncryptionCapabilities.cs rename SMBLibrary/SMB2/Structures/{ => NegotiateContext}/NegotiateContext.cs (55%) create mode 100644 SMBLibrary/SMB2/Structures/NegotiateContext/PreAuthIntegrityCapabilities.cs create mode 100644 SMBLibrary/Services/Exceptions/InvalidLevelException.cs create mode 100644 SMBLibrary/Services/Exceptions/UnsupportedLevelException.cs rename SMBServer.VS2019.sln => SMBServer.sln (100%) diff --git a/ClientExamples.md b/ClientExamples.md index 67dc699a..ba923efb 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) { @@ -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,4 @@ if (status == NTStatus.STATUS_SUCCESS) status = fileStore.CloseFile(fileHandle); } status = fileStore.Disconnect(); -``` \ No newline at end of file +``` diff --git a/Readme.md b/Readme.md index 3c4ef8e9..fb779449 100644 --- a/Readme.md +++ b/Readme.md @@ -63,6 +63,12 @@ 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. + Contact: ======== If you have any question, feel free to contact me. diff --git a/SMBLibrary.Tests/Client/SMB2ClientTests.cs b/SMBLibrary.Tests/Client/SMB2ClientTests.cs new file mode 100644 index 00000000..48efb8e6 --- /dev/null +++ b/SMBLibrary.Tests/Client/SMB2ClientTests.cs @@ -0,0 +1,97 @@ +/* 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.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 int m_serverPort; + private TcpListener m_tcpListener; + private bool m_clientConnected; + + [TestInitialize] + public void Initialize() + { + m_serverPort = 1000 + new Random().Next(50000); + 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); + + SMB2Client client = new SMB2Client(); + int timeoutInMilliseconds = 1000; + + 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, timeoutInMilliseconds); + 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); + SMB2Client client = new SMB2Client(); + int timeoutInMilliseconds = 1000; + + 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, timeoutInMilliseconds); + stopwatch.Stop(); + }).Start(); + + while (!m_clientConnected) + { + Thread.Sleep(1); + } + Assert.IsFalse(isConnected); + Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200); + } + } +} diff --git a/SMBLibrary.Tests/IntegrationTests/LoginTests.cs b/SMBLibrary.Tests/IntegrationTests/LoginTests.cs new file mode 100644 index 00000000..a77ea063 --- /dev/null +++ b/SMBLibrary.Tests/IntegrationTests/LoginTests.cs @@ -0,0 +1,88 @@ +/* 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.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 int m_serverPort; + private SMBServer m_server; + + [TestInitialize] + public void Initialize() + { + m_serverPort = 1000 + new Random().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, 5000); + + // 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, 5000); + + // 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, 5000); + 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, 5000); + + // Act + NTStatus status = client.Login("", "John", "Password"); + + // Assert + Assert.AreEqual(NTStatus.STATUS_LOGON_FAILURE, status); + } + } +} 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.Win32/Security/SSPIHelper.NTLM.cs b/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs new file mode 100644 index 00000000..c43d8634 --- /dev/null +++ b/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs @@ -0,0 +1,227 @@ +/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. + * + * You can redistribute this program and/or modify it under the terms of + * the GNU Lesser Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + */ +using System; +using System.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); + 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); + if (result != SEC_E_OK && result != SEC_I_CONTINUE_NEEDED) + { + if (result == SEC_E_INVALID_HANDLE) + { + throw new Exception("InitializeSecurityContext failed, Invalid handle"); + } + 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")); + } + } + 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")); + } + } + } + } +} 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..913df3fb 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; @@ -78,11 +77,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 + 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 +137,11 @@ public static byte[] DesLongEncrypt(byte[] key, byte[] plainText) public static Encoding GetOEMEncoding() { +#if NETSTANDARD2_0 + return ASCIIEncoding.GetEncoding(28591); +#else return Encoding.GetEncoding(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); +#endif } /// @@ -239,5 +257,98 @@ public static byte[] KXKey(byte[] sessionBaseKey, NegotiateFlags negotiateFlags, return keyExchangeKey; } } + + /// + /// 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); + } + + private 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[] concatendated = ByteUtils.Concatenate(exportedSessionKey, nullTerminatedEncodedString); + return MD5.Create().ComputeHash(concatendated); + } + + public static byte[] ComputeClientSealKey(byte[] exportedSessionKey) + { + return ComputeSealKey(exportedSessionKey, true); + } + + public static byte[] ComputeServerSealKey(byte[] exportedSessionKey) + { + return ComputeSealKey(exportedSessionKey, false); + } + + private 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[] concatendated = ByteUtils.Concatenate(exportedSessionKey, nullTerminatedEncodedString); + return MD5.Create().ComputeHash(concatendated); + } + + public static byte[] ComputeMechListMIC(byte[] exportedSessionKey, byte[] message) + { + return ComputeMechListMIC(exportedSessionKey, message, 0); + } + + public static byte[] ComputeMechListMIC(byte[] exportedSessionKey, byte[] message, int seqNum) + { + // [MS-NLMP] 3.4.4.2 + byte[] signKey = ComputeClientSignKey(exportedSessionKey); + byte[] sequenceNumberBytes = LittleEndianConverter.GetBytes(seqNum); + byte[] concatendated = ByteUtils.Concatenate(sequenceNumberBytes, message); + byte[] fullHash = new HMACMD5(signKey).ComputeHash(concatendated); + byte[] hash = ByteReader.ReadBytes(fullHash, 0, 8); + + byte[] sealKey = ComputeClientSealKey(exportedSessionKey); + byte[] encryptedHash = RC4.Encrypt(sealKey, hash); + + byte[] version = new byte[] { 0x01, 0x00, 0x00, 0x00 }; + return ByteUtils.Concatenate(ByteUtils.Concatenate(version, encryptedHash), sequenceNumberBytes); + } } } 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/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..fe9115cb --- /dev/null +++ b/SMBLibrary/Client/Authentication/IAuthenticationClient.cs @@ -0,0 +1,16 @@ +/* Copyright (C) 2023-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. + */ +namespace SMBLibrary.Client.Authentication +{ + public interface IAuthenticationClient + { + /// Credentials blob or null if security blob is invalid + byte[] InitializeSecurityContext(byte[] securityBlob); + + byte[] GetSessionKey(); + } +} diff --git a/SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs b/SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs new file mode 100644 index 00000000..9d563e0a --- /dev/null +++ b/SMBLibrary/Client/Authentication/NTLMAuthenticationClient.cs @@ -0,0 +1,140 @@ +/* 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 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 = NTLMAuthenticationHelper.GetNegotiateMessage(m_domainName, m_userName, m_password, m_authenticationMethod); + 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[] 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; + } + + 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/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..56a5cb84 100644 --- a/SMBLibrary/Client/Helpers/NTLMAuthenticationHelper.cs +++ b/SMBLibrary/Client/Helpers/NTLMAuthenticationHelper.cs @@ -1,4 +1,4 @@ -/* Copyright (C) 2017-2018 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, @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Security.Cryptography; -using SMBLibrary.Authentication.GSSAPI; using SMBLibrary.Authentication.NTLM; using Utilities; @@ -15,27 +14,8 @@ namespace SMBLibrary.Client { public class NTLMAuthenticationHelper { - public static byte[] GetNegotiateMessage(byte[] securityBlob, string domainName, AuthenticationMethod authenticationMethod) + public static byte[] GetNegotiateMessage(string domainName, 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; - } - NegotiateMessage negotiateMessage = new NegotiateMessage(); negotiateMessage.NegotiateFlags = NegotiateFlags.UnicodeEncoding | NegotiateFlags.OEMEncoding | @@ -46,9 +26,13 @@ public static byte[] GetNegotiateMessage(byte[] securityBlob, string domainName, NegotiateFlags.AlwaysSign | NegotiateFlags.Version | NegotiateFlags.Use128BitEncryption | - NegotiateFlags.KeyExchange | NegotiateFlags.Use56BitEncryption; + if (!(userName == String.Empty && password == String.Empty)) + { + negotiateMessage.NegotiateFlags |= NegotiateFlags.KeyExchange; + } + if (authenticationMethod == AuthenticationMethod.NTLMv1) { negotiateMessage.NegotiateFlags |= NegotiateFlags.LanManagerSessionKey; @@ -61,44 +45,14 @@ public static byte[] GetNegotiateMessage(byte[] securityBlob, string domainName, negotiateMessage.Version = NTLMVersion.Server2003; negotiateMessage.DomainName = domainName; negotiateMessage.Workstation = Environment.MachineName; - if (useGSSAPI) - { - SimpleProtectedNegotiationTokenInit outputToken = new SimpleProtectedNegotiationTokenInit(); - outputToken.MechanismTypeList = new List(); - outputToken.MechanismTypeList.Add(GSSProvider.NTLMSSPIdentifier); - outputToken.MechanismToken = negotiateMessage.GetBytes(); - return outputToken.GetBytes(true); - } - else - { - return negotiateMessage.GetBytes(); - } + 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; @@ -139,6 +93,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 +105,13 @@ public static byte[] GetAuthenticateMessage(byte[] securityBlob, string domainNa byte[] keyExchangeKey; if (authenticationMethod == AuthenticationMethod.NTLMv1 || authenticationMethod == AuthenticationMethod.NTLMv1ExtendedSessionSecurity) { - if (authenticationMethod == AuthenticationMethod.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 +121,33 @@ 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 // 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 +162,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 +185,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 42143cf0..34218a31 100644 --- a/SMBLibrary/Client/Helpers/ServerServiceHelper.cs +++ b/SMBLibrary/Client/Helpers/ServerServiceHelper.cs @@ -23,7 +23,7 @@ public class ServerServiceHelper } /// - /// 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 async Task<(NTStatus status, IEnumerable result)> ListShares(INTFileStore namedPipeShare, string serverName, ShareType? shareType, CancellationToken cancellationToken) @@ -37,7 +37,7 @@ public class ServerServiceHelper 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 3b954933..087520e0 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-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,5 +40,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/SMB2Client.cs b/SMBLibrary/Client/SMB2Client.cs index 63500c1d..e19d5126 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-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, @@ -12,6 +12,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; +using SMBLibrary.Client.Authentication; using SMBLibrary.NetBios; using SMBLibrary.SMB2; using Utilities; @@ -27,13 +28,15 @@ 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 = 15000; + public static readonly int DefaultResponseTimeoutInMilliseconds = 15000; 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 object m_incomingQueueLock = new object(); private List m_incomingQueue = new List(); @@ -55,6 +58,7 @@ 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; public SMB2Client() @@ -62,22 +66,42 @@ public SMB2Client() } /// - /// 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 Task<(bool Success, string ErrorMessage)> ConnectAsync(string serverName, SMBTransportType transport, CancellationToken cancellationToken) + { + return ConnectAsync(serverName, transport, DefaultResponseTimeoutInMilliseconds, cancellationToken); + } + + public Task<(bool Success, string ErrorMessage)> ConnectAsync(IPAddress serverAddress, SMBTransportType transport, CancellationToken cancellationToken) + { + return ConnectAsync(serverAddress, transport, DefaultResponseTimeoutInMilliseconds, cancellationToken); + } + + // /// + // /// 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 Task<(bool Success, string ErrorMessage)> ConnectAsync(string serverName, SMBTransportType transport, int responseTimeoutInMilliseconds, CancellationToken cancellationToken) { 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]; - return ConnectAsync(serverAddress, transport, cancellationToken); + IPAddress serverAddress = IPAddressHelper.SelectAddressPreferIPv4(hostAddresses); + return ConnectAsync(serverAddress, transport, responseTimeoutInMilliseconds, cancellationToken); } - public async Task<(bool Success, string ErrorMessage)> ConnectAsync(IPAddress serverAddress, SMBTransportType transport, CancellationToken cancellationToken) + public Task<(bool Success, string ErrorMessage)> ConnectAsync(IPAddress serverAddress, SMBTransportType transport, int responseTimeoutInMilliseconds, CancellationToken cancellationToken) + { + int port = (transport == SMBTransportType.DirectTCPTransport ? DirectTCPPort : NetBiosOverTCPPort); + return ConnectAsync(serverAddress, transport, port, responseTimeoutInMilliseconds, cancellationToken); + } + + public async Task<(bool Success, string ErrorMessage)> ConnectAsync(IPAddress serverAddress, SMBTransportType transport, int port, int responseTimeoutInMilliseconds, CancellationToken cancellationToken) { if (m_serverName == null) { @@ -87,15 +111,7 @@ public SMB2Client() m_transport = transport; if (!m_isConnected) { - int port; - if (transport == SMBTransportType.NetBiosOverTCP) - { - port = NetBiosOverTCPPort; - } - else - { - port = DirectTCPPort; - } + m_responseTimeoutInMilliseconds = responseTimeoutInMilliseconds; var ConResult1 = ConnectSocket(serverAddress, port); if (!ConResult1.Success) @@ -106,7 +122,7 @@ public SMB2Client() 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); await TrySendPacketAsync(m_clientSocket, sessionRequest, cancellationToken); @@ -154,7 +170,7 @@ public SMB2Client() private (bool Success, string ErrorMessage) 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 { @@ -165,9 +181,9 @@ public SMB2Client() return (false, "Could not establish connection to a remote host: " + e.Message); } - 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, string.Empty); } @@ -181,7 +197,14 @@ public SMB2Client() request.Dialects.Add(SMB2Dialect.SMB202); request.Dialects.Add(SMB2Dialect.SMB210); request.Dialects.Add(SMB2Dialect.SMB300); - +#if SMB302_CLIENT + request.Dialects.Add(SMB2Dialect.SMB302); +#endif +#if SMB311_CLIENT + request.Dialects.Add(SMB2Dialect.SMB311); + request.NegotiateContextList = GetNegotiateContextList(); + m_preauthIntegrityHashValue = new byte[64]; +#endif await TrySendCommandAsync(request, cancellationToken); var command = WaitForCommand(request.MessageID); @@ -213,7 +236,15 @@ public void Disconnect() if (m_isConnected) { m_clientSocket.Disconnect(false); + m_clientSocket.Close(); + lock (m_connectionState.ReceiveBuffer) + { + m_connectionState.ReceiveBuffer.Dispose(); + } m_isConnected = false; + m_messageID = 0; + m_sessionID = 0; + m_availableCredits = 1; } } @@ -222,14 +253,21 @@ public Task LoginAsync(string domainName, string userName, string pass return LoginAsync(domainName, userName, password, AuthenticationMethod.NTLMv2, cancellationToken); } - public async Task LoginAsync(string domainName, string userName, string password, AuthenticationMethod authenticationMethod, CancellationToken cancellationToken) + public Task LoginAsync(string domainName, string userName, string password, AuthenticationMethod authenticationMethod, CancellationToken cancellationToken) + { + var spn = $"cifs/{m_serverName}"; + var authenticationClient = new NTLMAuthenticationClient(domainName, userName, password, spn, authenticationMethod); + return Login(authenticationClient, cancellationToken); + } + + public async Task Login(IAuthenticationClient authenticationClient, CancellationToken cancellationToken) { 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; @@ -244,11 +282,12 @@ public async Task LoginAsync(string domainName, string userName, strin { if (response.Header.Status == NTStatus.STATUS_MORE_PROCESSING_REQUIRED && response is SessionSetupResponse) { - byte[] authenticateMessage = NTLMAuthenticationHelper.GetAuthenticateMessage(((SessionSetupResponse)response).SecurityBuffer, domainName, userName, password, authenticationMethod, out m_sessionKey); + byte[] authenticateMessage = authenticationClient.InitializeSecurityContext(((SessionSetupResponse)response).SecurityBuffer); if (authenticateMessage == null) { return NTStatus.SEC_E_INVALID_TOKEN; } + m_sessionKey = authenticationClient.GetSessionKey(); m_sessionID = response.Header.SessionID; request = new SessionSetupRequest(); @@ -261,12 +300,23 @@ public async Task LoginAsync(string domainName, string userName, strin 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) + SessionFlags sessionFlags = ((SessionSetupResponse)response).SessionFlags; + if ((sessionFlags & SessionFlags.IsGuest) > 0) { - 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); + // [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); + } + + if (m_dialect >= SMB2Dialect.SMB300) + { + 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); } } return response.Header.Status; @@ -351,54 +401,64 @@ private void OnClientSocketReceive(IAsyncResult ar) ConnectionState state = (ConnectionState)ar.AsyncState; Socket clientSocket = state.ClientSocket; - if (!clientSocket.Connected) - { - return; - } - - int numberOfBytesReceived = 0; - try - { - numberOfBytesReceived = clientSocket.EndReceive(ar); - } - catch (ArgumentException) // The IAsyncResult object was not returned from the corresponding synchronous method on this class. - { - return; - } - catch (ObjectDisposedException) - { - Log("[ReceiveCallback] EndReceive ObjectDisposedException"); - return; - } - catch (SocketException ex) - { - Log("[ReceiveCallback] EndReceive SocketException: " + ex.Message); - return; - } - - if (numberOfBytesReceived == 0) + lock (state.ReceiveBuffer) { - m_isConnected = false; - } - else - { - NBTConnectionReceiveBuffer buffer = state.ReceiveBuffer; - buffer.SetNumberOfBytesReceived(numberOfBytesReceived); - ProcessConnectionBuffer(state); - + 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 + { + 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(); + } + } } } } @@ -415,7 +475,9 @@ private void ProcessConnectionBuffer(ConnectionState state) } catch (Exception) { + Log("[ProcessConnectionBuffer] Invalid packet"); state.ClientSocket.Close(); + state.ReceiveBuffer.Dispose(); break; } @@ -431,7 +493,7 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) if (packet is SessionMessagePacket) { byte[] messageBytes; - if (m_dialect == SMB2Dialect.SMB300 && SMB2TransformHeader.IsTransformHeader(packet.Trailer, 0)) + if (m_dialect >= SMB2Dialect.SMB300 && SMB2TransformHeader.IsTransformHeader(packet.Trailer, 0)) { SMB2TransformHeader transformHeader = new SMB2TransformHeader(packet.Trailer, 0); byte[] encryptedMessage = ByteReader.ReadBytes(packet.Trailer, SMB2TransformHeader.Length, (int)transformHeader.OriginalMessageSize); @@ -452,9 +514,15 @@ 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) @@ -462,9 +530,12 @@ private void ProcessPacket(SessionPacket packet, ConnectionState state) NegotiateResponse negotiateResponse = (NegotiateResponse)command; if ((negotiateResponse.Capabilities & Capabilities.LargeMTU) > 0) { - // [MS-SMB2] 3.2.5.1 Receiving Any Message - If the message size received exceeds Connection.MaxTransactSize, the client MUST disconnect the connection. - // Note: Windows clients do not enforce the MaxTransactSize value, we add 256 bytes. - int maxPacketSize = SessionPacket.HeaderLength + (int)Math.Min(negotiateResponse.MaxTransactSize, ClientMaxTransactSize) + 256; + // [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); @@ -498,14 +569,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) { @@ -534,7 +612,8 @@ internal SessionPacket WaitForSessionResponsePacket() { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - while (stopwatch.ElapsedMilliseconds < ResponseTimeoutInMilliseconds) + + while (stopwatch.ElapsedMilliseconds < m_responseTimeoutInMilliseconds) { if (m_sessionResponsePacket != null) { @@ -591,7 +670,7 @@ internal async Task TrySendCommandAsync(SMB2Command request, bool encryptData, C if (m_availableCredits < request.Header.CreditCharge) { // SMB server did not send packet with credits on time (i.e. throttling, or too much connections) or the credit packet was lost. - if (!await WaitForAmountOfCredits(request.Header.CreditCharge, ResponseTimeoutInMilliseconds, cancellationToken)) + if (!await WaitForAmountOfCredits(request.Header.CreditCharge, m_responseTimeoutInMilliseconds, cancellationToken)) { throw new Exception($"Not enough credits ({m_availableCredits} Available, {request.Header.CreditCharge} CreditCharge)."); } @@ -611,7 +690,7 @@ internal async Task TrySendCommandAsync(SMB2Command request, bool encryptData, C 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 @@ -633,6 +712,24 @@ internal async Task TrySendCommandAsync(SMB2Command request, bool encryptData, C } } + /// 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 @@ -656,8 +753,16 @@ public uint MaxWriteSize return m_maxWriteSize; } } + + public bool IsConnected + { + get + { + return m_isConnected; + } + } - public static Task TrySendCommandAsync(Socket socket, SMB2Command request, byte[] encryptionKey, CancellationToken cancellationToken = default) + public Task TrySendCommandAsync(Socket socket, SMB2Command request, byte[] encryptionKey, CancellationToken cancellationToken = default) { SessionMessagePacket packet = new SessionMessagePacket(); if (encryptionKey != null) @@ -668,12 +773,16 @@ public static Task TrySendCommandAsync(Socket socket, SMB2Command request, byte[ 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)); + } } return TrySendPacketAsync(socket, packet, cancellationToken); } - public static async Task TrySendPacketAsync(Socket socket, SessionPacket packet, CancellationToken cancellationToken = default) + public async Task TrySendPacketAsync(Socket socket, SessionPacket packet, CancellationToken cancellationToken = default) { try { @@ -682,9 +791,11 @@ public static async Task TrySendPacketAsync(Socket socket, SessionPacket packet, } catch (SocketException) { + m_isConnected = false; } catch (ObjectDisposedException) { + m_isConnected = false; } } } diff --git a/SMBLibrary/Client/SMB2FileStore.cs b/SMBLibrary/Client/SMB2FileStore.cs index f64fe125..3e6ebd74 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, @@ -41,7 +41,7 @@ public SMB2FileStore(SMB2Client client, uint treeID, bool encryptShareData) request.ImpersonationLevel = ImpersonationLevel.Impersonation; await TrySendCommandAsync(request, cancellationToken); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); var fileStatus = FileStatus.FILE_DOES_NOT_EXIST; if (response != null) @@ -57,21 +57,23 @@ public SMB2FileStore(SMB2Client client, uint treeID, bool encryptShareData) return (response.Header.Status, handle, fileStatus); } - return (NTStatus.STATUS_INVALID_SMB, null, fileStatus); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, null, fileStatus); } public async Task CloseFileAsync(object handle, CancellationToken cancellationToken) { CloseRequest request = new CloseRequest(); request.FileId = (FileID)handle; + await TrySendCommandAsync(request, cancellationToken); - 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 async Task<(NTStatus status, byte[] data)> ReadFileAsync(object handle, long offset, int maxCount, CancellationToken cancellationToken) @@ -81,9 +83,9 @@ public async Task CloseFileAsync(object handle, CancellationToken canc request.FileId = (FileID)handle; request.Offset = (ulong)offset; request.ReadLength = (uint)maxCount; - + await TrySendCommandAsync(request, cancellationToken); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { byte[] data = null; @@ -94,7 +96,7 @@ public async Task CloseFileAsync(object handle, CancellationToken canc return (response.Header.Status, data); } - return (NTStatus.STATUS_INVALID_SMB, null); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, null); } public async Task<(NTStatus status, int numberOfBytesWritten)> WriteFileAsync(object handle, long offset, byte[] data, CancellationToken cancellationToken) @@ -106,7 +108,7 @@ public async Task CloseFileAsync(object handle, CancellationToken canc request.Data = data; await TrySendCommandAsync(request, cancellationToken); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { int numberOfBytesWritten = 0; @@ -119,7 +121,7 @@ public async Task CloseFileAsync(object handle, CancellationToken canc return (response.Header.Status, numberOfBytesWritten); } - return (NTStatus.STATUS_INVALID_SMB, 0); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, 0); } public async Task FlushFileBuffersAsync(object handle, CancellationToken cancellationToken) @@ -128,7 +130,7 @@ public async Task FlushFileBuffersAsync(object handle, CancellationTok request.FileId = (FileID) handle; await TrySendCommandAsync(request, cancellationToken); - 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) @@ -137,7 +139,7 @@ public async Task FlushFileBuffersAsync(object handle, CancellationTok } } - 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) @@ -161,7 +163,7 @@ public NTStatus UnlockFile(object handle, long byteOffset, long length) request.FileName = fileName; await TrySendCommandAsync(request, cancellationToken); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { var result = new List(); @@ -171,12 +173,16 @@ public NTStatus UnlockFile(object handle, long byteOffset, long length) result.AddRange(page); request.Reopen = false; await TrySendCommandAsync(request, cancellationToken); - 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, Enumerable.Empty()); + } } return (response.Header.Status, result); } - return (NTStatus.STATUS_INVALID_SMB, Enumerable.Empty()); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, Enumerable.Empty()); } public async Task<(NTStatus status, FileInformation result)> GetFileInformationAsync(object handle, FileInformationClass informationClass, CancellationToken cancellationToken) @@ -188,7 +194,7 @@ public NTStatus UnlockFile(object handle, long byteOffset, long length) request.FileId = (FileID)handle; await TrySendCommandAsync(request, cancellationToken); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { FileInformation result = null; @@ -199,7 +205,7 @@ public NTStatus UnlockFile(object handle, long byteOffset, long length) return (response.Header.Status, result); } - return (NTStatus.STATUS_INVALID_SMB, null); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, null); } public async Task SetFileInformationAsync(object handle, FileInformation information, CancellationToken cancellationToken) @@ -211,13 +217,13 @@ public async Task SetFileInformationAsync(object handle, FileInformati request.SetFileInformation(information); await TrySendCommandAsync(request, cancellationToken); - 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 async Task<(NTStatus status, FileSystemInformation result)> GetFileSystemInformationAsync(FileSystemInformationClass informationClass, CancellationToken cancellationToken) @@ -243,7 +249,7 @@ public async Task SetFileInformationAsync(object handle, FileInformati request.FileId = (FileID)handle; await TrySendCommandAsync(request, cancellationToken); - SMB2Command response = m_client.WaitForCommand(request.MessageID); + SMB2Command response = m_client.WaitForCommand(request.MessageID, out bool connectionTerminated); if (response != null) { FileSystemInformation result = null; @@ -254,7 +260,7 @@ public async Task SetFileInformationAsync(object handle, FileInformati return (response.Header.Status, result); } - return (NTStatus.STATUS_INVALID_SMB, null); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, null); } public NTStatus SetFileSystemInformation(FileSystemInformation information) @@ -272,7 +278,7 @@ public NTStatus SetFileSystemInformation(FileSystemInformation information) request.FileId = (FileID)handle; await TrySendCommandAsync(request, cancellationToken); - 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) @@ -282,7 +288,7 @@ public NTStatus SetFileSystemInformation(FileSystemInformation information) return (response.Header.Status, result); } - return (NTStatus.STATUS_INVALID_SMB, result); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, result); } public Task SetSecurityInformation(object handle, SecurityInformation securityInformation, SecurityDescriptor securityDescriptor) @@ -311,7 +317,7 @@ public NTStatus Cancel(object ioRequest) request.Input = input; request.MaxOutputResponse = (uint)maxOutputLength; await TrySendCommandAsync(request, cancellationToken); - 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) @@ -321,7 +327,7 @@ public NTStatus Cancel(object ioRequest) return (response.Header.Status, output); } - return (NTStatus.STATUS_INVALID_SMB, output); + return (connectionTerminated ? NTStatus.STATUS_INVALID_SMB : NTStatus.STATUS_IO_TIMEOUT, output); } public async Task DisconnectAsync() @@ -340,6 +346,10 @@ public async Task DisconnectAsync() private Task TrySendCommandAsync(SMB2Command request, CancellationToken cancellationToken) { request.Header.TreeID = m_treeID; + if (!m_client.IsConnected) + { + throw new InvalidOperationException("The client is no longer connected"); + } return m_client.TrySendCommandAsync(request, m_encryptShareData, cancellationToken); } 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/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/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..b6707258 100644 --- a/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/ACE.cs +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/ACE.cs @@ -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/AccessDeniedACE.cs b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessDeniedACE.cs new file mode 100644 index 00000000..bcaa0e2b --- /dev/null +++ b/SMBLibrary/NTFileStore/Structures/SecurityInformation/ACE/AccessDeniedACE.cs @@ -0,0 +1,48 @@ +/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. + * + * You can redistribute this program and/or modify it under the terms of + * the GNU Lesser Public License as published by the Free Software Foundation, + * either version 3 of the License, or (at your option) any later version. + */ +using Utilities; + +namespace SMBLibrary +{ + public class AccessDeniedACE : ACE + { + public const int FixedLength = 8; + + public AceHeader Header; + 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/NetBios/NBTConnectionReceiveBuffer.cs b/SMBLibrary/NetBios/NBTConnectionReceiveBuffer.cs index 47148d1e..c013fd1c 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 +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 + m_buffer = ArrayPool.Shared.Rent(bufferLength); +#else m_buffer = new byte[bufferLength]; +#endif } public void IncreaseBufferSize(int bufferLength) { +#if NETSTANDARD2_0 + 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 + ArrayPool.Shared.Return(m_buffer); +#endif m_buffer = buffer; } @@ -110,6 +128,19 @@ private void RemovePacketBytes() } } + public void Dispose() + { +#if NETSTANDARD2_0 + 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/Readme.md b/SMBLibrary/Readme.md index 7f98fbee..83ba59c5 100644 --- a/SMBLibrary/Readme.md +++ b/SMBLibrary/Readme.md @@ -55,7 +55,7 @@ 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. -Client code examples can be found [here](ClientExamples.md). +Client code examples can be found [here](../ClientExamples.md). Contact: ======== diff --git a/SMBLibrary/RevisionHistory.txt b/SMBLibrary/RevisionHistory.txt index d03156b1..b9dfec26 100644 --- a/SMBLibrary/RevisionHistory.txt +++ b/SMBLibrary/RevisionHistory.txt @@ -459,3 +459,54 @@ 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. 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..d3a2e09a 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) diff --git a/SMBLibrary/SMB2/Commands/SMB2Command.cs b/SMBLibrary/SMB2/Commands/SMB2Command.cs index 0738cbf7..479aa361 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. /// /// 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 56439a9b..b3e2e29a 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-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, @@ -107,6 +107,18 @@ public static byte[] DecryptMessage(byte[] key, SMB2TransformHeader transformHea return Utilities.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/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.csproj b/SMBLibrary/SMBLibrary.csproj index 95aae547..7b7478c9 100644 --- a/SMBLibrary/SMBLibrary.csproj +++ b/SMBLibrary/SMBLibrary.csproj @@ -1,6 +1,10 @@  netstandard2.1 + net20;net40;netstandard2.0 + SMBLibrary + 1.5.3.4 + 1573;1591 SMBLibrary SMBLibraryCore.Client SMBLibraryCore.Client @@ -13,6 +17,8 @@ 1.6.0 1573;1591 false + Tal Aloni + Copyright © Tal Aloni 2014-2024 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 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 @@ -33,4 +39,21 @@ + + + + 4.5.1 + + + + + + <_Parameter1>SMBLibrary.Tests + + + + + + + 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/SMBServer.VS2019.sln b/SMBServer.sln similarity index 100% rename from SMBServer.VS2019.sln rename to SMBServer.sln 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/Utilities.csproj b/Utilities/Utilities.csproj index a09200a3..b808c782 100644 --- a/Utilities/Utilities.csproj +++ b/Utilities/Utilities.csproj @@ -3,6 +3,10 @@ netstandard2.1 Utilities Utilities + net20;net40;netstandard2.0 + Utilities + Utilities + Copyright © Tal Aloni 2005-2023 From bdb41e1809a3b00ec30108a08829d4f8cd46497b Mon Sep 17 00:00:00 2001 From: Nadim El Makhoud Date: Mon, 26 Aug 2024 16:13:36 +0200 Subject: [PATCH 2/3] Removed redundant code parts --- SMBLibrary.Tests/Client/SMB2ClientTests.cs | 97 -------- .../IntegrationTests/LoginTests.cs | 88 ------- .../SMB2/NegotiateRequestParsingTests.cs | 79 ------ .../SMB2/NegotiateResponseParsingTests.cs | 87 ------- SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs | 227 ------------------ 5 files changed, 578 deletions(-) delete mode 100644 SMBLibrary.Tests/Client/SMB2ClientTests.cs delete mode 100644 SMBLibrary.Tests/IntegrationTests/LoginTests.cs delete mode 100644 SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs delete mode 100644 SMBLibrary.Tests/SMB2/NegotiateResponseParsingTests.cs delete mode 100644 SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs diff --git a/SMBLibrary.Tests/Client/SMB2ClientTests.cs b/SMBLibrary.Tests/Client/SMB2ClientTests.cs deleted file mode 100644 index 48efb8e6..00000000 --- a/SMBLibrary.Tests/Client/SMB2ClientTests.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* 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.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 int m_serverPort; - private TcpListener m_tcpListener; - private bool m_clientConnected; - - [TestInitialize] - public void Initialize() - { - m_serverPort = 1000 + new Random().Next(50000); - 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); - - SMB2Client client = new SMB2Client(); - int timeoutInMilliseconds = 1000; - - 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, timeoutInMilliseconds); - 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); - SMB2Client client = new SMB2Client(); - int timeoutInMilliseconds = 1000; - - 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, timeoutInMilliseconds); - stopwatch.Stop(); - }).Start(); - - while (!m_clientConnected) - { - Thread.Sleep(1); - } - Assert.IsFalse(isConnected); - Assert.IsTrue(stopwatch.ElapsedMilliseconds < 200); - } - } -} diff --git a/SMBLibrary.Tests/IntegrationTests/LoginTests.cs b/SMBLibrary.Tests/IntegrationTests/LoginTests.cs deleted file mode 100644 index a77ea063..00000000 --- a/SMBLibrary.Tests/IntegrationTests/LoginTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* 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.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 int m_serverPort; - private SMBServer m_server; - - [TestInitialize] - public void Initialize() - { - m_serverPort = 1000 + new Random().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, 5000); - - // 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, 5000); - - // 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, 5000); - 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, 5000); - - // Act - NTStatus status = client.Login("", "John", "Password"); - - // Assert - Assert.AreEqual(NTStatus.STATUS_LOGON_FAILURE, status); - } - } -} diff --git a/SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs b/SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs deleted file mode 100644 index a2602235..00000000 --- a/SMBLibrary.Tests/SMB2/NegotiateRequestParsingTests.cs +++ /dev/null @@ -1,79 +0,0 @@ -/* 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 deleted file mode 100644 index 6a4bf99a..00000000 --- a/SMBLibrary.Tests/SMB2/NegotiateResponseParsingTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* 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.Win32/Security/SSPIHelper.NTLM.cs b/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs deleted file mode 100644 index c43d8634..00000000 --- a/SMBLibrary.Win32/Security/SSPIHelper.NTLM.cs +++ /dev/null @@ -1,227 +0,0 @@ -/* Copyright (C) 2014-2017 Tal Aloni . All rights reserved. - * - * You can redistribute this program and/or modify it under the terms of - * the GNU Lesser Public License as published by the Free Software Foundation, - * either version 3 of the License, or (at your option) any later version. - */ -using System; -using System.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); - 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); - if (result != SEC_E_OK && result != SEC_I_CONTINUE_NEEDED) - { - if (result == SEC_E_INVALID_HANDLE) - { - throw new Exception("InitializeSecurityContext failed, Invalid handle"); - } - 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")); - } - } - 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")); - } - } - } - } -} From bb67d6de09dd45c3c3cf4b52d8b5f38415885583 Mon Sep 17 00:00:00 2001 From: Nadim El Makhoud Date: Tue, 27 Aug 2024 12:39:54 +0200 Subject: [PATCH 3/3] Updated version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 607192c5..1b3803f9 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/AArnott/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.7.1", + "version": "1.7.2", "assemblyVersion": { "precision": "revision" },