SCCM 2007 用户交互式任务序列





5.00/5 (2投票s)
这是一个关于扩展 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”的文件夹,并将我的实验放在里面。

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

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

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

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

你需要将 _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_ 时(方法如下所示)会设置 User
、Domain
和 OwnerID
。
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 日 - 代码演练