Don't Always Believe What You See - with a Decompiler

Over the last few days I’ve been experimenting a bit with variations of conditions to see their advantages and disadvantages. In this context I got a surprise when analyzing the source with a decompiler. Let’s look at the source code to be examined. We have an array of boolean variables that are evaluated using different procedures. On the one hand via a nested sequence of If statements and on the other hand via a non-nested sequence of If statements.

//-Begin----------------------------------------------------------------

using System;
using System.Reflection;

public class ConditionTest {

  public struct conditions {

    public bool Condition1;
    public bool Condition2;

    public conditions(bool Condition1, bool Condition2) {
      this.Condition1 = Condition1;
      this.Condition2 = Condition2;
    }

  }

  public static void Main(string[] args) {

    conditions[] CollectionOfConditions = new conditions[] {
      new conditions(true, false),
      new conditions(false, true)
    };

    //-Variant1---------------------------------------------------------
    foreach (conditions Conditions in CollectionOfConditions) {
      if (Conditions.Condition1) {
        if (Conditions.Condition2) {
          Console.WriteLine("Condition2");
        } else {
          continue;
        }
      } else {
        continue;
      }
    }

    //-Variant2---------------------------------------------------------
    foreach (conditions Conditions in CollectionOfConditions) {
      if (!Conditions.Condition1) {
        continue;
      }
      if (!Conditions.Condition2) {
        continue;
      } else {
        Console.WriteLine("Condition2");
      }
    }

  }

}

//-End------------------------------------------------------------------

After compiling this source code and decompiling it again for analysis purposes, I got a surprise. The Main procedure looks like this:

// ConditionTest
// Token: 0x06000001 RID: 1 RVA: 0x00002050 File Offset: 0x00000250
public static void Main(string[] args)
{
	ConditionTest.conditions[] CollectionOfConditions = new ConditionTest.conditions[]
	{
		new ConditionTest.conditions(true, false),
		new ConditionTest.conditions(false, true)
	};
	foreach (ConditionTest.conditions Conditions in CollectionOfConditions)
	{
		if (Conditions.Condition1)
		{
			if (Conditions.Condition2)
			{
				Console.WriteLine("Condition2");
			}
		}
	}
	foreach (ConditionTest.conditions Conditions in CollectionOfConditions)
	{
		if (Conditions.Condition1)
		{
			if (Conditions.Condition2)
			{
				Console.WriteLine("Condition2");
			}
		}
	}
}

No difference between Variant1 and Variant2.

Then I looked at the compiled code with the Intermediate Language Disassembler (IL DASM).
Hint: I did the indentations manually.
The branches (opcode br) with their jump targets can be seen very good.

Variant1:

IL_003c:  ldloc.0
IL_003d:  stloc.3
IL_003e:  ldc.i4.0
IL_003f:  stloc.s    CS$7$0002
IL_0041:  br.s       IL_0092
IL_0043:    ldloc.3
IL_0044:    ldloc.s    CS$7$0002
IL_0046:    ldelema    ConditionTest/conditions
IL_004b:    ldobj      ConditionTest/conditions
IL_0050:    stloc.1
IL_0051:    nop
IL_0052:    ldloca.s   Conditions
IL_0054:    ldfld      bool ConditionTest/conditions::Condition1
IL_0059:    ldc.i4.0
IL_005a:    ceq
IL_005c:    stloc.s    CS$4$0003
IL_005e:    ldloc.s    CS$4$0003
IL_0060:    brtrue.s   IL_0088
IL_0062:      nop
IL_0063:      ldloca.s   Conditions
IL_0065:      ldfld      bool ConditionTest/conditions::Condition2
IL_006a:      ldc.i4.0
IL_006b:      ceq
IL_006d:      stloc.s    CS$4$0003
IL_006f:      ldloc.s    CS$4$0003
IL_0071:      brtrue.s   IL_0082
IL_0073:        nop
IL_0074:        ldstr      "Condition2"
IL_0079:        call       void [mscorlib]System.Console::WriteLine(string)
IL_007e:        nop
IL_007f:        nop
IL_0080:        br.s       IL_0085
IL_0082:      nop
IL_0083:      br.s       IL_008c
IL_0085:      nop
IL_0086:      br.s       IL_008b
IL_0088:    nop
IL_0089:    br.s       IL_008c
IL_008b:    nop
IL_008c:    ldloc.s    CS$7$0002
IL_008e:    ldc.i4.1
IL_008f:    add
IL_0090:    stloc.s    CS$7$0002
IL_0092:  ldloc.s    CS$7$0002
IL_0094:  ldloc.3
IL_0095:  ldlen
IL_0096:  conv.i4
IL_0097:  clt
IL_0099:  stloc.s    CS$4$0003
IL_009b:  ldloc.s    CS$4$0003
IL_009d:  brtrue.s   IL_0043

Variant2:

IL_00ab:  ldloc.0
IL_00ac:  stloc.3
IL_00ad:  ldc.i4.0
IL_00ae:  stloc.s    CS$7$0002
IL_00b0:  br.s       IL_00f5
IL_00b2:    ldloc.3
IL_00b3:    ldloc.s    CS$7$0002
IL_00b5:    ldelema    ConditionTest/conditions
IL_00ba:    ldobj      ConditionTest/conditions
IL_00bf:    stloc.1
IL_00c0:    nop
IL_00c1:    ldloca.s   Conditions
IL_00c3:    ldfld      bool ConditionTest/conditions::Condition1
IL_00c8:    stloc.s    CS$4$0003
IL_00ca:    ldloc.s    CS$4$0003
IL_00cc:    brtrue.s   IL_00d1
IL_00ce:      nop
IL_00cf:      br.s       IL_00ef
IL_00d1:    ldloca.s   Conditions
IL_00d3:    ldfld      bool ConditionTest/conditions::Condition2
IL_00d8:    stloc.s    CS$4$0003
IL_00da:    ldloc.s    CS$4$0003
IL_00dc:    brtrue.s   IL_00e1
IL_00de:      nop
IL_00df:      br.s       IL_00ef
IL_00e1:    nop
IL_00e2:    ldstr      "Condition2"
IL_00e7:    call       void [mscorlib]System.Console::WriteLine(string)
IL_00ec:    nop
IL_00ed:    nop
IL_00ee:    nop
IL_00ef:    ldloc.s    CS$7$0002
IL_00f1:    ldc.i4.1
IL_00f2:    add
IL_00f3:    stloc.s    CS$7$0002
IL_00f5:  ldloc.s    CS$7$0002
IL_00f7:  ldloc.3
IL_00f8:  ldlen
IL_00f9:  conv.i4
IL_00fa:  clt
IL_00fc:  stloc.s    CS$4$0003
IL_00fe:  ldloc.s    CS$4$0003
IL_0100:  brtrue.s   IL_00b2

It can be clearly seen that the different variants are also implemented in the compiled code as they were coded in the source.

Conclusion

As we can see, in this approach the source code is implemented in the compilation as it was programmed. The decompiler builds source code from the compiled code. The reverse engineered source code is functional, but does not correspond to the actual source code. When a decompiler is used, the result is only a functional code representation and does not have to correspond to what the programmer actually coded.

As we can see from this example, we cannot believe everything we see. :wink:

4 Likes

P.S. You can find more information about C# and the Common Language Infrastructure here.

Thanks for an interesting post! This also means that people can steal your app, decompile, repackage and release it as their own! And now we can’t even sue them because the source code differ. :unamused:

1 Like

Der frühe Vogel fängt den Wurm! :hushed:

Spaß beiseite - How does this relate to developing with UiPath and what can I learn from this and/or apply to my work except the somewhat generic statement “we cannot believe everything we see”.

Thanks,
Lukas

Hi @ptrobot,
sure, this is a possible perspective, even if pessimistic. Generally, I don’t assume that someone wants to steal your specific code. That was not my approach at all, here it was exclusively about the analysis of reengineering tools and which aspects should be considered.
Best regards
Stefan

1 Like

@lukasziebold

Hallo Lukas,
in my opinion, there are a few perspectives, also and especially in the context of UiPath:

  • UiPath bases on dotNET. In this context it is also (more or less) important to know what is build from source code, e.g. also with the Invoke Code activity, if you are interested in.
  • If you want to look at your own code, which is running in the context of a UiPath workflow, don’t be surprised if you see something different with a decompiler.

The title is an eye-catcher, not a generic statement.

You are right when you say that this topic will touch not very many. Nevertheless, we have the space here to “illuminate” that :wink:, even if it is very special.
Best regards
Stefan

1 Like