Skip to content

Commit 48d8ee7

Browse files
committed
feat(smbserver): add SMB 3.1.1 support with pre-auth integrity hash and KBKDF signing
1 parent 9a5621d commit 48d8ee7

1 file changed

Lines changed: 144 additions & 12 deletions

File tree

‎impacket/smbserver.py‎

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
from pyasn1.codec.der import encoder, decoder
5252

5353
# For signing
54-
from impacket import smb, nmb, ntlm, uuid
54+
from Cryptodome.Hash import SHA512
55+
from impacket import smb, nmb, ntlm, uuid, crypto
5556
from impacket import smb3structs as smb2
5657
from impacket.spnego import SPNEGO_NegTokenInit, TypesMech, MechTypes, SPNEGO_NegTokenResp, ASN1_AID, \
5758
ASN1_SUPPORTED_MECH
@@ -72,6 +73,42 @@
7273
STATUS_SMB_BAD_UID = 0x005B0002
7374
STATUS_SMB_BAD_TID = 0x00050002
7475

76+
_SUPPORTED_DIALECTS = (smb2.SMB2_DIALECT_311, smb2.SMB2_DIALECT_21, smb2.SMB2_DIALECT_002)
77+
_PREAUTH_HASH_ZERO = b'\x00' * 64
78+
79+
80+
def _preauth_hash_update(current: bytes, data: bytes) -> bytes:
81+
"""SHA-512(current || data), one step of the SMB 3.1.1 pre-auth integrity hash chain.
82+
83+
MS-SMB2 §3.3.5.4 (negotiate) and §3.3.5.5.3 (session setup) describe how the server
84+
iteratively hashes request and response bytes into the connection/session pre-auth value:
85+
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/b39f253e-4963-40df-8dff-2f9040ebbeb1
86+
"""
87+
h = SHA512.new()
88+
h.update(current)
89+
h.update(data)
90+
return h.digest()
91+
92+
93+
def _build_smb311_preauth_context() -> bytes:
94+
"""Build a serialised SMB2_PREAUTH_INTEGRITY_CAPABILITIES negotiate context (SHA-512, no salt).
95+
96+
MS-SMB2 §2.2.3.1.1, SMB2_PREAUTH_INTEGRITY_CAPABILITIES structure definition:
97+
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5a07bd66-4734-4af8-abcf-5a44ff7ee0e5
98+
"""
99+
preauth = smb2.SMB2PreAuthIntegrityCapabilities()
100+
preauth['HashAlgorithmCount'] = 1
101+
preauth['SaltLength'] = 0
102+
preauth['HashAlgorithms'] = struct.pack('<H', 0x0001)
103+
preauth['Salt'] = b''
104+
ctx = smb2.SMB2NegotiateContext()
105+
ctx['ContextType'] = smb2.SMB2_PREAUTH_INTEGRITY_CAPABILITIES
106+
ctx_data = preauth.getData()
107+
ctx['DataLength'] = len(ctx_data)
108+
ctx['Reserved'] = 0
109+
ctx['Data'] = ctx_data
110+
return ctx.getData()
111+
75112

76113
# Utility functions
77114
# and general functions.
@@ -2834,7 +2871,7 @@ def default(connId, smbServer, SMBCommand, recvPacket):
28342871

28352872
class SMB2Commands:
28362873
@staticmethod
2837-
def smb2Negotiate(connId, smbServer, recvPacket, isSMB1=False):
2874+
def smbNegotiate(connId, smbServer, recvPacket, isSMB1=False):
28382875
connData = smbServer.getConnectionData(connId, checkStatus=False)
28392876

28402877
respPacket = smb2.SMB2Packet()
@@ -2857,13 +2894,21 @@ def smb2Negotiate(connId, smbServer, recvPacket, isSMB1=False):
28572894
SMBCommand = smb.SMBCommand(recvPacket['Data'][0])
28582895

28592896
dialects = SMBCommand['Data'].split(b'\x02')
2860-
if b'SMB 2.002\x00' in dialects or b'SMB 2.???\x00' in dialects:
2897+
if b'SMB 2.???\x00' in dialects:
2898+
respSMBCommand['DialectRevision'] = smb2.SMB2_DIALECT_WILDCARD
2899+
connData['Dialect'] = smb2.SMB2_DIALECT_WILDCARD
2900+
elif b'SMB 2.002\x00' in dialects:
28612901
respSMBCommand['DialectRevision'] = smb2.SMB2_DIALECT_002
2902+
connData['Dialect'] = smb2.SMB2_DIALECT_002
28622903
else:
2863-
# Client does not support SMB2 fallbacking
28642904
raise Exception('SMB2 not supported, fallbacking')
28652905
else:
2866-
respSMBCommand['DialectRevision'] = smb2.SMB2_DIALECT_002
2906+
negRequest = smb2.SMB2Negotiate(recvPacket['Data'])
2907+
offered = set(negRequest['Dialects'])
2908+
selected = next((d for d in _SUPPORTED_DIALECTS if d in offered), smb2.SMB2_DIALECT_002)
2909+
respSMBCommand['DialectRevision'] = selected
2910+
connData['Dialect'] = selected
2911+
28672912
respSMBCommand['ServerGuid'] = b'A' * 16
28682913
respSMBCommand['Capabilities'] = 0
28692914
respSMBCommand['MaxTransactSize'] = 65536
@@ -2886,12 +2931,40 @@ def smb2Negotiate(connId, smbServer, recvPacket, isSMB1=False):
28862931
respSMBCommand['Buffer'] = blob.getData()
28872932
respSMBCommand['SecurityBufferLength'] = len(respSMBCommand['Buffer'])
28882933

2889-
respPacket['Data'] = respSMBCommand
2934+
if connData.get('Dialect') == smb2.SMB2_DIALECT_311:
2935+
SMB2Commands._smb311_negotiate(connData, recvPacket, respSMBCommand, respPacket)
2936+
else:
2937+
respPacket['Data'] = respSMBCommand
28902938

28912939
smbServer.setConnectionData(connId, connData)
28922940

28932941
return None, [respPacket], STATUS_SUCCESS
28942942

2943+
@staticmethod
2944+
def _smb311_negotiate(connData: dict, recvPacket, respSMBCommand, respPacket) -> None:
2945+
"""Handle SMB 3.1.1 negotiate: initialise pre-auth hash chain and append mandatory negotiate context.
2946+
2947+
MS-SMB2 §3.3.5.4, Receiving an SMB2 NEGOTIATE Request (3.1.1 NegotiateContextList processing is inline):
2948+
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/b39f253e-4963-40df-8dff-2f9040ebbeb1
2949+
"""
2950+
connData['PreauthIntegrityHashValue'] = _preauth_hash_update(
2951+
_PREAUTH_HASH_ZERO, recvPacket.rawData)
2952+
2953+
# Encryption context omitted intentionally: advertising it would cause the client to enable
2954+
# session encryption, which this server does not implement.
2955+
ctx_bytes = _build_smb311_preauth_context()
2956+
sec_buf_len = len(respSMBCommand['Buffer'])
2957+
ctx_pad = (8 - sec_buf_len % 8) % 8
2958+
respSMBCommand['NegotiateContextCount'] = 1
2959+
respSMBCommand['NegotiateContextOffset'] = 0x80 + sec_buf_len + ctx_pad
2960+
respSMBCommand['Padding'] = b'\x00' * ctx_pad
2961+
respSMBCommand['NegotiateContextList'] = ctx_bytes
2962+
2963+
respPacket['Data'] = respSMBCommand
2964+
2965+
connData['PreauthIntegrityHashValue'] = _preauth_hash_update(
2966+
connData['PreauthIntegrityHashValue'], respPacket.getData())
2967+
28952968
@staticmethod
28962969
def _kerberos_auth(token, connData, smbServer):
28972970
try:
@@ -3247,12 +3320,54 @@ def smb2SessionSetup(connId, smbServer, recvPacket):
32473320
else:
32483321
smbServer.log("Unknown or unsupported security blob type", logging.ERROR, connData=connData)
32493322
return [SMB2Commands.generic_negTokenResp()], None, STATUS_MORE_PROCESSING_REQUIRED
3250-
3323+
3324+
is_311 = (connData.get('Dialect') == smb2.SMB2_DIALECT_311)
3325+
3326+
if is_311:
3327+
# MS-SMB2 §3.3.5.5.3, Handling GSS-API Authentication:
3328+
# the session pre-auth hash is forked from the connection hash at the first
3329+
# SESSION_SETUP round and updated with each request/response pair.
3330+
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ed93f06-a1d2-4837-8954-fa8b833c2654
3331+
is_ntlm_round1 = (token[:8] == b'NTLMSSP\x00' and len(token) >= 12
3332+
and struct.unpack('<L', token[8:12])[0] == 0x01)
3333+
if is_ntlm_round1 or 'SessionPreauthIntegrityHashValue' not in connData:
3334+
connData['SessionPreauthIntegrityHashValue'] = connData.get('PreauthIntegrityHashValue', _PREAUTH_HASH_ZERO)
3335+
connData['SessionPreauthIntegrityHashValue'] = _preauth_hash_update(
3336+
connData['SessionPreauthIntegrityHashValue'], recvPacket.rawData)
3337+
32513338
if authType in [TypesMech['MS KRB5 - Microsoft Kerberos 5'], TypesMech['KRB5 - Kerberos 5'], TypesMech['KRB5 - Kerberos 5 - User to User']]:
32523339
respSMBCommand, errorCode = SMB2Commands._kerberos_auth(token, connData, smbServer)
32533340
elif authType == TypesMech['NTLMSSP - Microsoft NTLM Security Support Provider']:
32543341
respSMBCommand, errorCode = SMB2Commands._ntlm_auth(token, connData, smbServer, rawNTLM)
32553342

3343+
if is_311:
3344+
if errorCode == STATUS_MORE_PROCESSING_REQUIRED:
3345+
resp_pkt = smb2.SMB2Packet()
3346+
resp_pkt['Flags'] = smb2.SMB2_FLAGS_SERVER_TO_REDIR
3347+
resp_pkt['Status'] = errorCode
3348+
resp_pkt['CreditRequestResponse'] = recvPacket['CreditRequestResponse']
3349+
resp_pkt['Command'] = recvPacket['Command']
3350+
resp_pkt['CreditCharge'] = recvPacket['CreditCharge']
3351+
resp_pkt['Reserved'] = recvPacket['Reserved']
3352+
resp_pkt['SessionID'] = connData['Uid']
3353+
resp_pkt['MessageID'] = recvPacket['MessageID']
3354+
resp_pkt['TreeID'] = recvPacket['TreeID']
3355+
resp_pkt['Data'] = respSMBCommand.getData()
3356+
connData['SessionPreauthIntegrityHashValue'] = _preauth_hash_update(
3357+
connData['SessionPreauthIntegrityHashValue'], resp_pkt.getData())
3358+
elif errorCode == STATUS_SUCCESS:
3359+
# Derive the SMB 3.1.1 signing key via SP 800-108 counter-mode KBKDF.
3360+
# Label=b"SMBSigningKey\x00", Context=SessionPreauthIntegrityHashValue, L=128 bits.
3361+
# MS-SMB2 §3.1.4.2, Generating Cryptographic Keys:
3362+
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/da4e579e-02ce-4e27-bbce-3fc816a3ff92
3363+
# NIST SP 800-108r1, KDF in Counter Mode (referenced from §3.1.4.2 as [SP800-108]):
3364+
# https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf
3365+
session_key = connData.get('SigningSessionKey', b'')
3366+
if session_key:
3367+
connData['SigningSessionKey'] = crypto.KDF_CounterMode(
3368+
session_key, b'SMBSigningKey\x00',
3369+
connData['SessionPreauthIntegrityHashValue'], 128)
3370+
32563371
# From now on, the client can ask for other commands
32573372
connData['Authenticated'] = True
32583373
# For now, just switching to nobody
@@ -3324,7 +3439,10 @@ def smb2TreeConnect(connId, smbServer, recvPacket):
33243439

33253440
# Sign the packet if needed
33263441
if connData['SignatureEnabled']:
3327-
smbServer.signSMBv2(respPacket, connData['SigningSessionKey'])
3442+
if connData.get('Dialect', smb2.SMB2_DIALECT_002) >= smb2.SMB2_DIALECT_30:
3443+
smbServer.signSMBv3(respPacket, connData['SigningSessionKey'])
3444+
else:
3445+
smbServer.signSMBv2(respPacket, connData['SigningSessionKey'])
33283446
smbServer.setConnectionData(connId, connData)
33293447

33303448
return None, [respPacket], errorCode
@@ -4143,7 +4261,7 @@ def fsctlValidateNegotiateInfo(connId, smbServer, ioctlRequest):
41434261
validateNegotiateInfoResponse['Capabilities'] = 0
41444262
validateNegotiateInfoResponse['Guid'] = b'A' * 16
41454263
validateNegotiateInfoResponse['SecurityMode'] = 1
4146-
validateNegotiateInfoResponse['Dialect'] = smb2.SMB2_DIALECT_002
4264+
validateNegotiateInfoResponse['Dialect'] = connData.get('Dialect', smb2.SMB2_DIALECT_002)
41474265

41484266
smbServer.setConnectionData(connId, connData)
41494267
return validateNegotiateInfoResponse.getData(), errorCode
@@ -4350,7 +4468,7 @@ def __init__(self, server_address, handler_class=SMBSERVERHandler, config_parser
43504468
}
43514469

43524470
self.__smb2Commands = {
4353-
smb2.SMB2_NEGOTIATE: self.__smb2CommandsHandler.smb2Negotiate,
4471+
smb2.SMB2_NEGOTIATE: self.__smb2CommandsHandler.smbNegotiate,
43544472
smb2.SMB2_SESSION_SETUP: self.__smb2CommandsHandler.smb2SessionSetup,
43554473
smb2.SMB2_LOGOFF: self.__smb2CommandsHandler.smb2Logoff,
43564474
smb2.SMB2_TREE_CONNECT: self.__smb2CommandsHandler.smb2TreeConnect,
@@ -4659,7 +4777,18 @@ def signSMBv2(self, packet, signingSessionKey, padLength=0):
46594777
packetData = packet.getData() + b'\x00' * padLength
46604778
signature = hmac.new(signingSessionKey, packetData, hashlib.sha256).digest()
46614779
packet['Signature'] = signature[:16]
4662-
# print "%s" % packet['Signature'].encode('hex')
4780+
4781+
def signSMBv3(self, packet, signingKey, padLength=0):
4782+
"""Sign an SMB 3.x packet with AES-CMAC using the derived signing key.
4783+
4784+
MS-SMB2 §3.1.4.1, Signing an Outgoing Message (SMB 3.x uses AES-CMAC per RFC 4493):
4785+
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/a3e9ea1e-53c8-4cff-94bd-d98fb20417c0
4786+
"""
4787+
packet['Signature'] = b'\x00' * 16
4788+
packet['Flags'] |= smb2.SMB2_FLAGS_SIGNED
4789+
packetData = packet.getData() + b'\x00' * padLength
4790+
signature = crypto.AES_CMAC(signingKey, packetData, len(packetData))
4791+
packet['Signature'] = signature[:16]
46634792

46644793
def processRequest(self, connId, data):
46654794

@@ -4890,7 +5019,10 @@ def processRequest(self, connId, data):
48905019
packet['NextCommand'] = len(packet) + padLen
48915020

48925021
if connData['SignatureEnabled']:
4893-
self.signSMBv2(packet, connData['SigningSessionKey'], padLength=padLen)
5022+
if connData.get('Dialect', smb2.SMB2_DIALECT_002) >= smb2.SMB2_DIALECT_30:
5023+
self.signSMBv3(packet, connData['SigningSessionKey'], padLength=padLen)
5024+
else:
5025+
self.signSMBv2(packet, connData['SigningSessionKey'], padLength=padLen)
48945026

48955027
if hasattr(packet, 'getData'):
48965028
finalData.append(packet.getData() + padLen * b'\x00')

0 commit comments

Comments
 (0)