Search code examples
.netclrcilformal-verification

Weird behaviour with switch statement in catch block


I'm currently working on a .NET Obfuscator and in my control flow obfuscation I'm noticing a weird behaviour. Somehow there is an inconsistent stack size in my generated code.

Original IL:

{
._Statements:

IL_0000: ldstr "MethodD"
IL_0005: call System.Void System.Console::WriteLine(System.String)


Try{
._Statements:

IL_000a: ldc.i4 128736421
IL_000f: stloc.0

IL_0010: ldc.i4 12386123
IL_0015: stloc.1

IL_0016: ldstr "Result D: {0}"
IL_001b: ldloc.0
IL_001c: ldloc.1
IL_001d: add
IL_001e: box System.Int32
IL_0023: call System.String System.String::Format(System.String,System.Object)
IL_0028: call System.Void System.Console::WriteLine(System.String)
IL_002d: leave.s IL_0057
}
Handler{
._Statements:

IL_002f: ldstr "Something horrible happened! MethodD"
IL_0034: call System.Void System.Console::WriteLine(System.String)
IL_0039: call System.Void System.Console::WriteLine(System.Object)

IL_003e: ldc.i4 129387123
IL_0043: ldc.i8 1283769182434
IL_004c: stloc.2
IL_004d: conv.i8
IL_004e: ldloc.2
IL_004f: add
IL_0050: call System.Void System.Console::WriteLine(System.Int64)
IL_0055: leave.s IL_0057
}
._Statements:

IL_0057: call System.Void System.Console::WriteLine()

IL_005c: ret

}

Control flow obfuscated IL:

/* 0x00000480 727D010070   */ IL_0000: ldstr     "MethodD"
/* 0x00000485 280800000A   */ IL_0005: call      void [mscorlib]System.Console::WriteLine(string)
.try
{
    /* 0x0000048A 16           */ IL_000A: ldc.i4.0
    /* 0x0000048B 0D           */ IL_000B: stloc.3
    /* 0x0000048C 2B00         */ IL_000C: br.s      IL_000E

    /* 0x0000048E 09           */ IL_000E: ldloc.3
    // loop start (head: IL_000F)
    /* 0x0000048F 4503000000000000000B00000016000000 */ IL_000F: switch    (IL_0020, IL_002B, IL_0036)

    /* 0x000004A0 20A55CAC07   */ IL_0020: ldc.i4    128736421
    /* 0x000004A5 0A           */ IL_0025: stloc.0
    /* 0x000004A6 17           */ IL_0026: ldc.i4.1

    /* 0x000004A7 0D           */ IL_0027: stloc.3
    /* 0x000004A8 09           */ IL_0028: ldloc.3
    /* 0x000004A9 2BE4         */ IL_0029: br.s      IL_000F

    /* 0x000004AB 204BFFBC00   */ IL_002B: ldc.i4    12386123
    /* 0x000004B0 0B           */ IL_0030: stloc.1
    /* 0x000004B1 18           */ IL_0031: ldc.i4.2
    /* 0x000004B2 0D           */ IL_0032: stloc.3
    /* 0x000004B3 09           */ IL_0033: ldloc.3
    /* 0x000004B4 2BD9         */ IL_0034: br.s      IL_000F
    // end loop

    /* 0x000004B6 728D010070   */ IL_0036: ldstr     "Result D: {0}"
    /* 0x000004BB 06           */ IL_003B: ldloc.0
    /* 0x000004BC 07           */ IL_003C: ldloc.1
    /* 0x000004BD 58           */ IL_003D: add
    /* 0x000004BE 8C0F000001   */ IL_003E: box       [mscorlib]System.Int32
    /* 0x000004C3 281500000A   */ IL_0043: call      string [mscorlib]System.String::Format(string, object)
    /* 0x000004C8 280800000A   */ IL_0048: call      void [mscorlib]System.Console::WriteLine(string)
    /* 0x000004CD DE43         */ IL_004D: leave.s   IL_0092
} // end .try
catch [mscorlib]System.Exception
{
    /* 0x000004CF 16           */ IL_004F: ldc.i4.0
    /* 0x000004D0 1304         */ IL_0050: stloc.s   V_4
    /* 0x000004D2 2B00         */ IL_0052: br.s      IL_0054

    /* 0x000004D4 1104         */ IL_0054: ldloc.s   V_4
    // loop start (head: IL_0056)
    /* 0x000004D6 45020000000000000016000000 */ IL_0056: switch    (IL_0063, IL_0079)

    /* 0x000004E3 72A9010070   */ IL_0063: ldstr     "Something horrible happened! MethodD"
    /* 0x000004E8 280800000A   */ IL_0068: call      void [mscorlib]System.Console::WriteLine(string)
    /* 0x000004ED 280D00000A   */ IL_006D: call      void [mscorlib]System.Console::WriteLine(object)
    /* 0x000004F2 17           */ IL_0072: ldc.i4.1
    /* 0x000004F3 1304         */ IL_0073: stloc.s   V_4
    /* 0x000004F5 1104         */ IL_0075: ldloc.s   V_4
    /* 0x000004F7 2BDD         */ IL_0077: br.s      IL_0056
    // end loop

    /* 0x000004F9 20734AB607   */ IL_0079: ldc.i4    129387123
    /* 0x000004FE 21E2289BE62A010000 */ IL_007E: ldc.i8    1283769182434
    /* 0x00000507 0C           */ IL_0087: stloc.2
    /* 0x00000508 6A           */ IL_0088: conv.i8
    /* 0x00000509 08           */ IL_0089: ldloc.2
    /* 0x0000050A 58           */ IL_008A: add
    /* 0x0000050B 281600000A   */ IL_008B: call      void [mscorlib]System.Console::WriteLine(int64)
    /* 0x00000510 DE00         */ IL_0090: leave.s   IL_0092
} // end handler

/* 0x00000512 17           */ IL_0092: ldc.i4.1
/* 0x00000513 1305         */ IL_0093: stloc.s   V_5
/* 0x00000515 2B00         */ IL_0095: br.s      IL_0097

/* 0x00000517 1105         */ IL_0097: ldloc.s   V_5
// loop start (head: IL_0099)
/* 0x00000519 45020000000000000001000000 */ IL_0099: switch    (IL_00A6, IL_00A7)

/* 0x00000526 2A           */ IL_00A6: ret

/* 0x00000527 280400000A   */ IL_00A7: call      void [mscorlib]System.Console::WriteLine()
/* 0x0000052C 16           */ IL_00AC: ldc.i4.0
/* 0x0000052D 1305         */ IL_00AD: stloc.s   V_5
/* 0x0000052F 1105         */ IL_00AF: ldloc.s   V_5
/* 0x00000531 2BE6         */ IL_00B1: br.s      IL_0099
// end loop
/* 0x00000533 2A           */ IL_00B3: ret

My analysis of stack behaviour (for obfuscated IL):

IL_004F     Pushes: 1   Pops: 0     Stack usage: 2 (2 because the CLR pushes the Exception object onto the stack before the first instruction in the catch block)
IL_0050     Pushes: 0   Pops: 1     Stack usage: 1
IL_0052     Pushes: 0   Pops: 0     Stack usage: 1
IL_0054     Pushes: 1   Pops: 0     Stack usage: 2
IL_0056     Pushes: 0   Pops: 1     Stack usage: 1
IL_0063     Pushes: 1   Pops: 0     Stack usage: 2
IL_0068     Pushes: 0   Pops: 1     Stack usage: 1
IL_006D     Pushes: 0   Pops: 1     Stack usage: 0 (Exception object gets consumed)
IL_0072     Pushes: 1   Pops: 0     Stack usage: 1
IL_0073     Pushes: 0   Pops: 1     Stack usage: 0
IL_0075     Pushes: 1   Pops: 0     Stack usage: 1
IL_0077     Pushes: 0   Pops: 0     Stack usage: 1
back to IL_0056
IL_0056     Pushes: 0   Pops: 1     Stack usage: 0 (This consumes the int32 with value 1 from IL_0075, which jumps to IL_0079)
IL_0079     Pushes: 1   Pops: 0     Stack usage: 1
IL_007E     Pushes: 1   Pops: 0     Stack usage: 2
IL_0087     Pushes: 0   Pops: 1     Stack usage: 1
IL_0088     Pushes: 1   Pops: 1     Stack usage: 1
IL_0089     Pushes: 1   Pops: 0     Stack usage: 2
IL_008A     Pushes: 1   Pops: 2     Stack usage: 1
IL_008B     Pushes: 0   Pops: 1     Stack usage: 0
IL_0090     Pushes: 0   Pops: all   Stack usage: 0

dnSpy is telling me that at IL_0077 is the inconsistent stack size, yet my analysis tells me that everything gets consumed. Funnily enough when I either dupe the exception object on the stack (IL_006D dup) or IL_0075 it works. The .NET Runtime also refuses to run the code and throws an "Invalid program exception" before I manually fix it. IMO duping those instructions is an error and will corrupt the stack. Can anybody tell me why exactly this behaviour occurs?

Edit 05.07.2022 20:38 - Fix typos and add stack analysis


Solution

  • This is simply not how stack verification works. There is no dynamic analysis of what any variables might or might not be. The analysis only looks at what types the objects on the stack are, and it requires the stack to be the same no matter how you reached that point.

    A switch is simply analyzed as a bunch of conditional br jumps. So at IL_0075 the stack is empty, you load an int then jump back to IL_0056. But analysis has already passed IL_0056, at which the stack contained Exception int but now it's only int, hence the inconsistent stack error.

    Take a careful read of ECMA-335 Section III 1.8.1.1:

    The verification algorithm does not take advantage of any data values during its simulation (e.g., it does not perform constant propagation), but uses only type assignments

    The algorithm shall also fail if there is an existing stack state at the next instruction address (for conditional branches ... there might be more than one such address) that cannot be merged with the stack state just computed"