$theTitle=wp_title(" - ", false); if($theTitle != "") { ?>
About Virtualization, VDI, SBC, Application Compatibility and anything else I feel like
2 Mar // php the_time('Y') ?>
In part 1 I’ve described the theoretical parts needed for a custom autologon application implementation.
But there are some practical problems which I will describe here.
1) I use the LsaLogonUser function to log in the user. However, if I do not pass not null for the LocalGroups parameter, msgina.dll fails to process the logon.
Why? Because it looks for the SE_GROUP_LOGON_ID SID and treat it as logon SID. So we have to add the logon SID manually:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | function GetAdditionalSIDS(StringSIDS : TStringDynArray) : JWAWindows.PTokenGroups; var Len : Integer; I : Integer; begin Result := nil; Len := Length(StringSIDS); if (Len > 0) then begin Result := GetMemory(SizeOf(Result^) + SizeOf(Result^.Groups[0]) * (Len)); with Result^ do begin GroupCount := Len; for I := Low(StringSIDs) to High(StringSIDs) do Groups[I] := ConvertSid(StringSIDs[i]); // hardcode: last sid will be manually added sid, so make it logon sid Groups[High(StringSIDs)].Attributes := Groups[High(StringSIDs)].Attributes or SE_GROUP_LOGON_ID; end; end; end; |
2) In case we want to create a token using NtCreateToken, we need to add the Everyone Group (otherwise Winlogon will fail to load the user profile):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | const EveryoneSID = 'S-1-1-0'; procedure CheckAdditionalSIDs(var SIDs : TStringDynArray; WorkingMode : TWorkingMode; const UserName, Domain, PrimarySID : String; IncludeEveryoneSID : Boolean); var SID : PSID; SidLength : DWORD; DomainName : PChar; DomainLen : DWORD; Name : SID_NAME_USE; begin if (WorkingMode = wmCreateToken) then begin if (IncludeEveryoneSID) then begin SetLength(SIDs, Length(SIDs) + 1); SIDs[Length(SIDs) - 1] := EveryoneSID; end; SetLength(SIDs, Length(SIDs) + 1); SIDs[Length(SIDs) - 1] := PrimarySID; end; if (Length(SIDs) > 0) and (WorkingMode = wmNormalLogin) then begin SidLength := 0; DomainLen := 0; LookupAccountName(PChar(Domain), PChar(UserName), nil, SidLength, nil, DomainLen, Name); if (GetLastError <> ERROR_INSUFFICIENT_BUFFER) then RaiseLastOSError; SID := GetMemory(SidLength); try DomainName := GetMemory(DomainLen); try Win32Check(LookupAccountName(PChar(Domain), PChar(UserName), SID, SidLength, DomainName, DomainLen, Name)); SetLength(SIDs, Length(SIDs) + 1); SIDs[Length(SIDs) - 1] := ConvertSIDToString(SID); finally FreeMemory(DomainName); end; finally FreeMemory(SID); end; end; end; |
3) On the X64 versions of Windows XP and all version of Windows 2003 a service process doesn’t have the SeCreateTokenName privilege. But we can open lsass.exe process and copy it’s token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | type TProcessAccessToken = record hToken : THandle; hThread : THandle; end; procedure AdjustToken; var LsassProcess : THandle; LsassToken : THandle; ProcessToken : TProcessAccessToken; DuplicatedToken : THandle; begin if (Is2003) then begin EnableToken(SE_DEBUG_NAME); LsassProcess := FindProcessInSession(0, 'lsass.exe'); if (LsassProcess = INVALID_HANDLE_VALUE) then begin Writeln('Unable to find lsass.exe process'); Abort; end; try Win32Check(OpenProcessToken(LsassProcess, TOKEN_ALL_ACCESS, LsassToken)); try Win32Check(DuplicateTokenEx(LsassToken, TOKEN_ALL_ACCESS, nil, SecurityImpersonation, TokenPrimary, DuplicatedToken)); try ProcessToken.hThread := GetCurrentThread; ProcessToken.hToken := DuplicatedToken; EnableToken(SE_ASSIGNPRIMARYTOKEN_NAME); NtCheck(NtSetInformationProcess(GetCurrentProcess, ProcessAccessToken, @ProcessToken, SizeOf(ProcessToken))); finally CloseHandle(DuplicatedToken); end; finally CloseHandle(LsassToken); end; finally CloseHandle(LsassProcess); end; end; end; |
4) In case we create the token manually, we have to specify the logon session. As there is no way to create a session without injecting into lsass.exe, I use a SYSTEM luid as a logon session luid. It has the side-effect of showing the user as SYSTEM regardless of the actual used SID.
5) Sometimes opening the pipe fails (it may have been connected to some other session’s winlogon). I try to open the pipe with short timeout and post a disconnect pipe message:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const WPARAM_DISCONNECT_PIPE = 20; procedure TryToConnectToPipe; var PipeHandle : THandle; begin PipeHandle := INVALID_HANDLE_VALUE; try PipeHandle := OpenPipe(100); except on E : EOSError do if E.ErrorCode <> ERROR_SEM_TIMEOUT then Raise; end; try DoLogonNotifyMessage(WPARAM_DISCONNECT_PIPE, 0); // emulate _WinstationNotifyDisconnectPipe, force pipe to be reconnected. finally if (PipeHandle <> INVALID_HANDLE_VALUE) then CloseHandle(PipeHandle); end; end; |
6) In Windows 2003 (or on XP x64) you should read the BOOL variable from the pipe after you’ve written to it. But it may not work as Winlogon simply writes the value and then disconnects the pipe, so sometimes the data can not be read (subject to threading issue). I simply ignore the read error, since even if it shows that the credentials were accepted by Winlogon, they may have been rejected by gina
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | procedure ProcessPipe(PipeHandle : THandle; Buffer : PLengthBuffer); var NumberOfBytes : Cardinal; Res : BOOL; begin Win32Check(WriteFile(PipeHandle, Buffer, Buffer^.Length, @NumberOfBytes, nil)); Win32Check(NumberOfBytes = Buffer^.Length); if (Is2003) then begin if (ReadFile(PipeHandle, @Res, SizeOf(Res), @NumberOfBytes, nil) and (NumberOfBytes = SizeOf(Res)) and not Res) then begin Writeln('Credentials were not accepted'); Abort; end; end end; |
7) In some extremely rare cases our process may exit before Winlogon has read the credentials and duplicated them (actuallyI was able to repeat this only under debugging winlogon).
To be sure that our credentials have been correctly processed, I create a different thread and wait for console logon event:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function WaitForLogonThreadProc(Parameter: Pointer): Integer; var EventFlags : Cardinal; begin Result := 0; try repeat Win32Check(WTSWaitSystemEvent(WTS_CURRENT_SERVER_HANDLE, WTS_EVENT_LOGON, EventFlags)); until EventFlags and WTS_EVENT_LOGON <> 0; except on E : EOSError do Result := EOsError(E).ErrorCode; on E : Exception do Result := ERROR_GEN_FAILURE; end; EndThread(Result); end; function WaitForLogonAsync : THandle; begin Result := BeginThread(nil, 64*1024, @WaitForLogonThreadProc, nil, 0, PCardinal(nil)^); if Result = 0 then RaiseLastOSError; end; |
8) If Fast User Switching is enabled and we are on a Workstation OS, Winlogon ignores the WLX_SAS_TYPE_AUTHENTICATED message. However, before execution we can disable the AllowMultipleTSSessions key, and enable it again after the execution. Of course, we do have to remember that under WOW we need to use the special registry flag to access real HKLM hive.
9) In case session 0 has a logged-on user, Winlogon also ignores the WLX_SAS_TYPE_AUTHENTICATED message. I use WTSQueryUserToken to get the token of the logged on user. If the function succeeds, then a user is logged on. There are some problems calling this function on 64 bit systems, and they are described in Querying a user token under 64 bit version of 2003/XP
10) If the screensaver is running, we’ll have to wait until someone breaks the screensaver. So we need to stop screensaver programmatically. MSDN recommends posting the WM_CLOSE message to the foreground window, so let’s do it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | function CreateStopScreenSaverThreadProc(Parameter: Pointer): Integer; var DesktopHandle : THandle; begin Result := 0; try DesktopHandle := OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, False, DESKTOP_READOBJECTS); if (DesktopHandle = 0) then RaiseLastOSError; try Win32Check(SetThreadDesktop(DesktopHandle)); Win32Check(PostMessage(GetForegroundWindow(), WM_CLOSE, 0, 0)); finally CloseDesktop(DesktopHandle); end; except on E : EOSError do Result := EOsError(E).ErrorCode; on E : Exception do Result := ERROR_GEN_FAILURE; end; EndThread(Result); end; function CreateStopScreenSaverThread : THandle; begin Result := BeginThread(nil, 64*1024, @CreateStopScreenSaverThreadProc, nil, 0, PCardinal(nil)^); if Result = 0 then RaiseLastOSError; end; procedure CheckScreenSaver; var IsScreenSaverRunning : BOOL; ThreadHandle : THandle; Res : DWORD; begin Win32Check(SystemParametersInfo(SPI_GETSCREENSAVERRUNNING, 0, @IsScreenSaverRunning, 0)); if (IsScreenSaverRunning) then begin ThreadHandle := CreateStopScreenSaverThread; Res := WaitForSingleObject(ThreadHandle, INFINITE); if (Res <> WAIT_OBJECT_0) then begin SetLastError(Res); RaiseLastOSError; end; Win32Check(GetExitCodeThread(ThreadHandle, Res)); SetLastError(Res); Win32Check(Res = ERROR_SUCCESS); end; end; |
11) Although you can send an empty username, some GINA’s, at least msgina.dll require it to be not-null. So we can use an empty string (#0) instead. You may need to overwrite this procedure to increase compatibility with your GINA:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | procedure CheckGinaCompatibility(var UserName : String; var Domain : String); var Buffer : array [0..MAX_COMPUTERNAME_LENGTH] of Char; Size : DWORD; begin if (UserName = '') then UserName := #0; // msgina.dll expects user name to be not empty if ((Domain = '') and (AnsiCompareText(GetGINAName, 'FMLogin.dll') = 0)) then begin Size := SizeOf(Buffer); ZeroMemory(@Buffer, Size); Win32Check(GetComputerName(Buffer, Size)); Domain := Buffer; end; end; |
12) There is incompatibility with some custom GINA’s, for example, with FMLogin, which subclasses “Sas Window”. I’ve tested the sample with original msgina.dll, so the further compatibility should be tested with the exact gina.
13) Getting a session process list on x64 may fail if you do not allocate buffers correctly – described here
So we’re ready to go on to the implementation which will be described in the part 3.
Leave a reply