Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Steganography VII - Hiding more Text in .NET Assemblies

0.00/5 (No votes)
9 Apr 2004 1  
Another article about hiding bytes at the end of methods in a .NET Assembly.

Introduction

In the last article, Steganography VI, only void methods could be used, so the length of the hidden message was very restricted. This article enhances the application:

  • All methods with a return type of void, bool, int32 or string can be used.
  • A key file defines how the message is hidden.

The difference between void and non-void-methods is not very big. At the end of a non-void method the stack is not empty, it contains a value of the declared type. That means this application has to read the name of the return type from the method's declaration and declare an additional local variable. At the line before the first "ret", it has to store the stack's content (which is only the return value and nothing else) into the additional variable, insert the lines with the secret bytes, and then load the value back onto the stack.

For example, take a look at this int-method:

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 IL_0006 and IL_0007, and then clean up the stack again before loading the return value:

.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 optimized 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 .maxlength-line is going to be deleted, just as a .locals init-line with variable names that are never used. Remember that effect whenever you make up a new byte-disguise.

Using a key stream

In the last article, we always used these two line to hide an int32:

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 byte 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  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  bytes into the message stream

    //if ==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 InvalidProgramException. That is because the second variation puts two values onto the stack:

    .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 .maxstack-value is 2 or greater in every method. The .maxstack-line is one of the lines we've only copied yet:

    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 .maxstack-lines, otherwise we would destroy assemblies which contain methods with a maxstack of 1. We cannot find these lines with something like Array.IndexOf(".maxstack 1"), because the exact line is not known - just think about the line numbers, tabs and spaces ILDAsm inserts into every line. So we'll copy the method's body line-by-line:

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 values

A 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  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 ldloc returnvalue, when extracting the hidden message.

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 GetReturnType. Adding more variations of dummy-code is more difficult, you have to change ProcessMethodHide, ProcessMethodExtract and GetLinesPerMethod - and remember to raise the .maxstack value if needed.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here