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:

objectpascal Download
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;
  1. 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):
objectpascal Download
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;
  1. 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:
objectpascal Download
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;
  1. 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.

  2. 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:

objectpascal Download
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;
  1. 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
objectpascal Download
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;
  1. 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:

objectpascal Download
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.

  1. 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

  2. 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:

objectpascal Download
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;
  1. 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:
objectpascal Download
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;
  1. 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.

  2. 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.