$theTitle=wp_title(" - ", false); if($theTitle != "") { ?>
About Virtualization, VDI, SBC, Application Compatibility and anything else I feel like
Let’s write our own Credential Server implementation.
At first, we need to create a named pipe with a unique name. Let’s construct the pipe name using a GUID – this should be unique, but we can do it in a cycle to be absolutely sure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function CreateGinaPipe(out PipeName : String) : THandle; var Guid : TGUID; Res : HResult; begin repeat Res := CreateGUID(Guid); if Res <> S_OK then begin Writeln(Format('Faild to get GUID (%d)', [Res])); Abort; end; PipeName := Format('\\.\pipe\OurLogonCreds%s', [GUIDToString(Guid)]); Result := CreateNamedPipe(PChar(PipeName), PIPE_ACCESS_OUTBOUND or FILE_FLAG_WRITE_THROUGH, 0, 1, 0, 0, NMPWAIT_USE_DEFAULT_WAIT, nil); until (Result <> INVALID_HANDLE_VALUE) or (GetLastError <> ERROR_PIPE_BUSY); if Result = INVALID_HANDLE_VALUE then RaiseLastOSError; end; |
We need to save the pipe name in a registry key Name under the hive HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Credentials. By default, only system has access to this key. But if you want to make FUS available to some user(s) and/or group(s), you can modify this registry key permission (but don’t forget about possible security flaws).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | const GinaKey : String = 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Credentials'; GinaKeyName : String = 'Name'; procedure CreateGinaRegistryKey(Const Name:String); begin with TRegistry.Create(GetRegistryMode) do try RootKey := HKEY_LOCAL_MACHINE; if not OpenKey(GinaKey, true) then begin Writeln(Format('Failed to open KEY %s', [GinaKey])); Abort; end; WriteString(GinaKeyName, Name); finally Free; end; end; |
Ok, now the pipe and the registry key have been created. The data format of the pipe is:
1 2 3 4 5 6 7 8 9 | PGinaPipeRecord = ^TGinaPipeRecord; TGinaPipeRecord = record TotalSize : DWORD; Seed : DWORD; // only low byte is used UserName_U : UNICODE_STRING; Domain_U : UNICODE_STRING; Password_U : UNICODE_STRING; DataLabel : record end; // dynamic part end; |
Note that all the UNICODE_STRING structures are normalized: their Buffer members should be point to memory inside the block, and it should be not a pointer, but an offset from the beginning of the record. What is the Seed field? The Lower byte of it is used to encrypt the Password_U using the undocumented RtlRunEncodeUnicodeString function (check http://www.d–b.webpark.pl/reverse01_en.htm for description). The original msgina.dll uses the lower byte of GetTickCount for it, let’s do this as well. Now we can write the function which will create the memory block for the pipe:
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 | procedure CopyAndSkipString(var P : Pointer; var Str : PWideChar); var Len : Cardinal; begin Len := (Length(Str) + 1) * SizeOf(Str[0]); CopyMemory(P, Str, Len); Str := P; Inc(Cardinal(P), Len); end; procedure NormalizeUnicodeString(Str : PUNICODE_STRING; Base : Pointer); begin Dec(Cardinal(Str^.Buffer), Cardinal(Base)); end; function CreateGinaPipeRecord32(UserName : PWideChar; Domain : PWideChar; Password : PWideChar) : PGinaPipeRecord; var BufferSize : Cardinal; P : Pointer; begin BufferSize := SizeOf(TGinaPipeRecord) + (Length(UserName) + Length(Domain) + Length(Password) + 3) * SizeOf(WideChar); Result := GetMemory(BufferSize); with Result^ do try ZeroMemory(Result, BufferSize); TotalSize := BufferSize; Seed := Byte(GetTickCount); P := @DataLabel; CopyAndSkipString(P, UserName); CopyAndSkipString(P, Domain); CopyAndSkipString(P, Password); RtlInitUnicodeString(@UserName_U, UserName); RtlInitUnicodeString(@Domain_U, Domain); RtlInitUnicodeString(@Password_U, Password); RtlRunEncodeUnicodeString(@Seed, @Password_U); NormalizeUnicodeString(@UserName_U, Result); NormalizeUnicodeString(@Domain_U, Result); NormalizeUnicodeString(@Password_U, Result); except FreeMemory(Result); Raise; end; end; |
Ok, now we can write the data to the pipe. We need to do it in another thread since we are using FILE_FLAG_WRITE_THROUGH while accessing the pipe (which means that write function does NOT return until the client has read our data). So first the total number of structures is written; then the whole structure is transmitted (yes, with total size again!).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | function SendCredentialThreadProc(Parameter: PThreadDataRec): Integer; var NumberOfBytesWritten : Cardinal; begin Result := 0; with Parameter^, Data^ do begin if not ConnectNamedPipe(PipeHandle, nil) then Result := GetLastError else if not WriteFile(PipeHandle, Data, SizeOf(TotalSize), @NumberOfBytesWritten, nil) or (NumberOfBytesWritten <> SizeOf(TotalSize)) then Result := GetLastError else if not WriteFile(PipeHandle, Data, TotalSize, @NumberOfBytesWritten, nil) or (NumberOfBytesWritten <> TotalSize) then Result := GetLastError else if not DisconnectNamedPipe(PipeHandle) then Result := GetLastError; EndThread(Result); end; end; |
Now, while our data is being written, we disconnect the console and wait for the thread to finish writing to the pipe:
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 | function MyShellStartCredentialServer(UserName : PWideChar; Domain : PWideChar; Password : PWideChar; Timeout : DWORD) : DWORD; stdcall; var ThreadRec : TThreadDataRec; PipeName : String; ThreadHandle : THandle; begin Result := 0; ThreadRec.PipeHandle := CreateGinaPipe(PipeName); try CreateGinaRegistryKey(PipeName); try ThreadRec.Data := CreateGinaPipeRecord(UserName, Domain, Password); try ThreadHandle := SendCredentialAsync(@ThreadRec); try if not WinStationDisconnect(0, WTSGetActiveConsoleSessionId, True) then begin Writeln('Failed to disconnect console session'); Abort; end; WaitForCredentials(ThreadHandle, Timeout); finally CloseHandle(ThreadHandle); end; finally FreeMemory(ThreadRec.Data); end; finally DestroyGinaRegistryKey; end; finally CloseHandle(ThreadRec.PipeHandle); end; end; |
Hurray, we’ve done it! So, now, if you’ll adjust the permission of the gina key (HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Credentials) to give everyone write access (read is not needed), we can use this function from any account!
P.S.1. x64 part
The x64 version of Windows XP does the FUS in exactly the same way; however, there are 2 factors which will prevent this program for working:
1) This program is 32 bit. 32 bit programs are accessing the HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node registry key instead of HKEY_LOCAL_MACHINE\SOFTWARE; So we can add KEY_WOW64_64KEY flag to force native registry key access.
2) UNICODE_STRING is 16 bytes long instead of 8. So we need to write some small procedure which will adjust the created record for x64:
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 | type PUNICODE_STRING_64 = ^UNICODE_STRING_64; UNICODE_STRING_64 = record Length: USHORT; MaximumLength: USHORT; Alignment1 : DWORD; Buffer: PWSTR; Alignment2 : DWORD; end; procedure MoveUnicodeString(var TargetPointer : Pointer; TargetBase : Pointer; TargetStr : PUNICODE_STRING_64; SourceBase : Pointer; SourceStr : PUnicodeString); begin TargetStr^.Length := SourceStr^.Length; TargetStr^.MaximumLength := SourceStr^.MaximumLength; Cardinal(TargetStr^.Buffer) := Cardinal(TargetPointer) - Cardinal(TargetBase);// normalize the offset CopyMemory(TargetPointer, Pointer(Cardinal(SourceBase) + Cardinal(SourceStr^.Buffer)), SourceStr^.MaximumLength); Inc(Cardinal(TargetPointer), SourceStr^.MaximumLength); end; function GetGinaRecordFor64(ExistingRecord : PGinaPipeRecord) : PGinaPipeRecord_64; var Size : DWORD; P : Pointer; begin Size := SizeOf(TGinaPipeRecord_64) + ExistingRecord^.TotalSize - SizeOf(ExistingRecord^); Result := GetMemory(Size); try ZeroMemory(Result, Size); Result^.TotalSize := Size; Result^.Seed := ExistingRecord^.Seed; P := @Result^.DataLabel; MoveUnicodeString(P, Result, @Result^.UserName_U, ExistingRecord, @ExistingRecord^.UserName_U); MoveUnicodeString(P, Result, @Result^.Domain_U, ExistingRecord, @ExistingRecord^.Domain_U); MoveUnicodeString(P, Result, @Result^.Password_U, ExistingRecord, @ExistingRecord^.Password_U); FreeMemory(ExistingRecord); except FreeMemory(Result); Raise; end; end; |
Now this program is working for x64 Windows XP as well 🙂
P.S.2. – TODO list
1) Adjust the pipe security to protect it from read by anyone except SYSTEM.
2) Change the pipe write function for using overlapped structure – terminating a thread is not really a good way!
3) Validate passed credentials before writing them to the pipe
4) Refactor the code as class
SwitchUser.zip (3292 downloads )
9 Responses for "Executing a Fast User Switch programmatically – part 2"
[…] the next part I’ll show you how to create your own implementation of the Credential […]
Hi! What about Vista??
Hi
In Vista msgina support was dropped; and current credential providers just do not support Fast User Switching. You can simulate the behaviour of “Switch User” button by just executing WTSDisconnectSession(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, True);
Oh! Thanx, I knew it, but how “auto login” in some User (Not logged) after WTSDisconnectSession?
I haven’t done any researches in this area yet.
Any way to get WTSConnectSession to work? Every time I use it, it just disconnects the session, and returns error 2250 (ERROR_NOT_CONNECTED).
NVM, the documentation of
WTSConnectSession is wrong (not surprising). The first argument should be the target session id to connect to, and second argument should be the current session id to connect from.
Hey Remko, are you aware of the awesome Super Fast User Switcher powertoy Microsoft had for XP but which they pulled later? Is such a thing possible for Vista/Windows 7?
[…] is also a slightly modified version of “Switch User” application, which now adjust the value of AllowMultipleTSSessions and ForceFriendlyUI keys […]
Leave a reply