About Terminal Server, Citrix, Delphi and other stuff
26 Nov
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:
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).
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:
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:
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!).
Now, while our data is being written, we disconnect the console and wait for the thread to finish writing to the pipe:
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:
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 (247)
8 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?
Leave a reply