Added signature verification support for SMB2 client

This commit is contained in:
Christian Ristig 2025-05-09 09:41:05 +02:00
parent ef4508271c
commit fe40563d65
2 changed files with 358 additions and 9 deletions

View file

@ -0,0 +1,264 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using SMBLibrary.Client;
using System;
using System.Net;
using SMBLibrary.Server;
using SMBLibrary.Authentication.GSSAPI;
using SMBLibrary.Client.Authentication;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace SMBLibrary.Tests.Client
{
[TestClass]
public class SMB2SigningTests
{
private TcpRelay m_tcpRelay;
private SMBServer m_smbServer;
private SMB2Client client;
private static readonly byte[] m_sessionKey = new byte[16];
private static readonly byte[] m_fakeIdentifier = new byte[] { 1, 2, 3, 4, 5, 6 };
[TestInitialize]
public void Initialize()
{
new Random().NextBytes(m_sessionKey);
SMBShareCollection shares = new SMBShareCollection();
GSSProvider securityProvider = new GSSProvider(new FakeAuthenticationProvider());
m_smbServer = new SMBServer(shares, securityProvider);
m_smbServer.Start(IPAddress.Any, SMBTransportType.NetBiosOverTCP, 30002, false, true, true, null);
m_tcpRelay = new TcpRelay();
Task.Run(m_tcpRelay.Start);
client = new SMB2Client();
client.SigningRequired = true;
m_tcpRelay.SignatureManipulationEnabled = true;
Assert.IsTrue(client.Connect(IPAddress.Loopback, SMBTransportType.DirectTCPTransport, 30001, 3000));
}
[TestCleanup]
public void CleanUp()
{
client?.Disconnect();
m_tcpRelay.Stop();
m_smbServer.Stop();
}
[TestMethod]
public void When_SignatureIsValid_ShouldNotBeInvalid()
{
client.DisconnectOnInvalidSignature = false;
m_tcpRelay.SignatureManipulationEnabled = false;
Assert.AreEqual(NTStatus.STATUS_SUCCESS, client.Login(new FakeAuthenticationClient()));
client.TreeConnect("test", out NTStatus status);
Assert.AreEqual(NTStatus.STATUS_OBJECT_PATH_NOT_FOUND, status);
Assert.IsTrue(client.IsConnected);
}
[TestMethod]
public void When_SigningEnabledAndSignatureIsInvalid_LoginShouldFail()
{
client.DisconnectOnInvalidSignature = false;
m_tcpRelay.SignatureManipulationEnabled = true;
Assert.AreEqual(NTStatus.STATUS_INVALID_SMB, client.Login(new FakeAuthenticationClient()));
Assert.IsTrue(client.IsConnected);
}
[TestMethod]
public void When_SigningEnabledAndSignatureIsInvalid_LoginShouldDisconnect()
{
client.DisconnectOnInvalidSignature = true;
m_tcpRelay.SignatureManipulationEnabled = true;
Assert.AreEqual(NTStatus.STATUS_INVALID_SMB, client.Login(new FakeAuthenticationClient()));
Assert.IsFalse(client.IsConnected);
}
[TestMethod]
public void When_SigningEnabledAndSignatureIsInvalid_CommandShouldFail()
{
m_tcpRelay.SignatureManipulationEnabled = false;
client.DisconnectOnInvalidSignature = false;
Assert.AreEqual(NTStatus.STATUS_SUCCESS, client.Login(new FakeAuthenticationClient()));
m_tcpRelay.SignatureManipulationEnabled = true;
client.TreeConnect("test", out NTStatus status);
Assert.AreEqual(NTStatus.STATUS_INVALID_SMB, status);
Assert.IsTrue(client.IsConnected);
}
[TestMethod]
public void When_SigningEnabledAndSignatureIsInvalid_CommandShouldDisconnect()
{
m_tcpRelay.SignatureManipulationEnabled = false;
client.DisconnectOnInvalidSignature = true;
Assert.AreEqual(NTStatus.STATUS_SUCCESS, client.Login(new FakeAuthenticationClient()));
m_tcpRelay.SignatureManipulationEnabled = true;
client.TreeConnect("test", out NTStatus status);
Assert.AreEqual(NTStatus.STATUS_INVALID_SMB, status);
Assert.IsFalse(client.IsConnected);
}
private class FakeAuthenticationClient : IAuthenticationClient
{
public byte[] GetSessionKey()
{
return m_sessionKey;
}
public byte[] InitializeSecurityContext(byte[] securityBlob)
{
SimpleProtectedNegotiationTokenInit initToken = new SimpleProtectedNegotiationTokenInit();
initToken.MechanismTypeList = new List<byte[]>() { m_fakeIdentifier };
return initToken.GetBytes(true);
}
}
private class FakeAuthenticationProvider : IGSSMechanism
{
public byte[] Identifier => m_fakeIdentifier;
public NTStatus AcceptSecurityContext(ref object context, byte[] inputToken, out byte[] outputToken)
{
outputToken = null;
return NTStatus.STATUS_SUCCESS;
}
public bool DeleteSecurityContext(ref object context)
{
return true;
}
public object GetContextAttribute(object context, GSSAttributeName attributeName)
{
switch (attributeName)
{
case GSSAttributeName.SessionKey:
return m_sessionKey;
}
return null;
}
}
private class TcpRelay
{
private TcpListener m_relay;
private TcpClient m_localClient;
private TcpClient m_remoteClient;
private Thread m_clientToServer;
private Thread m_serverToClient;
public bool SignatureManipulationEnabled { get; set; } = false;
public void Start()
{
m_remoteClient = new TcpClient();
m_remoteClient.Connect(IPAddress.Loopback, 30002);
m_relay = new TcpListener(IPAddress.Any, 30001);
m_relay.Start();
m_localClient = m_relay.AcceptTcpClient();
NetworkStream localStream = m_localClient.GetStream();
NetworkStream remoteStream = m_remoteClient.GetStream();
m_clientToServer = new Thread(() => ForwardData(localStream, remoteStream));
m_serverToClient = new Thread(() => ForwardData(remoteStream, localStream));
m_clientToServer.Start();
m_serverToClient.Start();
}
public void Stop()
{
m_localClient?.Dispose();
m_remoteClient?.Dispose();
m_relay?.Stop();
m_clientToServer.Join();
m_serverToClient.Join();
}
private void ForwardData(NetworkStream input, NetworkStream output)
{
byte[] buffer = new byte[4096];
int bytesRead;
try
{
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
{
if (TryGetSmb2HeaderOffset(buffer, bytesRead, out int smbOffset) && IsSigned(buffer, smbOffset) && SignatureManipulationEnabled)
{
ManipulateSignature(buffer, smbOffset);
}
output.Write(buffer, 0, bytesRead);
}
}
catch
{
// Likely disconnected
}
}
private static bool TryGetSmb2HeaderOffset(byte[] data, int length, out int offset)
{
offset = -1;
// NetBIOS Session Header?
if (length >= 68 && data[0] == 0x00)
{
if (data[4] == 0xFE && data[5] == (byte)'S' && data[6] == (byte)'M' && data[7] == (byte)'B')
{
offset = 4;
return true;
}
}
else if (length >= 64 && data[0] == 0xFE && data[1] == (byte)'S' && data[2] == (byte)'M' && data[3] == (byte)'B')
{
offset = 0;
return true;
}
return false;
}
private static bool IsSigned(byte[] data, int offset)
{
uint flags = BitConverter.ToUInt32(data, offset + 16);
return (flags & 0x00000008) != 0;
}
private static void ManipulateSignature(byte[] data, int offset)
{
Random rand = new Random();
// Change a random signature byte
int idx = rand.Next(48, 48 + 16);
// Generate a new random byte and make sure it is different from the current value
int byteValue;
do
{
byteValue = rand.Next(0, 256);
}
while (data[offset + idx] == byteValue);
data[offset + idx] = (byte) byteValue;
}
}
}
}

View file

@ -1,5 +1,5 @@
/* Copyright (C) 2017-2025 Tal Aloni <tal.aloni.il@gmail.com>. 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.
@ -59,10 +59,15 @@ namespace SMBLibrary.Client
private byte[] m_preauthIntegrityHashValue; // SMB 3.1.1
private ushort m_availableCredits = 1;
private byte[] m_sessionSetupResponseMessage;
public SMB2Client()
{
}
public bool SigningRequired { get; set; } = false;
public bool DisconnectOnInvalidSignature { get; set; } = false;
/// <param name="serverName">
/// 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.
@ -197,7 +202,7 @@ namespace SMBLibrary.Client
private bool NegotiateDialect()
{
NegotiateRequest request = new NegotiateRequest();
request.SecurityMode = SecurityMode.SigningEnabled;
request.SecurityMode = SigningRequired ? SecurityMode.SigningRequired | SecurityMode.SigningEnabled : SecurityMode.SigningEnabled;
request.Capabilities = Capabilities.Encryption;
request.ClientGuid = Guid.NewGuid();
request.ClientStartTime = DateTime.Now;
@ -219,7 +224,7 @@ namespace SMBLibrary.Client
m_dialect = response.DialectRevision;
// [MS-SMB2] 3.3.5.7 If Connection.Dialect is "3.1.1" and Session.IsAnonymous and Session.IsGuest
// are set to FALSE and the request is not signed or not encrypted, then the server MUST disconnect the connection.
m_signingRequired = (response.SecurityMode & SecurityMode.SigningRequired) > 0 ||
m_signingRequired = SigningRequired || (response.SecurityMode & SecurityMode.SigningRequired) > 0 ||
response.DialectRevision == SMB2Dialect.SMB311;
m_maxTransactSize = Math.Min(response.MaxTransactSize, ClientMaxTransactSize);
m_maxReadSize = Math.Min(response.MaxReadSize, ClientMaxReadSize);
@ -256,7 +261,7 @@ namespace SMBLibrary.Client
}
SessionSetupRequest request = new SessionSetupRequest();
request.SecurityMode = SecurityMode.SigningEnabled;
request.SecurityMode = SigningRequired ? SecurityMode.SigningRequired | SecurityMode.SigningEnabled : SecurityMode.SigningEnabled;
request.SecurityBuffer = negotiateMessage;
TrySendCommand(request);
SMB2Command response = WaitForCommand(request.MessageID);
@ -270,7 +275,7 @@ namespace SMBLibrary.Client
m_sessionID = response.Header.SessionID;
request = new SessionSetupRequest();
request.SecurityMode = SecurityMode.SigningEnabled;
request.SecurityMode = SigningRequired ? SecurityMode.SigningRequired | SecurityMode.SigningEnabled : SecurityMode.SigningEnabled;
request.SecurityBuffer = authenticateMessage;
TrySendCommand(request);
response = WaitForCommand(request.MessageID);
@ -301,6 +306,26 @@ namespace SMBLibrary.Client
m_encryptionKey = SMB2Cryptography.GenerateClientEncryptionKey(m_sessionKey, m_dialect, m_preauthIntegrityHashValue);
m_decryptionKey = SMB2Cryptography.GenerateClientDecryptionKey(m_sessionKey, m_dialect, m_preauthIntegrityHashValue);
}
// [MS-SMB2] 3.2.5.1.3 Verifying the Signature
// If signature verification fails, the client MUST discard the received message.
// The client MAY also choose to disconnect the connection.
try
{
if (!VerifySignature(m_sessionSetupResponseMessage, response))
{
m_isLoggedIn = false;
if (DisconnectOnInvalidSignature)
{
Disconnect();
}
return NTStatus.STATUS_INVALID_SMB;
}
}
finally
{
m_sessionSetupResponseMessage = null;
}
}
return response.Header.Status;
}
@ -526,14 +551,42 @@ namespace SMBLibrary.Client
// and the client MUST NOT attempt to locate the request, but instead process it as follows:
// If the command field in the SMB2 header is SMB2 OPLOCK_BREAK, it MUST be processed as specified in 3.2.5.19.
// Otherwise, the response MUST be discarded as invalid.
if (command.Header.MessageID != 0xFFFFFFFFFFFFFFFF || command.Header.Command == SMB2CommandName.OplockBreak)
if (command.Header.MessageID == 0xFFFFFFFFFFFFFFFF && command.Header.Command != SMB2CommandName.OplockBreak)
{
lock (m_incomingQueueLock)
return;
}
// [MS-SMB2] 3.2.5.1.3 Verifying the Signature
// If signature verification fails, the client MUST discard the received message.
// The client MAY also choose to disconnect the connection.
if (m_isLoggedIn)
{
// This check covers all messages except the final session setup message which is treated seperately
if (!VerifySignature(messageBytes, command))
{
m_incomingQueue.Add(command);
m_incomingQueueEventHandle.Set();
if (DisconnectOnInvalidSignature)
{
m_isLoggedIn = false;
Disconnect();
}
return;
}
}
else if ((command.Header.Command == SMB2CommandName.SessionSetup) &&
(command.Header.Status == NTStatus.STATUS_SUCCESS))
{
// The final session setup message already has a valid signature, but the session/signing key have not been set yet (m_isLoggedIn is false).
// The message is evaluated within the Login() method, which will then determine the keys and set m_isLoggedIn to true.
// Nevertheless, the signature must be verified. So the raw message is preserved here and the signature is verified later in the
// Login() method, after the keys have been set.
m_sessionSetupResponseMessage = messageBytes;
}
lock (m_incomingQueueLock)
{
m_incomingQueue.Add(command);
m_incomingQueueEventHandle.Set();
}
}
else if ((packet is PositiveSessionResponsePacket || packet is NegativeSessionResponsePacket) && m_transport == SMBTransportType.NetBiosOverTCP)
{
@ -552,6 +605,38 @@ namespace SMBLibrary.Client
}
}
// [MS-SMB2] 3.2.5.1.3 Verifying the Signature
private bool VerifySignature(byte[] messageBytes, SMB2Command command)
{
if (!m_signingRequired)
{
// Client and server require no signing at all
return true;
}
if (messageBytes == null)
{
throw new ArgumentNullException("messageBytes");
}
bool isUnspecifiedMessageId = command.Header.MessageID == 0xFFFFFFFFFFFFFFFF;
bool isPendingMessage = command.Header.Status == NTStatus.STATUS_PENDING;
if (m_encryptSessionData || isUnspecifiedMessageId || isPendingMessage)
{
// Signing is not required
return true;
}
byte[] key = (m_dialect >= SMB2Dialect.SMB300) ? m_signingKey : m_sessionKey;
Array.Clear(messageBytes, SMB2Header.SignatureOffset, command.Header.Signature.Length);
byte[] signature = SMB2Cryptography.CalculateSignature(key, m_dialect, messageBytes, 0, messageBytes.Length);
signature = ByteReader.ReadBytes(signature, 0, command.Header.Signature.Length);
return ByteUtils.AreByteArraysEqual(signature, command.Header.Signature);
}
internal SMB2Command WaitForCommand(ulong messageID)
{
return WaitForCommand(messageID, out bool _);