Click here to Skip to main content
15,883,705 members
Articles / Programming Languages / C#
Article

Steganography VII - Hiding more Text in .NET Assemblies

Rate me:
Please Sign up or sign in to vote.
4.69/5 (29 votes)
9 Apr 2004CDDL3 min read 56.9K   1.1K   28   13
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:

C#
private int intTest(){
    int a = 1;
    return a;
}

The C# compiler translates it like that:

MSIL
.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:

MSIL
.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:

MSIL
.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:

MSIL
ldc.i4 65;
stloc myvalue

As you've already seen above, these lines can hide the same data:

MSIL
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.

C#
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:

C#
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:

C#
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:

C#
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:

C#
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.

C#
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)


Written By
Software Developer
Germany Germany
Corinna lives in Hanover/Germany and works as a C# developer.

Comments and Discussions

 
GeneralExe Steganography Pin
Jon Pajela21-Feb-05 18:45
Jon Pajela21-Feb-05 18:45 
GeneralRe: Exe Steganography Pin
Corinna John27-Feb-05 4:12
Corinna John27-Feb-05 4:12 
GeneralRe: Exe Steganography Pin
Jon Pajela6-Mar-05 17:13
Jon Pajela6-Mar-05 17:13 
GeneralRe: Exe Steganography [modified] Pin
The_Mega_ZZTer8-Nov-07 11:26
The_Mega_ZZTer8-Nov-07 11:26 
QuestionVC6++ HELP HOW??? Pin
cnncnn18-Aug-04 20:22
cnncnn18-Aug-04 20:22 
GeneralInteresting, but... Pin
Inverarity29-Jul-04 7:40
sussInverarity29-Jul-04 7:40 
Your steganography series is fantastic, but this pair doesn't quite do it for me. Many points for actually using MSIL, but... I just don't really see the utility of hiding 32-bit chunks of data. Am I missing something?

OTOH, I do see the utility of encrypting a whole string into non-ascii bytes and then using this technique to distribute its chunks through many methods in a large assembly. Given that alphanumeric ascii text comprises a relatively short range of values...

Well, let's see: 0x30-0x39, 0x41-0x5A, 0x61-0x7A comprise 62 different characters. We'll throw in two more characters character, let's say "." and ",", to round out an even 6 bits' worth of characters. If we jam the characters together, we can fit five characters per Int32 (padding out the value however we like) -- and, as a bonus, the bytes will look like garbage to the casual observer.

Well, anyway, it might be a good way to send a password securely to a friend Smile | :)

Hey, what do you know about #Blob compression? There may be fun to be had there, too...
GeneralRe: Interesting, but... Pin
Corinna John31-Jul-04 11:19
Corinna John31-Jul-04 11:19 
GeneralRe: Interesting, but... Pin
Inverarity2-Aug-04 11:54
sussInverarity2-Aug-04 11:54 
GeneralInteresting Article Pin
Brian Delahunty1-Jun-04 2:20
Brian Delahunty1-Jun-04 2:20 

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

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