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

SCCM 2007 用户交互式任务序列

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2011年8月31日

CPOL

5分钟阅读

viewsIcon

55139

downloadIcon

721

这是一个关于扩展 SCCM 2007 任务序列的行为以与用户交互的教程

引言

微软说这不可能... 我不这么认为!

SCCM 2007 有一个非常有用的功能叫做操作系统部署 (OSD)。OSD 任务序列 (TS) 是一个非常强大的条件引擎,可以执行从安装应用程序到运行工具以返回有关正在运行 TS 的远程系统信息的任何操作。了解这一点,OSD 和 TS 的创建初衷是让你像使用 MDT 一样自动化映像过程,但 TS 有潜力做更多的事情。这就是这个工具发挥作用的地方。想象一下,你需要按照特定顺序安装一组应用程序,这已经可以做到,但你需要用户与此安装过程进行交互(回答问题或选择选项)。目前,SCCM 2007 任务序列没有与用户交互的能力。在网上搜索,你会发现一些关于如何实现它的零星讨论(见此链接)。我借鉴了类似博客的想法,并使用 http://pinvoke.net/ 作为我需要的 Windows API 调用的参考,创建了一个 C# 版本。

使用 SCCM 2007 控制台进行交互式任务序列

我设置这个项目只包含基本功能。你可以根据自己的需求进行扩展。我们开始使用 SCCM 2007 控制台中的 TS,然后回顾代码!

首先,你需要打开你的 SCCM 2007 控制台并导航到 OSD 部分。在 OSD 下,你会看到 TS 部分。你可以将它们组织到功能文件夹中。我创建了一个名为“Test”的文件夹,并将我的实验放在里面。

sccm_console.png

创建文件夹后,你可以点击“新建任务序列”,然后会出现一个向导。

new_task_sequence.png

选择“创建新的自定义任务序列”。这将为你提供一个空的 TS。

new_TS.png

在你的新 TS 中,点击“添加” > “常规” > “运行命令行”。

ts_add.png

这就是魔术发生的地方。我们创建的这个应用程序只不过是一个应用程序启动器,它将在 TS 中的任何给定步骤,在满足任何给定条件后,向用户显示给定的应用程序,从而改变 TS 默认的对用户隐藏一切的行为!

pick_a_package.png

你需要将 _launcher.exe_ 和 launch2interactiveuser.exe 与你希望用户在步骤中使用的任何其他可执行文件一起放在 SCCM 包中。“包”复选框需要被选中并使用。

Using the Code

现在让我们来看看代码。这个过程基本上是复制当前登录用户的令牌,并使用该复制的令牌执行目标可执行文件。

我在直接从代码启动外部应用程序时遇到一个问题……我需要退出代码返回到 _launch2interactiveuser.exe_,以便我可以将其返回给 TS 引擎,以便在 TS 步骤中做出更多决策。当在登录用户上下文中启动时,启动的应用程序在不同的进程中运行。为了解决这个问题,你会看到下面有一个名为 _launcher.exe_ 的小型可执行文件,我用它将退出代码传递回 _launch2interactiveuser.exe_。我相信有更优雅的方法可以做到这一点,但我追求的是一种快速简单的方法,在企业中几乎适用于所有情况。

namespace launch2InteractiveUser
{
    class Program
    {

下面提供一些我们需要的 DLL 的引用,用于将应用程序启动到已登录用户的会话中并复制他们的令牌。

[DllImport("advapi32", SetLastError = true),
SuppressUnmanagedCodeSecurityAttribute]
static extern int OpenProcessToken(
System.IntPtr ProcessHandle, // handle to process
int DesiredAccess, // desired access to process
ref IntPtr TokenHandle // handle to open access token
);

[DllImport("kernel32", SetLastError = true),
SuppressUnmanagedCodeSecurityAttribute]
static extern bool CloseHandle(IntPtr handle);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public extern static bool DuplicateToken(IntPtr ExistingTokenHandle,
int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

public const int TOKEN_DUPLICATE = 2;
public const int TOKEN_QUERY = 0X00000008;
public const int TOKEN_IMPERSONATE = 0X00000004;
public static string User;
public static string Domain;
public static string OwnerSID; 
static void Main(string[] args)
{
    string app = "";
    string arg = ""; 

因此,以下命令行参数将我们启动到用户的目标应用程序配置好。

int timeout = 1800000;

foreach (string s in args)
{
    if (s.Contains("/app="))
        app = s.Substring(5);
    if (s.Contains("/args="))
        arg = s.Substring(6);
    if (s.Contains("/timeout="))
        timeout = Int32.Parse(s.Substring(9));
}
string USERID = "";

在这里,我们调用 WMI 来获取登录用户。

using (ManagementClass computer = new ManagementClass("Win32_ComputerSystem"))
{
    ManagementObjectCollection localComputer = computer.GetInstances();
    foreach (ManagementObject mo in localComputer)
    {
        try
        {
            //get login user data
            USERID = mo["UserName"].ToString();
            Console.WriteLine("User name in Win32_ComputerSystem(" + USERID + ")");
        }
        catch (Exception er)
        {
            Environment.Exit(999);
        }
    }
}
//Console.ReadKey(true);

如果用户可用,我们会查找由该用户运行的任何进程。调用 _GetProcessInfoByPID_ 时(方法如下所示)会设置 UserDomainOwnerID

if (USERID != "")
{
    int PID = 0;
    int SID = 0;
    Process[] localByName = Process.GetProcesses();
    int x = 0;
    foreach (Process p in localByName)
    {
        string results = GetProcessInfoByPID(p.Id);
        Console.WriteLine(results + " " + User + " " + Domain);
        string s = Domain + "\\" + User;
        if (s.ToLower() == USERID.ToLower() && p.Responding)
        {
            PID = p.Id;
            SID = p.SessionId;
            x = 1;
            break;
        }
    }

现在我们有了用户上下文中的任何进程,我们需要模拟该进程分配给该用户的令牌并复制它。Windows API 有一个方便的方法可以做到这一点,所以让我们调用 OpenProcessToken()

if (x == 1)
{
    IntPtr hToken = IntPtr.Zero;
    
    Process proc = Process.GetProcessById(PID);
    if (OpenProcessToken(proc.Handle,
    TOKEN_QUERY | TOKEN_IMPERSONATE | TOKEN_DUPLICATE,
    ref hToken) != 0)
    {
        try
        {
            string path2 = System.IO.Path.GetDirectoryName
            (System.Reflection.Assembly.GetExecutingAssembly().Location);

由于我们需要获取退出代码,我们将调用我们在这里定义的小型启动器应用程序。

FileInfo launcher = new FileInfo(path2+@"\launcher.exe");
if (!launcher.Exists)
{
    CloseHandle(hToken);
    Console.WriteLine("Missing Launcher in execution directory");
    Environment.Exit(997);
}

DateTime start = DateTime.Now;
DateTime end = DateTime.Now.AddMinutes(30);
try
{
    end = DateTime.Now.AddSeconds(Double.Parse(args[1]));
}
catch { }

这就是魔术!!!! CreateProcessAsUser() 是 Windows API 调用,它会像用户自己双击可执行文件一样生成应用程序。我们基本上是用参数调用 _launcher.exe_ 来启动我们的目标应用程序。我们监视进程并等待它完成。我们在项目早期设置了 30 分钟的超时,但也可以传递一个超时参数。

CreateProcessAsUser(hToken, app,arg);

Process myProcess = Process.GetProcessById(proInfo.dwProcessID);

myProcess.WaitForExit(timeout);

//string path = System.IO.Path.GetDirectoryName
(System.Reflection.Assembly.GetExecutingAssembly().Location);
string path = Environment.GetEnvironmentVariable("Temp");

现在进程已经运行完毕,我们去获取退出代码。

FileInfo exitCodeFile = new FileInfo(path+"\\exit.results");

if (exitCodeFile.Exists)
{
    string code = "";
    using (StreamReader sr = new StreamReader(exitCodeFile.FullName))
    {
        code = sr.ReadToEnd();
    }
    
    exitCodeFile.OpenText().Dispose();
    
    Console.WriteLine("Exit Code: " + code);
    
    exitCodeFile.Delete();
    //searchingForExitCodeFile = false;
    CloseHandle(hToken);
    //Console.ReadKey(true);
    Environment.Exit(Int32.Parse(code));
}
else if (DateTime.Now > end)
{
    Console.WriteLine("Process has timed out.");
    CloseHandle(hToken);
    //Console.ReadKey(true);
    Environment.Exit(996);
}
else
{
    Console.WriteLine("Exit.results file missing or something bad happened");
    CloseHandle(hToken);
    //Console.ReadKey(true);
    Environment.Exit(995);
}

非常重要!!!你必须始终对你的复制令牌执行 CloseHandle()

                }
                finally
                {
                    CloseHandle(hToken);
                }
            }
            else
            {
                string s = String.Format("OpenProcess Failed {0}, 
                privilege not held", Marshal.GetLastWin32Error());
                throw new Exception(s);
            }
        }
        else
        {
            Environment.Exit(998);
        }
    }
    else
    {
        Environment.Exit(999);
    }
    
    //All done
} 

这是上面提到的帮助我们查找用户启动的任何进程的方法。

static string GetProcessInfoByPID(int PID)
        {
            User = String.Empty;
            Domain = String.Empty;
            OwnerSID = String.Empty;
            string processname = String.Empty;
            try
            {
                ObjectQuery sq = new ObjectQuery
                    ("Select * from Win32_Process Where ProcessID = '" + PID + "'");
                ManagementObjectSearcher searcher = new ManagementObjectSearcher(sq);
                if (searcher.Get().Count == 0)
                    return OwnerSID;
                foreach (ManagementObject oReturn in searcher.Get())
                {
                    string[] o = new String[2];
                    //Invoke the method and populate the o var with the user name and domain
                    oReturn.InvokeMethod("GetOwner", (object[])o);

                    //int pid = (int)oReturn["ProcessID"];
                    processname = (string)oReturn["Name"];
                    //dr[2] = oReturn["Description"];
                    User = o[0];
                    if (User == null)
                        User = String.Empty;
                    Domain = o[1];
                    if (Domain == null)
                        Domain = String.Empty;
                    string[] sid = new String[1];
                    oReturn.InvokeMethod("GetOwnerSid", (object[])sid);
                    OwnerSID = sid[0];
                    return OwnerSID;
                }
            }
            catch
            {
                return OwnerSID;
            }
            return OwnerSID;
        } 

以下是 Windows API 调用的包装器。这里的大部分代码是从 http://pinvoke.net/ 借用的存根。

static ProcessUtility.PROCESS_INFORMATION proInfo = 
			new ProcessUtility.PROCESS_INFORMATION();

        static void CreateProcessAsUser(IntPtr token, string app, string args)
        {
            IntPtr hToken = token;
            //IntPtr hToken = WindowsIdentity.GetCurrent().Token;
            IntPtr hDupedToken = IntPtr.Zero;

            ProcessUtility.PROCESS_INFORMATION pi = 
			new ProcessUtility.PROCESS_INFORMATION();

            try
            {
                ProcessUtility.SECURITY_ATTRIBUTES sa = 
			new ProcessUtility.SECURITY_ATTRIBUTES();
                sa.Length = Marshal.SizeOf(sa);

                bool result = ProcessUtility.DuplicateTokenEx(
                      hToken,
                      ProcessUtility.GENERIC_ALL_ACCESS,
                      ref sa,
                      (int)ProcessUtility.SECURITY_IMPERSONATION_LEVEL.
				SecurityIdentification,
                      (int)ProcessUtility.TOKEN_TYPE.TokenPrimary,
                      ref hDupedToken
                   );

                if (!result)
                {
                    throw new ApplicationException("DuplicateTokenEx failed");
                }


                ProcessUtility.STARTUPINFO si = new ProcessUtility.STARTUPINFO();
                si.cb = Marshal.SizeOf(si);
                //si.lpDesktop = String.Empty;winsta0\default
                si.lpDesktop = "winsta0\\default";
                

                string execPath = System.IO.Path.GetDirectoryName
		(System.Reflection.Assembly.GetExecutingAssembly().Location);

                ProcessUtility.PROFILEINFO profileInfo = new ProcessUtility.PROFILEINFO();
                
                try
                {
                    result = ProcessUtility.LoadUserProfile(hDupedToken, ref profileInfo);
                }
                catch { }

                if (!result)
                {
                    int error = Marshal.GetLastWin32Error();
                    string message = String.Format("LoadUserProfile Error: {0}", error);
                    throw new ApplicationException(message);
                }
                
                if (args == "")
                {
                    
                    result = ProcessUtility.CreateProcessAsUser(
				hDupedToken,       //Token
				null,               //App Name
				execPath + "\\launcher.exe \"" + 
					app + "\"", //CmdLine
				ref sa,            //Security Attribute
				ref sa,            //Security Attribute
				true,             //Bool inherit handle??
				0,                 //Int Creation Flags
				IntPtr.Zero,       //Environment
				execPath,          //Current Directory"c:\\"
				ref si,            //StartInfo
				ref pi             //ProcessInfo
);
                }
                else
                {
                    result = ProcessUtility.CreateProcessAsUser(
				hDupedToken,       //Token
				null,               //App Name
				execPath + "\\launcher.exe \"" + app +"\" \"" + 
					args + "\"",               //CmdLine
				ref sa,            //Security Attribute
				ref sa,            //Security Attribute
				true,             //Bool inherit handle??
				0,                 //Int Creation Flags
				IntPtr.Zero,       //Environment
				execPath,          //Current Directory"c:\\"
				ref si,            //StartInfo
				ref pi             //ProcessInfo
                                   );
                }

                try
                {
                    result = ProcessUtility.UnloadUserProfile
				(hDupedToken, profileInfo.hProfile);
                }
                catch { }

                proInfo = pi;

                if (!result)
                {
                    int error = Marshal.GetLastWin32Error();
                    string message = String.Format
				("CreateProcessAsUser Error: {0}", error);
                    throw new ApplicationException(message);
                }
            }
            finally
            {
                if (pi.hProcess != IntPtr.Zero)
                    ProcessUtility.CloseHandle(pi.hProcess);
                if (pi.hThread != IntPtr.Zero)
                    ProcessUtility.CloseHandle(pi.hThread);
                if (hDupedToken != IntPtr.Zero)
                    ProcessUtility.CloseHandle(hDupedToken);
            }
        }
    }

    public class ProcessUtility
    {
        [StructLayout(LayoutKind.Sequential)]
        public struct STARTUPINFO
        {
            public Int32 cb;
            public string lpReserved;
            public string lpDesktop;
            public string lpTitle;
            public Int32 dwX;
            public Int32 dwY;
            public Int32 dwXSize;
            public Int32 dwXCountChars;
            public Int32 dwYCountChars;
            public Int32 dwFillAttribute;
            public Int32 dwFlags;
            public Int16 wShowWindow;
            public Int16 cbReserved2;
            public IntPtr lpReserved2;
            public IntPtr hStdInput;
            public IntPtr hStdOutput;
            public IntPtr hStdError;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct PROCESS_INFORMATION
        {
            public IntPtr hProcess;
            public IntPtr hThread;
            public Int32 dwProcessID;
            public Int32 dwThreadID;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct PROFILEINFO
        {
            public int dwSize;
            public int dwFlags;
            [MarshalAs(UnmanagedType.LPTStr)]
            public String lpUserName;
            [MarshalAs(UnmanagedType.LPTStr)]
            public String lpProfilePath;
            [MarshalAs(UnmanagedType.LPTStr)]
            public String lpDefaultPath;
            [MarshalAs(UnmanagedType.LPTStr)]
            public String lpServerName;
            [MarshalAs(UnmanagedType.LPTStr)]
            public String lpPolicyPath;
            public IntPtr hProfile;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct SECURITY_ATTRIBUTES
        {
            public Int32 Length;
            public IntPtr lpSecurityDescriptor;
            public bool bInheritHandle;
        }

        public enum SECURITY_IMPERSONATION_LEVEL
        {
            SecurityAnonymous,
            SecurityIdentification,
            SecurityImpersonation,
            SecurityDelegation
        }

        public enum TOKEN_TYPE
        {
            TokenPrimary = 1,
            TokenImpersonation
        }

        public const int GENERIC_ALL_ACCESS = 0x10000000;

        [
           DllImport("kernel32.dll",
              EntryPoint = "CloseHandle", SetLastError = true,
              CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)
        ]
        public static extern bool CloseHandle(IntPtr handle);

        [
           DllImport("advapi32.dll",
              EntryPoint = "CreateProcessAsUser", SetLastError = true,
              CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)
        ]
        public static extern bool
           CreateProcessAsUser(IntPtr hToken, string lpApplicationName, 
				string lpCommandLine,
                               	ref SECURITY_ATTRIBUTES lpProcessAttributes, 
				ref SECURITY_ATTRIBUTES lpThreadAttributes,
                               	bool bInheritHandle, Int32 dwCreationFlags, 
				IntPtr lpEnvrionment,
                               	string lpCurrentDirectory, 
				ref STARTUPINFO lpStartupInfo,
                               	ref PROCESS_INFORMATION lpProcessInformation);

        [
           DllImport("advapi32.dll",
              EntryPoint = "DuplicateTokenEx")
        ]

        public static extern bool
           DuplicateTokenEx(IntPtr hExistingToken, Int32 dwDesiredAccess,
                            ref SECURITY_ATTRIBUTES lpThreadAttributes,
                            Int32 ImpersonationLevel, Int32 dwTokenType,
                            ref IntPtr phNewToken);

        [
            DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)
        ]
        public static extern bool 
            LoadUserProfile(IntPtr hToken, 
                            ref PROFILEINFO lpProfileInfo);

        [
            DllImport("userenv.dll", SetLastError = true, CharSet = CharSet.Auto)
        ]
        public static extern bool 
            UnloadUserProfile(IntPtr hToken, 
                                IntPtr hProfile);
    }

_Launcher.exe_ 应用程序。正如我之前所说,我不得不设计一种方法来传递退出代码。就我而言,用户和 SYSTEM 帐户可以共享的最佳位置是 Environment.GetEnvironmentVariable("Temp"); 目录。

 namespace launcher
{
    class Program
    {
        static void Main(string[] args)
        {
            char[] split = { ' ' };

            Process process = new Process();
            process.StartInfo.FileName = args[0];
            if (args.Length > 1)
            {
                process.StartInfo.Arguments = args[1];
            }
            process.StartInfo.WindowStyle = ProcessWindowStyle.Normal;
            process.Start();
            process.WaitForExit();
            int exitCode = process.ExitCode;
            string path = Environment.GetEnvironmentVariable("Temp");
            using (StreamWriter outfile =
            new StreamWriter(path + @"\exit.results"))
            {
                outfile.Write(exitCode.ToString());
                outfile.Close();
                outfile.Dispose();
            }
        }
    }
} 

关注点

这解决了“你能否部署所有这些基于最终用户决定的自定义进程与 SCCM?”的问题。通常的答案是否定的。通常的过程可能包含让用户点击某个东西来启动它,或者发送电子邮件链接等选项。你依赖于其他人来完成你的任务。现在你可以强制将任务呈现给用户。如果用户拒绝,你也会在 TS 中记录下来。

历史

  • 2011 年 8 月 30 日 - 初始发布
  • 2011 年 8 月 31 日 - 代码演练
© . All rights reserved.