Background
Alice is a Windows programmer in company A. Bob is a network administrator at company B. Company B uses a product P of company A. It so happened that there was a problem in the product P at B's site. The problem could not be replicated elsewhere, so Alice was asked to go to B's site and find out more about the problem and if possible fix it. The problem was that the product P had an NT service component and it was failing for some reasons. Luckily, Visual C++ was installed on the machine and company B had purchased the source code of the product P. Therefore, Alice could debug at B's site.
Bob gave a user name and password so that Alice could log on to the machine and do debugging. When Alice started the debugger and attached it to the NT service, she got an "Access denied" error. The logon credentials provided by Bob was that of a regular user with very minimal privileges (for security reasons Bob could not give the password of an administrative account to Alice). Alice being an experienced Windows programmer knew that she needed the SE_DEBUG_NAME
privilege to debug programs running under different credentials. In this particular case, the service was running as a low privilege domain account. Luckily for Alice, Bob decided to grant her the SE_DEBUG_NAME
privilege. The image below shows the screen shot of the "Local security settings" MMC snap in, on a Windows XP machine through which the privilege can be set.
Double clicking on "Debug Programs" pops up the following dialog box.
An Administrator can add or remove users or groups in the above dialog, to grant or deny them the privilege. Bob adds Alice to the list and she successfully managed to debug the service. It turned out that the network account under whose credentials the service was running, did not have access to a directory. The problem was fixed and Alice got a big thank you from Bob.
The problem
Two years later the product P was migrated to .NET - it was rewritten in C#. Bob installed the new version of the software on his server. It turned out that the new version of the product gave the same access denied problem. Bob checked to make sure that he set permissions correctly on different directories used by the application. It turned out that everything was set correctly. Alice was once again asked to go to B's site to fix the problem. Bob prepared the machine for Alice by installing Visual Studio .NET on it and also giving her the privilege to debug programs. So once again Alice tries to attach the debugger. Since the program was now running under Common Language Runtime, she selected the program type to be Common Language Runtime, from the list of program types. As soon as she clicked OK, she got the access denied error as shown in the screen shot.
Alice then decided to attach the native debugger by selecting the program type to be native and it worked. Alice double checks by trying to attach the common language runtime debugger and it failed again. So, she asked Bob to run the debugger under his credentials. She was able to successfully attach the debugger. Unfortunately, she could not do much debugging this way, as Bob refused to allow Alice to run the debugger under his credentials. The rest of this article tries to find some means to help Alice.
Understanding the problem
Here is a statement of the problem - SE_DEBUG_NAME
privilege is not enough to debug CLR applications running under different credentials, if the privilege is held by a non-admin account. The SE_DEBUG_NAME
privilege is a very powerful privilege. Any account having this privilege can terminate a process, inject DLLs and allocate, read and write to a process's memory. Therefore, I was surprised to find out that it still did not allow access to CLR debugging. Luckily the source code of cordbg (a command line CLR debugger) comes with the framework SDK. Running cordbg in VS.NET debugger and trying to attach cordbg to a process running under a different user's credential revealed that, deep inside the debugging API, a particular call to OpenEvent
failed with access denied. It became obvious that the CLR debugging API was trying to communicate with the debuggee using this event.
Next, I viewed the debuggee process by using the procexp tools from sysinternals. Here is how a typical .NET application looks in procexp.
The following objects are of interest:-
CorDBDebuggerAttachedEvent_2956
- an event kernel object
CorDBIPCSetupSyncEvent_2956
- another event kernel object
Cor_Private_IPCBlock_2956
- a file mapping kernel object
Investigating more .NET applications using procexp revealed that as soon as CLR loads in a process, it creates these three events. The number at the end of the objects' name is the process ID of the process. The procexp tool also allows you to check the security settings of each object (recall that you can set security for each kernel object in windows NT based OSs). Here is how the security setting of one of these objects, looks like.
Basically, only the Administrators and the account under which the process is running (aspnet in the example above) have full access to the object. No one else is granted any access. It turns out to be that this is the default security setting for any new kernel object. Remember the SECURITY_ATTRIBUTES
parameter in many Win32 functions which is almost always passed as NULL
. This is what happens if it is passed as NULL
(which by the way is the right thing to do in most cases). Passing NULL
as SECURITY_ATTRIBUTES
indicates that OS should select default security setting for the object. The default security settings are set in the process/thread token. If the default settings in the token is not modified by explicit calls to security API functions, only the Adminsitrators group and the user account under which the application is running, have full access to any new kernel object.
What this proves is that CLR obviously uses default settings to create the three kernel objects required for debugging. As a result any attempt to debug the application by another non-admin account fails, while accessing any of these kernel objects.
The solution
Fortunately, security settings of any kernel object can be modified after the kernel object is created using SetKernelObjectSecurity
/SetNamedSecurityInfo
functions. The security settings can be modified by code running in the context of the owner of the object. Luckily the SE_DEBUG_NAME
privilege allows the privilege holder to inject a DLL into any process, thereby allowing code to execute in the security context of the target process (hence the owner). The code thus injected can modify the security settings of the objects. Here is the code snippet of how DLL injection is done by the add-in. The dwProcessID
variable used in the snippet holds the process ID of the target that needs to be enabled for debug.
CHandle hProcess(OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessID));
HandleCheckNotNull(hProcess);
TCHAR szPath[_MAX_PATH + 1];
LPVOID lpvAddress = VirtualAllocEx(hProcess, NULL, sizeof(szPath),
MEM_COMMIT, PAGE_READWRITE);
if (lpvAddress == NULL)
AtlThrowLastWin32();
GetModuleFileName(_AtlModule.GetModuleInstance(), szPath, _MAX_PATH);
szPath[_MAX_PATH] = 0;
TCHAR* szFileName = PathFindFileName(szPath);
int nAllowedLen = PtrToInt(szFileName - szPath);
lstrcpyn(szFileName, TEXT("aclchg.dll"), _MAX_PATH - nAllowedLen);
Win32Check(WriteProcessMemory(hProcess, lpvAddress,
szPath, sizeof(szPath), NULL));
CAtlFileMapping<SID> mapping;
CString strMappingName;
strMappingName.Format(szACLChgCommData, dwProcessID);
mapping.MapSharedMem(MAX_SID_SIZE, strMappingName);
CDacl dacl;
dacl.AddAllowedAce(Sids::World(), FILE_MAP_ALL_ACCESS);
Win32Check(AtlSetDacl(mapping.GetHandle(), SE_KERNEL_OBJECT, dacl));
CAccessToken token;
Win32Check(token.GetProcessToken(TOKEN_QUERY));
CSid sidUser;
Win32Check(token.GetUser(&sidUser));
memcpy(mapping, sidUser.GetPSID(), sidUser.GetLength());
DWORD dwThreadID = 0;
CHandle hThread(CreateRemoteThread(hProcess, NULL, 0,
reinterpret_cast<LPTHREAD_START_ROUTINE>(LoadLibraryW),
lpvAddress, 0, &dwThreadID));
HandleCheckNotNull(hThread);
The code does the following:-
- Allocates memory in the target process and writes the full location of the DLL to inject (ACLChg.dll) to memory location.
- Creates a shared memory section and writes the SID of the user who launched the process (devenv.exe) to the memory section. The name of the file mapping also includes the target process ID.
- Finally it uses
CreateRemoteThread
to start a new thread at the address LoadLibraryW
. This allows the ACLChg.dll to be injected in target process.
Changing the ACLs
The new ATL security classes simplify programming Windows security, a lot. The code for ACLChg.dll which is responsible for modifying the ACL of the target process, makes use of the new security classes. This is all done in the DllMain
function. Here is the code that does it.
CAtlFileMapping<SID> mapping;
CString strMappingName;
strMappingName.Format(szACLChgCommData, GetCurrentProcessId());
HRCheck(mapping.MapSharedMem(MAX_SID_SIZE, strMappingName));
CSid sidToAdd(*mapping);
AdjustObjectSecurity(szDebuggerAttachedEvent, sidToAdd);
AdjustObjectSecurity(szDBIPCSetupSyncEvent, sidToAdd);
AdjustObjectSecurity(szPrivateIPCBlock, sidToAdd);
The code reads the SID supplied in the shared memory section created by the process injecting the DLL and adjusts the security of the three kernel objects used by the CLR debugging system. Here is how the AdjustObjectSecurity
function looks like.
void AdjustObjectSecurity(LPCTSTR szObjetNamePrefix, CSid& sidToAdd)
{
CString strObjectName;
strObjectName.Format(szObjetNamePrefix, GetCurrentProcessId());
CDacl dacl;
Win32Check(AtlGetDacl(strObjectName, SE_KERNEL_OBJECT, &dacl));
Win32Check(dacl.AddAllowedAce(sidToAdd, GENERIC_ALL));
Win32Check(AtlSetDacl(strObjectName, SE_KERNEL_OBJECT, dacl));
}
It gets the existing DACL of the kernel object and adds a new access allowed ACE which grants all permissions to the supplied SID. Finally it replaces the DACL of the kernel object with the new DACL.
Using the add-in
Lets see how our friend Alice can use the add-in to solve her debugging problem. Here are the steps:-
- Download the add-in binaries zip file and extract ACLChg.dll and CLRDebugEnable.dll. Both these DLLs need to be present in the same folder.
- Register the add-in by calling
RegSvr32 CLRDebugEnable.dll
. This registers the add-in only for the current user. It does not need any sort of admin privileges for registration. So Alice does not actually need Bob to install this DLL.
- Launch Visual Studio.NET
- The add-in will add a menu option "Enable CLR Debugging" within the Tools menu. Selecting the menu options shows the following dialog.
- The above dialog only shows the processes which has the CLR loaded which Alice cannot debug. Selecting any of these processes enables it to be debugged
- Finally Alice can attach the CLR debugger to the process selected in the previous step. This can be done from the "Debug Processes" menu option in the Tools menu.
Final thoughts
The code currently works with both CLR 1.0 and 1.1. Since the code uses undocumented features from CLR, which I found out from my own research, it may or may not work with the next major release of CLR.