Я уже несколько часов стучал головой об этом, поэтому решил, что пришло время спросить. Я начну с описания ситуации на высоком уровне. Вы можете найти весь исходный код: https://github.com/Jay-Rad/InstaTech_Client. Этот вопрос относится только к проекту в «/ InstaTech_Service /».Как запустить новый сеанс входа в систему Windows (RDP или консоль) программно
Обзор
Клиент InstaTech является приложением дистанционного управления, который использует WebSockets и делает исходящее соединение с сервером ASP.NET для реле со зрителем. У меня разные версии, но все они работают примерно одинаково (версия Electron пытается использовать WebRTC прежде, чем использовать сырые websockets). Часть зрителя приложения находится на веб-сайте, а демо-версию можно найти здесь: https://instatech.org/Demo/Remote_Control
В версиях WPF (C#) и Electron представлен графический интерфейс с произвольным идентификатором, который они должны предоставить человеку, удаленному на свой компьютер (аналогично TeamViewer). Когда сеанс запущен, они захватывают экран по-разному. Для C# я использую pinvoke для BitBlt, чтобы скопировать изображение в графику в памяти, которая затем отправляется через websocket. Последующие снимки экрана сравниваются с предыдущим, чтобы создать ящик, который включает в себя измененные пиксели, затем отправляется обрезанная секция. Входы мыши и клавиатуры принимаются клиентом и выполняются через pinvoke на keybd_event и mouse_event. Они отлично работают.
Служба, которую я создал, работает аналогичным образом, но вот отличия. Сама служба запускается в сеансе 0 в разделе «Системная учетная запись». Он подключается к серверу и прослушивает websocket. Когда выполняется соединение и выполняется просмотр экрана, он запускает отдельный интерактивный процесс в сеансе пользователя в WinSta0 \ Default. Как только веб-узел нового процесса подключается, сервер начинает ретранслировать сообщения между ним и зрителем вместо службы и зрителя.
Хотя новый процесс запускается интерактивно в сеансе пользователя, он запускается под учетной записью системы. Это достигается с помощью pinvoke для CreateProcessAsUser и дублирования токена доступа winlogon.exe.
Проблема
Это решение работает хорошо, если кто-то уже вошел в систему, даже если через RDP. Однако, если никто не зарегистрирован или компьютер не заблокирован, я не могу взаимодействовать с экраном входа в систему. Когда я делаю захват экрана, я обнаруживаю, что захват завершился неудачно, что означало бы, что рабочий стол WinSta0 \ Default больше не активен. Поскольку я использую CreateProcessAsUser, я могу просто переключить настольные компьютеры на WinSta0 \ Winlogon. Я все еще вижу это (даже если никто не вошел в систему), но он не будет принимать никаких входов. Я понимаю, что это по соображениям безопасности. Ну, странно, некоторые движения мыши «проскальзывают», если я перемещаю их и заставляю курсор перемещаться, а остальные отправляются на рабочий стол по умолчанию и выполняются после входа в систему.
Таким образом, проблема в том, что Я не могу получить учетную запись, зарегистрированную на компьютере с этой настройкой. Если это имеет значение, я не хочу взаимодействовать с рабочим столом Winlogon, если кто-то еще уже вошел в систему и заблокировал компьютер. Я только хочу, чтобы иметь возможность войти в систему, если никто другой не использует его, или это моя учетная запись, которая вошла в систему и на экране блокировки.
Покушение Solutions
Я предполагаю, что нет никакого способа, чтобы обойти невозможность отправить смоделированные входы на рабочий стол Winlogon. (Исправление: То есть, используя функции mouse_event и keybd_event. Я видел, как это делают другие приложения, такие как TeamViewer и Microsoft SCCM Remote Control. Я не уверен, как они это делают.) Если это возможно, я думаю, что это был бы самый прямой маршрут. Но вот некоторые вещи, которые я изучил, сосредоточились на том, чтобы начать новый сеанс входа в систему.
Pinvoke to LsaLogonUser. Я не уверен, выполнит ли это то, что мне нужно, но я все равно попытался. Однако, хотя вызов LsaLogonUser сообщает об успехе, дескриптор, который я получаю от LsaRegisterLogonProcess (вне до lsaHan), равен 0. Я не уверен, что я делаю неправильно. Я не слишком хорошо знаком с вызовами Win32 и пытаюсь забрать его, когда я иду. Возможно, вызывающий процесс не имеет необходимых прав. Я пробовал называть это из службы в сеансе 0 и из процесса, выполняемого в интерактивном сеансе. Пример того, что я делаю, ниже.
Клиентская библиотека COM-клиентов Microsoft Terminal Services. Я не слишком глубоко вникнул в это, но мне интересно, можно ли использовать это для запуска сеанса входа в RDP. После сеанса входа в систему RDP создайте новый процесс InstaTech в этом сеансе и подключитесь к нему. Я сомневаюсь, что это сработает, если попытка RDP будет пытаться с того же компьютера.
Credential Provider. Я сталкивался с доверенными поставщиками во время исследования. Я не уверен, что если вы создадите, вы решите проблему, но похоже, что это было бы очень сложное начинание.
Есть ли у кого-нибудь предложения? Или я чего-то не хватает?
Если вы хотите перекомпилировать сервис и проверить его, я создал временную учетную запись администратора на сервере. Там будет показан любой компьютер с установленной службой, и вы можете войти в систему, используя эту учетную запись. Пожалуйста, имейте в виду, что любой, кто прочитает это сообщение, сможет получить доступ к любым компьютерам, на которых запущена служба, поэтому убедитесь, что они находятся в изолированной среде.
Имя пользователя: администратор
Пароль: 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 = "[email protected]",
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;
}
Вы когда-нибудь находили решение этого вопроса? Не могли бы вы поделиться? – LorneCash
Да. Я дал краткий ответ ниже. Полную реализацию можно найти здесь: https://github.com/Jay-Rad/InstaTech_Client/blob/master/InstaTech_Service/Socket.cs Соответствующая часть начинается с строки, которая имеет «случай» ConnectUnattended «: '(строка 468). Класс ADVAPI32, который он использует, можно найти по адресу: https://github.com/Jay-Rad/InstaTech_Client/blob/master/Win32_Classes/ADVAPI32.cs – Jared