Im letzten Artikel wurden nur
void
-Methoden verwendet, was die Länge der versteckten Nachricht stark eingeschränkt hat.
Dieser Artikel erweitert das Programm:
void
, bool
, int32
oder string
können verwendet werden.Der Unterschied zwischen void
- und nicht-void
-Methoden ist nicht sehr groß.
Am Ende einer nicht-void
-Methode ist der Stack nicht leer, er enthält einen Wert des deklarierten Typs.
Das heisst, diese Anwendung muss den Namen des Rückgabetyps aus der Methodensignatur lesen,
und eine zusätzliche lokale Variable deklarieren. In der Zeile vor dem ersten “ret” muss sie den
Stack-Inhalt (der den Rückgabewert enhält, und sonst nichts) in dieser Variablen speichern,
die Zeilen mit den versteckten Bytes einfügen, und dann den Wert zurück auf den Stack laden.
Schau Dir als Beispiel diese int
-Methode an:
private int intTest(){ int a = 1; return a; }
Der C# compiler übersetzt sie so:
.method private hidebysig instance int32 intTest() cil managed { // Code size 8 (0x8) .maxstack 1 .locals init ([0] int32 a, [1] int32 CS$00000003$00000000) IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: stloc.1 IL_0004: br.s IL_0006 IL_0006: ldloc.1 IL_0007: ret } // end of method Form1::intTest
Der Compiler hat eine zweite Variable erzeugt, um den Rückgabewert abzulegen.
Am Ende der Methode wird dieser Wert auf den Stack gelegt, das ist schon alles.
Also wird nichts kaputt gehen, wenn wir ein paar Zeilen zwischen `IL_0006`
und `IL_0007` schreiben, und anschließend den Stack aufräumen
bevor wir wieder den Rückgabewert laden:
.method private hidebysig instance int32 intTest() cil managed { // Code size 8 (0x8) .maxstack 2 //Stack-Größe anpassen .locals init ([0] int32 a, [1] int32 CS$00000003$00000000) .locals init (int32 myvalue) IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: stloc.1 IL_0004: br.s IL_0006 IL_0006: ldloc.1 .locals init (int32 returnvalue) //Variable hinzufügen stloc returnvalue //Rückgabewert zwischenspeichern ldstr "DEBUG - current value is: {0}" //etwas das wie alter Debug-Code aussieht ldc.i4 111 //Heir ist unser versteckter Wert box [mscorlib]System.Int32 call void [mscorlib]System.Console::WriteLine(string, object) ldloc returnvalue //Rückgabewert dorthin zurücklegen, wo er her kam IL_0007: ret } // end of method Form1::intTest
Jetzt kann ILAsm den Code re-compilieren. Wenn man ihn wieder decompiliert, kann man sehen, dass ILAsm die Variablendeklarationen optimiert hat:
.method private hidebysig instance int32 intTest() cil managed { // Code size 36 (0x24) .maxstack 2 .locals init (int32 V_0, //ILAsm hat die lokalen Variablen zusammengefaßt ! int32 V_1, int32 V_2, int32 V_3) IL_0000: ldc.i4.1 IL_0001: stloc.0 IL_0002: ldloc.0 IL_0003: stloc.1 IL_0004: br.s IL_0006 IL_0006: ldloc.1 IL_0007: stloc V_3 IL_000b: ldstr "DEBUG - current value is: {0}" IL_0010: ldc.i4 0x6f IL_0015: box [mscorlib]System.Int32 IL_001a: call void [mscorlib]System.Console::WriteLine(string, object) IL_001f: ldloc V_3 IL_0023: ret } // end of method Form1::intTest
ILAsm räumt meine Zeilen auf, ist das nicht nett? Nein, das ist überhaupt nicht nett, denn wir
können uns nicht darauf verlassen, dass unsere eingefügten Zeilen noch vorhanden sind,
nachdem der IL Code compiliert und wieder decompiliert wurde.
Das heisst, was immer wir einfügen, um Teile der geheimen Nachricht zu verstecken, muss einen Sinn ergeben.
Eine zusätzliche .maxlength
-Zeile wird verschwinden, genau wie eine
.locals init
-Zeile mit Variablen, die nie verwendet werden.
Bitte denke an diesen Effekt, wenn Du Dir neue Byte-Verkleidungen ausdenkst!
Im letzten Artikel wurden immer diese zwei Zeilen verwendet, um einen int32
zu verstecken:
ldc.i4 65; stloc myvalue
Wie Du schon oben gesehen hast, können diese Zeilen die gleichen Daten verstecken:
ldstr "DEBUG - current value is: {0}" ldc.i4 65 box [mscorlib]System.Int32 call void [mscorlib]System.Console::WriteLine(string, object)
Es gibt hunderte solcher Blöcke, welche Variantion sollen wir verwenden? Wir werden alle verwenden, oder - um das Beispiel einfach zu lassen - diese zwei Variationen. Der Benutzer kann eine beliebige Datei angeben, und für jeden Vier-Byte-Block liest die Anwendung ein Byte aus dieser Datei: Wenn es eine gerade Zahl ist, wird die erste Variation verwendet, sonst die Zweite.
private bool ProcessMethodHide(String[] lines, ref int indexLines, Stream message, Stream key){ //... //Zeilen fü [bytesPerMethod] Bytes aus dem Nachrichten-Stream einfügen //jeweils 4 bytes zu einem Int32 kombinieren int keyValue; //aktueller Wert aus dem Schlüssel-Stream for(int n=0; n<bytesPerMethod; n+=4){ isMessageComplete = GetNextMessageValue(message, out currentMessageValue); //nächstes Bytes aus dem Schlüssel lesen if( (keyValue=key.ReadByte()) < 0){ key.Seek(0, SeekOrigin.Begin); keyValue=key.ReadByte(); } if(keyValue % 2 == 0){ //der Schlüssel ist gerade - erste Variation verwenden writer.WriteLine("ldc.i4 "+currentMessageValue.ToString()); writer.WriteLine("stloc myvalue"); }else{ //der Schlüssel ist ungerade - zweite Variation verwenden writer.WriteLine("ldstr \"DEBUG - current value is: {0}\""); writer.WriteLine("ldc.i4 "+currentMessageValue.ToString()); writer.WriteLine("box [mscorlib]System.Int32"); writer.WriteLine("call void [mscorlib]System.Console::WriteLine(string, "); writer.WriteLine( "object)" ); //ILDAsm fü hier einen Zeilenumbruch ein } } //... }
Bei der ersten Variation müssen wird die versteckte Konstante in der ersten Zeile suchen, bei der zweiten Variation müssen wir sie aus der zweiten Zeile lesen. Beim Auslesen der versteckten Nachricht müssen wir also die erste Zeile überspringen, wenn das Schlüssel-Byte ungerade ist:
private bool ProcessMethodExtract(String[] lines, ref int indexLines, Stream message, Stream key){ //[bytesPerMethod] Bytes in den Nachrichten-Stream lesen //wenn [bytesPerMethod]==0 ist, wurde es noch nicht gelesen for(int n=0; (n<bytesPerMethod)||(bytesPerMethod==0); n+=4){ if(bytesPerMethod > 0){ //nächstes Bytes aus dem Schlüssel lesen if( (keyValue=key.ReadByte()) < 0){ key.Seek(0, SeekOrigin.Begin); keyValue=key.ReadByte(); } if(keyValue % 2 == 1){ //ldc.i4 steht in der zweiten Zeile des versteckten Blocks indexLines++; } } //ILDAsm setzt Zeilennummern - Anfange der Anweisung finden indexValue = lines[indexLines].IndexOf("ldc.i4"); if(indexValue >= 0){ //...
Jetzt können wir Daten verstecken und extrahieren, aber viele re-compilierte
Assemblies stürzen mit einer InvalidProgramException
ab.
Das kommt daher, dass die zweite Variation zwei Werte auf den Stack legt:
.maxstack 1 //eine kleine Methode verwendet nur eine Variable auf einmal ... ldstr "DEBUG - current value is: {0}" ldc.i4 0x6f //wir versuchen, einen zweiten Wert auf den Stack zu laden box [mscorlib]System.Int32 call void [mscorlib]System.Console::WriteLine(string, object)
Deshalb müssen wir sicherstellen, dass der .maxstack
-Wert in jeder
Methode mindestens 2 ist.
Die .maxstack
-Zeile ist eine von denen, die wir bisher unbeachtet kopiert haben:
CopyBlock(lines, startIndex, endIndex); //... private void CopyBlock(String[] lines, int start, int end){ String[] buffer = new String[end-start]; Array.Copy(lines, start, buffer, 0, buffer.Length); writer.WriteLine(String.Join(writer.NewLine, buffer)); }
Jetzt müssen wir die .maxstack
-Zeilen finden und anpassen, sonst würden
wir Assemblies zerstören, die Methoden mit einem maxstack von 1 enthalten.
Diese Zeilen können wir nicht mit Array.IndexOf(".maxstack 1")
finden, weil die
exakte Zeile unbekannt ist - denk nur mal an die Zeilennummern, Tabs und Leerzeichen, die ILDAsm
in jede Zeile einfügt. Also werden wir die Methoden Zeile für Zeile kopieren:
private void CopyBlockAdjustStack(String[] lines, int start, int end){ for(int n=start; n<end; n++){ if(lines[n].IndexOf(".maxstack ")>0){ //Stack-Größe lesen int indexStart = lines[n].IndexOf(".maxstack "); int maxStack = int.Parse( lines[n].Substring(indexStart+10).Trim() ); //maxstack muss 2 oder größer sein if(maxStack < 2){ lines[n] = ".maxstack 2"; } } writer.WriteLine(lines[n]); } }
Der Rückgabetyp einer Methode wird im Header deklariert, das heisst wir müssen ihn lesen und zwischenspeichern, sobald wir eine neue Methde betreten:
private String GetReturnType(String line){ String returnType = null; if(line.IndexOf(" void ") > 0){ returnType = "void"; } else if(line.IndexOf(" bool ") > 0){ returnType = "bool"; } else if(line.IndexOf(" int32 ") > 0){ returnType = "int32"; } else if(line.IndexOf(" string ") > 0){ returnType = "string"; } return returnType; } private bool ProcessMethodHide(String[] lines, ref int indexLines, Stream message, Stream key){ //.. //Rückgabewert der aktuellen Methode lesen String returnType = GetReturnType(lines[indexLines]); if(returnType != null){ //void/bool/int32/string-Methode gefunden //... //Position des letzten ".locals init" und ersten "ret" suchen positionInitLocals = positionRet = 0; SeekLastLocalsInit(lines, ref indexLines, ref positionInitLocals, ref positionRet); //... //Rest der Methode bis zur Zeile vor "ret" kopieren CopyBlockAdjustStack(lines, indexLines, positionRet); //nächste Zeile ist "ret" - auf dem Stack kann nichts kaputt gehen indexLines = positionRet; if(returnType != "void"){ //not a void method - store the return value writer.Write(writer.NewLine); writer.WriteLine(".locals init ("+returnType+" returnvalue)"); writer.WriteLine("stloc returnvalue"); } //Zeile für [bytesPerMethod] Bytes vom Nachrichten-Stream einfügen //4 Bytes zu einem Int32 kombinieren int keyValue; for(int n=0; n<bytesPerMethod; n+=4){ //... } //... if(returnType != "void"){ //keine void-Methode - Rückgabewert zurück auf den Stack laden writer.WriteLine("ldloc returnvalue"); } //... } //else diese Methode auslassen }
Wir müssen die Zeile ldloc returnvalue
beim Extrahieren nur auslassen.
private bool ProcessMethodExtract(String[] lines, ref int indexLines, Stream message, Stream key){ bool isMessageComplete = false; int positionRet, //index der "ret"-Zeile positionStartOfMethodLine; //index der ersten Zeile String returnType = GetReturnType(lines[indexLines]); int keyValue = 0; if(returnType != null){ //void/bool/int32/string-gethode gefunden //ein Teil der Nachricht ist hier versteckt //... //Position des "ret" suchen positionRet = SeekRet(lines, ref indexLines); if(bytesPerMethod == 0){ //zwei Zeilen zurück gehen - dort haben wir "ldc.i4 "+bytesPerMethod eingefügt indexLines = positionRet - 2; }else{ //[linesPerMethod] Zeilen pro erwartetem Nachrichten-Byte zurück gehen //dort haben wir "ldc.i4 "+currentByte eingefügt linesPerMethod = GetLinesPerMethod(key); indexLines = positionRet - linesPerMethod; } if(returnType != "void"){ indexLines--; //die Zeile "ldloc returnvalue" überspringen } //... } }
Jetzt können wir eine Schlüssel-Datei anwenden, und die meisten Methoden ausnutzen.
Wenn Du mehr Methoden verwenden möchtest, brauchst Du nur die Methode GetReturnType
anzupassen.
Mehr Variationen von Dummy-Code einzufügen ist etwas aufwändiger, Du musst
ProcessMethodHide
, ProcessMethodExtract
und GetLinesPerMethod
anpassen - und denke daran, gegebenenfalls den .maxstack
zu vergrößern.