You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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块入口处就把异常对象存储到本地变量,不要让异常对象残存在栈
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 14:02:15