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