November 7, 2025
Going NTLM-less is all the rage in Windows environments these days. NTLM is considered a "legacy" authentication protocol, the use of which is highly discouraged. Microsoft has declared as much: that NTLM is dead (dreprecated) and that it will be gradually phased out and that organizations should seek to reduce and remove as much NTLM usage throughout their enterprise as possible, effective immediately.
But as with most things in life, that's easier said than done.
There are still hundreds of programs, applications and services - both Microsoft and third party - that still use or rely on NTLM, either because of poor design, or misconfiguration. No one - not even Microsoft themselves - has a complete inventory of all the things lurking out there that still rely on or use NTLM.
Windows introduced some Group Policy settings that make it easy for you to completely disable NTLM in your environment. If you're lucky enough to be doing a green field deployment, you should give it a try. Just be aware that you are likely to encounter authentication problems with various apps and services (and even native Windows components, foreshadowing what this blog post is about,) that may still be trying to use NTLM.
Those policies are:
As time goes on we see more Windows organizations attempting to disable NTLM in their environments. Then those organizations encounter something that doesn't work right after disabling NTLM. Then they call us to help fix it.
After one brave customer enabled the Group Policies mentioned above in their environments, one of the things they noticed that broke was their monitoring software. The software that remotely monitors their Windows machines for things like CPU usage, disk usage, etc., the kind of performance monitoring that takes place in every enterprise.
A very important and essential activity that absolutely must work.
To make this as simple as possible, assume a domain controller named DC1.lab.contoso.com, and two member servers, SERVER1.lab.contoso.com and SERVER2.lab.contoso.com.
The customer found that even outside of their monitoring software, just Performance Monitor (perfmon) by itself would not work when attempting to connect from SERVER1 to SERVER2:

This unusual Kerberos error can also be observed at the same time (if the relevant debug logging is turned on) :
Event 3, Security-Kerberos A Kerberos error message was received: on logon session Client Time: Server Time: 2:28:16.0000 11/6/2025 Z Error Code: 0x7 KDC_ERR_S_PRINCIPAL_UNKNOWN Extended Error: Client Realm: Client Name: Server Realm: LAB.CONTOSO.COM Server Name: krbtgt/\ Target Name: krbtgt/\@LAB.CONTOSO.COM Error Text: File: onecore\ds\security\protocols\kerberos\client2\kerbtick.cxx Line: 12f7 Error Data is in record data.
This is unusual because "krbtgt/\" has an extra backslash character after the name that shouldn't be there. "krbtgt" and "krbtgt/dc1.lab.contoso.com" for example, are normal and expected things for a Kerberos client to ask for. But "krbtgt/\" is not.
You'll see the same unusual Kerberos queries in a network trace. And as expected, the domain controller will be answering those queries with KDC_ERR_S_PRINCIPAL_UNKNOWN, because it's not a valid SPN to be asking for.
On the target server, we found events in the NTLM Operational log (again, assuming relevant logging is enabled,) that an attempt to use NTLM by the Remote Registry service was blocked:
Event 4002, NTLM NTLM server blocked: Incoming NTLM traffic to servers that is blocked Calling process PID: 3068 Calling process name: C:\Windows\System32\svchost.exe (remote registry service) Calling process LUID: 0x3E5 Calling process user identity: LOCAL SERVICE Calling process domain identity: NT AUTHORITY Mechanism OID: (NULL) NTLM authentication requests to this server have been blocked. If you want this server to allow NTLM authentication, set the security policy Network Security: Restrict NTLM: Incoming NTLM Traffic to Allow all.
Despite all this, we find that we did actually successfully acquire a Kerberos service ticket for cifs/SERVER2.lab.contoso.com, even though the connection failed! That's because there are two authentications that take place when you connect to a machine remotely through Perfmon. The first connection to SERVER2\IPC$ worked correctly with Kerberos. But the second connection to the \pipe\winreg (remote registry) RPC tower failed.
The reason the Remote Registry service is involved is because at the API level, performance counter data is accessed just like registry data.
Here is a call stack of perfmon on SERVER1 trying and failing to connect to the remote machine:
DbgID ThreadID User Kernel Position Approx. System Time COM-Initialized 1 dbc (0n3516) 0s 0s 0000380B:0D5B Thursday, November 6, 2025 1:38:02.630 AM ApartmentType_STA # Call Site Info 0 RPCRT4!Ndr64ClientNegotiateTransferSyntax+0x783 1 RPCRT4!Ndr64pClientSetupTransferSyntax+0x86d 2 RPCRT4!NdrpClientCall3+0x944 ncacn_np \\SERVER2:\PIPE\winreg 3 RPCRT4!NdrClientCall3+0xf0 4 ADVAPI32!PerflibV2EnumerateCounterSet+0x33 5 ADVAPI32!PerfEnumerateCounterSet+0x1f91b 6 pdh!PERF_MACHINE::EnumerateCounterSet+0x5a 7 pdh!PERF_MACHINE::ConnectMachine+0x18c 8 pdh!PERF_MACHINE::Connect+0x49 9 pdh!PdhConnectMachineW+0x6a a pdhui!PdhiLoadNewMachine+0x82 b pdhui!PdhiBrowseCtrDlg_MACHINE_COMBO+0x181 c pdhui!PdhiBrowseCtrDlg_WM_COMMAND+0xe6 d pdhui!BrowseCounterDlgProc+0xa5 e USER32!UserCallDlgProcCheckWow+0x14b f USER32!DefDlgProcWorker+0xcb 10 USER32!DefDlgProcW+0x36 11 USER32!UserCallWinProcCheckWow+0x319 12 USER32!CallWindowProcAorW+0x6e 13 USER32!CallWindowProcW+0x8e 14 MFC42u!_AfxActivationWndProc+0x9d 15 USER32!UserCallWinProcCheckWow+0x319 16 USER32!SendMessageWorker+0x23c 17 USER32!SendMessageW+0x10f 18 USER32!IsDialogMessageW+0x224 19 USER32!DialogBox2+0x170 1a USER32!InternalDialogBox+0x14b 1b USER32!DialogBoxIndirectParamAorW+0x58 1c USER32!DialogBoxParamW+0x7d 1d pdhui!PdhUiBrowseCountersExHW+0x67 1e sysmon!BrowseCounters+0xd2 1f sysmon!CSysmonControl::AddCounters+0xc2 20 sysmon!SysmonCtrlWndProc+0x769 21 USER32!UserCallWinProcCheckWow+0x319 22 USER32!SendMessageWorker+0x23c 23 USER32!SendMessageW+0x10f 24 Comctl32_7ffe51720000!CToolbar::TBOnLButtonUp+0x19c 25 Comctl32_7ffe51720000!CToolbar::ToolbarWndProc+0x4099c 26 Comctl32_7ffe51720000!CToolbar::s_ToolbarWndProc+0x54 27 USER32!UserCallWinProcCheckWow+0x319 28 USER32!CallWindowProcAorW+0x6e 29 USER32!CallWindowProcW+0x8e 2a Comctl32_7ffe51720000!CallNextSubclassProc+0xa8 2b Comctl32_7ffe51720000!DefSubclassProc+0x67 2c Comctl32_7ffe51720000!TTSubclassProc+0xc9 2d Comctl32_7ffe51720000!CallNextSubclassProc+0xa8 2e Comctl32_7ffe51720000!MasterSubclassProc+0xa7 2f USER32!UserCallWinProcCheckWow+0x319 30 USER32!DispatchMessageWorker+0x1d2 31 MFC42u!CWinThread::PumpMessage+0x72 32 mmc!CAMCApp::PumpMessage+0x46 33 MFC42u!CWinThread::Run+0x96 34 MFC42u!AfxWinMain+0xbc 35 mmc!__wmainCRTStartup+0x1dd 36 KERNEL32!BaseThreadInitThunk+0x10 37 ntdll!RtlUserThreadStart+0x2b
And here is an example call stack from lsass on SERVER1 while processing one of these "krbtgt/\" requests:
DbgID ThreadID User Kernel Position Approx. System Time 1 290 (0n656) 0s 0s 00000B5E:085C Thursday, November 6, 2025 1:04:04.004 AM # Call Site Info 0 kerberos!__KerbGetTgsTicket+0x2c38 1 kerberos!KerbGetTgsTicketEx+0x19c 2 kerberos!KerbGetTgsTicketWithBranchEx+0x129 3 kerberos!KerbGetServiceTicketInternal+0xeb7 4 kerberos!KerbGetServiceTicketEx+0xce Ticket: SERVER2@\ 5 kerberos!KerbGetServiceTicket+0x78 6 kerberos!SpInitLsaModeContext+0x2860 7 lsasrv!WLsaInitContext+0x2f6 8 lsasrv!NegBuildRequestToken+0x11c1 9 lsasrv!NegGenerateInitialToken+0x51 a lsasrv!NegInitLsaModeContext+0x2cc b lsasrv!WLsaInitContext+0x2f6 c lsasrv!SspiExProcessSecurityContext+0x82e d SspiSrv!SspirProcessSecurityContext+0x28d e RPCRT4!Invoke+0x73 f RPCRT4!Ndr64StubWorker+0xb79 10 RPCRT4!NdrServerCallAll+0x3c 11 RPCRT4!DispatchToStubInCNoAvrf+0x22 12 RPCRT4!RPC_INTERFACE::DispatchToStubWorker+0x1af 13 RPCRT4!RPC_INTERFACE::DispatchToStub+0xf8 14 RPCRT4!LRPC_SBINDING::DispatchToStub+0x2c 15 RPCRT4!LRPC_SCALL::DispatchRequest+0x33c 16 RPCRT4!LRPC_SCALL::QueueOrDispatchCall+0x131 17 RPCRT4!LRPC_SCALL::HandleRequest+0x837 Client: PID: 0x5b8 TID: 0xdbc 18 RPCRT4!LRPC_SASSOCIATION::HandleRequest+0x24d 19 RPCRT4!LRPC_ADDRESS::HandleRequest+0x181 1a RPCRT4!LRPC_ADDRESS::ProcessIO+0x897 1b RPCRT4!LrpcIoComplete+0xc9 1c ntdll!TppAlpcpExecuteCallback+0x280 1d ntdll!TppWorkerThread+0x448 1e KERNEL32!BaseThreadInitThunk+0x10 1f ntdll!RtlUserThreadStart+0x2b
The domain name is supplied as "\" instead of an actual domain name. So while a query for an SPN such as "krbtgt/lab.contoso.com" would be considered normal and expected, the domain is specified as just "\" so the query becomes "krbtgt/\".
There was initially an assumption that pdh.dll (performance data helper) was somehow forming this SPN incorrectly, however after TTD (Time Travel Debugging) review, we didn't find pdh.dll creating any SPNs at all. Perfmon simply passes exactly what the user has entered into that text box in the GUI (\\SERVER2) down to the underlying layers of RPC, SSPI, etc..
\\SERVER2 is not a valid SPN. Therefore it isn't valid for Kerberos. Therefore Kerberos fails, NTLM fallback is attempted, but that fails too because NTLM has been blocked by policy.
There is some code in the Kerberos client running in lsass that processes incoming target names and tries to make sense out of them. This code scans the string that was given to it - "\\SERVER2" in this case - from right to left looking for special characters such as @, /, \, etc.
When the code encounters its first backslash character, kerberos makes the assumption at that point we're dealing with an NT4-style name, such as CONTOSO\labuser. Therefore the kerb client assumes everything to the left of the rightmost backslash must represent the domain name. Even though in this case the only thing after the rightmost backslash is another backslash.
And so the domain name is assumed to be "\". Which of course we know is nonsense. Everything goes downhill after that.
So funny thing, now that we know about this logic, we can exploit it to our advantage. If we now go back to Perfmon and connect to \\SERVER2\ with an extra backslash at the end, Kerberos will work properly again, and Perfmon connects to the remote machine normally!
This has the stark implication that accessing remote performance counters in this fashion has been forcing people to use NTLM over Kerberos since day 1. For decades. And most people just never knew it until they completely disabled NTLM in their environment and exposed this problem..
This is... suboptimal behavior, and this naming syntax is treated inconsistently across the Windows ecosystem. But this is how the behavior has always been, for 25+ years. You must understand Windows was using the \\servername UNC format since before Windows even used Kerberos. And this misbehavior is now so deeply entrenched both inside of and outside of Windows, I'll be surprised if it's possible to make any change without breaking someone else.
For example, consider the PowerShell Get-Counter cmdlet, where using the "\\" UNC syntax is required.
The authentication team is not interested in implementing a fix for that very reason. They will just say "it is the application's responsibility to pass us a valid SPN if they want to use Kerberos." I can't really blame them, as I said, I'm sure any conceivable change they made would end up causing someone else to break.
I'll see if the Perfmon team will consider at least removing the "\\" prefix by default from the perfmon GUI. But again, we couldn't just stop supporting that syntax either. Imagine all the millions of people around the world who have been accessing remote perf counters in scripts, monitoring tools, etc., for decades, that would break if we stopped allowing that syntax.
All I know for sure is that the current behavior of "NTLM by default" cannot continue.