Einfach gesagt, ich war eines Tages langweilt und fragte meinen Freund Maximus Hackerman, mir etwas zu tun. Umgehend schickte er eine einfache ausführbare Datei mit dem Namen "Reversem.exe" (Virustotal Link) mit einem einfachen Kommentar zu "Drucken Sie die versteckte Nachricht in Ihrer eigenen Anwendung". Natürlich wurden die CPP -Header -Dateien nicht zur Verfügung gestellt. Nach Ausführen der ausführbaren Datei wird einfach als Konsolenanwendung geöffnet, druckt "alle magischen Pakete, bald aus, BEEP BOOP!" und dann fast sofort; Ich denke, "bald" ist subjektiv.
Glücklicherweise scheint es kein Anti-Debugging zu geben, also fühlte sich Hackerman an diesem Tag nett.
Nach dem Anbringen von Windbg und einem Disassembler können wir sehen, dass die Anwendung, was anfänglich zu sein scheint, eine uneingeschränkte divide by zero ausnahme.
Bei näherer Betrachtung stellen wir jedoch fest, dass diese Ausnahme kurz vor dem Drucken ihrer einzigen sichtbaren Nachricht ausgelöst wird. Es ist wichtig zu beachten, dass zwei VEHs (vektorierte Ausnahmehandler) vor diesem Zeitpunkt geringfügig registriert sind. Daher ist diese Ausnahme wahrscheinlich beabsichtigt und verwendet, um den Kontrollfluss zu magen. Günstiger Trick wirklich, um uns wirklich abzuwerfen!
Wenn Sie die Fahrten für den Moment vergessen, kann man davon ausgehen, dass bei der „Senden“ von Paketen die WINSOCK -Send -Funktion dazu verwendet wird (obwohl die Anwendung eindeutig sagt, dass sie "magische" Pakete sind, also werden wir sehen).
Wie erwartet stellt sich heraus, dass Sie Pakete nicht nach Magie senden können, sodass die Winsock send -Funktion tatsächlich importiert wird.
Indem wir einen Haltepunkt für die Sendungsfunktion platzieren, können wir einen Blick darauf werfen und feststellen, ob es relevante Daten gibt, die wir zum Zeitpunkt des Sendens abstrahieren können. Leider sind die Daten bereits verschlüsselt, wenn der Puffer in die Funktion geladen wird. Wir können jedoch feststellen, dass der Puffer immer 3 Bytes in Länge hat, der Socket immer an einen hartcodierten Wert von 0x69 ( schön ) gebunden ist und dass der Send -Funktionsbrauch insgesamt 14 -mal erreicht ist.
Es gibt ein paar Möglichkeiten, um die Verschlüsselung umzugehen. Einer soll es vollständig umkehren, was sich als viel Aufwand herausstellen könnte, ein anderer ist es, die gewünschten Daten vor der Verschlüsselung zu lokalisieren und sie abstrahieren, bevor sie verschlüsselt wird. Letzteres ist deutlich einfacher als erstere, also gehen wir damit ein; Fühlen Sie sich frei, die Verschlüsselung umzukehren, ich habe einen kurzen Blick gesehen und es ist nicht schrecklich. Alternativ können Sie, wenn Sie Lust haben, immer den Steckdose -Wert -Wert überschreiben und einen Empfangsantragsvertrag damit abschließen.
Leider gibt es im Segment schreibgeschützter Datenabschlüsse keine nützlichen Zeichenfolgen, abgesehen von der anfänglichen Nachricht, daher keine hilfreichen Zeiger aus diesem Aspekt!
Wenn wir zu den Vehfahrten zurückkehren, können wir feststellen, dass beide registrierten Handler verwendet werden, um ein paar unbekannte Objekte in einen zugewiesenen Speicherpuffer zu kopieren. Dies scheint durch die Verwendung von MEMCPY zu erfolgen, wobei eine Funktion als zweiter Parameter verwendet wird, was wiederum eine hartcodierte Ganzzahl als zweiter Parameter verwendet. Es ist erwähnenswert, dass diese hartcodierten Werte zu keinem Zeitpunkt 14 überschreiten. Ich habe nur einen der VEHs in den Screenshot aufgenommen, da sie im Grunde genommen identisch mit Ausnahme der hartgesottenen Werte sind.
Durch das Breakpoining in einer der Memcpy -Funktionen und das Überprüfen der Unterfunktion im zweiten Parameter können wir sehen, dass die hartcodierte Ganzzahl (13 / 0dh im folgenden Beispiel) auf das erste Byte des A1 -Parameters eingestellt ist und A1+1 ein Zeichen enthält, kurz nachdem der Zuteil der Zuteilung enthält.
Wenn wir einige der anderen 14 Aufrufe dieser Funktion überprüfen, können wir das gleiche Verhalten wiederholen. Wenn wir die Zeichen von 1 bis 14 anordnen, können wir die entsprechende Zahl als Bestellanzeige verwenden und sehen, dass sie anfangen, einige lesbare Wörter auszuräumen. Jetzt könnten wir super faul sein und einfach eine Konsolen -App erstellen, um die Nachricht auszudrucken, sobald wir wissen, was sie ist, aber das fühlt sich wirklich wie Betrug an. Außerdem kann sich die Nachricht irgendwann ändern. Schreiben wir also eine Codehöhle, um die Werte abzufangen, bevor sie verschlüsselt sind.
Entschuldigung, aber dafür verwenden wir C ++! Wenn Sie C#bevorzugen, können Sie die gesamte Plattform für 9,7 Millionen Funktionen durchlaufen und hierher zurückkehren, wenn Sie fertig sind. Wie auch immer, zuerst müssen wir entscheiden, wo die Höhle codiert werden soll. Zum Glück wissen wir schon! Durch die Analyse der obigen Funktion wissen wir jedoch kurz, dass RDX durch den hervorgehobenen Punkt den Index enthält und RDX+1 das entsprechende Zeichen enthält. Im Folgenden finden Sie den Montagecode für die diskutierte Funktion.
Jetzt, logischerweise, ist der beste Ort, um den Sprung von mov [rsp+arg_8], rdx zu machen, da wir uns nicht wirklich um den dritten Parameter kümmern, sondern RDX -Register und RDX+1 abfangen möchten. Um diese Kinder zu tun, benötigen wir ein paar Bytes: 10 Bytes für die MOV -Anweisung, um die Adresse unserer Codehöhle in ein Register zu verschieben (wir werden RAX später in den 10 -Uhr -Nachrichten mehr verwenden) und 2 Bytes für die JMP -Anweisung, um zum Register zu springen. Für diejenigen unter Ihnen, die PTBS aus weiteren Mathematik haben, ist das bei 12 Bytes. Bevor wir alle Tante Bessie gehen und diesen Code mit WriteProcessMemory spaghettifizieren, müssen wir bedenken, dass es keinen idealen Ort gibt, um 12 Bytes in der obigen Montage zu ersetzen. Wenn wir von Offset 0x2905 ( mov [rsp+arg_8], rdx ) springen möchten, und wir brauchen 12 Bytes, um 0x2917 auszugleichen, was zwischen zwei MOV -Anweisungen ausgeht. Wenn wir einfach nur unsere Bytes dort schreiben würden, würde sie die Versammlung völlig beeinträchtigen und wahrscheinlich einige, ähm, „interessante“ Nebenwirkungen verursachen. Infolgedessen wird es einfacher sein (vielleicht ein bisschen hackiger, sorry, tut mir leid), einige One-Byte-Anweisungen für das Polsterung hinzuzufügen und bis zum Ende einer Anweisung abzurunden. Willkommen an Bord, 0x90.
Wie auch immer, jetzt, wo wir wissen, was unser Plan ist, schreiben wir also einen Code: Cue Intensive Hacker Man Music !
Im Folgenden sieht der anfängliche Sprung in unsere Codehöhle aus, sobald die Bytes in die Montage der Anwendung geschrieben sind, mit seiner eigenen NOP -Folie.
Bevor wir jedoch tatsächlich eine Baugruppe schreiben und ersetzen, müssen wir die Anwendung in einem suspendierten Zustand starten. Dies stoppt die Anwendungslaufzeit in einem frühen Stadium, sodass Speicheränderungen vorgenommen werden können, bevor die Anwendung auf die Anweisung kommt, an der wir interessiert sind. Der folgende Code -Extrakt zeigt diesen Prozess, und ich werde nicht zu viel übergehen, wie Sie sie in den Quelldateien dieses Repo anzeigen können, und es spricht meistens für sich.
Nachdem der Prozess hervorgebracht wird, müssen wir die Basisadresse des Prozesses erwerben. Normalerweise können Sie dafür EnumProcessModules verwenden, aber da wir sofort den Hauptprozess -Thread aussetzen, enthält der PEB keine voll besiedelte PEB_LDR_DATA -Struktur, insbesondere die InMemoryOrderModuleList , sodass wir derzeit nicht die Basisadresse erhalten können. Dies scheint übrigens nirgendwo auf MSDN dokumentiert zu werden. Zum Glück ist dies relativ leicht zu umgehen. Wenn Sie den Prozess sehr schnell wieder aufnehmen, die Module abfragen und dann den Prozess resuspendieren, können wir die Informationen erhalten, die wir benötigen, ohne dass der Prozess zu viel voranschreitet. Wie bei den meisten Dingen in Windows wiederholt Microsoft gerne, dass das Betriebssystem das überlegen ist, indem er die Funktionen, die wir benötigen, erneut nicht dokumentiert: NtSuspendProcess und NtResumeProcess . Bequem ist mein Vater mit Bill Gates befreundet und er sagt mir, dass diese Funktionen in ntdll.dll gekoppelt sind, damit wir sie mit der unten stehenden Klasse abrufen können, die ich zuvor gemacht habe:
Nachdem wir die beiden Funktionen haben, die wir benötigen, können wir den Prozess wieder aufnehmen, die Module abfragen und den Prozess neu spendeten:
Sie fragen sich vielleicht, warum wartet die Schleife auf zwei Modulentdeckungen im Gegensatz zu einem? Nun, Microsoft möchte einen Huttrick mit einem Fleischbällchen im hinteren Teil des nicht verkauften Spaghetti -Netzes erzielen, indem er nicht erwähnt, dass das erste von EnumProcessModules gefundene erste Modul ntdll.dll und das zweite die ausführliche Datei sein wird. Dies klingt zwar vernünftig, sobald die ausführbare Datei gefunden wurde, werden die Indizes mit ntdll.dll ausgetauscht. Hier ist ein Beispiel:
Das Ergebnis nach Abfrage nur das erste Modul:
Das Ergebnis nach Abfrage zwei Module:
Bevor wir weiter gehen, müssen wir den Assemblercode schreiben, der den ersten Sprung in die Codehöhle und die Codehöhle selbst durchführt. Im Wesentlichen überschreiben wir einen Speicher, der am Offset 0x2905 beginnt, einen Sprung machen, unseren Code -Höhlen -Spionieren machen und dann auf 0x2911 zurückkehren, um den normalen Programmfluss fortzusetzen. Zunächst erklären wir das Verschieben der Adresse in das RAX -Register als MOV RAX, 0x0 , da die Adresse, zu der wir springen, dynamisch ist und wir wissen noch nicht, was es ist. RAX ist ein sicheres Register für die Verwendung, da es volatile Register ist und sowieso kurz darauf überschrieben wird. Lustige Tatsache, so programmierte Nintendo den ursprünglichen Mario Bros -Plattformer mit vielen Sprüngen (sagen Sie mir, ich bin lustig)! Unten ist, wie der Code im Compiler aussieht. Es kann auf andere Weise erstellt werden, aber ich habe mich entschieden, die erforderlichen Montageanweisungen in Bytecode zu verbreiten. Wenn Sie dies zu Hause tun möchten, verwenden Sie diese Website.
Der Code für die tatsächliche Codehöhle ist etwas komplexer, und die Logik dafür wird auch in dieser Datei kommentiert, aber hier ist der grobe Prozess:
R10 reserviert habenRDX , der den Paketindex enthält, in R11BRDX , der den Charakter enthält, in R11B+12 ) auf R10 ( 0x2911 ) überschritten haben.R10 Wir müssen auch den Montagecode umschreiben, den wir (mit der NOP -Folie) in unsere Codehöhle überschrieben haben, um den Stapel usw. zu erhalten. Dieser Code wird in der Region „ Predetermined Assembly “ mit Ausnahme der NOPS verwiesen. Unten finden Sie die Codehöhle in ihrer hässlichen Herrlichkeit:Meistens schwer ein Teil.
Zu diesem Zeitpunkt haben wir im Wesentlichen alles, was wir brauchen, um die versteckte Nachricht aus dem Gedächtnis herauszusaugen, wir müssen sie nur implementieren. Zusammenfassend haben wir einen Handle zu einem suspendierten Prozess, den wir hervorgebracht haben, 2 Byte -Arrays, die die Montagelogik darstellen, die Basisadresse des suspendierten Vorgangs, in modules[0] gespeicherte [0] und den Versatz, wo wir die Sprunglogik schreiben müssen. Der folgende Code-Snippet erstellt die Adresse, von der die Adresse der Codehöhle (zu springen), die Adresse unseres 3-Byte-Speicherspeichers, die Adressen in den Montagecode schreibt und dann den Assembly-Code den suspendierten Vorgang schreibt, bevor er wieder aufnimmt:
Das magische Gießen des Harry -Potter -Stils für den codeCaveStorageAddr besteht darin, die Adresse in Bytes umzuwandeln, und die hartcodierten Werte in den Schleifen sind für die dynamischen Adressplatzierungen in den Montage -Arrays. Dies kann natürlich viel sauberer erfolgen (schreiben Sie keine magischen Zahlen Kinder), ich bin nur faul, nachdem ich all diese Bytes manuell ausschreiben musste. Die letzten Teile, die zu schreiben sind, sind die Loops, die mit ReadProcessMemory, dem Speicher, die Zeichen und Indizes in ein geordnetes Byte -Array speichern, das Byte unter Verwendung von WriteProcessMemory signalisieren, wenn das Lesen des aktuellen Speichers fertig ist, und drucken Sie die versteckte Nachricht. Wir wissen, dass die Sendungsfunktion nur 14 Mal auftritt. Daher beenden wir die WHLE -Schleife, sobald das Byte -Array gefüllt ist. Dies könnte mit mehr Speicherbearbeitungen geändert werden, um unserer Anwendung zu signalisieren, dass der Prozess „beendet“ und unsere Schleife gestoppt werden kann, anstatt einen hartcodierten Wert von 14 zu verwenden, dies funktioniert jedoch für dieses Beispiel.
Das Ergebnis? Unsere versteckte Nachricht ist in unserer eigenen Konsolenanwendung gedruckt! Wenn jemand neugierig ist, ist NPT ein Hinweis auf eine andere Software-Maximums-Hacker-What-I-Called-Him.