Vorwort
Dieser etwas kleinere Artikel dreht sich um das Thema WinAPI-Crypting.
Warum sollte man sich damit überhaupt befassen?
API-Crypting ist immer dann hilfreich, wenn man möchte, dass erst während der Laufzeit bekannt wird, welche Funktionen aufgerufen werden. Zum Beispiel wenn man das Leben eines Reversers erschweren möchte oder ähnliches.
Was steckt hinter dem API-Crypting
Umgang mit Dlls
Unter Windows haben wir die Möglichkeit eine beliebige Dll-Datei zu laden und alle Funktionen, die diese Dll anbietet, aufzurufen. Dabei müssen wir nur den Namen der Dll und den Funktionsnamen kennen.
Das ganze arbeitet dann mit den drei Funktionen LoadLibrary, GetProcAddress und FreeLibrary. LoadLibrary lädt eine beliebige Dll-Datei in den virtuellen Speicher des aufrufenden Prozesses und gibt dabei ein Handle zu dieser Dll zurück. GetProcAddress durchsucht dann die Export-Tabelle, also die Tabelle in der alle Funktionen stehen, die außerhalb des Moduls angesprochen werden können, nach unserer Funktion und gibt die Addresse zurück. FreeLibrary entlädt die Dll aus dem aufrufenden Prozess.
HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);
FARPROC WINAPI GetProcAddress(HMODULE hModule,LPCSTR lpProcName);
BOOL WINAPI FreeLibrary(HMODULE hModule);
Funktionszeiger
C++ ermölicht es Objekte über ihre Addresse anzusprechen. Doch warum sollte man dies nur auf Objekte beschränken. Durch type-defines können wir sogenannte Funktionszeiger definieren und diese auf eine Addresse im speicher setzen.
Anschließend können wir diese Funktionszeiger wie normale Funktionen aufrufen.
typedef int (WINAPI * MessageBoxPtr)(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
MessageBoxPtr Message = Addresse;
Message(0,"Durch Funktionszeiger aufgerufen","Information",0);
Das Vorgehen beim WinAPI-Crypting
Zunächst benötigen wir folgendes
– den Namen der Funktion in der Export-Tabelle
– den Namen der Dll-Datei in der die Funktion enthalten ist.
– einen Funktionszeigern, den wir auf die Addresse im Speicher setzen können
Der Funktionsname als auch der Name der Dll-Datei werden als verschlüsselte Strings im SRC gespeichert.
Beim Ausführen des Programms werden die Strings entschlüsselt, die Dll in den Prozesspeicher geladen und ein Funktionszeiger auf die Addresse der Funktion gesetzt. Zuletzt kann man den Funktionszeiger wie gewohnt aufrufen.
Ein kleines Beispiel
Angenommen wir möchten folgenden SRC mithilfe von WinAPI-Crypting verschlüsseln
#include <iostream>
using namespace std;
int main(int argc, char** argv)
{
MessageBoxA(0,"Normaler Funktionsaufruf","Information",0);
return 0;
}
Der einzige WinAPI-Aufruf den wir haben ist der Aufruf von MessageBoxA und durch die MSDN erfahren wir, dass die Funktion in der Dll „User32.dll“ als Ascii-Funktion (MessageBoxA) und als Unicode(Widechar)-Funktion (MessageBoxW) enthalten ist. Also schnell einen Funktionszeiger gebastelt, die Dll per LoadLibraryA laden lassen und die Funktionsaddresse per GetProcAddress() erhalten. Ich hab in dem folgenden Code schon den Namen der Dll und den Namen der Funktion in einem std::string gespeichert
#include <iostream>
#include <string>
using namespace std;
typedef int (WINAPI * MessageBoxPtr)(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
std::string Dll = "User32.dll";
std::string Function = "MessageBoxA";
int main(int argc, char** argv)
{
MessageBoxPtr Message = (MessageBoxPtr) GetProcAddress(LoadLibraryA(Dll.c_str()),Function.c_str());
if(Message != 0)
Message(0,"Durch Funktionszeiger aufgerufen","Information",0);
return 0;
}
Was bleibt noch über? Naja es heißt ja „WinAPI-Crypting„, also sollten wir auch den Funktionsnamen und den Namen der Dll verschlüsseln und zur Laufzeit übersetzen. Da ich hier nicht näher auf verschiedene Verschlüsselungsalgorithmen eingehen möchte, verschiebe ich einfach jeden Buchstaben um eine Stelle in der Ascii-Tabelle. So erhalte ich für „User32.dll“ „Vtfs43/emm“ und für „MessageBoxA“ „NfttbhfCpyB“.
Der gesamte Code sieht nun wie folgt aus
#include <iostream>
#include <string>
using namespace std;
typedef int (WINAPI * MessageBoxPtr)(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UINT uType);
std::string Dll = "Vtfs43/emm";
std::string Function = "NfttbhfCpyB";
void decrypt_strings();
int main(int argc, char** argv)
{
decrypt_strings();
MessageBoxPtr Message = (MessageBoxPtr) GetProcAddress(LoadLibraryA(Dll.c_str()),Function.c_str());
Message(0,"Durch Funktionszeiger aufgerufen","Information",0);
return 0;
}
void decrypt_strings()
{
unsigned int i;
for(i=0;i<Dll.length();i++)
{
Dll[i] = (char) (((int) Dll[i])-1);
}
for(i=0;i<Function.length();i++)
{
Function[i] = (char) (((int) Function[i])-1);
}
}
Abschließende Worte
Ich hoffe ihr hattet, trotz des schlechten Themas, Spaß beim Lesen und der ein oder andere Leser hat etwas neues erfahren.
mfg
TgZero