Nach einiger Zeit ohne neuen Content habe ich mal wieder was für euch vorbereitet
Vor etwa 2 Jahren haben Opcodez und ich angefangen etwas Gamehacking für einen recht bekannten Online Shooter zu betreiben. Jetzt nach langer Zeit habe ich unsere alten Codes mal wieder ausgegraben und sie aktualisiert und erweitert. Hierbei war es notwendig das betroffene Spiel mit OllyDbg und CheatEngine zu analysieren. Doch leider gestaltete sich diese Angelegenheit nicht so einfach wie erhofft. Das Spiel startet insgesammt 3 Prozesse, welche alle nach bekannten Prozessnamen, Fensterklassen oder Speicherhashs scannen. Meine Plugins für OllyDbg versteckten den Debugger zwar erfolgreich vor dem aktiven Prozess, allerdings nicht vor den anderen 2 Redundanzwächtern.
Um nun doch eine Analyse ohne sofortigen Crash zu ermöglichen, schrieb ich mir eine DLL, welche den Zugriff auf unbekannte Prozesse anhand einer WhiteList verhindert ..
Prozesswhitelist:
const WhiteList: array[1..11] of String = ( 'System', 'smss.exe', 'csrss.exe', 'wininit.exe', 'services.exe', 'lsass.exe', 'lsm.exe', 'winlogon.exe', 'svchost.exe', 'taskhost.exe', 'dwm.exe', ); function IsProtectedProcess(ProcessName: String): Boolean; var I: Integer; begin Result := true; for I := Low(WhiteList) to High(WhiteList) do begin if (AnsiLowerCase(ProcessName) = AnsiLowerCase(WhiteList[I])) then begin Result := false; Exit; end; end; end;
1) Zu allererst ist es hier natürlich sinnvoll, dass die geschützten Prozesse bei einer Prozessenumeration erst gar nicht auftauchen. Der tiefste aus dem Usermode erreichbare Angriffspunkt hierfür befindet hier bei in der undokumentierten nativen NtQuerySystemInformation() API, welche neben einer ganzen Reihe von allgemeinen System Informationen auch eine Prozessliste erzeugen kann. Der Aufbau der Funktion gestaltet sich folgendermaßen:
function NtQuerySystemInformation( SystemInformationClass: SYSTEM_INFORMATION_CLASS; SystemInformation: PVOID; SystemInformationLength: ULONG; ReturnLength: PULONG): NTSTATUS; stdcall;
Der erste Parameter spezifiziert die angeforderten Informationen, in unserem Falle SystemProcessesAndThreadsInformation, der zweite Parameter ist der Rückgabebuffer, in dem sich die Informationen nach Ausführung befinden, der dritte Parameter enthält die Länge dieses Buffers und der letzte Parameter gibt nach Ausführung die effektive Größe der Rückgabe an.
Bei der Prozessliste handelt es sich im Prinzip um eine einfach verkettete Liste mit folgender Elementdefinition, von der uns lediglich die Werte NextEntryDelta, ProcessId und ProcessName interessieren:
type _SYSTEM_PROCESSES = record NextEntryDelta: ULONG; ThreadCount: ULONG; Reserved1: array[0..5] of ULONG; CreateTime: LARGE_INTEGER; UserTime: LARGE_INTEGER; KernelTime: LARGE_INTEGER; ProcessName: UNICODE_STRING; BasePriority: KPRIORITY; ProcessId: ULONG; InheritedFromProcessId: ULONG; HandleCount: ULONG; SessionId: ULONG; Reserved2: ULONG; VmCounters: VM_COUNTERS; PrivatePageCount: ULONG; IoCounters: IO_COUNTERSEX; Threads: array[0..0] of SYSTEM_THREADS; end; SYSTEM_PROCESSES = _SYSTEM_PROCESSES; PSYSTEM_PROCESSES = ^SYSTEM_PROCESSES;
Das Prinzip der verketteten Liste macht es uns extrem einfach einzelne Elemente zu entfernen. CurrentItem + NextEntryDelta zeigt immer auf den Beginn des nächsten Items. NextEntryDelta des vorherigen Elements muss also lediglich um die Größe des zu versteckenden Elements erhöht werden, um letzteres aus der Liste auszublenden. Zur Sicherheit löschen wir zusätzlich den Prozessnamen und setzten die ProzessId auf 0. Der Code im NtQuerySystemInformation Hook sähe also folgendermaßen aus:
function C_NtQuerySystemInformation( SystemInformationClass: SYSTEM_INFORMATION_CLASS; SystemInformation: PVOID; SystemInformationLength: ULONG; ReturnLength: PULONG): NTSTATUS; stdcall; function FindNextProcessDelta(Process: PSYSTEM_PROCESSES): DWord; var Current: PSYSTEM_PROCESSES; begin Result := Process^.NextEntryDelta; if (Process^.NextEntryDelta = 0) then Exit; // Alle nachfolgenden Elemente durchgehen Current := PSYSTEM_PROCESSES(Cardinal(Process) + Process^.NextEntryDelta); repeat // Eigenen Prozess nicht unlinken if (Current^.ProcessId = GetCurrentProcessId) then Exit; if IsProtectedProcess(Current^.ProcessName.Buffer) then begin if (Current^.NextEntryDelta > 0) then begin // NextEntryDelta um eigene Größe erhöhen und Speicher leeren Inc(Result, Current^.NextEntryDelta); ZeroMemory(Current, Current^.NextEntryDelta); end else begin // Letzter Prozess in der Liste, NextEntryDelta = 0 zurückgeben Result := 0; ZeroMemory(Current, Current^.NextEntryDelta); Exit; end; end else begin Break; end; Current := PSYSTEM_PROCESSES(Cardinal(Process) + Result); until false; end; var Process: PSYSTEM_PROCESSES; begin Result := O_NtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if (SystemInformationClass = SystemProcessesAndThreadsInformation) and (NT_SUCCESS(Result)) then begin // Alle Einträge der Liste durchgehen Process := SystemInformation; repeat // NextEntryDelta modifizieren Process^.NextEntryDelta := FindNextProcessDelta(Process); if (Process^.NextEntryDelta = 0) then Break; Process := PSYSTEM_PROCESSES(Cardinal(Process) + Process^.NextEntryDelta); until false; end; end;
Entschuldigt meinen „Wurschtelcode“, ich habe das nur schnell frei Hand geschrieben und nicht weiter optimiert oder auf Eleganz geachtet.
2) Nachdem nun alle unbekannten Prozesse getilgt wurden, stellte ich zu meiner Verwunderung fest, dass besagtes Wächterprogramm doch glatt versucht mittels OpenProcess() sämtliche ProzessIds von 0 bis MAXWORD zu öffnen :rolleyes:
Also gut kein Problem, hooken wir also auch noch NtOpenProcess() und geben bei unbekannten Prozessen ein Nullhandle zurück. Das erste Problem ist nun anhand der ProzessId an den Prozessnamen zu gelangen. Gegen das Auflisten aller Prozesse und abschließenden ProzessId Vergleich spricht allerdings die Tatsache, dass wir die unbekannten Prozesse bereits aus der Prozessliste entfernt haben und so nur noch sehr umständlich an eine vollständige Liste gelangen würden. Das MSDN rät in diesem Falle zu
GetModuleFileNameEx(). Ruft man diese API allerdings aus einem 32 bit Prozess heraus auf und versucht den Namen eines 64 bit Prozesses auszulesen, so schlägt der Aufruf fehl. Ab Vista könnten wir alternativ auf die QueryFullProcessImageName () Funktion zurückgreifen. Da wir aber bei Möglichkeit auch noch XP Systeme unterstützen wollen, müssen wir uns mal wieder durch eine native API retten.
NtQueryInformationProcess() erlaubt die Abfrage einiger Prozessinformationen wie unter anderem auch dem kompletten Pfad der ausführbaren Datei. Folgender Beispielcode liefert uns den Prozessnamen anhand der ProzessID:
function GetProcessNameByPID(PID: DWord): String; var ProcessName: array[0..MAX_PATH - 1] of WideChar; ReturnLength: ULONG; hProcess: THandle; begin Result := ''; hProcess := InternalOpenProcess(PROCESS_QUERY_INFORMATION, false, PID); if (hProcess <> 0) and (hProcess <> INVALID_HANDLE_VALUE) then try if NT_SUCCESS(NtQueryInformationProcess(hProcess, ProcessImageFileName, @ProcessName[0], MAX_PATH, @ReturnLength)) then begin Result := PWideChar(@ProcessName[0]); Result := ExtractFileName(Result); end; finally CloseHandle(hProcess); end; end;
Aufmerksamen Lesern mag aufgefallen sein, dass ich statt des normalen OpenProcess Aufrufs eine eigene Funktion namens InternalOpenProcess verwendet habe. Würde ich innerhalb des NtOpenProcess Callbacks OpenProcess aufrufen, würde wieder der gehookte Code ausgeführt und der Callback aufgerufen. Dies passiert dann so lange, bis ein Stack Overflow auftritt und das Programm abstürzt. Bei InternalOpenProcess handelt es sich um eine eigene Implementation von OpenProcess, wobei ich hier O_NtOpenProcess anstelle der Original Funktion verwende:
function InternalOpenProcess(dwDesiredAccess: DWord; InheritHandle: LongBool; dwProcessID: Cardinal): THandle; stdcall; var ObjectAttributes: TObjectAttributes; ClientId: TClientId; Status: NTSTATUS; begin ObjectAttributes.Length := SizeOf(TObjectAttributes); ObjectAttributes.RootDirectory := 0; ObjectAttributes.ObjectName := nil; if InheritHandle then begin ObjectAttributes.Attributes := OBJ_INHERIT; end else begin ObjectAttributes.Attributes := 0; end; ObjectAttributes.SecurityDescriptor := nil; ObjectAttributes.SecurityQualityOfService := nil; ClientId.UniqueProcess := dwProcessID; ClientId.UniqueThread := 0; Status := O_NtOpenProcess(@Result, dwDesiredAccess, @ObjectAttributes, @ClientId); if (not NT_SUCCESS(Status)) then begin SetLastError(RtlNtStatusToDosError(Status)); end; end;
Der eigentliche NtOpenProcess Callback gestaltet sich nach der ganzen Vorarbeit nun recht einfach. Es wird geprüft, ob der Prozess sichtbar ist oder nicht und dementsprechend ein Ergebnis zurückgegeben:
function C_NtOpenProcess(ProcessHandle: PHANDLE; DesiredAccess: ACCESS_MASK; ObjectAttributes: POBJECT_ATTRIBUTES; ClientId: PCLIENT_ID): NTSTATUS; stdcall; var ProcessName: String; begin ProcessName := GetProcessNameByPID(ClientId^.UniqueProcess); if (ProcessName = '') or IsProtectedProcess(ProcessName) then begin ProcessHandle^ := 0; Result := STATUS_INVALID_CID; end else begin Result := O_NtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); end; end;
3) Soweit so gut! Fehlt also nur noch eine Funktion, mit der wir verhindern, dass nach bekannten Fensternamen oder Fensterklassen gesucht werden kann. Die zwei Standardfunktionen für diese Arbeit sind
FindWindowA() und FindWindowW().
Beim Konzipieren des entsprechenden Hook Callbacks dachte ich an die GetWindowThreadProcessId() Funktion, welche die ProzessID anhand eines Fensterhandles liefert. Schreiben wir uns also wieder eine kleine Hilfsfunktion, welche auf GetProcessNameByPID aufbaut:
function IsProtectedWindow(Window: HWND): Boolean; var PID: DWord; Process: THandle; ProcessName: String; begin Result := false; GetWindowThreadProcessId(Window, @PID); if (PID <> 0) then begin ProcessName := GetProcessNameByPID(PID); if (ProcessName = '') or IsProtectedProcess(ProcessName) then begin Result := true; end; end; end;
Anhand der IsProtectedWindow Funktion können wir nun auf einfache Art und Weise feststellen, ob das entsprechende Fenster zu einem unsichtbaren Fenster gehört und die Rückgabe abermals modifizieren:
function C_FindWindowA(lpClassName, lpWindowName: PAnsiChar): HWND; stdcall; begin Result := O_FindWindowA(lpClassName, lpWindowName); if (Result <> 0) and (Result <> INVALID_HANDLE_VALUE) then begin if IsProtectedWindow(Result) then Result := 0; end; end; function C_FindWindowW(lpClassName, lpWindowName: PWideChar): HWND; stdcall; begin Result := O_FindWindowW(lpClassName, lpWindowName); if (Result <> 0) and (Result <> INVALID_HANDLE_VALUE) then begin if IsProtectedWindow(Result) then Result := 0; end; end;
Ich hoffe dieses kleine Tutorial hat euch gefallen und vielleicht auch ein paar Anfängern in diesem Bereich Lust auf Mehr gemacht. Hier sieht man nocheinmal sehr deutlich, wie viel Potenzial in Hooks und auch in den nativen undokumentierten Windows APIs steckt.
© opcodez.wordpress.com