If you want to obtain a user’s token in a Terminal Server or Citrix session (eg to launch a process in a session) you can call the WTSQueryUserToken function.

On the x64 versions of Windows XP and Server 2003 this function fails however and returns ERROR_INSUFFICIENT_BUFFER ("The data area passed to a system call is too small.") when called from a 32 bit process.

Internally WTSQueryUserToken calls the undocumented function WinstationQueryInformationW with the WinStationUserToken class (14) and passing a WINSTATIONUSERTOKEN struct, filled with caller ProcessId and ThreadId.

But on x64 Windows the size of this structure is 24 bytes, while on 32 bit Windows the size of the structure is 12 bytes!

Ok, let’s try to call WinstationQueryInformationW function directly:

objectpascal Download
type
  THANDLE_64 = record
    Handle : THandle;
    Alignment : DWORD;
  end;

function GetUserToken(SessionId : DWORD; out TokenHandle : THandle) : BOOL;
var
  WINSTATIONUSERTOKEN64 : record
    ProcessId : THANDLE_64;
    ThreadId : THANDLE_64;
    TokenHandle : THANDLE_64;
  end;

  Res : DWORD;
  RetLength : DWORD;
begin
  if (IsWow64) then begin
    with WINSTATIONUSERTOKEN64 do
    begin
      ProcessId.Handle := GetCurrentProcessId;
      ProcessId.Alignment := 0;

      ThreadId.Handle := GetCurrentThreadId;
      ThreadId.Alignment := 0;

      TokenHandle.Handle := 0;
      TokenHandle.Alignment := 0;
    end;
    Result := WinstationQueryinformationW(WTS_CURRENT_SERVER, SessionId, WinstationUserToken, @WINSTATIONUSERTOKEN64, SizeOf(WINSTATIONUSERTOKEN64), RetLength);
    if (Result) then
      TokenHandle := WINSTATIONUSERTOKEN64.TokenHandle.Handle;

  end else
    Result := WTSQueryUserToken(SessionId, TokenHandle);
end;

But this call returns ERROR_INSUFFICIENT_BUFFER again! Why?

The reason is that WinstationQueryInformationW does some buffer checks/adjustments, before calling the real function, RpcWinStationQueryInformation.

Unfortunately, in some cases the buffer size is (incorrectly) hardcoded while the receiver (termsrv.dll) in many cases allows any value greater then required.

The buffer check is done in an internal function called ValidUserBuffer:

cpp Download
bool __stdcall ValidUserBuffer(unsigned int BufferSize, int WinStationInformationClass)
{

  switch ( WinStationInformationClass )
  {
    case 25:
      return BufferSize >= 0x48;
    case 0:
      return BufferSize == 8;
    case 2:
      return BufferSize == 568;
    case 5:
      return BufferSize == 264;
    case 7:
    case 11:
    case 12:
    case 13:
    case 16:
    case 17:
    case 18:
    case 19:
    case 20:
      return true;
    case 9:
      return BufferSize == 652;
    case 10:
      return BufferSize == 4;
    case 26:
      return BufferSize == 16;
    case 27:
      return BufferSize >= 0xCC;
    case 29:
      return BufferSize >= 0x20;
    case 34:
      return BufferSize >= 0x60;
    case 33:
      return BufferSize >= 0x600;
    case 32:
    case 35:
    case 36:
      return BufferSize >= 1;
    case 24:
    case 28:
    case 30:
    case 31:
    case 37:
      return BufferSize >= 4;
    case 8:
      return BufferSize == 1216;
    case 6:
      return BufferSize == 2296;
    case 14:
      return BufferSize == 12;
    case 21:
      return BufferSize >= 9;
    default:
      return false;
    case 1:
      return BufferSize == 2664;
    case 3:
      return BufferSize == 300;
    case 4:
      return BufferSize == 736;
  }
}

So, as you can see, there is no way to succeed using the original winsta.dll!

The only workaround is to patch it (and possibly distribute it with your app) or use RpcWinStationQueryInformation directly.

The code below works fine:

objectpascal Download
function GetUserToken(SessionId : DWORD; out TokenHandle : THandle) : BOOL;
var
  WINSTATIONUSERTOKEN64 : record
    ProcessId : THANDLE_64;
    ThreadId : THANDLE_64;
    TokenHandle : THANDLE_64;
  end;

  Res : DWORD;
  RetLength : DWORD;
begin
  if (IsWow64) then begin
    with WINSTATIONUSERTOKEN64 do
    begin
      ProcessId.Handle := GetCurrentProcessId;
      ProcessId.Alignment := 0;

      ThreadId.Handle := GetCurrentThreadId;
      ThreadId.Alignment := 0;

      TokenHandle.Handle := 0;
      TokenHandle.Alignment := 0;
    end;

    Result := RpcWinStationQueryInformation(WinStationGetLocalServerHandle, Res, SessionId, WinStationUserToken, @WINSTATIONUSERTOKEN64, SizeOf(WINSTATIONUSERTOKEN64), RetLength);
    if (Result) then
      TokenHandle := WINSTATIONUSERTOKEN64.TokenHandle.Handle
    else
      SetLastError(RtlNtStatusToDosError(Res));
  end else
    Result := WTSQueryUserToken(SessionId, TokenHandle);
end;

However to use the RpcWinStationQueryInformation you need the MIDL compiler, which supports only C/C++, to generate the code.

Of course it can be done using Delphi but that is out of scope for this article.