[英]How to start a new Windows logon session (RDP or console) programmatically
我已經為此糾結了幾個小時,所以我想是時候問了。 我將從對情況的高級描述開始。 您可以在https://github.com/Jay-Rad/InstaTech_Client找到完整的源代碼。 此問題僅與“/InstaTech_Service/”中的項目有關。
概述
InstaTech 客戶端是一個遠程控制應用程序,它使用 websockets 並與 ASP.NET 服務器建立出站連接,以便與查看器進行中繼。 我有不同的版本,但它們的功能大致相同(Electron 版本在使用原始 websockets 之前首先嘗試 WebRTC)。 該應用程序的查看器部分是基於 Web 的,可以在此處找到演示: https : //instatetech.org/Demo/Remote_Control
WPF (C#) 和 Electron 版本提供一個帶有隨機 ID 的 GUI,他們必須將其提供給遠程訪問其計算機的人員(類似於 TeamViewer)。 會話開始后,它們會以不同的方式捕獲屏幕。 對於 C#,我使用對 BitBlt 的 pinvoke 將圖像復制到內存中的圖形,然后通過 websocket 發送。 隨后的屏幕截圖與前一個屏幕截圖進行比較以創建一個包含更改像素的框,然后發送該裁剪部分。 鼠標和鍵盤輸入由客戶端接收並通過 pinvoke 執行到 keybd_event 和 mouse_event。 這些工作得很好。
我創建的服務以類似的方式工作,但這里有不同之處。 服務本身在系統帳戶下的會話 0 中運行。 它連接到服務器並偵聽 websocket。 當建立連接並請求屏幕查看時,它會在 WinSta0\\Default 中的用戶會話中啟動一個單獨的交互進程。 一旦新進程的 websocket 連接,服務器開始在它和查看器之間中繼消息,而不是服務和查看器。
盡管新進程在用戶會話中以交互方式啟動,但它在系統帳戶下運行。 這是通過對 CreateProcessAsUser 進行 pinvoke 並復制 winlogon.exe 訪問令牌來實現的。
問題
如果有人已經登錄,即使通過 RDP,此解決方案也能正常工作。 但是,如果沒有人登錄或計算機被鎖定,我將無法與登錄屏幕進行交互。 進行屏幕捕獲時,我正在檢測捕獲是否失敗,這意味着 WinSta0\\Default 桌面不再處於活動狀態。 由於我使用的是 CreateProcessAsUser,因此我可以將桌面切換到 WinSta0\\Winlogon 就好了。 我仍然可以看到它(即使沒有人登錄),但它不會接受任何輸入。 我知道這是出於安全原因設計的。 好吧,奇怪的是,如果我四處移動鼠標並導致光標重新定位,一些鼠標移動會“滑過”,但其余的會被發送到默認桌面並在重新登錄后執行。
所以問題是我無法使用此設置將帳戶登錄到計算機。 如果重要的話,如果其他人已經登錄並鎖定了計算機,我不在乎與 Winlogon 桌面交互。 我只希望能夠在沒有其他人使用它的情況下登錄,或者是我的帳戶登錄並處於鎖定屏幕。
嘗試的解決方案
我假設無法避免無法將模擬輸入發送到 Winlogon 桌面。 (更正:也就是說,使用 mouse_event 和 keybd_event 函數。我見過其他應用程序這樣做,例如 TeamViewer 和 Microsoft SCCM Remote Control。不過,我不確定他們是如何做到的。)如果有可能,我想那將是最直接的路線。 但這里有一些我已經研究過的重點是啟動新的登錄會話。
Pinvoke 到 LsaLogonUser。 我不確定這是否能實現我所追求的目標,但我還是嘗試了。 但是,即使對 LsaLogonUser 的調用報告成功,我從 LsaRegisterLogonProcess(輸出到 lsaHan)獲得的句柄是 0。我不確定我做錯了什么。 我對 Win32 調用不太熟悉,並試圖在我走的時候接聽它。 也許調用進程沒有必要的權限。 我已經嘗試從會話 0 中的服務和交互式會話中運行的進程調用它。 我正在做的一個例子如下。
Microsoft 終端服務活動客戶端 COM 庫。 我沒有深入研究這個,但我想知道是否可以使用它來啟動 RDP 登錄會話。 創建 RDP 登錄會話后,在該會話中生成一個新的 InstaTech 進程並連接到它。 不過,如果 RDP 連接是從同一台計算機上嘗試的,我懷疑這是否可行。
憑據提供程序。 我在研究時遇到了憑證提供者。 我不確定創建一個是否能解決問題,但聽起來這將是一項非常復雜的工作。
有沒有人有任何建議? 還是我完全錯過了什么?
如果您想重新編譯服務並進行測試,我在服務器上創建了一個臨時管理員帳戶。 任何安裝了該服務的計算機都會顯示在那里,您可以使用此帳戶登錄。 請記住,閱讀這篇文章的任何人都可以訪問運行該服務的任何計算機,因此請確保它處於隔離環境中。
用戶名:管理員
密碼:plzh@lpm3purdyplz
該服務是自安裝的。 通過 -install 開關進行安裝,通過 -uninstall 進行卸載。 EXE 被復制到 %programdata%\\InstaTech,服務從那里啟動它。
謝謝!
參考代碼
public static void CreateNewSession()
{
var kli = new SECUR32.KERB_INTERACTIVE_LOGON()
{
MessageType = SECUR32.KERB_LOGON_SUBMIT_TYPE.KerbInteractiveLogon,
UserName = "myusername@someplace.com",
Password = "superencryptedstring"
};
IntPtr pluid;
IntPtr lsaHan;
ulong secMode;
uint authPackID;
IntPtr kerbLogInfo;
SECUR32.LSA_STRING logonProc = new SECUR32.LSA_STRING()
{
Buffer = Marshal.StringToHGlobalAuto("InstaLogon"),
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon")),
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon"))
};
SECUR32.LSA_STRING originName = new SECUR32.LSA_STRING()
{
Buffer = Marshal.StringToHGlobalAuto("InstaLogon"),
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon")),
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("InstaLogon"))
};
SECUR32.LSA_STRING authPackage = new SECUR32.LSA_STRING()
{
Buffer = Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A"),
Length = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A")),
MaximumLength = (ushort)Marshal.SizeOf(Marshal.StringToHGlobalAuto("MICROSOFT_KERBEROS_NAME_A"))
};
IntPtr hLogonProc = Marshal.AllocHGlobal(Marshal.SizeOf(logonProc));
Marshal.StructureToPtr(logonProc, hLogonProc, false);
ADVAPI32.AllocateLocallyUniqueId(out pluid);
SECUR32.LsaRegisterLogonProcess(hLogonProc, out lsaHan, out secMode);
SECUR32.LsaLookupAuthenticationPackage(lsaHan, ref authPackage, out authPackID);
kerbLogInfo = Marshal.AllocHGlobal(Marshal.SizeOf(kli));
Marshal.StructureToPtr(kli, kerbLogInfo, false);
var ts = new SECUR32.TOKEN_SOURCE("Insta");
IntPtr profBuf;
uint profBufLen;
long logonID;
IntPtr logonToken;
SECUR32.QUOTA_LIMITS quotas;
SECUR32.WinStatusCodes subStatus;
SECUR32.LsaLogonUser(lsaHan, ref originName, SECUR32.SecurityLogonType.Interactive, authPackID, kerbLogInfo, (uint)Marshal.SizeOf(kerbLogInfo), IntPtr.Zero, ref ts, out profBuf, out profBufLen, out logonID, out logonToken, out quotas, out subStatus);
}
這是會話 0 中的服務用於在交互式會話中啟動另一個實例的方法。 我從這篇文章中得到了大部分內容: https : //www.codeproject.com/kb/vista-security/subvertingvistauac.aspx 。 我只添加了 RDP 會話查找。
public static bool OpenProcessAsSystem(string applicationName, out PROCESS_INFORMATION procInfo)
{
try
{
uint winlogonPid = 0;
IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;
procInfo = new PROCESS_INFORMATION();
// Obtain session ID for active session.
uint dwSessionId = Kernel32.WTSGetActiveConsoleSessionId();
// Check for RDP session. If active, use that session ID instead.
var rdpSessionID = GetRDPSession();
if (rdpSessionID > 0)
{
dwSessionId = rdpSessionID;
}
// 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;
}
}
// Obtain a handle to the winlogon process.
hProcess = Kernel32.OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);
// Obtain a handle to the access token of the winlogon process.
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
{
Kernel32.CloseHandle(hProcess);
return false;
}
// Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser.
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))
{
Kernel32.CloseHandle(hProcess);
Kernel32.CloseHandle(hPToken);
return false;
}
// By default, CreateProcessAsUser creates a process on a non-interactive window station, meaning
// the window station has a desktop that is invisible and the process is incapable of receiving
// user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user
// interaction with the new process.
STARTUPINFO si = new STARTUPINFO();
si.cb = (int)Marshal.SizeOf(si);
si.lpDesktop = @"winsta0\default"; // interactive window station parameter; basically this indicates that the process created can display a GUI on the desktop
// flags that specify the priority and creation method of the process
uint 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
);
// invalidate the handles
Kernel32.CloseHandle(hProcess);
Kernel32.CloseHandle(hPToken);
Kernel32.CloseHandle(hUserTokenDup);
return result;
}
catch
{
procInfo = new PROCESS_INFORMATION() { };
return false;
}
}
public static uint GetRDPSession()
{
IntPtr ppSessionInfo = IntPtr.Zero;
Int32 count = 0;
Int32 retval = WTSAPI32.WTSEnumerateSessions(WTSAPI32.WTS_CURRENT_SERVER_HANDLE, 0, 1, ref ppSessionInfo, ref count);
Int32 dataSize = Marshal.SizeOf(typeof(WTSAPI32.WTS_SESSION_INFO));
var sessList = new List<WTSAPI32.WTS_SESSION_INFO>();
Int64 current = (int)ppSessionInfo;
if (retval != 0)
{
for (int i = 0; i < count; i++)
{
WTSAPI32.WTS_SESSION_INFO sessInf = (WTSAPI32.WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)current, typeof(WTSAPI32.WTS_SESSION_INFO));
current += dataSize;
sessList.Add(sessInf);
}
}
uint retVal = 0;
var rdpSession = sessList.Find(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0);
if (sessList.Exists(ses => ses.pWinStationName.ToLower().Contains("rdp") && ses.State == 0))
{
retVal = (uint)rdpSession.SessionID;
}
return retVal;
}
我讓 SendInput 在登錄桌面上工作(而且,事實證明,在 UAC 安全桌面上)。 SetThreadDesktop 不能為您提供與最初在目標桌面中啟動進程時相同的權限。
因此,當我檢測到桌面更改時,我沒有調用 SetThreadDesktop,而是使用 CreateProcessAsUser 在新桌面中啟動了另一個進程。 然后我示意觀眾切換並關閉當前進程。
編輯(多年后):我最終錯了。 您只需要確保您的當前線程在當前桌面中沒有任何打開的窗口或掛鈎。 由於這只是為調用線程(而不是進程)設置桌面,因此后續線程也需要調用它。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.