Поиск иголки в стоге сена или как PsExec шифрует трафик
В марте 2014 года у популярной утилиты для администрирования появилась возможность шифрования трафика. К сожалению или к счастью, это единственная информация о том, как PsExec шифрует трафик. Под катом мы заглянем во внутренности PsExec и OS Windows и узнаем что происходит на самом деле. Конечная цель этого поста - понять как можно расшифровать перехваченный PsExec
трафик.
PsExec
Данное программное обеспечения поставляется с пакетом программ Sysinternals Suite
и позволяет системным администраторам выполнять команды на удаленных компьютерах.
Визитной карточкой PsExec
служит PSEXESVC
сервис, который запускается на удаленном пк и выполняет команды клиента.
Если на удаленном хосте не установлена служба psexecsvc.exe
, то экземпляр передается по сети и устанавливается в %SystemRoot%
.
Коммуникация с сервисом происходит с помощью пайпов:
Шифрование
Для шифрования трафика PsExec использует CryptEncrypt
и CryptDecrypt
API. Как говорил Марк в своем твите, если ОС Windows XP или Windows Server 2003 - для обмена ключами шифрования используется алгоритм Диффи-Хеллмана.
В остальных случаях ключ генерируется следующим образом:
К 16 неизвестным байт добавляется строчка Sysinternals Rocks
и хэшируется SHA1
. Этот хэш используется как AES_256 ключ для последующего шифрования.
NtFsControlFile
Чтобы получить 16 неизвестных байт, PsExec открывает “файл” \\Device\\LanmanRedirector\\{device}\\ipc$
, где device - Адрес удаленного хоста.
После чего с помощью NtFsControlFile
IOCTL кода 0x1401a3
заполняется 16 неизвестных байт из output
буффера.
К сожалению, информации по данному IOCTL в открытом доступе я не нашел, поэтому решил заглянуть в слитые исходники Windows XP.
Для начала поймем какой номер функции используется в IOCTL:
python .\ioctl.py 0x1401a3
[*] Device Type: FILE_DEVICE_NETWORK_FILE_SYSTEM
[*] Function Code: 0x68
[*] Access Check: FILE_ANY_ACCESS
[*] I/O Method: METHOD_NEITHER
[*] CTL_CODE(FILE_DEVICE_NETWORK_FILE_SYSTEM, 0x68, METHOD_NEITHER, FILE_ANY_ACCESS)
grep
кода функции по исходникам Windows XP выдал следующий результат:
grep -r "104, METHOD_NEITHER, FILE_ANY_ACCESS" | grep define
sdk/inc/ntddnfs.h:#define FSCTL_LMR_GET_CONNECTION_INFO _RDR_CONTROL_CODE(104, METHOD_NEITHER, FILE_ANY_ACCESS)
Поиск по FSCTL_LMR_GET_CONNECTION_INFO
привел в функцию base\fs\rdr2\rdbss\smb.mrx\fsctl.c#L1737
:
else if (FsControlCode == FSCTL_LMR_GET_CONNECTION_INFO)
{
PSMBCEDB_SERVER_ENTRY pServerEntry = SmbCeGetAssociatedServerEntry(capFcb->pNetRoot->pSrvCall);
Status = STATUS_INVALID_PARAMETER;
pOutputDataBuffer = pLowIoContext->ParamsFor.FsCtl.pOutputBuffer;
OutputDataBufferLength = pLowIoContext->ParamsFor.FsCtl.OutputBufferLength;
if (pOutputDataBuffer && (OutputDataBufferLength==sizeof(LMR_CONNECTION_INFO_3)))
{
try
{
ProbeForWrite(
pOutputDataBuffer,
OutputDataBufferLength,
1);
if (!memcmp(pOutputDataBuffer,EA_NAME_CSCAGENT,sizeof(EA_NAME_CSCAGENT)))
{
MRxSmbGetConnectInfoLevel3Fields(
(PLMR_CONNECTION_INFO_3)(pOutputDataBuffer),
pServerEntry,
TRUE);
Status = STATUS_SUCCESS;
}
}
Здесь pOutputDataBuffer
используется как структура PLMR_CONNECTION_INFO_3
. На 0x2c
оффесете которой как раз находится UserSessionKey
размером 16 байт.
typedef struct _LMR_CONNECTION_INFO_3_32 {
UNICODE_STRING_32 UNCName; // Name of UNC connection
ULONG ResumeKey; // Resume key for this entry.
DEVICE_TYPE SharedResourceType; // Type of shared resource
ULONG ConnectionStatus; // Status of the connection
ULONG NumberFilesOpen; // Number of opened files
UNICODE_STRING_32 UserName; // User who created connection.
UNICODE_STRING_32 DomainName; // Domain of user who created connection.
ULONG Capabilities; // Bit mask of remote abilities.
UCHAR UserSessionKey[MSV1_0_USER_SESSION_KEY_LENGTH]; // User session key
UCHAR LanmanSessionKey[MSV1_0_LANMAN_SESSION_KEY_LENGTH]; // Lanman session key
UNICODE_STRING_32 TransportName; // Transport connection is active on
...
Следовательно те неизвестные 16 байт - сессионный ключ, который создается при NTLMv2 аутентификации.
NTLMv2 и ключ сессии
NTLMv2 — встроенный в операционные системы семейства Microsoft Windows протокол сетевой аутентификации. Широко применяется в различных сервисах на их базе. Изначально был предназначен для повышения безопасности аутентификации путём замены устаревших LM и NTLM v1. NTLMv2 был введён начиная с Windows NT 4.0 SP4 и используется версиями Microsoft Windows вплоть до Windows 10 включительно.
В схеме аутентификации, реализованной при помощи SMB или SMB2 сообщений, вне зависимости от того, какой вид диалекта аутентификации будет использован, процесс аутентификации происходит следующим образом:
- Клиент пытается установить соединение с сервером и посылает запрос, в котором информирует сервер, на каких диалектах он способен произвести аутентификации.
- Сервер из полученного от клиента списка диалектов (по умолчанию) выбирает наиболее защищённый диалект (например, NTLMv2), затем отправляет ответ клиенту.
- Клиент, определившись с диалектом аутентификации, пытается получить доступ к серверу и посылает запрос NEGOTIATE_MESSAGE.
- Сервер получает запрос от клиента и посылает ему ответ CHALLENGE_MESSAGE, в котором содержится случайная (random) последовательность из 8 байт. Она называется Server Challenge.
- Клиент, получив от сервера последовательность Server Challenge, при помощи своего пароля производит шифрование этой последовательности, а затем посылает серверу ответ AUTHENTICATE_MESSAGE, который содержит 24 байта.
- Сервер, получив ответ, производит ту же операцию шифрования последовательности Server Challenge, которую произвёл клиент. Затем, сравнив свои результаты с ответом от клиента, на основании совпадения разрешает или запрещает доступ.
В трафике это выглядит следующим образом:
Алгоритм создания ключа сессии:
- Вычисляется
NTLM_hash = MD4(password)
- Вычисляется
NTLMv2_hash = hmac_md5(NTLM_hash, username.upper()+domain)
- Вычисляется
NtProofStr = hmac_md5(NTLMv2_hash, server_challenge + blob)
- Вычисляется
session_key = hmac_md5(NTLMv2_hash, NtProofStr)
Если же был выбран Negotiate key exchange
, то session_key
превращается в RC4 ключ для шифрования рандомной 16 байтовой последовательности, которая потом и передается в трафике как session_key
.
Следовательно чтобы расшифровать трафик PsExec необходимо иметь пароль пользователя и 3 пакета NTLMSSP_AUTH
, верно? Не совсем.
Как оказалось, сессионный ключ, который вычисляется при NTLMv2 аутентификации и сессионный ключ, который получает PsExec с помощью NtFsControlFile
не совпадают.
Ищем иголку в стоге сена
Сначала я думал, что я неправильно вычисляю NTLMv2 сессионный ключ. Чтобы проверить это, я решил написать библиотеку, которая будет перехватывать NTLM функции и выписывать нужные мне значения. Как известно, за аутентификацию в системе отвечает процесс lsass.exe
.
В частности, за NTLM отвечают библиотеки msv1_0.dll
и NtlmShared.dll
.
Чтобы понять, какие функции мне нужно перехватить, я использовал frida-trace
:
> frida-trace lsass.exe -I NtlmShared.dll
/* TID 0x850 */
13818 ms NtLmAlterRtlEqualUnicodeString()
13818 ms NtLmAlterRtlEqualUnicodeString()
13818 ms NtLmAlterRtlEqualUnicodeString()
13818 ms MsvpPutClearOwfsInPrimaryCredential()
13818 ms MsvpLm20GetNtlm3ChallengeResponse()
13818 ms | MsvpNtlm3Response()
13818 ms | | MsvpCalculateNtlm3Owf()
Три функции бросаются в глаза:
MsvpLM20GetNtlm3ChallengeResponse - Вычисляет NTLM_response, LM_response
MsvpNtlm3Response - Вычисляет UserSessionKey, NtProofStr
MsvpCalculateNtlm3Owf - Вычисляет NTLMv2_hash
Так же функция SsprMakeSessionKey
в msv1_0.dll
используется для создания сессионного ключа и RC4
шифрования.
Перехватив эти функции получаем следущее:
[+] Panda online
[*] Setting up hooks
[+] 0x00007ffd07d70000 msv1_0.dll
[+] 0x00007ffd07d50000 NtlmShared.dll
[*] Hooking 0x00007ffd07dd0a10
[*] Hooking 0x00007ffd07d518c0
[*] Hooking 0x00007ffd07d51c30
[*] Hooking 0x00007ffd07dcb470
[+] Done setting up hooks
[*] Called LM20GetNtlm3ChallengeResponse
UserPassword: Administratork5y4m8d2
Domain: DESKTOP-5SNDDCH
[*] Called MsvpNtlm3Response
NTLM Hash: d110843d880486c2e9eb8cae52a3f7e5
UserPassword: Administratork5y4m8d2
Domain: DESKTOP-5SNDDCH
ServerChallenge: 14aa123977e1bbb9
[*] Called MsvpCalculateNtlm3Owf
NTLM Hash: d110843d880486c2e9eb8cae52a3f7e5
UserPassword: Administratork5y4m8d2
Domain: DESKTOP-5SNDDCH
NTLMv2 Hash: 9755854f7b92eb35dc4dd4021ecab92b
[*] Return MsvpCalculateNtlm3Owf
UserSessionKey: 980b87df588aff63810cdc3ca1e19ea5
LmSessionKey: 980b87df588aff631200000000000000
NtProofStr: 2047ba24f0b3d6d69ef8bfc73dcfbe78
[*] Return MsvpNtlm3Response
SessionKey: 980b87df588aff63810cdc3ca1e19ea5
LM Response:
ClientChallenge: 0000000000000000
Response: 00000000000000000000000000000000
NTLM Response:
ClientChallenge: 7406b827e31472d9
TimeStamp: 0x1d7345390ec3462
NtProofStr: 2047ba24f0b3d6d69ef8bfc73dcfbe78
[*] Return LM20GetNtlm3ChallengeResponse
[*] Called SsprMakeSessionKey
UserSessionKey: 980b87df588aff63810cdc3ca1e19ea5
LanmanSessionKey: 980b87df588aff63
DatagramSessionKey before call: 00000000000000000000000000000000
ContextSessionKey before call: cec7c38bc5650ebff207947d8996eaa1
DatagramSessionKey after call: 333924f59319745239e4f0d3eafcaba7
ContextSessionKey after call: cec7c38bc5650ebff207947d8996eaa1
[*] Return SsprMakeSessionKey
[+] Exiting..
На выходе ContextSessionKey
cec7c38bc5650ebff207947d8996eaa1
- расшифрованный сессионый ключ, DatagramSessionKey
333924f59319745239e4f0d3eafcaba7
его RC4 зашифрованная версия, но при этом PsExec получает a082f9980639311e0422545662243274
ключ. Значит дело не в lsass и нужно копать глубже.
Идем в ядро
За SMB и SMB2 соединение отвечают 2 драйвера - mrxsmb.sys
и mrxsmb20.sys
соответственно. Драйвер mrxsmb20.sys
использует mrxsmb.sys
как библиотеку для NTLM криптографии и работу с SMB протоколом.
Поскольку над сессионным ключом явно происходят какие-то действия, я решил перехватывать все крипто функции.
0: kd> x mrxsmb!SmbCrypto*
fffff800`65217d78 mrxsmb!SmbCryptoCreateCipherKeys (void)
fffff800`652033c8 mrxsmb!SmbCryptoSp800108CtrHmacSha256DeriveKey (void)
fffff800`652058f0 mrxsmb!SmbCryptoKeyTableRemove (void)
fffff800`65217ac8 mrxsmb!SmbCryptoKeyTableInsert (void)
fffff800`65264b5c mrxsmb!SmbCryptoInitialize (void)
fffff800`6525f720 mrxsmb!SmbCryptoHashCreate (void)
fffff800`65264980 mrxsmb!SmbCryptoReadCipherSuiteOrderPolicySetting (void)
fffff800`65203e70 mrxsmb!SmbCryptoUpdatePreauthIntegrityHashValue (SmbCryptoUpdatePreauthIntegrityHashValue)
fffff800`6525d1e0 mrxsmb!SmbCryptoCiphers = <no type information>
fffff800`65219380 mrxsmb!SmbCryptoCreateApplicationKey (SmbCryptoCreateApplicationKey)
fffff800`6526ac4c mrxsmb!SmbCryptoDeinitialize (SmbCryptoDeinitialize)
fffff800`65204bb0 mrxsmb!SmbCryptoKeyTableReferenceObject (SmbCryptoKeyTableReferenceObject)
fffff800`65262240 mrxsmb!SmbCryptoHashAlgorithmInitialize (SmbCryptoHashAlgorithmInitialize)
fffff800`65203350 mrxsmb!SmbCryptoCreateSigningKey (SmbCryptoCreateSigningKey)
fffff800`6525fb20 mrxsmb!SmbCryptoHashDestroy (SmbCryptoHashDestroy)
fffff800`65233da0 mrxsmb!SmbCryptoKeyTableCleanupObject (SmbCryptoKeyTableCleanupObject)
fffff800`6525d200 mrxsmb!SmbCryptoHashAlgorithms = <no type information>
fffff800`652049e0 mrxsmb!SmbCryptoKeyTableCompareHashKeys (SmbCryptoKeyTableCompareHashKeys)
fffff800`6523ab90 mrxsmb!SmbCryptoEncrypt (SmbCryptoEncrypt)
fffff800`6525d1c0 mrxsmb!SmbCryptoSp800108CtrHmacAlgHandle = <no type information>
fffff800`6523aaa4 mrxsmb!SmbCryptoDecrypt (SmbCryptoDecrypt)
fffff800`65217c88 mrxsmb!SmbCryptoCreateClientCipherKeys (SmbCryptoCreateClientCipherKeys)
fffff800`6525d060 mrxsmb!SmbCryptoNonceCounter = <no type information>
fffff800`65204a10 mrxsmb!SmbCryptoHashGetOutputLength (SmbCryptoHashGetOutputLength)
Для этого был написан WinDbg скрипт, который трейсил и логировал вызовы функций. Из всего списка перехваченных функций меня привлекла SmbCryptoCreateCipherKeys
. Она вызывалась одной из первых и только 1 раз.
Эта функция имеет только 1 xref с очень интересным названием, а 2-ым аргументом передается 16 байтовый ключ.
Выпишем этот ключ с помощью WinDbg скрипта:
/// <reference path="JSProvider.d.ts" />
"use strict";
const log = x => host.diagnostics.debugLog(x+'\n');
const ok = x => log(`[+] ${x}`);
const warn = x => log(`[!] ${x}`);
const err = x => log(`[-] ${x}`);
const u8 = x => host.memory.readMemoryValues(x, 1, 1)[0];
const u16 = x => host.memory.readMemoryValues(x, 1, 2)[0];
const u32 = x => host.memory.readMemoryValues(x, 1, 4)[0];
const u64 = x => host.memory.readMemoryValues(x, 1, 8)[0];
const mem_read_array = (x, y) => host.memory.readMemoryValues(x, y);
const mem_read_string = x => host.memory.readString(x);
const mem_read_wstring = x => host.memory.readWideString(x);
function hex(arr) {
return Array.from(arr, function(byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('')
}
function handle_create_cipher_keys() {
ok('mrxsmb!SmbCryptoCreateCipherKeys hit!');
let regs = host.currentThread.Registers.User;
let args = [regs.rcx, regs.rdx, regs.r8, regs.r9];
let secret = mem_read_array(args[1], 16);
let method = mem_read_string(args[3]);
ok('Method: ' + method + ', Secret: ' + hex(secret));
}
function invokeScript() {
let control = host.namespace.Debugger.Utility.Control;
// Hook SmbCryptoCreateCipherKeys
let bp_1 = control.SetBreakpointAtOffset("SmbCryptoCreateCipherKeys", 0, "mrxsmb");
bp_1.Command = 'dx @$scriptContents.handle_create_cipher_keys(); gc';
ok('Press "g" to run the target.');
}
Получаем:
[+] mrxsmb!SmbCryptoCreateCipherKeys hit!
[+] Method: SMBC2SCipherKey, Secret: cec7c38bc5650ebff207947d8996eaa1
@$scriptContents.handle_create_cipher_keys()
И что мы видим, это наш расшифрованный сессионный ключ!
Дальше я потрейсил где вызывается эта функция. Стэк вызовов получился следующим:
1: kd> k
# Child-SP RetAddr Call Site
00 ffffd98e`0e95d378 fffff800`65217d56 mrxsmb!SmbCryptoCreateCipherKeys
01 ffffd98e`0e95d380 fffff800`65261fdd mrxsmb!SmbCryptoCreateClientCipherKeys+0xce
02 ffffd98e`0e95d400 fffff800`65217aa3 mrxsmb!RxCeCreateAndRegisterCryptoKeys+0x7d
03 ffffd98e`0e95d480 fffff800`652a51a1 mrxsmb!VctCreateAndRegisterCryptoKeys+0x43
04 ffffd98e`0e95d4d0 fffff800`652dbfbf mrxsmb20!ValidateSessionSetupSecurityBlob+0x435
05 ffffd98e`0e95d620 fffff800`6520696e mrxsmb20!Smb2SessionSetup_Finalize+0xdf
06 ffffd98e`0e95daa0 fffff800`6520688d mrxsmb!SmbCepFinalizeExchange+0x7a
07 ffffd98e`0e95dae0 fffff800`64d554b2 mrxsmb!SmbCepFinalizeExchangeWorker+0x4d
Как видно, mrxsmb20 вызывает эту функцию в последней стадии установки сессии с удаленным хостом.
В mrxsmb20!ValidateSessionSetupSecurityBlob
, которая передает сессионный ключ далее по стэку можно заметить, что он используется еще в одной функции ниже.
Сама Smb2DeriveApplicationKey:
То есть если версия диалекта (SMB протокола) >= 3.0, создается ApplicationKey, который перезаписывает UserSessionKey. Если же нет, UserSessionKey не изменяется.
SmbCryptoCreateApplicationKey
- обертка над SP800-108 hmac SHA256
, где в качестве ключа - используется наш сессионный ключ.
Попробуем перехватить ApplicationKey сразу после вызова SmbCryptoSp800108CtrHmacSha256DeriveKey
и залогировать его:
...
function handle_create_application_key() {
ok('mrxsmb!SmbCryptoCreateApplicationKey hit!');
let regs = host.currentThread.Registers.User;
let app_key_ptr = u64(host.parseInt64(regs.rsp).add(0x30));
let app_key = mem_read_array(app_key_ptr, 16);
ok('ApplicationKey: ' + hex(app_key));
}
function invokeScript() {
let control = host.namespace.Debugger.Utility.Control;
// Hook SmbCryptoCreateCipherKeys
let bp_1 = control.SetBreakpointAtOffset("SmbCryptoCreateCipherKeys", 0, "mrxsmb");
bp_1.Command = 'dx @$scriptContents.handle_create_cipher_keys(); gc';
// Hook SmbCryptoCreateApplicationKey
let bp_2 = control.SetBreakpointAtOffset("SmbCryptoCreateApplicationKey", 107, "mrxsmb");
bp_2.Command = 'dx @$scriptContents.handle_create_application_key(); gc';
ok('Press "g" to run the target.');
}
Получаем:
[+] mrxsmb!SmbCryptoCreateApplicationKey hit!
[+] ApplicationKey: a082f9980639311e0422545662243274
@$scriptContents.handle_create_application_key()
Та-да! Мы нашли ключ, который использует PsExec!
Выводы
Вся работа с SMB хорошо описана в impacket библиотеке.
А получение Application Key для SMB 3.X диалекта можно найти здесь.
Стоит заметить, что ApplicationKey используется только если версия диалекта >= 3, во всех остальных случаях для расшифровки трафика достаточно достать NTLMv2 SessionKey.
Код библиотеки и WinDbg скрипт доступны тут.
Референсы
http://davenport.sourceforge.net/ntlm.html
https://sensepost.com/blog/2019/recreating-known-universal-windows-password-backdoors-with-frida/
http://en.verysource.com/code/277230_1/context.cxx.html
Конец.