Tag: CreateProcessWithTokenW

  • 信息学竞赛在线评测系统的安全性解决方案探讨

    • 信息学竞赛在线评测系统的安全性解决方案探讨

    一个高质量的 OJ 系统, Judger 要求有很高的安全性,因为它需要运行用户提交上来的代码,而用户会提交什么样的代码是无法预知的。 Judger 首先要把用户的代码编译成可执行文件,然后运行这个可执行文件,所以 Judger 的安全性主要是限制解题程序的行为,阻止它执行危险的操作。

    根据 DCS 评级,要达到 ProCode 以上级别的 Judger 程序,需要至少达到以下的安全目标:

    1. 阻止选手程序读写文件;
    2. 阻止选手程序读写注册表;
    3. 阻止选手程序修改系统设置(例如修改系统时间等);
    4. 阻止选手程序关闭系统(包含注销等操作);
    5. 阻止选手程序调用 API 函数;
    6. 阻止选手程序恶意提交代码(例如通过一些语言提供的支持库重复提交自身代码)。

     
    对于第四点,程序设计者可以将运行的进程放入一个作业对象中(可以参考《Windows核心编程》第五章)。对于第五点和第六点,可以将编译器中有关 API 的头文件和库文件删除,令其不能通过编译。
    本文主要关注如何解决第一、第二和第三点,要实现这几个目标,仅仅靠判题程序是不能解决问题的,一定要对操作系统进行相应的配置。对于第一点,只需要在系统中新建一个用户,在各个驱动器中的安全设置中拒绝该用户的访问,然后以这个新用户的身份来运行解题程序即可。类似的,对于第二点,只要拒绝这个用户访问注册表的各个主键即可。第三点,由于修改系统设置需要管理员特权,因此去除这个用户的大部分特权,只保留必要的特权即可。为什么不去除所有特权呢?因为有些特权是必需的,这个会在下文讲解。总的来说,这个解决方案的主要思想就是建立一个几乎无权限的用户,然后以这个用户的身份去运行解题程序。下面主要说明一下一些需要注意的问题。
     
    一、用户特权设置
    刚刚建立的用户是没有显式赋予任何特权的,这是理想的设置,不过我们仍然要赋予它一个“允许本地登录”的特权,因为我们要获取该用户的访问令牌,这只有在用户登录后获得。
    具体步骤:运行“gpedit.msc”,定位到“计算机配置-Windows设置-安全设置-本地策略-用户权限分配”,右侧列表中显示的就是用户可以拥有的特权列表。找到“允许本地登录”项,并双击,点击“添加用户或组”,在弹出的窗口中点击“对象类型”,选中“组”,确定返回,然后在下面的文本框中输入刚刚建立的用户组的名称,依次点击“确定”即可。
     
    二、以新用户的身份启动选手程序
    这个问题是本文将要说明的重点。无论是以指定用户身份启动进程还是以特定被降权了的 Token 启动进程都有如下几个Windows API函数可以使用(本文假定程序员使用 Unicode ):
    BOOL WINAPI CreateProcessAsUser(
    __in HANDLE hToken,
    __in LPCTSTR lpApplicationName,
    __in LPTSTR lpCommandLine,
    __in LPSECURITY_ATTRIBUTES lpProcessAttributes,
    __in LPSECURITY_ATTRIBUTES lpThreadAttributes,
    __in BOOL bInheritHandles,
    __in DWORD dwCreationFlags,
    __in LPVOID lpEnvironment,
    __in LPCTSTR lpCurrentDirectory,
    __in LPSTARTUPINFO lpStartupInfo,
    __out LPPROCESS_INFORMATION lpProcessInformation
    );
     
    BOOL WINAPI CreateProcessWithLogonW(
    __in LPCWSTR lpUsername,
    __in LPCWSTR lpDomain,
    __in LPCWSTR lpPassword,
    __in DWORD dwLogonFlags,
    __in LPCWSTR lpApplicationName,
    __in LPWSTR lpCommandLine,
    __in DWORD dwCreationFlags,
    __in LPVOID lpEnvironment,
    __in LPCWSTR lpCurrentDirectory,
    __in LPSTARTUPINFOW lpStartupInfo,
    __out LPPROCESS_INFORMATION lpProcessInfo
    );
     
    BOOL WINAPI CreateProcessWithTokenW(
    __in HANDLE hToken,
    __in DWORD dwLogonFlags,
    __in LPCWSTR lpApplicationName,
    __in LPWSTR lpCommandLine,
    __in DWORD dwCreationFlags,
    __in LPVOID lpEnvironment,
    __in LPCWSTR lpCurrentDirectory,
    __in LPSTARTUPINFOW lpStartupInfo,
    __out LPPROCESS_INFORMATION lpProcessInfo
    );
     
    以上这三个函数中第二个和第三个均没有提供 ANSI 版本。
    因为需要启动的选手程序是需要分配给一个作业对象的,而 CreateProcessWithLogonW和 CreateProcessWithTokenW 函数启动的进程默认绑定到了一个匿名的对象,所以只能使用 CreateProcessAsUser 这个函数。
    CreateProcessAsUser 函数与 CreateProcess 函数的参数几乎完全一样,唯一不同的是多了一个 hToken 参数。这个参数是某个用户的 Token ,标识了这个用户的身份,新的进程就使用这个令牌来运行。要获得用户的令牌,需要使用 LogonUser 函数(关于它的使用,请参考MSDN)。
    调用该函数之后可以获得访问令牌,将该 Token 传入 CreateProcessAsUser 函数的第一个参数,就可以以指定用户的身份启动进程了。
    三、关于 CreateProcessAsUser
    关于 CreateProcessAsUser 函数,有很多需要注意的地方。首先,调用该函数的进程令牌中需要有两个特权: SE_ASSIGNPRIMARYTOKEN_NAME (替换一个进程级令牌)和 SE_INCREASE_QUOTA_NAME (为进程调整内存配额),否则函数调用会失败。这意味着 Judger 需要以较高权限的用户身份运行。例如,一个 Judger 普遍作为一个Windows服务运行,需要以 Local System 用户的身份来运行。很明显这样做存在安全隐患,因为 Local System 的权限实在是太大了。相比之下 CreateProcessWithLogonW 和 CreateProcessWithTokenW 则不需要这两个特权,安全性更佳。这也是使用 CreateProcessAsUser 的无奈之处。于是可以再建立一个用户专门用来运行判题程序,只赋予它这两个必要的特权。
    另一个要注意的问题是,以 CreateProcessAsUser 启动的进程默认是不能交互的,换句话说,这个进程不能显示窗口界面,以及不能执行与窗口有关的操作,例如接收命令行输入,向命令行窗口输出信息等,甚至不能通过管道与该进程的标准输入输出通信——即使在调用 CreateProcessAsUser 的时候指定了 CREATE_NO_WINDOW 这个标记也是如此。所以必须使启动后的进程是可交互的。
    这里要简单讲一下可交互的条件。在Windows中有两种对象, WindowStation 和 Desktop , WindowStation 是 Windows 中一种重要的安全对象, Desktop 是包含在 WindowStation 中的。这两种对象都可以存在多个,但是只有在某个特定的 Desktop 中启动的进程才是可交互的,这个 Desktop 名为“default”,位于名为“winsta0”的 WindowStation 中。要想在这个 Desktop 中启动进程,需要进程具有访问“winsta0”和“default”的权限。没有桌面权限连 Win32 子系统都启动不起来。
    要达到这个目的,需要允许上面建立的运行解题程序的用户访问这两个对象,这是在调用 LogonUser 函数获得令牌后进行的,而不是手动配置的。MSDN上有一篇题为《Starting an Interactive Client Process in C++》的文章对此进行了详述,文章中的代码比较多,也比较复杂,这里简单讲一下文章中代码的原理:

    1. 获取 winsta0 WindowStation 的句柄。
    2. 获取 winsta0 的安全描述符。
    3. 复制原来的安全描述符。
    4. 获取安全描述符中的 DACL 。
    5. 复制原来的 DACL 。
    6. 向复制得到的 DACL 中添加允许用户访问的 ACE 。
    7. 将新的 DACL 设置到复制得到的安全描述符中。
    8. 将新的安全描述符设置到 winsta0 中。

    对于default Desktop的操作也是一样的。
    至此,我们可以正确地启动选手程序,并可以保证它的安全性了。
     
    参考资料:
    [Windows]ACM在线判题系统的安全性解决方案
    Win7下超级管理员创建普通权限任务