While working on Smuggly I discovered that when I wanted to show the screensaver settings dialog the screensaver process was executing as the System user. Interestingly, this would allow most user settings to be stored and retrieved but passwords saved with DPAPI could not be retrieved. I realized I would have to create the process as a different user. After some digging, I found a method to do this using unmanaged code. I then ported it to C# using P/Invoke and created a function that can be called to invoke any process as the Desktop User.
Basically, the code performs the following actions:
Gets the process token.
Enables the ability in increase the quota assigned to the process.
Retrieves the shell window.
Determines the Process ID associated with the shell window.
Gets the process token for the shell process.
This token identifies the Desktop User.
Duplicates the process token and turns it into a primary token.
Creates the new process as the user specified by the token.
Here is the code:
using System;
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace FortAwesomeUtil.Win32
{
public class UAC
{
[StructLayout(LayoutKind.Sequential)]
struct LUID
{
public uint LowPart;
public int HighPart;
}
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool LookupPrivilegeValue(string lpSystemName, string lpName,
out LUID lpLuid);
const UInt32 SE_PRIVILEGE_ENABLED = 0x00000002;
[StructLayout(LayoutKind.Sequential)]
struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public UInt32 Attributes;
}
const Int32 ANYSIZE_ARRAY = 1;
struct TOKEN_PRIVILEGES
{
public UInt32 PrivilegeCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = ANYSIZE_ARRAY)]
public LUID_AND_ATTRIBUTES[] Privileges;
}
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)]bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
UInt32 Zero,
IntPtr Null1,
IntPtr Null2);
[DllImport("user32.dll")]
static extern IntPtr GetShellWindow();
const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege";
const int ERROR_NOT_ALL_ASSIGNED = 1300;
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[Flags()]
enum ProcessAccessFlags : int
{
/// Specifies all possible access flags for the process object.
AllAccess = CreateThread | DuplicateHandle | QueryInformation | SetInformation
| Terminate | VMOperation | VMRead | VMWrite | Synchronize,
/// Enables usage of the process handle in the CreateRemoteThread
/// function to create a thread in the process.
CreateThread = 0x2,
/// Enables usage of the process handle as either the source or target process
/// in the DuplicateHandle function to duplicate a handle.
DuplicateHandle = 0x40,
/// Enables usage of the process handle in the GetExitCodeProcess and
/// GetPriorityClass functions to read information from the process object.
QueryInformation = 0x400,
/// Enables usage of the process handle in the SetPriorityClass function to
/// set the priority class of the process.
SetInformation = 0x200,
/// Enables usage of the process handle in the TerminateProcess function to
/// terminate the process.
Terminate = 0x1,
/// Enables usage of the process handle in the VirtualProtectEx and
/// WriteProcessMemory functions to modify the virtual memory of the process.
VMOperation = 0x8,
/// Enables usage of the process handle in the ReadProcessMemory function to'
/// read from the virtual memory of the process.
VMRead = 0x10,
/// Enables usage of the process handle in the WriteProcessMemory function to
/// write to the virtual memory of the process.
VMWrite = 0x20,
/// Enables usage of the process handle in any of the wait functions to wait
/// for the process to terminate.
Synchronize = 0x100000
}
[DllImport("kernel32.dll")]
static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)]
bool bInheritHandle, uint dwProcessId);
[Flags()]
enum TokenAccessFlags : int
{
STANDARD_RIGHTS_REQUIRED = 0x000F0000,
STANDARD_RIGHTS_READ = 0x00020000,
TOKEN_ASSIGN_PRIMARY = 0x0001,
TOKEN_DUPLICATE = 0x0002,
TOKEN_IMPERSONATE = 0x0004,
TOKEN_QUERY = 0x0008,
TOKEN_QUERY_SOURCE = 0x0010,
TOKEN_ADJUST_PRIVILEGES = 0x0020,
TOKEN_ADJUST_GROUPS = 0x0040,
TOKEN_ADJUST_DEFAULT = 0x0080,
TOKEN_ADJUST_SESSIONID = 0x0100,
TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY),
TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY |
TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE |
TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT |
TOKEN_ADJUST_SESSIONID)
}
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool OpenProcessToken(IntPtr ProcessHandle,
TokenAccessFlags DesiredAccess, out IntPtr TokenHandle);
enum SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
}
enum TOKEN_TYPE
{
TokenPrimary = 1,
TokenImpersonation
}
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
extern static bool DuplicateTokenEx(
IntPtr hExistingToken,
TokenAccessFlags dwDesiredAccess,
IntPtr lpThreadAttributes,
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
TOKEN_TYPE TokenType,
out IntPtr phNewToken);
[Flags]
enum CreationFlags
{
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
CREATE_NEW_CONSOLE = 0x00000010,
CREATE_NEW_PROCESS_GROUP = 0x00000200,
CREATE_NO_WINDOW = 0x08000000,
CREATE_PROTECTED_PROCESS = 0x00040000,
CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
CREATE_SEPARATE_WOW_VDM = 0x00001000,
CREATE_SUSPENDED = 0x00000004,
CREATE_UNICODE_ENVIRONMENT = 0x00000400,
DEBUG_ONLY_THIS_PROCESS = 0x00000002,
DEBUG_PROCESS = 0x00000001,
DETACHED_PROCESS = 0x00000008,
EXTENDED_STARTUPINFO_PRESENT = 0x00080000
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
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 dwYSize;
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)]
struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto,
CallingConvention = CallingConvention.StdCall)]
extern static bool CloseHandle(IntPtr handle);
[StructLayout(LayoutKind.Sequential)]
struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern bool CreateProcessAsUserW(
IntPtr hToken,
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
CreationFlags dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
public static Process RunAsDesktopUser(string path, string args)
{
IntPtr currentToken;
int lastError;
if (!OpenProcessToken(System.Diagnostics.Process.GetCurrentProcess().Handle,
TokenAccessFlags.TOKEN_ADJUST_PRIVILEGES, out currentToken))
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
LUID myLUID;
if (!LookupPrivilegeValue(null, SE_INCREASE_QUOTA_NAME, out myLUID))
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
TOKEN_PRIVILEGES myTokenPrivileges;
myTokenPrivileges.PrivilegeCount = 1;
myTokenPrivileges.Privileges = new LUID_AND_ATTRIBUTES[1];
myTokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
myTokenPrivileges.Privileges[0].Luid = myLUID;
if (!AdjustTokenPrivileges(currentToken, false, ref myTokenPrivileges, 0,
IntPtr.Zero, IntPtr.Zero))
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
CloseHandle(currentToken);
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
if (lastError == ERROR_NOT_ALL_ASSIGNED)
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
IntPtr shellWindow = GetShellWindow();
if (shellWindow == IntPtr.Zero)
{
throw new InvalidOperationException("Unable to get shell window.");
}
uint processID = 0;
GetWindowThreadProcessId(shellWindow, out processID);
if (processID == 0)
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
IntPtr processHandle = OpenProcess(ProcessAccessFlags.QueryInformation, true, processID);
if (processHandle == IntPtr.Zero)
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
IntPtr shellProcessToken;
TokenAccessFlags tokenAccess = TokenAccessFlags.TOKEN_QUERY | TokenAccessFlags.TOKEN_ASSIGN_PRIMARY |
TokenAccessFlags.TOKEN_DUPLICATE | TokenAccessFlags.TOKEN_ADJUST_DEFAULT |
TokenAccessFlags.TOKEN_ADJUST_SESSIONID;
if (!OpenProcessToken(processHandle, tokenAccess, out shellProcessToken))
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
IntPtr newPrimaryToken;
if (!DuplicateTokenEx(shellProcessToken, tokenAccess, IntPtr.Zero,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out newPrimaryToken))
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
STARTUPINFO startupInfo = new STARTUPINFO();
startupInfo.cb = System.Runtime.InteropServices.Marshal.SizeOf(startupInfo);
startupInfo.lpDesktop = "";
PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION();
if (!CreateProcessAsUserW(newPrimaryToken, path, path + " " + args, IntPtr.Zero, IntPtr.Zero, false, 0,
IntPtr.Zero, null, ref startupInfo, out processInfo))
{
lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
throw new Win32Exception(lastError);
}
CloseHandle(processInfo.hProcess);
CloseHandle(processInfo.hThread);
return Process.GetProcessById(processInfo.dwProcessId);
}
}
}
And here is a sample use from Smuggly:
using System;
using System.Collections;
using System.ComponentModel;
using System.Configuration.Install;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using FortAwesomeUtil.Win32;
namespace InstallerAction
{
[RunInstaller(true)]
public partial class InstallScrFile : Installer
{
public InstallScrFile()
{
InitializeComponent();
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern uint GetWindowsDirectory(StringBuilder lpBuffer,
uint uSize);
protected override void OnAfterInstall(IDictionary savedState)
{
base.OnAfterInstall(savedState);
const int MaxPathLength = 255;
StringBuilder myStringBuilder = new StringBuilder(MaxPathLength);
int len = (int)GetWindowsDirectory(myStringBuilder, MaxPathLength);
string windowsDir = myStringBuilder.ToString(0, len);
string path = System.IO.Path.Combine(windowsDir, "Smuggly.scr");
string arguments = "/c";
OperatingSystem os = Environment.OSVersion;
// Check for Windows XP
if (os.Version.Major == 5 && os.Version.Minor > 0)
{
System.Diagnostics.Process myProcess = new System.Diagnostics.Process();
myProcess.StartInfo.Arguments = arguments;
myProcess.StartInfo.FileName = path;
myProcess.StartInfo.UseShellExecute = false;
myProcess.Start();
}
else
{
try
{
UAC.RunAsDesktopUser(path, arguments);
}
catch
{
// In this case we'll ignore this and let the user use the
// normal screensaver configuration dialog
}
}
}
}
}
This file is in the Fort Awesome Util Project and can be downloaded here.
References
A big thanks goes to:
Take 2
After my previous post (above), I received a question asking what exactly the call to AdjustTokenPrivileges was doing and why exactly it was needed. It turns out that, if you are willing to drop support for Windows 2000, it is not needed.
Since I love simplicity and I don't think I need to support Windows 2000. I am reposting the simplified process description and code.
Basically, the code performs the following actions:
Retrieves the shell window.
Determines the Process ID associated with the shell window.
Gets the process token for the shell process.
This token identifies the Desktop User.
Duplicates the process token and turns it into a primary token.
Creates the new process as the user specified by the token.
Here is the code:
using System; using System.Diagnostics; using System.ComponentModel; using System.Runtime.InteropServices; namespace FortAwesomeUtil.Win32 { public class UAC { [DllImport("user32.dll")] static extern IntPtr GetShellWindow(); const string SE_INCREASE_QUOTA_NAME = "SeIncreaseQuotaPrivilege"; const int ERROR_NOT_ALL_ASSIGNED = 1300; [DllImport("user32.dll", SetLastError = true)] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); [Flags()] enum ProcessAccessFlags : int { /// <summary>Specifies all possible access flags for the process object.</summary> AllAccess = CreateThread | DuplicateHandle | QueryInformation | SetInformation | Terminate | VMOperation | VMRead | VMWrite | Synchronize, /// <summary>Enables usage of the process handle in the CreateRemoteThread /// function to create a thread in the process.</summary> CreateThread = 0x2, /// <summary>Enables usage of the process handle as either the source or target process /// in the DuplicateHandle function to duplicate a handle.</summary> DuplicateHandle = 0x40, /// <summary>Enables usage of the process handle in the GetExitCodeProcess and /// GetPriorityClass functions to read information from the process object.</summary> QueryInformation = 0x400, /// <summary>Enables usage of the process handle in the SetPriorityClass function to /// set the priority class of the process.</summary> SetInformation = 0x200, /// <summary>Enables usage of the process handle in the TerminateProcess function to /// terminate the process.</summary> Terminate = 0x1, /// <summary>Enables usage of the process handle in the VirtualProtectEx and /// WriteProcessMemory functions to modify the virtual memory of the process.</summary> VMOperation = 0x8, /// <summary>Enables usage of the process handle in the ReadProcessMemory function to' /// read from the virtual memory of the process.</summary> VMRead = 0x10, /// <summary>Enables usage of the process handle in the WriteProcessMemory function to /// write to the virtual memory of the process.</summary> VMWrite = 0x20, /// <summary>Enables usage of the process handle in any of the wait functions to wait /// for the process to terminate.</summary> Synchronize = 0x100000 } [DllImport("kernel32.dll")] static extern IntPtr OpenProcess(ProcessAccessFlags dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwProcessId); [Flags()] enum TokenAccessFlags : int { STANDARD_RIGHTS_REQUIRED = 0x000F0000, STANDARD_RIGHTS_READ = 0x00020000, TOKEN_ASSIGN_PRIMARY = 0x0001, TOKEN_DUPLICATE = 0x0002, TOKEN_IMPERSONATE = 0x0004, TOKEN_QUERY = 0x0008, TOKEN_QUERY_SOURCE = 0x0010, TOKEN_ADJUST_PRIVILEGES = 0x0020, TOKEN_ADJUST_GROUPS = 0x0040, TOKEN_ADJUST_DEFAULT = 0x0080, TOKEN_ADJUST_SESSIONID = 0x0100, TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY), TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID) } [DllImport("advapi32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] static extern bool OpenProcessToken(IntPtr ProcessHandle, TokenAccessFlags DesiredAccess, out IntPtr TokenHandle); enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] extern static bool DuplicateTokenEx( IntPtr hExistingToken, TokenAccessFlags dwDesiredAccess, IntPtr lpThreadAttributes, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, TOKEN_TYPE TokenType, out IntPtr phNewToken); [Flags] enum CreationFlags { CREATE_BREAKAWAY_FROM_JOB = 0x01000000, CREATE_DEFAULT_ERROR_MODE = 0x04000000, CREATE_NEW_CONSOLE = 0x00000010, CREATE_NEW_PROCESS_GROUP = 0x00000200, CREATE_NO_WINDOW = 0x08000000, CREATE_PROTECTED_PROCESS = 0x00040000, CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, CREATE_SEPARATE_WOW_VDM = 0x00001000, CREATE_SUSPENDED = 0x00000004, CREATE_UNICODE_ENVIRONMENT = 0x00000400, DEBUG_ONLY_THIS_PROCESS = 0x00000002, DEBUG_PROCESS = 0x00000001, DETACHED_PROCESS = 0x00000008, EXTENDED_STARTUPINFO_PRESENT = 0x00080000 } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] 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 dwYSize; 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)] struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public int dwProcessId; public int dwThreadId; } [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] extern static bool CloseHandle(IntPtr handle); [StructLayout(LayoutKind.Sequential)] struct SECURITY_ATTRIBUTES { public int nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)] static extern bool CreateProcessAsUserW( IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, CreationFlags dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); public static Process RunAsDesktopUser(string path, string args) { int lastError; IntPtr shellWindow = GetShellWindow(); if (shellWindow == IntPtr.Zero) { throw new InvalidOperationException("Unable to get shell window."); } uint processID = 0; GetWindowThreadProcessId(shellWindow, out processID); if (processID == 0) { lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); throw new Win32Exception(lastError); } IntPtr processHandle = OpenProcess(ProcessAccessFlags.QueryInformation, true, processID); if (processHandle == IntPtr.Zero) { lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); throw new Win32Exception(lastError); } IntPtr shellProcessToken; TokenAccessFlags tokenAccess = TokenAccessFlags.TOKEN_QUERY | TokenAccessFlags.TOKEN_ASSIGN_PRIMARY | TokenAccessFlags.TOKEN_DUPLICATE | TokenAccessFlags.TOKEN_ADJUST_DEFAULT | TokenAccessFlags.TOKEN_ADJUST_SESSIONID; if (!OpenProcessToken(processHandle, tokenAccess, out shellProcessToken)) { lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); throw new Win32Exception(lastError); } IntPtr newPrimaryToken; if (!DuplicateTokenEx(shellProcessToken, tokenAccess, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out newPrimaryToken)) { lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); throw new Win32Exception(lastError); } STARTUPINFO startupInfo = new STARTUPINFO(); startupInfo.cb = System.Runtime.InteropServices.Marshal.SizeOf(startupInfo); startupInfo.lpDesktop = ""; PROCESS_INFORMATION processInfo = new PROCESS_INFORMATION(); if (!CreateProcessAsUserW(newPrimaryToken, path, path + " " + args, IntPtr.Zero, IntPtr.Zero, false, 0, IntPtr.Zero, null, ref startupInfo, out processInfo)) { lastError = System.Runtime.InteropServices.Marshal.GetLastWin32Error(); throw new Win32Exception(lastError); } CloseHandle(processInfo.hProcess); CloseHandle(processInfo.hThread); return Process.GetProcessById(processInfo.dwProcessId); } } }
This file is in the Fort Awesome Util Project and can be downloaded here.