65.9K
CodeProject 正在变化。 阅读更多。
Home

颠覆 32 位和 64 位架构下的 Vista UAC

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (143投票s)

2009年4月22日

MIT

14分钟阅读

viewsIcon

630099

downloadIcon

19224

本文说明了如何绕过 Vista UAC,以及如何从 Windows 服务正确启动交互式进程。

引言

*** 注意 *** 该项目已迁移到 GitHub:https://github.com/perspectivism/subverting-vista-uac

本文旨在说明如何在 Windows Vista 中从服务正确启动交互式进程,并演示如何以完全管理员权限启动该进程。交互式进程是指能够显示桌面 UI 的进程。

文章介绍了一个名为LoaderService的服务,该服务充当应用程序加载器,其目的是在启动时启动一个以管理员身份运行的命令提示符。文章最后提供了一个部分,讨论了如何将代码扩展用于更实际的用途。

Vista 中的会话

让我们从头开始……你刚刚启动了电脑并准备登录。当你登录时,系统会为你分配一个唯一的会话 ID。在 Windows Vista 中,第一个登录到计算机的用户由操作系统分配会话 ID 1。下一个登录的用户将被分配会话 ID 2。依此类推。你可以从任务管理器中的“用户”选项卡查看分配给每个已登录用户的会话 ID。

Task Manager - Users

请注意,我指出名为 Pero 的用户控制着控制台。在这种情况下,我指的是物理控制台。物理控制台包括显示器、键盘和鼠标。由于 Pero 控制着键盘、显示器和鼠标,他被认为是当前活动的(在线的)用户。但是,由于用户可以被模拟,因此更合适的说法是引用当前活动的会话,而不是当前活动的(在线的)用户。Win32 API 包含一个名为WTSGetActiveConsoleSessionId()的函数,该函数返回当前控制物理控制台的用户的会话 ID。如果我们现在调用该方法,它将返回 1,因为这是用户 Pero 的会话 ID。

Vista 中存在一个特殊的会话,其会话 ID 为 0。这通常称为会话 0。所有 Windows 服务都在会话 0 中运行,而会话 0 是非交互式的。非交互式意味着无法启动 UI 应用程序;但是,可以通过激活交互式服务检测服务 (ISDS) 来绕过这一点。这不是一个非常优雅的解决方案,本文将不涵盖。有一个简短的 5 分钟Channel 9 视频,演示了 ISDS,有兴趣的读者可以观看。本文假定 ISDS 不存在。现在,由于会话 0 不是用户会话,因此它无法访问视频驱动程序,因此任何渲染图形的尝试都将失败。会话 0 隔离是 Vista 中添加的一项安全功能,用于将系统进程和服务与潜在的恶意用户应用程序隔离。

事情就变得有趣了。这种隔离的原因是系统帐户(或系统用户)具有提升的权限,允许它在不受 Vista UAC 限制的情况下运行。如果所有内容都在系统帐户下运行,那么 Vista UAC 就可以被忽略了。

现在,我知道你在想什么:“如果 Windows 服务在会话 0 中运行,而会话 0 不能启动具有 UI 的进程,那么我们的加载服务如何能够启动一个不仅具有 UI,而且还在当前登录用户的会话中运行的新进程?”看看任务管理器进程选项卡中的这张截图,并特别注意winlogon.exe进程。

Task Manager - Processes

请注意,有两个winlogon.exe进程,而这两个进程的用户都是系统用户。系统用户是一个高度特权的(在线的)用户,不受我们之前讨论的 Vista UAC 的限制。另外,请注意显示winlogon.exe进程在哪个会话中运行的会话 ID。如果你还记得前面提到的,会话 ID 1 指的是用户 Pero 的会话,而会话 ID 2 指的是用户 Sienna 的会话。这意味着在 Pero 的会话中有一个以系统帐户运行的winlogon.exe进程。这也意味着在 Sienna 的会话中有一个以系统帐户运行的winlogon.exe进程。现在是时候提到 ID 大于 0 的任何会话都有能力启动交互式进程,即能够显示 UI 的进程。

解决方案可能还没有完全清楚,但很快就会清楚,因为现在是时候讨论我们的策略了!

我们的策略

首先,我们将创建一个在系统帐户下运行的 Windows 服务。该服务将负责在当前登录用户的会话中启动一个交互式进程。这个新创建的进程将显示 UI 并以完全管理员权限运行。当第一个用户登录到计算机时,该服务将被启动并在会话 0 中运行;但是,该服务启动的进程将在当前登录用户的桌面上运行。我们将此服务称为LoaderService

接下来,winlogon.exe进程负责管理用户登录和注销过程。我们知道,每个登录到计算机的用户都将拥有一个唯一的会话 ID 和与之对应的winlogon.exe进程。现在,我们上面提到,LoaderService在系统帐户下运行。我们也证实,计算机上的每个winlogon.exe进程都在系统帐户下运行。由于系统帐户是LoaderServicewinlogon.exe进程的所有者,我们的LoaderService可以复制winlogon.exe进程的访问令牌(和会话 ID),然后调用 Win32 API 函数CreateProcessAsUser,将一个进程启动到当前登录用户的当前活动会话中。由于复制的winlogon.exe进程访问令牌中的会话 ID 大于 0,我们可以使用该令牌启动一个交互式进程。

现在进入有趣的部分……代码!

代码

Windows 服务位于Toolkit项目中的LoaderService.cs文件中。下面是在启动LoaderService时调用的代码。

protected override void OnStart(string[] args)
{
    // the name of the application to launch
    String applicationName = "cmd.exe";

    // launch the application
    ApplicationLoader.PROCESS_INFORMATION procInfo;
    ApplicationLoader.StartProcessAndBypassUAC(applicationName, out procInfo);
}

上面的代码调用StartProcessAndBypassUAC(...)函数,该函数将启动一个命令提示符(具有完全管理员权限),作为新创建进程的一部分。有关新创建进程的信息将存储在变量procInfo中。

StartProcessAndBypassUAC(...)的代码位于ApplicationLoader.cs文件中。让我们剖析该函数,了解在会话 0 中运行的服务如何加载进程到当前登录用户的会话中。首先,我们将获取当前登录用户的会话 ID。这是通过调用 Win32 API 函数WTSGetActiveConsoleSessionId()来实现的。

// obtain the currently active session id; every logged on 
// User in the system has a unique session id
uint dwSessionId = WTSGetActiveConsoleSessionId();

接下来,我们将获取当前活动会话的winlogon.exe进程的进程 ID(PID)。请记住,当前有两个会话正在运行,如果我们复制了错误的会话的访问令牌,我们可能会将新进程启动到另一个用户的桌面上。

// obtain the process id of the winlogon process that 
// is running within the currently active session
Process[] processes = Process.GetProcessesByName("winlogon");
foreach (Process p in processes)
{
    if ((uint)p.SessionId == dwSessionId)
    {
        winlogonPid = (uint)p.Id;
    }
}

现在我们已经获得了winlogon.exe进程的 PID,我们可以使用该信息获取其进程句柄。要做到这一点,我们调用 Win32 API 函数OpenProcess(...)

// obtain a handle to the winlogon process
hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

获取进程句柄后,我们可以调用 Win32 API 函数OpenProcessToken(...)来获取winlogon.exe进程访问令牌的句柄。

// obtain a handle to the access token of the winlogon process
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
{
    CloseHandle(hProcess);
    return false;
}

有了访问令牌的句柄,我们就可以继续调用 Win32 API 函数DuplicateTokenEx(...)来复制访问令牌。

// Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser
// I would prefer to not have to use a security attribute variable and to just 
// simply pass null and inherit (by default) the security attributes
// of the existing token. However, in C# structures are value types and therefore
// cannot be assigned the null value.
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa);

// copy the access token of the winlogon process; 
// the newly created token will be a primary token
if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, 
        (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, 
        (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
{
    CloseHandle(hProcess);
    CloseHandle(hPToken);
    return false;
}

复制访问令牌有很多优点。在我们的案例中最值得注意的是,我们获得了一个新的主访问令牌副本,该副本还包含该复制令牌的相关登录会话。如果您参考上面显示两个winlogon.exe进程的任务管理器截图,您会注意到复制的会话 ID 将是 1,这是当前登录用户 Pero 的会话 ID。我们现在可以调用 Win32 API 函数CreateProcessAsUser,在当前登录用户的会话中启动一个新进程;在这种情况下,进程将在用户 Pero 的会话中启动。总而言之,下面的代码在会话 0 中运行,但将在会话 1 中启动一个新进程。

STARTUPINFO si = new STARTUPINFO();
si.cb = (int)Marshal.SizeOf(si);

// interactive window station parameter; basically this indicates 
// that the process created can display a GUI on the desktop
si.lpDesktop = @"winsta0\default";

// flags that specify the priority and creation method of the process
int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

// create a new process in the current User's logon session
bool result = CreateProcessAsUser(hUserTokenDup,  // client's access token
                                null,             // file to execute
                                applicationName,  // command line
                                ref sa,           // pointer to process SECURITY_ATTRIBUTES
                                ref sa,           // pointer to thread SECURITY_ATTRIBUTES
                                false,            // handles are not inheritable
                                dwCreationFlags,  // creation flags
                                IntPtr.Zero,      // pointer to new environment block 
                                null,             // name of current directory 
                                ref si,           // pointer to STARTUPINFO structure
                                out procInfo      // receives information about new process
                                );

上面的代码将启动一个以系统帐户在管理员身份下运行的命令提示符。我想评论一下参数 @"winsta0\default"。这是一个硬编码的String,Microsoft 任意选择它来指示操作系统,我们将在CreateProcessAsUser中启动的进程应具有对交互式窗口站和桌面的完全访问权限,这基本上意味着它被允许在桌面上显示 UI 元素。

代码就是这些。现在,让我们讨论如何使用 MSI 部署此服务,以及如何配置它以便在计算机启动时自动启动!

部署代码

部署代码的最有效方法是为此创建一个 MSI 安装程序。但是,我们必须首先执行一些任务来准备我们的服务进行安装。首先,我们需要为我们的LoaderService添加一个安装程序。要添加安装程序,请打开LoaderService.cs设计器。然后,右键单击并选择“添加安装程序”。

Add Installer To LoaderService

上述操作向项目中添加了一个名为ProjectInstaller的新类。该类继承自Installer类。在ProjectInstaller.cs的设计器中可以看到两个组件,我已经将它们重命名以提高清晰度:loaderServiceProcessInstallerloaderServiceInstallerloaderServiceProcessInstaller控件允许我们指定LoaderService将在其下运行的帐户。该帐户已设置为System

ProjectInstaller Screenshot

现在,我们可以添加一个设置项目。设置项目的首要输出设置为Toolkit项目,其中包含我们的LoaderService。这一步相当简单,我将不详细介绍。但是,我想评论一下,我们需要将ProjectInstaller类挂接到此 MSI。如果我们不这样做,那么Toolkit项目的内容将被部署,但LoaderService将不会被注册为 Windows 服务。要添加自定义操作,请右键单击设置项目,然后转到“视图”>“自定义操作”。从这里,您可以添加自定义操作。将自定义操作指定为Toolkit的首要输出足以提示它有一个自定义安装程序,在本例中是ProjectInstaller,需要运行。请记住,ProjectInstaller是实际上负责向 Windows 注册服务的安装程序类。

Loader Service Setup

现在,是时候运行代码并看到我们辛勤劳动的成果了!

运行代码

要验证代码是否按预期工作,我们将构建 MSI 并进行安装。当您安装 MSI 时,您会注意到一个 UAC 提示要求您确认安装。我的一位好海军朋友曾经告诉我,海军有句谚语:“一天是海军,永远是海军。”在黑客攻击和计算机安全领域,这可以翻译为:“一旦是管理员,就永远是管理员。”这是用户安装您的项目时将看到的唯一一次 UAC 弹出窗口。由于大多数 MSI 都需要管理员权限才能安装,因此用户对此应该不会感到意外。

安装后,您会注意到该服务已被注册为自动启动(由ProjectInstaller);但是,这只会在下次重启时发生。您也可以手动启动它。本文假定您已选择重启。请注意,当计算机重启时,您会看到一个以管理员身份运行的命令提示符。

Command Prompt

从这里,您可以输入regeditgpedit.msc,或任何您喜欢的命令,它将绕过 Vista UAC 提示。更重要的是,当前登录的用户甚至不需要是管理员就可以利用此命令提示符。原因是命令提示符是在系统帐户下运行的。从下面的截图中的任务管理器可以看到这一点。也请注意会话 ID。

Task Manager : cmd

但是,我们的LoaderService呢?它在哪里,在哪个会话中运行?让我们再看看任务管理器来找出答案。

Task Manager : LoaderService

我们已经成功绕过了 Vista UAC,并说明了如何从 Windows 服务正确启动交互式进程。但是,我们还可以做得更多!

基础知识之外

本节讨论的主题未包含在可下载的代码中。原因是让示例解决方案尽可能简单。下面的想法旨在说明如何扩展示例代码以支持不同类型的场景。

通用解决方案

大多数 Windows 服务在第一个用户登录到计算机时启动,并在会话 0 中启动。我们当前的代码编写方式是,只有第一个登录到计算机的用户才能在其会话中启动命令提示符。原因是我们的LoaderService从其OnStart函数启动进程。OnStart函数只执行一次,也就是服务首次启动时。由于第一个登录到系统的用户有效地启动了会话 0 中的所有服务,因此当OnStart函数调用StartProcessAndBypassUAC时,他是其会话 ID 将被检索的用户。为了清晰起见,LoaderService.cs中的OnStart函数已重复如下。

protected override void OnStart(string[] args)
{
    // the name of the application to launch
    String applicationName = "cmd.exe";

    // launch the application
    ApplicationLoader.PROCESS_INFORMATION procInfo;
    ApplicationLoader.StartProcessAndBypassUAC(applicationName, out procInfo);
}

那么,我们如何配置我们的LoaderService,以便在每个用户首次登录到计算机时为他们启动命令提示符?解决方案在于OnStart函数:要么连接一个Timer,要么启动一个Thread,该Thread每隔几秒钟(可以使用Thread.Sleep(1000)来控制Thread运行的频率)以无限循环运行。我们可以使用一个List<int>对象来跟踪我们已经启动了进程的所有会话 ID。每次我们的Thread执行时,我们都会检查会话 ID 是否已更改。当前活动会话 ID 可以通过调用WTSGetActiveConsoleSessionId()来检索。如果会话 ID 已更改,我们会检查我们是否已启动进程到该会话。如果我们还没有,那么我们调用StartProcessAndBypassUAC并将会话 ID 添加到List<int>对象。

按需启动应用程序

您可能不希望您的应用程序在用户登录到计算机时立即启动。您可能有一个应用程序,应该只在用户选择运行它时才加载。此外,您可能希望此应用程序和功能可供系统上的所有用户使用。那么问题是,我们的LoaderService如何在绕过 Vista UAC 的同时满足这一点?

在我们开始讨论之前,让我们快速谈谈 UAC 在文件和文件夹访问方面的应用。Vista 支持特殊文件夹的概念。Vista 中有几个特殊文件夹,但我们将重点关注Documents文件夹。在 .NET 中,您可以通过调用Environment.GetFolderPath(...)来查询特殊文件夹的位置。

如果您花足够的时间在电脑上,您可能会注意到您可以自由地创建、修改和删除位于您的Documents文件夹中的文件,而不会受到 UAC 的任何干扰。但是,如果您导航到另一个用户的Documents文件夹,您将看到一个 UAC 提示,要求管理员权限才能访问该文件夹。您可能还注意到有一个公共Documents文件夹,该文件夹被所有用户共享并可供访问。在 Vista 中,此特殊文件夹的路径是C:\Users\Public\Documents。系统上的任何用户都可以自由地在此创建、修改和删除文件,而不会受到 UAC 的任何干扰。

现在,我们可以构建一个解决方案!我们可以修改我们LoaderServiceOnStart函数,来启动FileSystemWatcher类的实例,并将其配置为监视公共Documents文件夹的变化,所有用户都可以访问该文件夹。我们将不得不创建一个新的控制台应用程序,通过文本文件与LoaderService通信(不要将此控制台应用程序与控制台会话混淆)。控制台应用程序的代码如下。

static void Main(string[] args)
{
    string filename = @"C:\Users\Public\Documents\appToLoad.txt";
    using (StreamWriter sw = new StreamWriter(filename, false))
    {
        sw.WriteLine("SessionID=" + WTSGetActiveConsoleSessionId());
        sw.WriteLine("ApplicationToLoad=cmd.exe");
        sw.Close();
    }
}

当看到appToLoad.txt文件时,LoaderService将解析该文件并在当前活动会话中启动一个命令提示符。此时,我们已经成功地说明了如何使用用户应用程序与服务通信,以及如何让它以完全管理员权限启动应用程序,同时绕过 Vista UAC。

参考文献

历史

  • 2023/06/18:文章和代码已迁移到 GitHub
  • 2009/04/21:首次发布。
© . All rights reserved.