Click here to Skip to main content
Click here to Skip to main content

Steganography VII - Hiding more Text in .NET Assemblies

By , 9 Apr 2004
Rate this:
Please Sign up or sign in to vote.

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 [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 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 [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 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, along with any associated source code and files, is licensed under The Common Development and Distribution License (CDDL)

About the Author

Corinna John
Software Developer
Germany Germany
Corinna lives in Hannover/Germany (CeBIT City) and works as a Delphi developer, though her favorite language is C#.

Comments and Discussions

 
GeneralExe Steganography Pinmember31337 H4X0R21-Feb-05 18:45 
GeneralRe: Exe Steganography PinmemberCorinna John27-Feb-05 4:12 
GeneralRe: Exe Steganography Pinmember31337 H4X0R6-Mar-05 17:13 
GeneralRe: Exe Steganography [modified] PinmemberThe_Mega_ZZTer8-Nov-07 11:26 
QuestionVC6++ HELP HOW??? Pinmembercnncnn18-Aug-04 20:22 
GeneralInteresting, but... PinsussInverarity29-Jul-04 7:40 
GeneralRe: Interesting, but... PinmemberCorinna John31-Jul-04 11:19 
GeneralRe: Interesting, but... PinsussInverarity2-Aug-04 11:54 
GeneralInteresting Article PinmemberBrian Delahunty1-Jun-04 2:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140415.2 | Last Updated 10 Apr 2004
Article Copyright 2004 by Corinna John
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid