逆向实战:我是怎么从这段汇编一眼看出它是 switch,而不是一串 if/else

有时候在逆向里,最关键的不是“会不会写 C”,而是看到一坨汇编时,你脑子里第一反应是什么结构

这篇就直接拿你这段汇编来说:我是怎么一步步判断它是 switch-case,而不是普通 if / else if / else 的。


1. 先把源码和目标放在脑子里

源码是一个非常简单的 switch:

int var = 10;
switch (var) {
case 1: printf("1\r\n"); break;
case 2: printf("2\r\n"); break;
case 3: printf("3\r\n"); break;
default: printf("default\r\n"); break;
}

逆向时我并不是一开始就假设它是 switch,而是先看汇编在“干什么”。

关键汇编如下(无关代码已省略):

; var = 10;
00C114FE  mov         dword ptr [ebp-4],0Ah  

; switch (var)
00C11505  mov         eax,dword ptr [ebp-4]  
00C11508  mov         dword ptr [ebp-8],eax  

; 依次比较 1 / 2 / 3
00C1150B  cmp         dword ptr [ebp-8],1  
00C1150F  je          00C1151F     ; case 1

00C11511  cmp         dword ptr [ebp-8],2  
00C11515  je          00C1152E     ; case 2

00C11517  cmp         dword ptr [ebp-8],3  
00C1151B  je          00C1153D     ; case 3

00C1151D  jmp         00C1154C     ; default

2. 第一眼看到的异常感:比较链太“整齐”了

我在逆向里看到这段时,第一个感觉是:

这不像人手写的 if/else,更像编译器生成的结构。

为什么?

2.1 同一个变量,被连续拿来和多个“干净的常量”比较

cmp [ebp-8], 1
je  case_1

cmp [ebp-8], 2
je  case_2

cmp [ebp-8], 3
je  case_3

注意几个细节:

  • 比较对象完全一样:全是 [ebp-8]
  • 比较值非常规整:1、2、3
  • 每次 cmp 后立刻 je 跳走

这在逆向里是一个非常强的信号。

如果是普通 if/else:

  • 有可能是 <><=
  • 有可能中途就 return
  • 有可能比较对象变来变去

但这里是一模一样的比较模板重复 N 次,只是立即数不同。

👉 这非常像 switch 的 case 常量。


3. 再往下看:每个分支都“自己干完事就跳走”

我顺着 je 跳到每个目标地址去看:

; case 1:
00C1151F  push        0C17B30h      ; "1\r\n"
00C11524  call        printf
00C11529  add         esp,4  
00C1152C  jmp         00C11559      ; ← 关键

; case 2:
00C1152E  push        0C17B34h      ; "2\r\n"
00C11533  call        printf
00C11538  add         esp,4  
00C1153B  jmp         00C11559

; case 3:
00C1153D  push        0C17B38h      ; "3\r\n"
00C11542  call        printf
00C11547  add         esp,4  
00C1154A  jmp         00C11559

这一步我基本已经 80% 确认是 switch 了。

为什么?

因为它们全部在做一件事

执行各自的 case 代码 → 无条件跳到同一个地址

这在 C 里只有一个非常典型的来源:

case X:
    ...
    break;

也就是说:

  • jmp 00C11559break;
  • 00C11559 ≈ switch 之后的统一出口

如果是 if/else,很多时候:

  • 不需要手动 jmp
  • 可以顺着自然流控制流往下走

switch + break 的汇编特征就是:多个分支 → 多个 jmp → 同一出口


4. 最后一个确认点:那个“兜底的 jmp”

回头再看比较链的结尾:

cmp [ebp-8], 3
je  case_3

jmp default

这条 jmp default 非常关键。

因为它说明一件事:

所有 case 都没命中 → 强制跳到某一个固定分支

这在 C 里,只能是:

default:

而且这个 default 块:

00C1154C  push        0C17B3Ch      ; "default\r\n"
00C11551  call        printf
00C11556  add         esp,4  
  • 没有被任何 je 直接命中
  • 只通过这一条 fallback jmp 进来

👉 100% 的 default 分支形态。


5. 到这里,我在脑子里已经还原出完整结构了

在没看源码前,我在逆向里已经可以直接写出这样的伪代码:

if (v == 1) {
    printf("1");
}
else if (v == 2) {
    printf("2");
}
else if (v == 3) {
    printf("3");
}
else {
    printf("default");
}

然后再结合:

  • 比较对象完全一致
  • 常量是离散整数
  • 所有分支都有 break 行为

👉 这不是人手写的 if/else,而是编译器生成的线性 switch。


6. 顺手对照一下完整示例代码(你给的)

#include <stdio.h>

void func()
{
    int var = 10;
    switch (var) {
    case 1:
        printf("1\r\n");
        break;
    case 2:
        printf("2\r\n");
        break;
    default:
        printf("default\r\n");
        break;
    }
}

int main() {
    func();
    return 0;
}

和汇编一一对应,没有任何矛盾点。


7. 我自己在逆向里总结的“线性 switch 判断口诀”

最后用人话总结一下这类 switch,我在逆向时脑子里的判断流程是这样的:

  1. 同一个变量被连续 cmp 多次
  2. cmp 的都是干净的常量(1、2、3…)
  3. 每个 cmp 后面都是 je,直接跳走
  4. 比较链最后有一个无条件 jmp → 兜底分支
  5. 每个分支末尾都 jmp 到同一个地址

只要这五条同时出现,我几乎不会再怀疑:

这是 switch,而且是 case 数量少、没用跳转表的那种。


如果你愿意,下一步我们可以直接接着写一篇:

  • “从汇编里区分:线性 switch vs 跳转表 switch”
  • 或者 “switch 没有 break(fall-through)在汇编里的真实样子”

这些在真实逆向里,比语法本身更容易踩坑。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐