Привет, мир! Недавно я решил пополнить свои знания протоколов NTLM'ом; и, к большому сожалению, стоящих материалов, которые бы подробно и полно описывали работу NTLM, я не нашел (есть лишь пара годных статей на английском языке, но и они, на мой взгляд, не дают нужного уровня глубины). Потому я решил написать статью, которая бы в подробностях рассказала о том, как работает данный протокол и удовлетворила бы даже самого душного нерда, каким автор и является.
В данной статье мы рассмотрим как в теории и на практике работает NTLM и какие в него включены меры безопасности, а в следующей статье (с выходом которой я постараюсь не временить) будут рассмотрены вариации атаки NTLM-Relay, которые помогут закрепить понимание данного протокола и составить более целостную картину.
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⚠WARNING⚠
Если это ваш первый материал по NTLM, то, вероятно, понять, что я тут написал, будет трудновато с первого раза. Так что, если что-то не понимаете – перечитывайте или спокойно идите дальше, а затем возвращайтесь к непонятой части. Ибо для того, чтобы понять какие-то детали, нужно сначало охватить всю картину целиком.
Общая идея
NTLM – протокол, который позволяет аутентифицировать пользователя для доступа к какому-либо сервису. Напомним, что аутентификация – процесс, при котором пользователь доказывает, что он тот, за кого себя выдает. В данном случае для доказательства будет использоваться знание пароля пользователя. Но пользователь не будет напрямую отправлять свой пароль. Вместо этого происходит следующее:
Сервер отправляет пользователю случайную строку.
Пользователь, получив эту строку, вычисляет хэш от этой строки, используя в качестве ключа свой пароль, и отправляет её обратно серверу.
Сервер, также вычисляет хэш с помощью пароля пользователя, копия которого у него хранится, и сравнивает её с полученной от пользователя.
Если обе строки совпадают, то пользователь аутентифицирован, иначе – неаутентифицирован.

Существует две версии NTLM: NTLMv1 и NTLMv2. Они отличаются лишь способом подсчета хэша и механизмами безопасности. NTLMv1 является небезопасным протоколом, недостатки которого закрывает NTLMv2. Сначала мы разберем актуальную версию NTLMv2, а затем ради интереса посмотрим на NTLMv1.
NTLM сам по себе не является самостоятельным протоколом: он не инкапсулируется в полезную нагрузку нижележащего протокола, не имеет своего места в сетевом стеке. Напротив, NTLM-сообщения включаются в другие протколы, которым требуется услуга аутентификации. Такими протоколами являются, например, SMB, LDAP и HTTP. Данные протоколы сами определяют, как и куда они будут помещать NTLM-сообщения; HTTP, к примеру, использует для этого заголовок Authorization.
NTLMSSP
NTLM реализуется с помощью API, которое возвращает NTLM-сообщения в виде массива байтов. Модуль проткола, использующего NTLM, просто вызывает это API, передавая в него нужные параметры, и полученный массив байт просто кладет в определенное поле своего протокола. Далее, код протокола на стороне извлекает NTLM-сообщение и "скармливает" его тому же API.
В Windows NTLM реализован с помощью Security Support Provider Interface (SSPI) – это набор SSP, которые предназначены для работы с протоколами аутентификации, например, NTLM или Kerberos. SSP – это отдельный набор DLL-файлов, которые используются для реализации какого-то конкретного протокола. В Windows реализацией NTLM является NTLMSSP. NTLMSSP использует три функции: AcquireCredentialsHandle, InitializeSecurityContext и AcceptSecurityContext.
Функция AcquireCredentialsHandle используется для того, чтобы получить хэндл (указатель на место в памяти, где начинаются какие-то данные. Тоже самое что и дескриптор в Linux) на учетные данные пользователя.
SECURITY_STATUS SEC_Entry AcquireCredentialsHandle( In SEC_CHAR pszPrincipal, In SEC_CHAR pszPackage, In ULONG fCredentialUse, In PLUID pvLogonID, In PVOID pAuthData, In SEC_GET_KEY_FN pGetKeyFn, In PVOID pvGetKeyArgument, Out PCredHandle phCredential, Out PTimeStamp ptsExpiry );
Функция InitializeSecurityContext(), получая хэндл на учетные данные пользователя, формирует контекст (security context). Контекст – это объект, в котором хранятся данные, необходимые для поддержания сессии, например, идентификат��ры сторон, ключи сессии, выбранные алгоритмы/шифры, флаги защиты. Данная функция возвращает NEGOTIATE-токен (тот самый массив байт, который представляет NTLM-сообщение), который потом нужно будет передать серверу.
SECURITY_STATUS SEC_Entry InitializeSecurityContext( Inopt_ PCredHandle phCredential, Inopt_ PCtxtHandle phContext, In SEC_CHAR *pszTargetName, In ULONG fContextReq, In ULONG Reserved1, In ULONG TargetDataRep, Inopt_ PSecBufferDesc pInput, In ULONG Reserved2, Inoutopt_ PCtxtHandle phNewContext, Inoutopt_ PSecBufferDesc pOutput, Out PULONG pfContextAttr, Outopt_ PTimeStamp ptsExpiry );
Функция AcceptSecurityContext на стороне сервера принимает токен, отправленный пользователем, и формирует контекст на стороне сервера. Данная функция возвращает CHALLENGE-токен, в котором содержится challenge, и который надо передать пользователю.
KSECDDDECLSPEC SECURITY_STATUS SEC_ENTRY AcceptSecurityContext( [in, optional] PCredHandle phCredential, [in, optional] PCtxtHandle phContext, [in, optional] PSecBufferDesc pInput, [in] unsigned long fContextReq, [in] unsigned long TargetDataRep, [in, out, optional] PCtxtHandle phNewContext, [in, out, optional] PSecBufferDesc pOutput, [out] unsigned long *pfContextAttr, [out, optional] PTimeStamp ptsExpiry );
Далее, на стороне пользователя опять вызывается функция InitializeSecurityContext, в которую передается CHALLENGE-токен сервера. Функция формирует AUTH-токен, в котором содержится ответ на challenge.
Сервер, получив ответный токен, вновь вызывает функцию AcceptSecurityContext и передает в нее этот токен. Если все успешно, то для пользователя создается сессия.

Продемонстрируем работу NTLMSSP с помощью bash-скрипта, который напрямую взаимодействует с API.

Команда New-LsaCredentialHandle вызывает функцию AcquireCredentialsHandle, которая возвращает нам хэндл на учетные данные пользователя. Команде New-LsaClientContext передается хэндл пользователя, данная команда вызывает функцию InitializeSecurityContext, которая создает контекст и возвращает нам токен. Далее на экран выводится содержимое этого токена. В нем можно наблюдать название токена: “NTLM NEGOTIATE”, а также флаги, содержащие информацию о том, что поддерживает и хочет использовать клиент.
Unicode говорит о поддержке Unicode.
Oem говорит о поддержке байтовой строки.
AlwaysSign говорит о том, что будет сгенерирован сессионный ключ.
ExtendedSessionSecurity говорит просьбе использовать более безопасные алгоритмы формирования ключей и NetNTLMv1-хэша (если используется NTLMv1).
RequestTarget говорит о просьбе предоставить информацию о сервере.
Version говорит о том, что пользователь отправил информацию о версии операционной системы и версии протокола NTLM.
Key128Bit и Key56Bit говорят о том, что пользователь может использовать 128-битный и 56-битный ключ соответственно.
Полученный таким образом токен должен быть передан серверу.

В данном коде на стороне сервера мы предоставляем функции AcceptSecurityContext NEGOTIATE-токен посредством команды Update-LSAServerContext. В качестве возвращаемого результата мы получаем CHALLENGE-токен. В нем мы видим флаги, которые сервер выбрал из предлагаемых клиентом (заметим, что сервер убрал флаг Oem и оставил Unicode). Также мы видим информацию о сервере (Target Info), которую запросил клиент. Поле Challenge содержит ту самую строку, которую для имитовставки далее будет использовать клиент.

Команда Update-LsaClientContext вызывает InitializeSecurityContext, куда передается CHALLENGE-токен. На выходе мы получаем AUTHENTICATE-токен, который содержит среди всего прочего ответ на challenge сервера. Далее, данный токен должен быть передан серверу.
Ответ на challenge сервера называется NetNTLMv2-хэшем и строится следующим образом:
Сначала мы высчитываем NTLM-хэш, который является MD4-хэшем пароля пользователя MD4(UNICODE(Passwd)). Затем мы высчитыв��ем ResponseKeyNT, который равняется выводу NTOWFv2 (NT one-way function version 2). Для этого мы конкатенируем имя пользователя и домен в высоком регистре и считаем MD5-HMAC, используя NTLM-хэш в качестве ключа.
Define NTOWFv2(Passwd, User, UserDom) as HMAC_MD5( MD4(UNICODE(Passwd)), UNICODE(ConcatenationOf(Uppercase(User),UserDom))) Set ResponseKeyNT to NTOWFv2(Passwd, User, UserDom)
Для того чтобы сформировать окончательный ответ для севера, пользователь конкатенирует кучу информации, куда включается challenge сервера, временная метка клиента, challenge клиента (случайное 8-байтовое число, сгенерированное клиентом), а также имя сервера. Responserversion и HiResponserversion, согласно акутальной документации, равны 1. Z(m) – m нулей. Данная строка называется temp.
Set temp to ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, Responserversion, HiResponserversion, Z(6), Time, ClientChallenge, Z(4), ServerName, Z(4))
Далее, клиент конкатенирует challenge сервера с temp и берет от этого HMAC_MD5, используя в качестве ключа ResponseKeyNT. Данная строка называется NTProofString. Серверу клиент отправляет NTProofString и temp.
Set NTProofStr to HMAC_MD5(ResponseKeyNT, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge,temp) Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp)
Передав AUTHENTICATE-токен серверу, посредством вызова функции AcceptSecurityContext командой Update-LsaServerContext, сервер проверяет ответ на challenge (секреты пользователей хранятся на сервере), и, если пользователь аутентифицирован, создает для него logon-сессию.

NTLM внутри SMB
Теперь, когда мы познакомились с NTLMSSP, посмотрим на дамп SMB-трафика.
Как видно из захваченного пакета, NTLM-сообщение находится в поле Security Blob. В начале мы видим обязательную сигнатуру NTLMSSP, которая знаменует всякое NTLM-сообщение, затем тип сообщения NTLMSSP_NEGOTIATE, а в конце взведенные флаги.

Далее мы видим Challenge-сообщение в котором содержится сам challenge, для которого мы будем считать имитовставку, флаги, которые выбрал сервер, а также информацию о сервере, включающую:
NetBIOS-имя домена/компьютера
DNS-имя домена/компьютера
Текущую отметку времени

В ответном сообщении мы видим наш NetNTLMv2-хэш, который содержится в поле NTLMv2 Response. Обратим внимание, что первые байты отводятся под NtProofString, а далее приводится вся инфомрация, используемая для формирования NtProofString.

Внимательный читатель заметит незнакомое поле Lan Manager Response. Данное поле нужно для pass-through authentication. Если сервер сам не аутентифицирует пользователя, то он может отправить значение в Lan Manager Response тому, кто ей занимается (например, контроллеру домена), а затем, получив ответ сервера, уже решать – предоставлять услуги клиенту, или слать сообщение с ошибкой. Ныне это поле, как видно, не используется. Считается это следующим образом:
Set LmChallengeResponse to ConcatenationOf(HMAC_MD5(ResponseKeyLM, ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, ClientChallenge)), ClientChallenge
После того, как аутентификация прошла успешно, пользователь и сервер могут начинать сессию, внутри которой они будут обмениваться нужными им сообщениями.
NTLMv1
NTLMv1 отличается от NTLMv2 только способом формирования NetNTLM-хэша, ключей шифрования (о них позже) и механизмов безопасности. Все остальные поля NTLM-сообщений и процесс обмены сообщениями одинаковы как для первой, так и для второй версии. Версии протокола никак не согласуются самим NTLM, о версии NTLM должны дого��ариваться протоколы, пользующиеся услугами NTLM.
Для начала мы высчитываем ResponseKeyLM.
LMOWFv1(Passwd, User, UserDom) as ConcatenationOf(DES(UpperCase(Passwd)[0..6],"KGS!@#$%"), DES(UpperCase(Passwd)[7..13],"KGS!@#$%")) Set ResponseKeyLM to LMOWFv1(Passwd, User, UserDom)
Обратите внимание, что берутся первые ЧЕТЫРНАДЦАТЬ!1!!1!! символов пароля, и при всем этом они переводятся В ВЫСОКИЙ РЕГИСТР!11!! Думаю, не стоит объяснять: почему использование такого алогритма подсчета ResponseKeyLM очень небезопасно.
Затем, если активен флаг Extended Session Security, ответ строится следующим образом:
Set NtChallengeResponse to DESL(ResponseKeyNT, MD5(ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge, ClientChallenge))[0..7]) Set LmChallengeResponse to ConcatenationOf(ClientChallenge,Z(16))
Если флаг ESS не установлен, то действуем следующим образом:
Set NtChallengeResponse to DESL(ResponseKeyNT,CHALLENGE_MESSAGE.ServerChallenge) If (NoLMResponseNTLMv1 is TRUE) Set LmChallengeResponse to NtChallengeResponse Else Set LmChallengeResponse to DESL(ResponseKeyLM,CHALLENGE_MESSAGE.ServerChallenge)
В документации Microsoft нет ни слова про флаг NoLMResponseNTLMv1. Видимо, каждый разработчик API должен настраивать это самостоятельно.
Рассмотрим захваченное NTLM-authenticate сообщение.

Как можно видеть из дампа, authenticate-сообщение ничем кроме NTLM-Response'а не отличается (остальные сообщения тоже. Можете проверить самостоятельно).
В данном случае Lan Manager Response уже используется не для pass-through authentication, а для совместимости с доисторическими серваками, которые функционуруют на версиях предшествующих Windows NT и используют суперстарую версию протокола, которая называется LM (который потом стал NTLM, типа, после Windows NT, ю ноу), и где это поле непосредственно использовалось для аутентификации.
NTLM-Relay
Протокол NTLM без применения каких-либо средств защиты таким, каким мы его до сих пор рассматривали, уязвим к Man-in-the-Middle-атаке под названием NTLM Relay. Допустим, хакер хочет аутентифицироваться от лица жертвы перед сервером. В таком случае, он может вынудить жертву аутентифицироваться перед ним (в следующей статье мы рассмотрим, как это можно сделать).
Жертва отправляет Negotiate-запрос хакеру, хакер переправляет его серверу.
Хакер получает Chellahe-ответ от сервера и отправляет его жертве.
Жертва формирует NetNTLM-hash с помощью своего секрета и отправляет его хакеру.
Хакер отправляет полученное Authenticate-сообщение серверу.
Если все верно, то сервер аутентифицирует хакера; и последний может пользоваться услугами сервиса от лица ничего не подозревающей жертвы.

Сервер даже не подозревает, что с ним взаимодействует атакующий. Ведь для него все выглядит, как обычный обмен сообщениями.

Для жервты все также происходит штатно. Она думает, что общается с нужным ей сервером (к примеру, за счет спуфинга), видит что ей посылают валидные NTLM-сообщения и ни о чем лихом даже не задумывается.
К счастью, или же сожалению тех, кто уже на радостях собрался получать доступ к админским шарам, в современных реализациях NTLM существуют механизмы, которые помогают избежать атаки NTLM-Relay.
Signing and Sealing
После прохождения аутентификации клиент начинает сессию с сервером. Для предотвращения NTLM-Relay обе стороны должны понимать, что они общаются друг с другом, а не с лиходеем. Для убеждения в этом они должны как-то демонстрировать знание секрета пользователя. Такой демонстрацией выступает подпись (signing) и шифрование с подписью (sealing). Услуги по шифрованию и подписиванию сообщений предоставляет то же API. Получается, NTLM позволяет не только аутентифицировать клинета, но и обеспечить как целостность, так и конфиденциальность.
Идея следующая: клиент формирует ключ случайным образом, шифрует его, используя свой секрет, затем этот ключ помещает в Authenticate-сообщение. Затем сервер расшифровывает его, и с помощью него как клиент, так сервер могут подписывать и шифровать сообщения друг для друга. Можете вернуться к рисункам с захваченными Authenticate-сообщениями и найти там Session Key – это и есть зашифрованный ключ.
Данный механизм защиты полностью исключает проведение атаки, ибо злоумышленник просто не сможет сгенерировать валидную подпись, которая потребна для принятия сообщения сервером.

Для использования подписи/(шфирования и подписи) должны быть установлены флаги NTLMSSP_NEGOTIATE_SIGN/NTLMSSP_NEGOTIATE_SEAL.
Формирование ключа
Рассмотрим то, как формируются ключи подписи и шифрования для NTLMv2. Для NTLMv1 процесс отличается, и желающие могут заглянуть в документацию к протоколу.
Для начала клиент формирует случайный 16-байтовый сессионный ключ ExprotedSessionKey, затем с помощью своего секрета формирует KeyExchangeKey, с помощью которого шифрует ExprotedSessionKey и отправляет его в Authenticate-сообщении.
Set ExportedSessionKey to NONCE(16) Set KeyExchangeKey to HMAC_MD5(ResponseKeyNT, NTProofStr) Set AUTHENTICATE_MESSAGE.EncryptedRandomSessionKey to \ RC4(KeyExchangeKey, ExportedSessionKey)
Для формирования ключа подписи и шифрования берется MD5-хэш от конкатенации ExportedSessionKey и константы (для сервера и клиента, подписи и шифрования они разные).
Set SignKey/SealKey to MD5(ConcatenationOf(ExportedSessionKey,"signing/sealing constant")
Алгоритм подписи и шифрования сообщений
Для подписи сообщений мы считаем имитовставку от конкатенации порядкого номера сообщения в сессии и самого сообщения, которое нам надо подписать. SeqNum - это порядковый номер сообщения, который обычно при начале сессии равняется нулю, а затем инкриментируется. Затем от имитовставки берутся первые 16 байт и шифруются; они и являются подписью сообщения (что, честно говоря, идет в разрез с тем, что обычно называют подписью, ведь тут мы имеем дело с имитозащитой, а не подписью... Но у Microsoft, видимо, свои взгляды на криптографию).
Set Signature to RC4(ExportedSessionKey, HMAC_MD5(SigningKey, ConcatenationOf(SeqNum, Message))[0..7]) Set SeqNum to SeqNum + 1
Если во время аутентификации был установлен флаг NTLMSSP_NEGOTIATE_SEAL, то вместе будет использовать как шифрование, так и подпись. Шифрование представляет собой просто применение алгороитма RC4.
Set Seal to RC4(SigningKey,Message)
Если протоколу, который пользуется услугами NTLM достаточно просто получить симметричный ключ, а услуги по шифрованию и подписи его не интересуют, достаточно включить флаг NTLMSSP_NEGOTIATE_ALWAYS_SIGN.
Подпись в SMB
Подпись сообщений сообщений это хорошо, но её ведь нужно еще согласовать. В SMB присутствует 3 состояния для подписи: Disabled, Enabled и Required. О них заявляют участники переговоров во время выбора версии SMB протокола еще до начала аутентификации.

В зависимости от комбинации этих флагов на клиенте и сервере будет приниматься решение о подписи.

Из приведенной выше таблицы мы можем сделать несколько важных выводов:
Состояние Disabled отсутствует у SMBv2, что делает его предпочтительнее чем SMBv1
По умолчанию контроллер домена требует использование подписи
Если сервер не является контроллером домена, то по умолчанию сообщения не подписываются
На следующем рисунке мы можем видеть как выглядит подпись внутри SMB.

Стоит отметить, что в SMB подпись расчитывается своим образом без использования услуг NTLMSSP. NTLM помогает SMB только обменом ключа подписи.
Service Binding
Данный механизм защиты предельно прост: клиент включает в NTLMv2 Response информацию о том, с кем он хочет взаимодействовать. Сервер проверяет эту информацию, и если она не протворечит идентичности сервера, то он аутентифицирует клиента, иначе – нет.
Имя, с которым пользователь взаимодействует он включает в атрибут Target Name, которое используется для получения NTProofString. Даже если злоумышленник изменит данный атрибут, то NTProofString, посчитанный сервером не будет совпадать с переданным.

Channel Binding
Этот механизм защиты похож на предыдущий, только в данном случае мы "привязываемся" не к сервису, перед которым хотим аутентифицироваться, а к защищенному каналу, который строим с этим сервисом. В случае TLS, этой "привязкой" будет служить сертификат, который мы получаем от сервера.

Можно видеть, что на 16-ом рисунке Channel Bindings пуст, так как там TLS не использовался.
Для совершения атаки тать построит два туннеля:
1. С жертвой, посылая ей свой сертификат.
2. С сервером, принимая его сертификат.
В таком случае, сервер будет ждать, что в Channel Binding будет находится его сертификат, и отклонит AUTH-сообщение, так как там будет находистя сертификат злоумышленника.

Важно отметить, что Target Name и Channel Binding не поддерживается в версииNTLMv1, также в Windows NT, Windows 2000, Windows XP, Windows Server 2003, Windows Vista и Windows Server 2008.
Хм🤔, а поч просто не заменить, что channel binding, что target name на те, которые примет сервак?
MIC
Для того, чтобы гарантировать, что в процессе обмена сообщениями ничего не было изменено: никакие лишние флаги не были взведены, никакие флаги не были обращены в ноль, а также, что важно channel binding и target name не были изменены, используется Message Integrity Code, который считается довольно-таки просто: мы берем сессионный ключ и с помощью него считаем HMAC_MD5 от конкатенации всех сообщений.
HMAC_MD5(Session key, NEGOTIATE_MESSAGE + CHALLENGE_MESSAGE + AUTHENTICATE_MESSAGE)
Теоретически, если протокол полгается на флаги sign и seal для подписи и ЗаПеЧаТыВаНиЯ, то без MIC мы бы могли отключить подпись и спокойно релеить NTLM (мб такой андеграундный протокол и существует).
В версиях Windows NT, Windows 2000, Windows XP, and Windows Server 2003 MIC не поддерживается.
Также в NTLMv1 можно полностью убрать MIC и сервер ничего не заподозрит, так как сущетсвует определенный флаг, который отвечает за то, предоставляет клиент MIC или нет, и в NTLMv1 этот флаг просто отсутствует.
Этот флаг находится в структуре AvPairs, которая добавляется только в NTLMv2, в NTLMv1 эта структура отсутствует. В этой структуре помимо флага, отвечающего за присутствие MIC находится также куча всего, включая Channel Binding и Target Info.

В Wireshark AvPairs показывается следующим образом

В данном источнике автор утверждает, что в LDAP подпись зиждется на флаге sign, но на практике я не смог заставить клиент и сервер не использовать подпись, отключая sign. Также я пробовал отключать seal, но шифрование пакетов все равно проходило. Если я рукожоп, и у вас это получилось – дайте знать в комментах.
Заключение
В данной статье мы с вами достаточно глубоко погрузились в работу протокола NTLM, узнали об атаке NTLM Relay, выяснили, какие механизмы защиты присутствуют в NTLM и поняли, почему использование NTLMv1 – прямой путь к компрометации ваших сервисов. Напомним, почему NTLMv1 небезопасен:
Слабая криптография
Полная уязвимость к NTLM Relay
Список используемой литературы
NTLM Relay // hackndo URL: https://en.hackndo.com/ntlm-relay/#channel-binding (дата обращения: 07.02.2026);
[MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol // Microsoft URL: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nlmp/b38c36ed-2804-4868-a9ff-8dd3182128e4 (дата обращения: 07.02.2026);
Forshaw J. Windows Security Internals: A Deep Dive into Windows Authentication, Authorization, and Auditing. – No Starch Press, 2024;
The NTLM Authentication Protocol and Security Support Provider // Davenport WebDAV-SMB Gateway URL: https://davenport.sourceforge.net/ntlm.html (дата обращения: 09.02.2026).
