Mirror

Impersonating a User on Windows NT is a three step process (Views: 101)


Problem/Question/Abstract:

How do I simulate the unix SU command under windows NT. In other words I want to run an app under a different user...

Answer:

This code was not written by me.
The original copyright information is still intact.

{*
SU.DPR for Delphi32 Pascal
by Fred - APIKing - de Jong, Heerlen, Netherlands 1997
    home: frejon@worldonline.nl, office: fjng@cbs.nl

su.cpp
   UNIX-like Substitute User for Windows NT

Usage:
   su [NewDomain\][NewUser] [command-line]
   where:
     NewDomain\ is desired domain logon (\\ is ok also)
     NewUser is the name of the user to be impersonated. Default is Administrator.
     command-line is the command to be executed, with parameters. Default is CMD (Console)

Authors:
   David Wihl (wihl@shore.net)
   Steffen Krause (skrause@informatik.hu-berlin.de)

Revision History:
xx-JUL-1995.
       - Removed restriction on command line (User can now specify anything)
       - Added NewDomain logon on command line
       - Added Unicode support but found bug in LogonUserW
03-JUL-1995. Initial public release

Design:
   Impersonating a User on Windows NT is a three step process:
   1-  Logon the User to create a Security identifier
   2-  Enabling access to the Windows Station so the newly logged on NewUser
     can interact. This is necessary even if the Administrator is logging on.
   3-  Creating a process using the Security identifier

   Different privileges are required for steps (1) and (3). Logging on a User
   (LogonUser()) requires the SeTcbPrivilege. Creating a process as another User
   CreateProcessAsUser()) requires SeAssignPrimary and SeIncreaseQuota privileges.
   To grant these privileges, see the Installation Section.

   These two Security API calls were only stablized in NT 3.51, build 1057. SU will
   not work with earlier versions.

   In NT, there is no direct equivalent of UNIX's rwsr-xr-x file permission.

Restrictions and Limitations:
   - There is no logging of failed or successful usage. A future may incorporate
     writing to the Event Log.

Installation:
   The easiest way to selectively grant the three privileges required to use this
   program is:

   1-  Start the User Manager (MUSRMGR)
   2-  Create a new group (e.g. "SU Users")
   3-  Add the three privileges to the group (via Policies\User Rights):
       "Act as part of the operating system"  - SeTcbPrivilege
       "Increase quotas"                      - SeIncreaseQuota
       "Replace a process level token"        - SeAssignPrimaryToken

     NOTE: The three privileges will only be visible if you check
     "Show Advanced User Rights" in the dialog box.
   4-  Add the desired users to the new group (via User\Properties\Group)

   This program was compiled under Visual C++ 2.1 with the June '95 SDK

For more information about Porting from UNIX to NT check the FAQ:
http://www.shore.net/~wihl/unix2nt.html

*}

program su;
{$APPTYPE CONSOLE}

uses
  Windows,
  SysUtils { already has SysErrorMessage function };

//{$R VersInfo.RES}

//
// CUSTOMIZATION OPTIONS - put 'em here
const
  DEFAULT_USER: string = 'Administrator';
    // if we don't specify a username, who are we?
  DEFAULT_CMD: string = 'cmd'; // if we don't specify a command, what do we do?
{$DEFINE VERBOSE} // quiet ?la UNIX, or chatty?
  //
  // END CUSTOMIZATION OPTIONS
  //
const
  SECURITY_DESCRIPTOR_REVISION = 1; // from winnt.h, missing in windows.pas
  ////////////////////////////////////////////////////////////////////////
  //                                                                    //
  //               NT Defined Privileges                                //
  //                                                                    //
  ////////////////////////////////////////////////////////////////////////
  SE_CREATE_TOKEN_NAME = 'SeCreateTokenPrivilege';
  SE_ASSIGNPRIMARYTOKEN_NAME = 'SeAssignPrimaryTokenPrivilege';
  SE_LOCK_MEMORY_NAME = 'SeLockMemoryPrivilege';
  SE_INCREASE_QUOTA_NAME = 'SeIncreaseQuotaPrivilege';
  SE_UNSOLICITED_INPUT_NAME = 'SeUnsolicitedInputPrivilege';
  SE_MACHINE_ACCOUNT_NAME = 'SeMachineAccountPrivilege';
  SE_TCB_NAME = 'SeTcbPrivilege';
  SE_SECURITY_NAME = 'SeSecurityPrivilege';
  SE_TAKE_OWNERSHIP_NAME = 'SeTakeOwnershipPrivilege';
  SE_LOAD_DRIVER_NAME = 'SeLoadDriverPrivilege';
  SE_system_PROFILE_NAME = 'SesystemProfilePrivilege';
  SE_systemTIME_NAME = 'SesystemtimePrivilege';
  SE_PROF_SINGLE_PROCESS_NAME = 'SeProfileSingleProcessPrivilege';
  SE_INC_BASE_PRIORITY_NAME = 'SeIncreaseBasePriorityPrivilege';
  SE_CREATE_PAGEFILE_NAME = 'SeCreatePagefilePrivilege';
  SE_CREATE_PERMANENT_NAME = 'SeCreatePermanentPrivilege';
  SE_BACKUP_NAME = 'SeBackupPrivilege';
  SE_RESTORE_NAME = 'SeRestorePrivilege';
  SE_SHUTDOWN_NAME = 'SeShutdownPrivilege';
  SE_DEBUG_NAME = 'SeDebugPrivilege';
  SE_AUDIT_NAME = 'SeAuditPrivilege';
  SE_system_ENVIRONMENT_NAME = 'SesystemEnvironmentPrivilege';
  SE_CHANGE_NOTIFY_NAME = 'SeChangeNotifyPrivilege';
  SE_REMOTE_SHUTDOWN_NAME = 'SeRemoteShutdownPrivilege';

  { ------------------------------------------------- }

  { support standard Error output, besides standard Output/Input }
var
  Error: TextFile;

procedure InitErrorOutput;
begin
  AssignFile(Error, EmptyStr);
  Rewrite(Error);
  TTextRec(Error).Handle := GetStdHandle(STD_ERROR_HANDLE);
end;

var
  _TokenizeStr: PChar = nil;
  _TokenizeLast: PChar = nil;

function Tokenize(const SourceText: string; const Delimiters: string): string;
{ this is my Delphi version of C's strtok():
    1st call: SourceText is not empty, next calls: SourceText is EmptyStr;
set of delimiters can change while tokenizing;
implicit string memory allocation is hidden for the outside:
    Tokenize only parses one SourceText at a time. }
var
  R, S: PChar;
begin
  if length(SourceText) = 0 then
    R := _TokenizeLast
  else
  begin { cleanup and (re)initialize }
    _TokenizeLast := nil;
    StrDispose(_TokenizeStr);
    _TokenizeStr := StrNew(PChar(SourceText));
    R := _TokenizeStr;
  end;
  if R <> nil then
  begin
    S := R; { find next delim }
    while (S^ <> chr(0)) and (StrScan(PChar(Delimiters), S^) = nil) do
      inc(S);
    if S^ <> chr(0) then
    begin
      S^ := chr(0); { got delim, truncate R result }
      inc(S); { skip over delims to set _TokenizeLast }
      while (S^ <> chr(0)) and (StrScan(PChar(Delimiters), S^) <> nil) do
        inc(S);
      if S^ <> chr(0) then
        _TokenizeLast := S;
    end;
    Result := string(R);
    if S^ = chr(0) then
    begin { cleanup early }
      _TokenizeLast := nil;
      StrDispose(_TokenizeStr);
      _TokenizeStr := nil;
    end
  end
  else
    Result := EmptyStr
end;

{ -------------------------------------------------------------- }
const
  DEFWINSTATION: string = 'WinSta0';
  DEFDESKTOP: string = 'Default';
  WHITESPACE: string = ' ' {SPACE} + chr(9) {TAB} + chr(10) {LF};
  DOMUSERSEP: string = '\';

procedure ErrorHandler(const errmsg: string);
var
  err: dword;
begin
  err := GetLastError;
  writeln(Error, 'Error: ', errmsg, '.');
  write(Error, SysErrorMessage(err));
end;

function SetUserObjectAllAccess(hUserObject: THANDLE): boolean;
var
  pSD: PSecurity_Descriptor;
  si: Security_Information; { dword }
begin
  (* Initialize a security descriptor. *)
  pSD := PSecurity_Descriptor(
    LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH));
  if pSD = nil then
  begin
    ErrorHandler('Can''t Allocate Local Memory');
    Result := FALSE;
    exit;
  end;
  if not InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION) then
  begin
    ErrorHandler('Can''t Initialize Security Descriptor');
    LocalFree(HLOCAL(pSD));
    Result := FALSE;
    exit;
  end;

{* Add a NULL disc. ACL to the security descriptor. *}
  if not SetSecurityDescriptorDacl(pSD,
    TRUE, // specifying a disc. ACL
    PACL(nil),
    FALSE) then // not a default disc. ACL
  begin
    ErrorHandler('Can''t Set Security Descriptor DACL');
    LocalFree(HLOCAL(pSD));
    Result := FALSE;
    exit;
  end;

{* Add the security descriptor to the userobject (like a window or a DDE
   conversation), NOT to a kernelobject (like a process, thread or event). *}
  si := DACL_SECURITY_INformATION;
  Result := SetUserObjectSecurity(hUserObject, si, pSD);

  LocalFree(HLOCAL(pSD));
  if not Result then
    ErrorHandler('Can''t Set NewUser Object Security')
end;

function GetUserObjectName(hUserObject: THandle; var Name: string): boolean;
var
  dw: DWord;
begin
  Name := EmptyStr;
  GetUserObjectInformation(hUserObject, UOI_NAME, PChar(Name), 0, dw);
  SetLength(Name, dw + 1);
  Result := GetUserObjectInformation(hUserObject, UOI_NAME, PChar(Name), dw, dw);
  if Result then
    SetLength(Name, dw - 1)
  else
    Name := EmptyStr;
end;

function GetPrivilegeDisplayName(const PrivilegeName: string): string;
{ PrivilegeName is of string type 'SE_'* }
var
  dw, li: DWord;
begin
  Result := EmptyStr;
  dw := 0;
  li := 0; { li:= dword(MAKELANGID(LANG_DEFAULT, LANG_USER)); }
  if not LookupPrivilegeDisplayName(nil, PChar(PrivilegeName), PChar(Result), dw, li)
    then
    dw := 256;
  SetLength(Result, dw + 1);
  if LookupPrivilegeDisplayName(nil, PChar(PrivilegeName), PChar(Result), dw, li) then
    SetLength(Result, StrLen(PChar(Result)))
  else
    Result := EmptyStr;
end;

function GetAccountInfo(var CurUser, CurDomain: string): boolean;
var
  dw, dw2: DWord;
  pSD: PSecurity_Descriptor;
  snu: Sid_Name_Use;
begin
  Result := False;
  dw := 255;
  Setlength(CurUser, dw + 1);
  if GetUserName(PChar(CurUser), dw) then
  begin
    SetLength(CurUser, dw - 1);
    dw2 := 256;
    SetLength(CurDomain, dw2);
    snu := SidTypeUser;
    pSD := nil;
    dw := 0; { get needed length for SID }
    LookUpAccountName(nil {LocalMachine}, PChar(CurUser),
      pSD, dw, PChar(CurDomain), dw2, snu);
    if dw <> 0 then
    begin
      pSD := PSecurity_Descriptor(LocalAlloc(LPTR, dw));
      if pSD <> nil then
      begin
        if LookUpAccountName(nil, PChar(CurUser), { get the real thing }
          pSD, dw, PChar(CurDomain), dw2, snu) then
        begin
          SetLength(CurDomain, dw2);
          Result := True;
        end
        else
          CurDomain := EmptyStr;
        LocalFree(HLOCAL(pSD));
      end;
    end;
  end
  else
    CurUser := EmptyStr;
end;

function GetMachineName: string;
var
  dw: DWord;
begin
  dw := MAX_COMPUTERNAME_LENGTH + 1;
  SetLength(Result, MAX_COMPUTERNAME_LENGTH + 1);
  if GetComputerName(PChar(Result), dw) then
    SetLength(Result, dw)
  else
    Result := EmptyStr;
end;

{ ---------------------------------------------------------- }
var
  CurUser, // Current User
  CurDomain, // Current Domain
  pwstr, // password string
  consoleTitle, // Title if new console only
  NewDomUser, // NewDomain\NewUser combination
  CommandLine, // command line we pass to the new process
  NewDomain, // NewDomain to log onto
  NewUser: string; // NewUser to log onto
  startUpInfo: TStartupInfo;
  procInfo: TProcessInformation; // child process info, from CreateProcessAsUser
  hDesktop: HDESK;
  hWindowStation: HWINSTA;
  hUserToken, hConsIn: THANDLE;
  OldConsInMode, NewConsInMode: DWORD;
  NTversion: TOSVersionInfo;
  S, DeskTopName, WinStaName: string;
  RC: integer;

begin { program }
  InitErrorOutput; // Attach outputfile Error to STDERR

  // Make sure we are using the minimum OS version.
  NTversion.dwOSVersionInfoSize := sizeof(TOSVersionInfo);
  if not GetVersionEx(NTversion) then
  begin
    ErrorHandler('Unable to get OS version');
    halt(1);
  end;
  if NTversion.dwPlatformId <> VER_PLATform_WIN32_NT then
  begin
    writeln(Error, 'SU will run only on Windows NT.');
    halt(1);
  end;
  if NTversion.dwBuildNumber < 1057 then // Commercial 3.51 release
  begin
    writeln(Error, 'SU requires at minimum NT version 3.51 build 1057.');
    halt(1);
  end;

  //{$IFDEF DEBUG}
  writeln('SU: NT Version ', NTversion.dwMajorVersion, '.',
    NTversion.dwMinorVersion, ', build ', NTversion.dwBuildNumber);
  // {$ENDIF}

  GetAccountInfo(CurUser, CurDomain);
  writeln('You are ', CurDomain, '\', CurUser);

  // Process the command line parameters
  Tokenize(string(CmdLine), WHITESPACE);
  NewDomUser := Tokenize(EmptyStr, WHITESPACE);
  if length(NewDomUser) = 0 then
  begin
    NewDomUser := DEFAULT_USER;
    CommandLine := DEFAULT_CMD;
  end
  else
  begin
    CommandLine := Tokenize(EmptyStr, EmptyStr);
    if length(CommandLine) = 0 then
      CommandLine := DEFAULT_CMD;
  end;
  if Pos(DOMUSERSEP, NewDomUser) > 0 then
  begin
    NewDomain := Tokenize(NewDomUser, DOMUSERSEP);
    NewUser := Tokenize(EmptyStr, DOMUSERSEP);
    if length(NewUser) = 0 then
      NewUser := DEFAULT_USER;
  end
  else
  begin
    NewDomain := EmptyStr;
    NewUser := NewDomUser;
  end;
  if (length(NewDomain) = 0) and
    ((NewUser = '-?') or (NewUser = '/?') or (NewUser = '?')) then
  begin
    writeln;
    writeln('Runs Windows NT commands under another user''s account.');
    writeln;
    writeln('SU [newdomain\][newuser] [command-line]');
    writeln;
    writeln('  [newdomain\]   Specifies desired domain logon (\\ is ok also).');
    writeln('  [newuser]      Specifies the name of the user to be impersonated.');
    writeln('                   The default is Administrator.');
    writeln('  [command-line] Specifies the command to be executed, with parameters.');
    writeln('                   The default is CMD (a new NT Console).');
    writeln;
    writeln('Requires three extended NT privileges:');
    writeln;
    writeln('  ', GetPrivilegeDisplayName(SE_TCB_NAME), ',');
    writeln('  ', GetPrivilegeDisplayName(SE_ASSIGNPRIMARYTOKEN_NAME), ' and');
    writeln('  ', GetPrivilegeDisplayName(SE_INCREASE_QUOTA_NAME), '.');
    writeln;
    writeln('These can be granted as User Rights with NT User Manager.');
    halt(0);
  end;

  // Turn off console mode echo, since we don't want clear-screen passwords
  system.Reset(Input); {GetStdHandle(STD_INPUT_HANDLE)}
  hConsIn := TTextRec(Input).Handle;

  //if hConsIn = INVALID_HANDLE_values then
  //begin
  //  ErrorHandler ('Can''t get handle of STDIN'); halt(1);
  //end;

  if not GetConsoleMode(hConsIn, OldConsInMode) then
  begin
    ErrorHandler('Can''t get current Console Mode');
    halt(1);
  end;
  NewConsInMode := OldConsInMode and (not ENABLE_ECHO_INPUT);
  if not SetConsoleMode(hConsIn, NewConsInMode) then
  begin
    ErrorHandler('Unable to turn off Echo');
    halt(1);
  end;

  // Ask for the password
{$IFDEF VERBOSE}
  if length(NewDomain) = 0 then
    S := CurDomain
  else
    S := NewDomain;
  writeln('Logging onto ', S, ' domain as ', NewUser, '.');
{$ENDIF}
  write('Enter password: ');
  readln(pwstr);
  // When echo is off and NewUser hits , CR-LF is not echoed, so do it for him
  writeln;
  if not SetConsoleMode(hConsIn, OldConsInMode) then
  begin
    ErrorHandler('Unable to reset previous console mode');
    halt(1);
  end;
  CloseHandle(hConsIn);

  // Do the Logon
  if not LogonUser(PChar(NewUser), PChar(NewDomain), PChar(pwstr),
    LOGON32_LOGON_INTERACTIVE,
    LOGON32_PROVIDER_DEFAULT, hUserToken) then
  begin
    case GetLastError of
      ERROR_PRIVILEGE_NOT_HELD:
        begin
          writeln(Error,
            'Error: you do not have the following extended User Right:');
          writeln(Error, GetPrivilegeDisplayName(SE_TCB_NAME), '.');
        end;
      ERROR_LOGON_FAILURE:
        ErrorHandler('LogonUser failed.');
      ERROR_ACCESS_DENIED:
        ErrorHandler('Access is denied');
    else
      ErrorHandler('Unable to logon');
    end;
    halt(2);
  end;

  // give the NewUser access to the current WindowStation and Desktop
  hWindowStation := GetProcessWindowStation;
  if not GetUserObjectName(hWindowStation, WinStaName) then
    WinStaName := DEFWINSTATION;
  if not SetUserObjectAllAccess(hWindowStation) then
  begin
    write(Error, 'Can''t set WindowStation ', WinStaName, ' security.');
    CloseHandle(hUserToken);
    halt(3);
  end;
  hDesktop := GetThreadDesktop(GetCurrentThreadId);
  if not GetUserObjectName(hDesktop, DeskTopName) then
    DeskTopName := DEFDESKTOP;
  if not SetUserObjectAllAccess(hDesktop) then
  begin
    write(Error, 'Can''t set Desktop ', DeskTopName, ' security.');
    CloseHandle(hUserToken);
    halt(3);
  end;

  // Set the STARTUPINFO for the new process
  if length(NewDomain) <> 0 then
    NewDomain := NewDomain + '\';
  consoleTitle := 'SU: ' + NewDomain + NewUser;
  FillChar(startUpInfo, sizeof(startUpInfo), 0);
  with startUpInfo do
  begin
    cb := sizeof(startUpInfo);
    lpTitle := PChar(consoleTitle);
    S := WinStaName + '\' + DeskTopName;
    lpDesktop := PChar(S);
  end;

  // Create the child process
  if not CreateProcessAsUser(hUserToken,
    nil, PChar(CommandLine), nil, nil, FALSE {no inherit handles},
    CREATE_NEW_CONSOLE or CREATE_NEW_PROCESS_GROUP,
    nil, nil, startUpInfo, procInfo) then
  begin
    case GetLastError of
      ERROR_PRIVILEGE_NOT_HELD:
        begin
          writeln(Error, 'Error: missing (one of) following extended User Rights:');
          writeln(Error, GetPrivilegeDisplayName(SE_ASSIGNPRIMARYTOKEN_NAME), ', or');
          writeln(Error, GetPrivilegeDisplayName(SE_INCREASE_QUOTA_NAME), '.');
          ErrorHandler(EmptyStr);
        end;
      ERROR_FILE_NOT_FOUND:
        ErrorHandler('Error: command in ''' + CommandLine + ''' not found.');
    else
      ErrorHandler('Error: CreateProcessAsUser failed.');
    end;
    RC := 4;
  end
  else
    RC := 0;

  CloseHandle(hWindowStation);
  CloseHandle(hDesktop);
  CloseHandle(hUserToken);

  if RC = 0 then
  begin
    CloseHandle(procInfo.hThread);
    CloseHandle(procInfo.hProcess);
  end;

  halt(RC);

end.

<< Back to main page