Prozess unter einem anderem Benutzer ausführen

Seit Windows NT entwickelt Microsoft auch Betriebssysteme, die mehrbenutzerfähig sind. Das heißt an einem Computer können mit diesen Betriebssystemen mehrere Benutzer arbeiten, wobei eine Rechteverwaltung zum Zuge kommt, die zum Beispiel verhindert, dass ein Benutzer die Daten eines anderen Benutzers einsehen kann, oder dass bestimmte Aktionen nur mit den Rechten eines bestimmten Kontos möglich sind. So ist es einem Konto mit eingeschränkten Rechten in der Regel nicht möglich Software zu installieren oder Einstellungen vorzunehmen, die benutzerübergreifend wirksam werden. Es gibt deswegen auf jedem Rechner ein Administratoren Konto, welches die nötigen Rechte hat, um den Computer zu administrieren. Dieses Rechtesystem dient also zum einen dazu, dass die Daten der Benutzer geschützt werden, als auch dazu, dass ein einzelner Benutzer durch Änderungen an der Konfiguration das System instabil bzw. ganz unbrauchbar macht, weil ihm einfach dazu die nötigen Rechte fehlen. Desweiteren hat diese Benutzerverwaltung natürlich auch noch einen Sicherheitsaspekt. Schädliche Software, die sich beispielsweise in Systemordnern installiert oder bestimmte Zweige der Registry "befällt", kann dies nicht tun, wenn man nur als Benutzer mit eingeschränkten Rechten am Rechner arbeitet und das Administratoren Konto nur dazu benutzt, wozu es da ist: zum Administrieren.

Es kann aber natürlich trotzdem vorkommen, dass man als Benutzer mit eingeschränkten Rechten Dinge erledigen muss zu denen man eigentlich Administratorenrechte bräuchte. Microsoft hat nun verschiedenen API Funktionen vorgesehen, um genau dies zu ermöglichen. Als da wären:

Natürlich benötigt man zu allen API Funktionen die nötigen Login-Daten wie den Benutzernamen und dessen Passwort. Damit sich jetzt nicht einfach schädliche Software höhere Rechte beschaffen kann, gibt es keine API Funktion, um sich das Passwort zu dem entsprechenden Account zu besorgen. Im folgendem Artikel werde ich mich erstmal damit beschäftigen, wie der eigenen Prozess einen anderen Benutzer verkörpen kann.

Die Schlüsselfunktion, um dem eigenen Prozess unter dem Konto eines anderen Bneutzers zu starten, ist die API Funktion ImpersonateLoggedOnUser:

BOOL ImpersonateLoggedOnUser(
  HANDLE hToken
);

Wie man sieht, benötigt diese Funktion ein Handle auf ein Token. In einem Token legt Windows nach der Anmeldung die Zugriffsrechte des Benutzers ab. Startet der Benutzer nun einen Prozess, wird dem Prozess das Token des Benutzers vererbt, so dass der Prozess mit den Zugriffsrechten des Benutzers läuft. Öffnet dieser Prozess nun eine Datei, wird anhand des Prozesstokens geprüft, ob der Prozess dies überhaupt darf. Jetzt wird auch deutlich, warum Zugriffsrechte erst wirksam werden, wenn sich der Benutzer ab- und wieder angemeldet hat, weil das Token erst gesetzt wird, wenn sich der Benutzer anmeldet. Eng verwandt mit dem Begriff des Tokens sind auch (Logon)Sessions, aber wir wollen es erstmal dabei bewenden lassen.

Um das nötige Token-Handle zu bekommen ruft man die Funktion LogonUser auf:

BOOL LogonUser(
  LPTSTR lpszUsername,
  LPTSTR lpszDomain,
  LPTSTR lpszPassword,
  DWORD dwLogonType,
  DWORD dwLogonProvider,
  PHANDLE phToken
);

Die ersten drei Parameter dürften klar sein: Benutzername, Passwort und eventuell die Domain, sollte es sich um keinen lokalen Account handeln. dwLogonType gibt den Typ der des Logons an, der ausgeführt werden soll. Details dazu finden sich im PSDK. Gleiches gilt für den nächsten Parameter: dwLogonProvider, welcher den "Provider" näher spezifiziert. Wichtig ist jedenfalls der letzte Parameter für uns, der uns unser gewünschtes Token-Handle zurückliefert.

Zusammengefasst sieht das ganze dann so aus:

function Impersonate(const User, PW: string): Boolean;
var
  LogonType         : Integer;
  LogonProvider     : Integer;
  TokenHandle       : THandle;
  strAdminUser      : string;
  strAdminDomain    : string;
  strAdminPassword  : string;
begin
  LogonType := LOGON32_LOGON_INTERACTIVE;
  LogonProvider := LOGON32_PROVIDER_DEFAULT or LOGON32_PROVIDER_WINNT50;
  strAdminUser := USER;
  strAdminDomain := '';
  strAdminPassword := PW;
  Result := LogonUser(PChar(strAdminUser), nil,
    PChar(strAdminPassword), LogonType, LogonProvider, TokenHandle);
  if Result then
  begin
    Result := ImpersonateLoggedOnUser(TokenHandle);
    CloseHandle(TokenHandle);
  end;
end;

Zu der API-Funktion LogonUser noch eine Bemerkungen: Unter Windows 2000 benötigt der aufrufende Prozess das Privileg SE_TCB_NAME, welches in der Regel nur Systemdienste haben. Ab Windows XP ist dieses Privileg nicht mehr nötig. Desweiteren ist es nötig, dass die Benutzerkonten mit Passwörtern versehen sind. Weitere Anmerkungen findet man in der Remarks-Sektion im PSDK.

Mit der Funktion RevertToSelf wird die Verkörperung eines anderen Benutzers wieder aufgehoben. Aus Sicherheitsgründen sollte man den Prozess beenden, wenn diese Funktion fehlschlägt, da sonst der Prozess weiter unter den Benutzerrechten des anderen Benutzers ausgeführt wird.

Anbei ein kleines Demo Programm, welches obiges Vorgehen demonstriert. Als Demonstrationsobjekt dient die Datei boot.ini, welche man normalerweise nicht als Benutzer mit eingeschränkten Rechten öffnen kann. Versucht man sie mit Hilfe des Demo-Programms zu öffnen, bekommt man die entsprechende Fehlermeldung: "Zugriff verweigert". Gibt man aber die Logindaten eines Administrators ein, der normalerweise dazu berechtigt ist die Datei zu öffnen, wird sie im Memo angezeigt. Parallel dazu wird in der Statusleiste der Benutzername ausgegeben unter dem das Programm zur Zeit agiert.

const
  USER = 'Hansbambel';
  PW = 'geheim';

function GetCurrUserName: string;
var
  Size : DWORD;
begin
  Size := MAX_COMPUTERNAME_LENGTH + 1;
  SetLength(Result, Size);
  if GetUserName(PChar(Result), Size) then
    SetLength(Result, Size)
  else
    Result := '';
end;

function Impersonate(const User, PW: string): Boolean;
var
  LogonType : Integer;
  LogonProvider : Integer;
  TokenHandle : THandle;
  strAdminUser : string;
  strAdminDomain : string;
  strAdminPassword : string;
begin
  LogonType := LOGON32_LOGON_INTERACTIVE;
  LogonProvider := LOGON32_PROVIDER_DEFAULT;
  strAdminUser := USER;
  strAdminDomain := '';
  strAdminPassword := PW;
  Result := LogonUser(PChar(strAdminUser), nil,
    PChar(strAdminPassword), LogonType, LogonProvider, TokenHandle);
  if Result then
  begin
    Result := ImpersonateLoggedOnUser(TokenHandle);
  end;
end;


procedure TForm1.Button1Click(Sender: TObject);
begin
  if Impersonate(USER, PW) then
  begin
    ShowMessage(GetCurrUserName);
    RevertToSelf;
    ShowMessage(GetCurrUserName);
  end
  else
    MessageBox(0, PChar(SysErrorMessage(GetLastError)), '', 0);
end;

Ich wurde in der Delphipraxis daraufhingewiesen, dass sich ImpersonateLoggedOnUser natürlich immer nur auf den aufrufenden Thread bezieht und nicht, wie ich geschrieben habe, auf den aufrufenden Prozess. Das heißt innerhalb eines eines Prozesses kann jeder Thread einen eigenen Benutzer verkörpern.

2012-01-05T14:59:31 +0100, mail+homepage[at]michael-puff.de