CLRDebugEnable:一个 Visual Studio .NET 插件,允许非管理员帐户调试在不同登录凭据下运行的 CLR 应用程序






4.94/5 (21投票s)
一个 Visual Studio .NET 插件,允许非管理员帐户调试在不同登录凭据下运行的 CLR 应用程序。
背景
Alice 是一家 A 公司(Company A)的 Windows 程序员。Bob 是 B 公司(Company B)的网络管理员。B 公司使用 A 公司的一款产品 P。恰巧,产品 P 在 B 公司的站点上出现了一个问题。这个问题在其他地方无法重现,因此 Alice 被要求去 B 公司的站点进一步了解问题并尽可能修复它。问题在于产品 P 有一个 NT 服务组件,该组件因某些原因而失败。幸运的是,机器上安装了 Visual C++,并且 B 公司购买了产品 P 的源代码。因此,Alice 可以在 B 公司的站点进行调试。
Bob 提供了一个用户名和密码,以便 Alice 可以登录到机器进行调试。当 Alice 启动调试器并将其附加到 NT 服务时,她收到了“访问被拒绝”的错误。Bob 提供的登录凭据是一个具有非常有限权限的普通用户(出于安全原因,Bob 不能将管理员帐户的密码给 Alice)。Alice 作为一名经验丰富的 Windows 程序员,她知道她需要 SE_DEBUG_NAME
权限才能调试在不同凭据下运行的程序。在这种特定情况下,该服务是以低权限域帐户运行的。幸运的是,Bob 决定授予 Alice SE_DEBUG_NAME
权限。下图显示了 Windows XP 机器上的“本地安全策略”MMC 管理单元的屏幕截图,可以通过该管理单元设置权限。
双击“调试程序”会弹出以下对话框。
管理员可以在上述对话框中添加或删除用户或组,以授予或拒绝他们权限。Bob 将 Alice 添加到列表中,她成功地调试了该服务。结果发现,运行该服务的网络帐户无权访问某个目录。问题得到了解决,Alice 也从 Bob 那里得到了一个大大的感谢。
问题所在
两年后,产品 P 迁移到了 .NET - 它被重写成了 C#。Bob 在他的服务器上安装了新版本的产品。结果发现,新版本的产品出现了同样的访问被拒绝问题。Bob 检查以确保他正确设置了应用程序使用的不同目录的权限。结果发现一切设置都正确。Alice 再次被要求去 B 公司的站点解决问题。Bob 通过在该机器上安装 Visual Studio .NET 并授予她调试程序的权限来为 Alice 准备机器。因此,Alice 再次尝试附加调试器。由于该程序现在运行在公共语言运行时 (Common Language Runtime) 下,她从程序类型列表中选择了“公共语言运行时”。在她单击 OK 后,她收到了如图所示的访问被拒绝错误。
Alice 接着决定通过选择程序类型为“本机”来附加本机调试器,并且成功了。Alice 通过尝试附加公共语言运行时调试器进行了双重检查,但再次失败。于是,她请 Bob 在他的凭据下运行调试器。她成功地附加了调试器。不幸的是,她无法做太多调试,因为 Bob 拒绝允许 Alice 在他的凭据下运行调试器。本文的其余部分将尝试找到一些方法来帮助 Alice。
理解问题
这是问题的陈述 - 如果权限由非管理员帐户持有,则 SE_DEBUG_NAME
权限不足以调试在不同凭据下运行的 CLR 应用程序。SE_DEBUG_NAME
权限非常强大。任何拥有此权限的帐户都可以终止进程、注入 DLL 以及分配、读取和写入进程内存。因此,我惊讶地发现它仍然不允许访问 CLR 调试。幸运的是,用于命令行 CLR 调试器的 cordbg 的源代码随框架 SDK 一起提供。在 VS.NET 调试器中运行 cordbg 并尝试将 cordbg 附加到以不同用户凭据运行的进程时,发现在调试 API 深处,一个对 OpenEvent
的特定调用因访问被拒绝而失败。很明显,CLR 调试 API 正试图通过此事件与被调试程序通信。
接下来,我使用 sysinternals 的 procexp 工具查看了被调试进程。典型的 .NET 应用程序在 procexp 中看起来是这样的。
以下对象是我们感兴趣的:
CorDBDebugAttachedEvent_2956
- 一个事件内核对象CorDBIPCSetupSyncEvent_2956
- 另一个事件内核对象Cor_Private_IPCBlock_2956
- 一个文件映射内核对象
使用 procexp 进一步调查 .NET 应用程序发现,一旦 CLR 加载到进程中,它就会创建这三个事件。对象名称末尾的数字是进程的进程 ID。procexp 工具还允许您检查每个对象的安全设置(回想一下,在基于 Windows NT 的操作系统上,您可以为每个内核对象设置安全)。一个对象的安全设置看起来是这样的。
基本上,只有管理员和运行进程的帐户(在上面的示例中为 aspnet)对该对象拥有完全访问权限。没有授予其他人任何访问权限。事实证明,这是任何新内核对象的默认安全设置。还记得许多 Win32 函数中的 SECURITY_ATTRIBUTES
参数,它几乎总是传递为 NULL
。如果将其传递为 NULL
,就会发生这种情况(顺便说一句,在大多数情况下,这是正确的做法)。将 NULL
作为 SECURITY_ATTRIBUTES
传递表示操作系统应为该对象选择默认安全设置。默认安全设置在进程/线程令牌中设置。如果令牌中的默认设置未通过显式调用安全 API 函数进行修改,则只有 *管理员* 组和运行应用程序的用户帐户对任何新内核对象拥有完全访问权限。
这证明了 CLR 显然使用默认设置来创建调试所需的三个内核对象。因此,任何其他非管理员帐户尝试调试该应用程序的尝试都会失败,而这些内核对象是可访问的。
解决方案
幸运的是,任何内核对象创建后的安全设置都可以使用 SetKernelObjectSecurity
/SetNamedSecurityInfo
函数进行修改。安全设置可以通过运行在对象所有者上下文中的代码进行修改。幸运的是,SE_DEBUG_NAME
权限允许权限持有者将 DLL 注入到任何进程中,从而允许代码在目标进程(因此是所有者)的安全上下文中执行。如此注入的代码可以修改对象的安全设置。这是插件如何执行 DLL 注入的代码片段。片段中使用的 dwProcessID
变量保存了需要启用调试的目标进程的 ID。
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)); //Setup communication data file mapping CAtlFileMapping<SID> mapping; CString strMappingName; strMappingName.Format(szACLChgCommData, dwProcessID); mapping.MapSharedMem(MAX_SID_SIZE, strMappingName); //Allow access to everyone so that //the target process can read from the object 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);
代码执行以下操作:
- 在目标进程中分配内存,并将要注入的 DLL(ACLChg.dll)的完整路径写入内存位置。
- 创建一个共享内存段,并将启动进程(devenv.exe)的用户的 SID 写入内存段。文件映射的名称也包含目标进程 ID。
- 最后,它使用
CreateRemoteThread
在LoadLibraryW
地址启动新线程。这允许 ACLChg.dll 被注入到目标进程中。
更改 ACL
新的 ATL 安全类大大简化了 Windows 安全编程。负责修改目标进程 ACL 的 ACLChg.dll 的代码利用了新的安全类。这一切都在 DllMain
函数中完成。这是执行此操作的代码。
CAtlFileMapping<SID> mapping; CString strMappingName; strMappingName.Format(szACLChgCommData, GetCurrentProcessId()); HRCheck(mapping.MapSharedMem(MAX_SID_SIZE, strMappingName)); CSid sidToAdd(*mapping); //Now we need to allow access to this sid for //the kernel objects required for debugging AdjustObjectSecurity(szDebuggerAttachedEvent, sidToAdd); AdjustObjectSecurity(szDBIPCSetupSyncEvent, sidToAdd); AdjustObjectSecurity(szPrivateIPCBlock, sidToAdd);
代码读取由注入 DLL 的进程创建的共享内存段中提供的 SID,并调整 CLR 调试系统使用的三个内核对象的安全性。AdjustObjectSecurity
函数看起来是这样的。
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));
}
它获取内核对象的现有 DACL,并添加一个新的允许访问 ACE,该 ACE 向提供的 SID 授予所有权限。最后,它用新的 DACL 替换内核对象的 DACL。
使用插件
让我们看看我们朋友 Alice 如何使用此插件来解决她的调试问题。步骤如下:
- 下载插件二进制 zip 文件并解压 ACLChg.dll 和 CLRDebugEnable.dll。这两个 DLL 都需要放在同一个文件夹中。
- 通过调用
RegSvr32 CLRDebugEnable.dll
来注册插件。这只会为当前用户注册插件。它不需要任何管理员权限进行注册。因此 Alice 实际上不需要 Bob 来安装这个 DLL。 - 启动 Visual Studio.NET
- 该插件将在 Tools 菜单中添加一个名为“Enable CLR Debugging”的菜单选项。选择菜单选项会显示以下对话框。
- 上述对话框只显示已加载 CLR 但 Alice 无法调试的进程。选择其中任何一个进程都可以对其进行调试
- 最后,Alice 可以将 CLR 调试器附加到上一步中选择的进程。这可以通过 Tools 菜单中的“Debug Processes”菜单选项来完成。
最后的想法
该代码目前支持 CLR 1.0 和 1.1。由于代码使用了来自 CLR 的未公开功能,这些功能是我自己研究出来的,因此它可能适用于也可能不适用于下一个主要版本的 CLR。