![]() |
|||||||||||
|
|
||||||||||
IntroductionIn the last article, Steganography VI,
only
The difference between For example, take a look at this
private int intTest(){
int a = 1;
return a;
}
The C# compiler translates it like that:
.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
The compiler has created a second variable to store the return value.
At the end of the method this value is put onto the stack, that's all.
So nothing is going to break, if we write some lines between
.method private hidebysig instance int32
intTest() cil managed
{
// Code size 8 (0x8)
.maxstack 2 //adjust the stack size
.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) //add a variable
stloc returnvalue //store the return value
ldstr "DEBUG - current value is: {0}" //something that looks like old debug code
ldc.i4 111 //this is our hidden value
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(string,
object)
ldloc returnvalue //put the return value back to where it came from
IL_0007: ret
} // end of method Form1::intTest
Now ILAsm can re-compile the code. If you decompile it again, you can see that ILAsm has optimised the variable declarations:
.method private hidebysig instance int32
intTest() cil managed
{
// Code size 36 (0x24)
.maxstack 2
.locals init (int32 V_0, //ILAsm has summarized the local variables !
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 cleans up my lines, isn't that nice? No, that's not nice at all, because we cannot
rely on our inserted lines to be still there after compiling and decompiling the IL code.
That means, whatever we insert to hide parts of the secret message has to make sense.
An additional Using a key streamIn the last article we always used these two line to hide an ldc.i4 65; stloc myvalue As you've already seen above, these lines can hide the same data:
ldstr "DEBUG - current value is: {0}"
ldc.i4 65
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(string, object)
There are hundreds of other blocks like that, so which variation shall we use? We'll use all variations, or - to keep it simple - those two variations. The user can specify a file of any format, and for each four-byte-block the application reads one bytes from this file: If the byte is even, it uses the first variation, otherwise it uses the second one:
private bool ProcessMethodHide(String[] lines, ref int indexLines,
Stream message, Stream key){
//...
//insert lines for [bytesPerMethod] bytes from the message stream
//combine 4 bytes in one Int32
int keyValue; //current value from the key file stream
for(int n=0; n<bytesPerMethod; n+=4){
isMessageComplete = GetNextMessageValue(message, out currentMessageValue);
//read the next byte from the key
if( (keyValue=key.ReadByte()) < 0){
key.Seek(0, SeekOrigin.Begin);
keyValue=key.ReadByte();
}
if(keyValue % 2 == 0){
//key value is even - use the first variation
writer.WriteLine("ldc.i4 "+currentMessageValue.ToString());
writer.WriteLine("stloc myvalue");
}else{
//key value is odd - use the second variation
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 inserts a line break here
}
}
//...
}
With the first variation we have to look for the constant in the first line, with the second variation we have to pick it from the second line. Extracting the hidden message we have to skip the first line unless the key byte is even:
private bool ProcessMethodExtract(String[] lines, ref int indexLines,
Stream message, Stream key){
//read [bytesPerMethod] bytes into the message stream
//if [bytesPerMethod]==0 it has not been read yet
for(int n=0; (n<bytesPerMethod)||(bytesPerMethod==0); n+=4){
if(bytesPerMethod > 0){
//read the next byte from the key
if( (keyValue=key.ReadByte()) < 0){
key.Seek(0, SeekOrigin.Begin);
keyValue=key.ReadByte();
}
if(keyValue % 2 == 1){
//ldc.i4 is the second line of the hidden block
indexLines++;
}
}
//ILDAsm creates line numbers - find the beginning of the instruction
indexValue = lines[indexLines].IndexOf("ldc.i4");
if(indexValue >= 0){
//...
Now we can hide and extract data, but many re-compiled assemblies will terminate with
an
.maxstack 1 //some small function uses only one variable at a time
...
ldstr "DEBUG - current value is: {0}"
ldc.i4 0x6f //we try to put a second variable onto the stack
box [mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(string,
object)
So we have to make sure that the
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));
}
Now we have to find and adjust the
private void CopyBlockAdjustStack(String[] lines, int start, int end){
for(int n=start; n<end; n++){
if(lines[n].IndexOf(".maxstack ")>0){
//parse the stack size
int indexStart = lines[n].IndexOf(".maxstack ");
int maxStack = int.Parse( lines[n].Substring(indexStart+10).Trim() );
//stack size must be 2 or greater
if(maxStack < 2){
lines[n] = ".maxstack 2";
}
}
writer.WriteLine(lines[n]);
}
}
Handling return valuesA method's return type is declared in its header, that means we have to read and store it when we enter the method:
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){
//..
//get the return type of the current method
String returnType = GetReturnType(lines[indexLines]);
if(returnType != null){
//found a method with return type void/bool/int32/string
//...
//get position of last ".locals init" and first "ret"
positionInitLocals = positionRet = 0;
SeekLastLocalsInit(lines, ref indexLines,
ref positionInitLocals, ref positionRet);
//...
//copy rest of the method until the line before "ret"
CopyBlockAdjustStack(lines, indexLines, positionRet);
//next line is "ret" - nothing left to damage on the stack
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");
}
//insert lines for [bytesPerMethod] bytes from the message stream
//combine 4 bytes in one Int32
int keyValue;
for(int n=0; n<bytesPerMethod; n+=4){
//...
}
//...
if(returnType != "void"){
//not a void method - load the return value back onto the stack
writer.WriteLine("ldloc returnvalue");
}
//...
} //else skip this method
}
We only have to skip the line
private bool ProcessMethodExtract(String[] lines, ref int indexLines,
Stream message, Stream key){
bool isMessageComplete = false;
int positionRet, //index of the "ret" line
positionStartOfMethodLine; //index of the method's first line
String returnType = GetReturnType(lines[indexLines]);
int keyValue = 0;
if(returnType != null){
//found a method with return type void/bool/int32/string
//a part of the message is hidden here
//...
//get position of "ret"
positionRet = SeekRet(lines, ref indexLines);
if(bytesPerMethod == 0){
//go 2 lines back - there we inserted "ldc.i4 "+bytesPerMethod
indexLines = positionRet - 2;
}else{
//go [linesPerMethod] lines per expected message-byte back
//there we inserted "ldc.i4 "+currentByte
linesPerMethod = GetLinesPerMethod(key);
indexLines = positionRet - linesPerMethod;
}
if(returnType != "void"){
indexLines--; //skip the line "ldloc returnvalue"
}
//...
}
}
Now we can use a key file, and exploit most of the methods.
If you want to make use of more methods, you only have to adjust the method |