catch块内switch语句异常:控制流混淆生成IL栈大小不一致
.NET控制流混淆栈不一致问题根因分析
问题现象
在开发.NET混淆器实现控制流混淆功能时出现异常:生成的程序集被dnSpy提示IL_0077位置存在栈大小不一致问题,直接运行时.NET运行时会抛出Invalid program exception异常拒绝执行。
反常的是,只要在IL_006D或IL_0075位置手动添加dup指令,代码即可正常通过验证运行,但从栈平衡逻辑判断,添加dup会产生多余栈元素,属于不符合IL规范的错误操作。
原始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 }
控制流混淆后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
手动栈行为分析
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
根本原因
栈分析逻辑存在本质误区:.NET CLR的IL验证是静态保守检查,不会模拟实际执行路径、也不会判断分支的可行性,它的核心校验规则是:
所有能跳转到同一个指令地址的控制流路径,到达该指令时的栈深度必须完全一致
之前的分析是按照「第一次进入switch走case0、第二次进入走case1」的实际执行流串行计算栈深,完全忽略了静态校验对分支交汇点的栈深一致性要求。
具体到这段catch块的代码,循环头IL_0056(switch指令位置)是两个控制流路径的交汇点:
- 路径1:从catch块入口顺序执行,经
IL_0054跳转到IL_0056,此时栈上残留catch块传入的异常对象+刚加载的状态int,栈深度为2 - 路径2:从循环尾部
IL_0077跳回IL_0056,此时异常对象已经被IL_006D的WriteLine(object)调用消费,栈上只有刚加载的状态int,栈深度为1
两条路径到达同一指令时栈深度差1,直接触发IL验证失败,这就是dnSpy报错、运行时抛出无效程序异常的核心原因。观察到的「加dup就能运行」的现象也完全符合这个逻辑:
- 在
IL_006D前加dup:复制栈顶的异常对象,调用WriteLine只消费1份副本,栈上还残留1份异常对象,跳回IL_0056时栈深度为2,和路径1的栈深一致,即可通过验证 - 在
IL_0075前加dup:复制栈顶的状态int,跳回IL_0056时栈上有2个int值,栈深度为2,同样满足一致性要求
这种加dup的方案只是取巧绕过验证,并不是正确的修复方式。实现控制流混淆时,拆分基本块、插入调度循环必须保证每个循环头、分支跳转目标点的入口栈深在所有入边上完全相等,正确的修复方案有两种:
- 在catch块入口处就把异常对象存储到本地变量,不要让异常对象残存在栈
相关产品推荐
相关产品推荐

