![]() |
||||||||||||
|
|
|||||||||||
Worum geht es?Jede Anwendung enthält viele Zeilen, die den Stack leer hinterlassen. Nach diesen Zeilen kann jeder Code eingefügt werden, sofern er den Stack wieder leer zurücklässt. Man kann ein paar Werte auf den Stack laden und wieder entfernen, ohne den Programmfluss zu stören. Stille Verstecke findenWerfen wir mal einen Blick auf den IL Assembler Language Code eines Assemblies. Jede Methode enthält Zeilen, die etwas auf den Stack schreiben oder herunter lesen. Wir können nicht immer vorhersagen, was genau bei welcher Zeile auf dem Stack liegt, darum sollten wir nichts zwischen zwei Zeilen ändern. Aber es gibt einige Zeilen, von denen man genau weiß, was auf dem Stack liegen muss.
Jede Methode enthält mindestens eine Hier ist der IL Assembler Language Code einer typischen
.method family hidebysig virtual instance void
Dispose(bool disposing) cil managed
{
// Code size 39 (0x27)
.maxstack 2
IL_0000: ldarg.1
IL_0001: brfalse.s IL_0016
IL_0003: ldarg.0
IL_0004: ldfld class [System]System.ComponentModel.Container
PictureKey.frmMain::components
IL_0009: brfalse.s IL_0016
IL_000b: ldarg.0
IL_000c: ldfld class [System]System.ComponentModel.Container
PictureKey.frmMain::components
IL_0011: callvirt instance void [System]System.ComponentModel
.Container::Dispose()
IL_0016: ldarg.0
IL_0017: ldarg.1
IL_0018: call instance void [System.Windows.Forms]System
.Windows.Forms.Form::Dispose(bool)
IL_0026: ret
}
Was wird also passieren, wenn wir eine neue lokale Variable einfügen und eine Konstante darin speichern, kurz bevor die Methode verlassen wird? Ja, nichts wird passieren, außer vielleicht einer minimalen Verzögerung.
.method family hidebysig virtual instance void
Dispose(bool disposing) cil managed
{
// Code size 39 (0x27)
.maxstack 2
.locals init (int32 V_0) //neue lokale Variable deklarieren
...
IL_001d: ldc.i4 0x74007a //Eine int32 Konstante laden
IL_0022: stloc V_0 //Die Konstante in der Variablen speichern
IL_0026: ret
}
In C# würde diese Methode so aussehen:
//Original
protected override void Dispose( bool disposing ) {
if( disposing ) {
if (components != null) {
components.Dispose();
}
}
base.Dispose( disposing );
}
//Version mit versteckter Variable
protected override void Dispose( bool disposing ) {
int myvalue = 0;
if( disposing ) {
if (components != null) {
components.Dispose();
}
}
base.Dispose( disposing );
myvalue = 0x74007a;
}
Wir haben gerade vier Bytes in einer Anwendung versteckt! Die IL Datei wird sich wieder fehlerfrei kompilieren lassen, und wenn jemand das neue Assembly disassembliert kann er den Wert 0x74007a wiederfinden. Einen geheimen Wert tarnenUm Leuten, die eine Anwendung disassemblieren und nach nutzlosen Variablen suchen, die Arbeit zu erschweren, kann man die versteckten Werte als vergessene Debug-Ausgabe tarnen:
ldstr bytearray(65 00) //Ein "A" laden...
stloc mystringvalue //...und wegspeichern
.maxstack 2 //Stackgrösse setzen, um Laufzeitfehler auszuschließen
ldstr "DEBUG - current value is: {0}"
ldloc mystringvalue //vergessenen Debug-Outout vortäuschen
call void [mscorlib]System.Console::WriteLine(string, object)
Um auch in Konsolenanwendungen unsichtbar zu bleiben, sollten wir die Variablen besser als Operationen tarnen. Wir könnten mehr lokale/statische/Instanz-Variablen einfügen, damit es so aussieht, als würden die Werde an anderer Stelle gebraucht werden: .maxstack 2 //Stack-Grösse anpassen ldc.i4 65 //"A" laden ldloc myintvalue //noch eine Variable laden - die Deklaraion steht irgendwo weiter oben add //65 + myintvalue stsfld int32 NameSpace.ClassName::mystaticvalue //Ergebnis vom Stack entfernen Dieses Beispiel soll demonstrieren, wie Informationen allgemein versteckt werden können, darum werden wir nur diese Variante verwenden: ldc.i4 65; stloc myvalue Man muss nicht für jedes Byte der Nachricht zwei Zeilen einfügen. Wir können bis zu vier Bytes in einen Int32-Wert stecken, und so nur eine halbe Zeile pro verstecktem Byte einfügen. Aber zuerst müssen wir wissen, wo genau wir dieses einfügen. Ein Disassembly analysierenBevor man eine IL Datei bearbeiten kann, wird ILDAsm.exe aufgerufen, um sie aus dem kompilierten Assembly zu erstellen.
Später rufen wir ILAsm.exe auf, um die Datei zu re-assemblieren. Der interessante Teil spielt sich dazwischen ab:
Wir müssen die Zeilen des IL Assembler Language Codes durchlaufen, die
/// <summary>Namespaces, Klassen und Methoden mit Rückgabetyp "void" auflisten</summary>
/// <param name="fileName">Name der IL Datei</param>
/// <param name="namespaces">Gibt die Namen gefundener Namespaces zurück</param>
/// <param name="classes">Gibt die Namen gefundener Klassen zurück</param>
/// <param name="voidMethods">Gibt die ersten Zeilen aller Methoden-Signaturen zurück</param>
public void Analyse(String fileName,
out ArrayList namespaces, out ArrayList classes, out ArrayList voidMethods){
//Rückgabelisten initialisieren
namespaces = new ArrayList(); classes = new ArrayList(); voidMethods = new ArrayList();
//Anfang der aktuellen Methode, oder null bei nicht-void Methoden
String currentMethod = String.Empty;
//IL Datei zeilenweise lesen
String[] lines = ReadFile(fileName);
//Für alle Zeilen der Datei: Listen füllen
for(int indexLines=0; indexLines<lines.Length; indexLines++){
if(lines[indexLines].IndexOf(".namespace ") > 0){
//Namespace gefunden!
namespaces.Add( ProcessNamespace(lines[indexLines]) );
}
else if(lines[indexLines].IndexOf(".class ") > 0){
//Klassen gefunden!
classes.Add( ProcessClass(lines, ref indexLines) );
}
else if(lines[indexLines].IndexOf(".method ") > 0){
//Methode gefunden!
currentMethod = ProcessMethod(lines, ref indexLines);
if(currentMethod != null){
//Methode gibt void zurück - auflisten
voidMethods.Add(currentMethod);
}
}
}
}
Mit der Anzahl verwendbarer Methoden können wir jetzt die Anzahl versteckter Bytes pro Methode berechnen: //Länge des Unicode-Strings + 1- Position für die Länge (wird wie immer mit der Nachricht versteckt) float messageLength = txtMessage.Text.Length*2 +1; //Bytes pro Methode int bytesPerMethod = (int)Math.Ceiling( (messageLength / (float)voidMethods.Count)); Endlich können wir anfangen. Die Methode
/// <summary>Versteckt oder extrahiert eine Nachricht in/aus einer IL Datei</summary>
/// <param name="fileNameIn">Name der IL Datei</param>
/// <param name="fileNameOut">Name für die Ausgabedatei - ignoriert, wenn [hide]==false</param>
/// <param name="message">Nachricht zum Verstecken, oder leerer Stream für die extrahierte Nachricht</param>
/// <param name="hide">true: [message] verstecken; false: eine Nachricht auslesen</param>
private void HideOrExtract(String fileNameIn, String fileNameOut, Stream message, bool hide){
if(hide){
//Zieldatei öffnen
FileStream streamOut = new FileStream(fileNameOut, FileMode.Create);
writer = new StreamWriter(streamOut);
}else{
//Anzahl der Bytes pro Methode ist noch unbekannt
//und wird der erste ausgelesene Wert sein
bytesPerMethod = 0;
}
//Quelldatei lesen
String[] lines = ReadFile(fileNameIn);
//nein, wir sind noch nicht fertig
bool isMessageComplete = false;
//Für alle Zeilen
for(int indexLines=0; indexLines<lines.Length; indexLines++){
if(lines[indexLines].IndexOf(".method ") > 0){
//Methode gefunden!
if(hide){
//einen Block von Bytes verstecken
isMessageComplete = ProcessMethodHide(lines, ref indexLines, message);
}else{
//Alle in dieser Methode versteckten Bytes auslesen
isMessageComplete = ProcessMethodExtract(lines, ref indexLines, message);
}
}else if(hide){
//Die Zeile gehört nicht zu einer verwendbaren Methode - einfach kopieren
writer.WriteLine(lines[indexLines]);
}
if(isMessageComplete){
break; //Nichts mehr zu tun
}
}
//Zieldatei schließen
if(writer != null){ writer.Close(); }
}
Die Nachricht versteckenDie Methode //Ein C# Compiler hat diesen Code produziert, der 5+2 addiert //Original C# code: //int x = 5; int y = 2; //mystaticval = x+y; .locals init ([0] int32 x, [1] int32 y) IL_0000: ldc.i4.5 IL_0001: stloc.0 IL_0002: ldc.i4.2 IL_0003: stloc.1 IL_0004: ldloc.0 IL_0005: ldloc.1 IL_0006: add IL_0007: stsfld int32 Demo.Form1::mystaticval IL_000c: ret Würden wir eine Deklaration am Anfang der Methode einfügen,
könnten wir den Code nicht re-assemblieren, da Slot 0 bereits von .locals init (int32 myvalue) .locals init ([0] int32 x, [1] int32 y) //Fehler! IL_0000: ldc.i4.5 IL_0001: stloc.0 ... Darum muss die zusätzliche lokale Variable nach dem letzten vorhandenen
/// <summary>Versteckt ein oder mehrere Bytes des Nachrichten-Streams in der IL Datei</summary>
/// <param name="lines">Zeilen der IL Datei</param>
/// <param name="indexLines">Aktueller Index in [lines]</param>
/// <param name="message">Stream der die Nachricht enthält</param>
/// <returns>true: letztes Byte wurde versteckt; false: noch mehr Nachrichten-Bytes warten</returns>
private bool ProcessMethodHide(String[] lines, ref int indexLines, Stream message){
bool isMessageComplete = false;
int currentMessageValue, //nächstes Byte zum Verstecken
positionInitLocals, //Index der letzten ".locals init"-Zeile
positionRet, //Index der "ret"-Zeile
positionStartOfMethodLine; //Index der ersten Zeile der Methode
writer.WriteLine(lines[indexLines]); //copy first line
//Ignorieren, wenn keine "void"-Methode
if(lines[indexLines].IndexOf(" void ") > 0){
//"void"-Methode gefunden
//Der Stack wird am Ende leer sein,
//also können wir (fast) alles Mögliche einfügen
indexLines++; //Nächste Zeile
//Anfang des Methoden-Blocks suchen, alle ausgelassenen Zeilen kopieren
int oldIndex = indexLines;
SeekStartOfBlock(lines, ref indexLines);
CopyBlock(lines, oldIndex, indexLines);
//Jetzt sind wir bei der öffnenden Klammer der Methode
positionStartOfMethodLine = indexLines;
//Zur ersten Zeile der Methode gehen
indexLines++;
//get position of last ".locals init" and first "ret"
positionInitLocals = positionRet = 0;
SeekLastLocalsInit(lines, ref indexLines, ref positionInitLocals, ref positionRet);
if(positionInitLocals == 0){
//kein .locals - Zeile am Anfang er Methode einfügen
positionInitLocals = positionStartOfMethodLine;
}
//Von Anfang bis letztem .locals kopieren, oder nichts (wenn kein .locals gefunden)
CopyBlock(lines, positionStartOfMethodLine, positionInitLocals+1);
indexLines = positionInitLocals+1;
//lokale Variable einfügen
writer.Write(writer.NewLine);
writer.WriteLine(".locals init (int32 myvalue)");
//Rest der Methode bis zur Zeile vor "ret" kopieren
CopyBlock(lines, indexLines, positionRet);
//Nächste Zeile ist "ret" - auf dem Stack kann nichts kaputtgehen
indexLines = positionRet;
//ldc/stloc Paare einfügen für [bytesPerMethod] Bytes aus dem Message Stream
//4 Bytes zu einem Int32 kombinieren
for(int n=0; n<bytesPerMethod; n+=4){
isMessageComplete = GetNextMessageValue(message, out currentMessageValue);
writer.WriteLine("ldc.i4 "+currentMessageValue.ToString());
writer.WriteLine("stloc myvalue");
}
//bytesPerMethod muss der letzte Wert in der ersten Methode sein
if(! isBytesPerMethodWritten){
writer.WriteLine("ldc.i4 "+bytesPerMethod.ToString());
writer.WriteLine("stloc myvalue");
isBytesPerMethodWritten = true;
}
//Aktuelle Zeile kopieren
writer.WriteLine(lines[indexLines]);
if(isMessageComplete){
//Nichts mehr gelesen, die Nachricht ist vollständig
//Rest der Quelldatei kopieren
indexLines++;
CopyBlock(lines, indexLines, lines.Length-1);
}
}
return isMessageComplete;
}
Versteckte Werte auslesenDie Methode Kein Schlüssel ?!Bestimmt ist Dir aufgefallen, dass diese Anwendung keinen Schlüssel verwendet, um die Nachricht zu verteilen.
Ein durchschnittliches Assembly enthält weniger WarnungDiese Beispiel-Anwendung funktioniert mit den Assemblies, mit denn ich sie getestet habe, könnte mit anderen Assemblies aber genausogut daneben gehen. Falls Du ein Assembly findest, mit dem sie nicht zurecht kommt, schreib mir eine Mail und ich schau nach, was ich falsch gemacht habe. |