Introduction
An application contains lots of lines which leave the stack empty. After these lines any code can be inserted, as long as it leaves the stack empty again. You can load some values onto the stack and store them off again, without disturbing the application's flow.
Finding silent hiding places
Let's take a look at an assembly's IL Assembler Language code. Each methods contains lines which put something onto the stack, or store something off the stack. We cannot always say what exactly is on the stack when a specific line executes, so we should not change anything between two lines. But there are some lines at which we know what is on the stack.
Every method has to contain at least one ret
instruction. When the runtime environment reaches a ret
, the stack must contain the return value and nothing else. That means, at a ret
instruction in a method returning a Int32
, the stack contains exactly one Int32
value. We could store it in a local variable, insert some code leaving the stack empty, and then put the return value back onto the stack. Nobody would notice it at runtime. There are much more lines like that, for example the closing brackets of .try {
and .catch {
blocks (definitly empty stack!) or method calls (only returned value of known type on the stack!). To keep the example simple, we are going to concentrate on void
methods and ignore all the others. When a void
method is left, the stack has to be empty, so we don't have to care about return values.
This is the IL Assembler Language code of a typical void Dispose()
method:
.method family hidebysig virtual instance void
Dispose(bool disposing) cil managed
{
.maxstack 2
IL_0000: ldarg.1
IL_0001: brfalse.s IL_0016
IL_0003: ldarg.0
IL_0004: ldfld class [System]System.ComponentModel.Container
PictureKey.frmMain::components
IL_0009: brfalse.s IL_0016
IL_000b: ldarg.0
IL_000c: ldfld class [System]System.ComponentModel.Container
PictureKey.frmMain::components
IL_0011: callvirt instance void [System]System.ComponentModel
.Container::Dispose()
IL_0016: ldarg.0
IL_0017: ldarg.1
IL_0018: call instance void [System.Windows.Forms]System
.Windows.Forms.Form::Dispose(bool)
IL_0026: ret
}
So what will happen, if we insert a new local variable and store a constant in it, just before the method returns? Yes, nothing will happen, except a little bit of performance decrease.
.method family hidebysig virtual instance void
Dispose(bool disposing) cil managed
{
.maxstack 2
.locals init (int32 V_0)
...
IL_001d: ldc.i4 0x74007a
IL_0022: stloc V_0
IL_0026: ret
}
In C# the methods would look like this:
protected override void Dispose( bool disposing ) {
if( disposing ) {
if (components != null) {
components.Dispose();
}
}
base.Dispose( disposing );
}
protected override void Dispose( bool disposing ) {
int myvalue = 0;
if( disposing ) {
if (components != null) {
components.Dispose();
}
}
base.Dispose( disposing );
myvalue = 0x74007a;
}
We have just hidden four bytes in an application! The IL file will re-compile without errors, and if somebody de-compiles the new assembly, he can find the value 0x74007a.
How to disguise a secret value
To make life harder for people who disassemble our application and look for useless variables, we can disguise the hidden values as forgotten debug output:
ldstr bytearray(65 00)
stloc mystringvalue
.maxstack 2
ldstr "DEBUG - current value is: {0}"
ldloc mystringvalue
call void [mscorlib]System.Console::WriteLine(string, object)
In order to stay invisible even in console applications, we should rather disguise it as an operation. We can insert more local/instance/static variables, to make it look like the values were needed somewhere else:
.maxstack 2
ldc.i4 65
ldloc myintvalue
add
stsfld int32 NameSpace.ClassName::mystaticvalue
This example demonstrates how to hide values at all, so only this version will be used:
ldc.i4 65
stloc myvalue
There is no need to insert two lines for each byte of the message. We can combine up to four bytes to one Int32 value, inserting only half a line per hidden byte. But first we have to know where to insert it at all.
Analysing the Disassembly
Before editing the IL file, we have to call ILDAsm.exe to create it from the compiled assembly. Afterwards we call ILAsm.exe to re-assemble it. The interesting part is between these two steps: We must walk through the lines of IL Assembler Language code, finding the void
methods, their last .locals init
line, and one ret
line. A message can contain more 4-byte blocks than there are void
methods in the file, so we have to count the methods and calculate the number of bytes to hide in each of them. The method Analyse
collects namespaces, classes and void
methods:
public void Analyse(String fileName,
out ArrayList namespaces, out ArrayList classes,
out ArrayList voidMethods){
namespaces = new ArrayList(); classes = new ArrayList();
voidMethods = new ArrayList();
String currentMethod = String.Empty;
String[] lines = ReadFile(fileName);
for(int indexLines=0; indexLines<lines.Length; indexLines++){
if(lines[indexLines].IndexOf(".namespace ") > 0){
namespaces.Add( ProcessNamespace(lines[indexLines]) );
}
else if(lines[indexLines].IndexOf(".class ") > 0){
classes.Add( ProcessClass(lines, ref indexLines) );
}
else if(lines[indexLines].IndexOf(".method ") > 0){
currentMethod = ProcessMethod(lines, ref indexLines);
if(currentMethod != null){
voidMethods.Add(currentMethod);
}
}
}
}
Given the number of usable methods, we can calculate the number of bytes per method:
float messageLength = txtMessage.Text.Length*2 +1;
int bytesPerMethod = (int)Math.Ceiling( (messageLength /
(float)voidMethods.Count));
Now we are ready to begin. The method HideOrExtract
uses the value of bytesPerMethod
to insert the lines for one or more 4-byte blocks above each ret
keyword.
private void HideOrExtract(String fileNameIn, String fileNameOut,
Stream message, bool hide){
if(hide){
FileStream streamOut = new FileStream(fileNameOut, FileMode.Create);
writer = new StreamWriter(streamOut);
}else{
bytesPerMethod = 0;
}
String[] lines = ReadFile(fileNameIn);
bool isMessageComplete = false;
for(int indexLines=0; indexLines<lines.Length; indexLines++){
if(lines[indexLines].IndexOf(".method ") > 0){
if(hide){
isMessageComplete = ProcessMethodHide(lines,
ref indexLines, message);
}else{
isMessageComplete = ProcessMethodExtract(lines,
ref indexLines, message);
}
}else if(hide){
writer.WriteLine(lines[indexLines]);
}
if(isMessageComplete){
break;
}
}
if(writer != null){ writer.Close(); }
}
Hiding the message
The method ProcessMethodHide
copies the method's header, and checks if the return type is void
. Then it looks for the last .locals init
line. If no .locals init
is found, the additional variable will be inserted at the beginning of the method. The hidden variable must be the last variable initialized in the method, because the compilers emitting IL Assembler Language often use slot numbers instead of names for local variables. Just imagine a desaster like that:
.locals init ([0] int32 x, [1] int32 y)
IL_0000: ldc.i4.5
IL_0001: stloc.0
IL_0002: ldc.i4.2
IL_0003: stloc.1
IL_0004: ldloc.0
IL_0005: ldloc.1
IL_0006: add
IL_0007: stsfld int32 Demo.Form1::mystaticval
IL_000c: ret
If we inserted an initialization at the beginning of the method, we could not re-assemble the code, because slot 0 is already in use by myvalue
:
.locals init (int32 myvalue)
.locals init ([0] int32 x, [1] int32 y)
IL_0000: ldc.i4.5
IL_0001: stloc.0
...
So the additional local variables has to be initialized after the last existing .locals init
. ProcessMethodHide
inserts a new local variable, jumps to the first ret
line and inserts ldc.i4/stloc pairs. The first value being hidden is the size of the message stream - the extracting method needs this value in order to know when to stop. The last value hidden in the first method is the count of message-bytes per method. It has to be placed right above the ret
line, because the extracting method has to find it without knowing how many lines to go back (because that depends on just this value).
private bool ProcessMethodHide(String[] lines, ref int indexLines,
Stream message){
bool isMessageComplete = false;
int currentMessageValue,
positionInitLocals,
positionRet,
positionStartOfMethodLine;
writer.WriteLine(lines[indexLines]);
if(lines[indexLines].IndexOf(" void ") > 0){
indexLines++;
int oldIndex = indexLines;
SeekStartOfBlock(lines, ref indexLines);
CopyBlock(lines, oldIndex, indexLines);
positionStartOfMethodLine = indexLines;
indexLines++;
positionInitLocals = positionRet = 0;
SeekLastLocalsInit(lines, ref indexLines, ref positionInitLocals,
ref positionRet);
if(positionInitLocals == 0){
positionInitLocals = positionStartOfMethodLine;
}
CopyBlock(lines, positionStartOfMethodLine, positionInitLocals+1);
indexLines = positionInitLocals+1;
writer.Write(writer.NewLine);
writer.WriteLine(".locals init (int32 myvalue)");
CopyBlock(lines, indexLines, positionRet);
indexLines = positionRet;
for(int n=0; n<bytesPerMethod; n+=4){
isMessageComplete = GetNextMessageValue(message,
out currentMessageValue);
writer.WriteLine("ldc.i4 "+currentMessageValue.ToString());
writer.WriteLine("stloc myvalue");
}
if(! isBytesPerMethodWritten){
writer.WriteLine("ldc.i4 "+bytesPerMethod.ToString());
writer.WriteLine("stloc myvalue");
isBytesPerMethodWritten = true;
}
writer.WriteLine(lines[indexLines]);
if(isMessageComplete){
indexLines++;
CopyBlock(lines, indexLines, lines.Length-1);
}
}
return isMessageComplete;
}
Extracting the hidden values
The method ProcessMethodExtract
looks for the first ret
line. If the number of bytes hidden in each method is still unknown, it jumps two lines back and extracts the number from the ldc.i4
line, which had been inserted as the last value in the first method. Otherwise it jumps back two lines per expected ldc.i4/stloc-pair, extracts the 4-byte blocks and writes them to the message stream. If an ldc.i4
is not found where it should be, the method throws an exception. The second extracted value (after the number bytes per method) is the length of the following message. When the message stream has reached this expected length, the isMessageComplete
flag is set, HideOrExtract
returns, and the extracted message is displayed. Extracting works just like hiding in reverse direction.
No key ?!
Sure you'll have noticed that this application doesn't use a key file to distribute the message. An intermediate assembly contains less void
methods than an intermediate sentence contains characters, so a distribution key as it is used in all preceeding articles would only mean pushing loads of additional nonsense-lines into a few methods, and that would be much too obvious.
A key file for this application could specify how to disguise the values - debug output, operations, instance fields, additional methods, and so on. I'll add such a feature in future versions, if somebody is interested in it.
Warning
This application works with the assemblies I've testet, but is might as well fail with other assemblies. If you find an assembly which causes it to crash, please tell me about it and I'll see what I've done wrong.