连续 switch-case 的跳转表实现

文章目录

  • 源代码
  • 汇编语句
  • [深度逆向分析:连续 switch-case 的跳转表实现](#深度逆向分析:连续 switch-case 的跳转表实现)
    • [1️⃣ 汇编分析:索引计算与边界检查](#1️⃣ 汇编分析:索引计算与边界检查)
    • [2️⃣ 跳转表数据解析(真实内存数据)](#2️⃣ 跳转表数据解析(真实内存数据))
    • [3️⃣ case 分支结构分析](#3️⃣ case 分支结构分析)
    • [4️⃣ default 分支](#4️⃣ default 分支)
    • [5️⃣ 完整执行流程示例(var = 10)](#5️⃣ 完整执行流程示例(var = 10))
    • [6️⃣ 深入经验总结](#6️⃣ 深入经验总结)
  • 跳转表被错误解释为指令

源代码

c 复制代码
#include <stdio.h>
#include <stdio.h>

void func()
{
    int var = 10;
    // 连续且密集的case值最容易生成跳转表
    switch (var) {
    case 1: printf("1\r\n"); break;
    case 2: printf("2\r\n"); break;
    case 3: printf("3\r\n"); break;
    case 4: printf("4\r\n"); break;
    case 5: printf("5\r\n"); break;
    case 6: printf("6\r\n"); break;
    case 7: printf("7\r\n"); break;
    case 8: printf("8\r\n"); break;
    case 9: printf("9\r\n"); break;
    case 10: printf("10\r\n"); break;
    case 11: printf("11\r\n"); break;
    case 12: printf("12\r\n"); break;
    default: printf("default\r\n"); break;
    }
}
int main() {
    func();
    return 0;
}

汇编语句

c 复制代码
     4: void func()
     5: {
004E5280  push        ebp  
004E5281  mov         ebp,esp  
004E5283  sub         esp,8  
004E5286  mov         dword ptr [ebp-8],0CCCCCCCCh  
004E528D  mov         dword ptr [ebp-4],0CCCCCCCCh  
004E5294  mov         ecx,4EC008h  
004E5299  call        004E1217  
     6:     int var = 10;
004E529E  mov         dword ptr [ebp-4],0Ah  
     7:     // 连续且密集的case值最容易生成跳转表
     8:     switch (var) {
004E52A5  mov         eax,dword ptr [ebp-4]  
004E52A8  mov         dword ptr [ebp-8],eax  
004E52AB  mov         ecx,dword ptr [ebp-8]  
004E52AE  sub         ecx,1  
004E52B1  mov         dword ptr [ebp-8],ecx  
004E52B4  cmp         dword ptr [ebp-8],0Bh  
004E52B8  ja          004E5388  
004E52BE  mov         edx,dword ptr [ebp-8]  
004E52C1  jmp         dword ptr [edx*4+004E53A4h]  
     9:     case 1: printf("1\r\n"); break;
004E52C8  push        4E8620h  
004E52CD  call        004E1082  
004E52D2  add         esp,4  
004E52D5  jmp         004E5395  
    10:     case 2: printf("2\r\n"); break;
004E52DA  push        4E8624h  
004E52DF  call        004E1082  
004E52E4  add         esp,4  
004E52E7  jmp         004E5395  
    11:     case 3: printf("3\r\n"); break;
004E52EC  push        4E8628h  
004E52F1  call        004E1082  
004E52F6  add         esp,4  
004E52F9  jmp         004E5395  
    12:     case 4: printf("4\r\n"); break;
004E52FE  push        4E862Ch  
004E5303  call        004E1082  
004E5308  add         esp,4  
004E530B  jmp         004E5395  
    13:     case 5: printf("5\r\n"); break;
004E5310  push        4E8630h  
004E5315  call        004E1082  
004E531A  add         esp,4  
004E531D  jmp         004E5395  
    14:     case 6: printf("6\r\n"); break;
004E531F  push        4E8634h  
004E5324  call        004E1082  
004E5329  add         esp,4  
004E532C  jmp         004E5395  
    15:     case 7: printf("7\r\n"); break;
004E532E  push        4E8638h  
004E5333  call        004E1082  
004E5338  add         esp,4  
004E533B  jmp         004E5395  
    16:     case 8: printf("8\r\n"); break;
004E533D  push        4E863Ch  
004E5342  call        004E1082  
004E5347  add         esp,4  
004E534A  jmp         004E5395  
    17:     case 9: printf("9\r\n"); break;
004E534C  push        4E8640h  
004E5351  call        004E1082  
004E5356  add         esp,4  
004E5359  jmp         004E5395  
    18:     case 10: printf("10\r\n"); break;
004E535B  push        4E8644h  
004E5360  call        004E1082  
004E5365  add         esp,4  
004E5368  jmp         004E5395  
    19:     case 11: printf("11\r\n"); break;
004E536A  push        4E864Ch  
004E536F  call        004E1082  
004E5374  add         esp,4  
004E5377  jmp         004E5395  
    20:     case 12: printf("12\r\n"); break;
004E5379  push        4E8654h  
004E537E  call        004E1082  
004E5383  add         esp,4  
004E5386  jmp         004E5395  
    21:     default: printf("default\r\n"); break;
004E5388  push        4E865Ch  
004E538D  call        004E1082  
004E5392  add         esp,4  
    22:     }
    23: }
004E5395  add         esp,8  
004E5398  cmp         ebp,esp  
004E539A  call        004E1168  
004E539F  mov         esp,ebp  
004E53A1  pop         ebp  
004E53A2  ret  
004E53A3  nop  
004E53A4  enter       4E52h,0  
004E53A8  ficom       dword ptr [edx+4Eh]  
004E53AB  add         ah,ch  
004E53AD  push        edx  
004E53AE  dec         esi  
004E53AF  add         dh,bh  
004E53B1  push        edx  
004E53B2  dec         esi  
004E53B3  add         byte ptr [eax],dl  
004E53B5  push        ebx  
004E53B6  dec         esi  
004E53B7  add         byte ptr [edi],bl  
004E53B9  push        ebx  
004E53BA  dec         esi  
004E53BB  add         byte ptr [esi],ch  
004E53BD  push        ebx  
004E53BE  dec         esi  
004E53BF  add         byte ptr ds:[4C004E53h],bh  
004E53C5  push        ebx  
004E53C6  dec         esi  
004E53C7  add         byte ptr [ebx+53h],bl  
004E53CA  dec         esi  
004E53CB  add         byte ptr [edx+53h],ch  
004E53CE  dec         esi  
004E53CF  add         byte ptr [ecx+53h],bh  
004E53D2  dec         esi  
004E53D3  add         ah,cl  

深度逆向分析:连续 switch-case 的跳转表实现

今天分析一个小函数 func(),它包含连续 case 值的 switch-case。目的:从汇编和内存数据角度彻底理解 jump table 的生成和工作方式。

源码:

c 复制代码
#include <stdio.h>

void func()
{
    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;
    case 4: printf("4\r\n"); break;
    case 5: printf("5\r\n"); break;
    case 6: printf("6\r\n"); break;
    case 7: printf("7\r\n"); break;
    case 8: printf("8\r\n"); break;
    case 9: printf("9\r\n"); break;
    case 10: printf("10\r\n"); break;
    case 11: printf("11\r\n"); break;
    case 12: printf("12\r\n"); break;
    default: printf("default\r\n"); break;
    }
}

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

1️⃣ 汇编分析:索引计算与边界检查

关键 switch 汇编:

asm 复制代码
004E52A5  mov   eax,[ebp-4]       ; eax = var
004E52A8  mov   [ebp-8],eax       ; 临时保存
004E52AB  mov   ecx,[ebp-8]       ; ecx = var,用于索引计算
004E52AE  sub   ecx,1             ; 减去最小 case,得到 0 基索引
004E52B1  mov   [ebp-8],ecx       ; 保存索引
004E52B4  cmp   [ebp-8],0Bh       ; 上界检查(11,对应 case 12)
004E52B8  ja    004E5388          ; 超出上界跳 default
004E52BE  mov   edx,[ebp-8]       ; edx = 索引
004E52C1  jmp   dword ptr [edx*4+004E53A4h] ; 跳转表访问

解读与思考

  1. sub ecx,1 → 编译器优化:最小 case 值是 1 → 转 0 基索引,这样 jump table 可以从 0 开始

  2. cmp [ebp-8],11 + ja default → 上界检查,保证索引不会越界

  3. jmp [edx*4 + 004E53A4h] → 间接跳转到对应 case

  4. 通过索引访问跳转表 → O(1) 选择 case,比 if-else 链快得多

✅ 从这些特征可以几乎确定这是 连续 case 值的 jump table switch


2️⃣ 跳转表数据解析(真实内存数据)

跳转表起始地址:0x004E53A4

地址 内存数据(小端) 对应跳转地址 对应 case
0x004E53A4 c8 52 4e 00 0x004E52C8 case 1
0x004E53A8 da 52 4e 00 0x004E52DA case 2
0x004E53AC ec 52 4e 00 0x004E52EC case 3
0x004E53B0 fe 52 4e 00 0x004E52FE case 4
0x004E53B4 10 53 4e 00 0x004E5310 case 5
0x004E53B8 1f 53 4e 00 0x004E531F case 6
0x004E53BC 2e 53 4e 00 0x004E532E case 7
0x004E53C0 3d 53 4e 00 0x004E533D case 8
0x004E53C4 4c 53 4e 00 0x004E534C case 9
0x004E53C8 5b 53 4e 00 0x004E535B case 10
0x004E53CC 6a 53 4e 00 0x004E536A case 11
0x004E53D0 79 53 4e 00 0x004E5379 case 12

每 4 字节存储一个 case 的入口地址,顺序严格对应索引 0~11 → case 1~12

注意小端序存储,这是 x86 常见格式


3️⃣ case 分支结构分析

例子:case 1 和 case 2

asm 复制代码
; case 1
004E52C8  push 4E8620h      ; push "1\r\n"
004E52CD  call 004E1082     ; printf
004E52D2  add  esp,4        ; 调整堆栈
004E52D5  jmp 004E5395      ; 统一出口

; case 2
004E52DA  push 4E8624h
004E52DF  call 004E1082
004E52E4  add  esp,4
004E52E7  jmp 004E5395

分析:

  1. 每个 case 都 push 字符串 → call printf → restore esp

  2. 统一跳回 0x004E5395,对应 switch 尾部 → break 的实现

  3. 所有 case 结构完全一致 → 便于 jump table 映射

  4. default 分支不同:在索引检查时直接跳过去,不经过 jump table


4️⃣ default 分支

asm 复制代码
004E5388  push 4E865Ch        ; push "default\r\n"
004E538D  call 004E1082
004E5392  add  esp,4
  • 上界检查:cmp index,11 + ja default → 只有索引 >11 才触发

  • 不走 jump table → 节省了对无效索引的访问

  • default 处理逻辑独立,保持安全


5️⃣ 完整执行流程示例(var = 10)

  1. var = 10 → sub 1 → index = 9

  2. cmp 9,11 → 不跳 default

  3. jmp [9*4 + 004E53A4][0x004E53C8] → 0x004E535B → case 10

  4. 执行 case 10 → push "10\r\n" → call printf → add esp → jmp switch 尾部

  5. 完成 switch 执行 → 返回函数


6️⃣ 深入经验总结

识别 jump table switch 的特征

  1. 索引计算 + 边界检查

    • sub/add min_case → 转 0 基索引

    • cmp max_index + ja/jb → default 分支

  2. 间接跳转表

    • jmp [reg*4 + table_addr] → 核心跳转

    • 紧跟跳转表的内存字节 → 4 字节 per case → 小端序

  3. case 分支统一模式

    • push 数据 → call → add esp → jmp 统一出口
  4. default 分支

    • 边界检查触发 → 独立执行
  5. 执行效率

    • 跳转表 O(1),相比 if-else 链(O(n) cmp/jcc)性能更优
  6. 可视化推理

    复制代码
    var -> index -> jump table -> case address -> case branch -> switch exit
  7. 实战技巧

    • 看到 jmp [reg*4 + imm] 后,别直接当指令看

    • 检查 jump table 内存数据 → 还原 case 映射

    • 对连续 case、密集 case 这种模式尤其有效


💡 总结

  • 每条 jump table 数据都对应一个 case 地址

  • 索引计算 + 边界检查 → default

  • case 分支结构统一 → break 的实现

  • jump table switch 高效、安全、清晰


跳转表被错误解释为指令

啊,这正是你在反汇编里经常会遇到的误报指令问题。看到你贴的这一段:

asm 复制代码
004E53A4  enter       4E52h,0  
004E53A8  ficom       dword ptr [edx+4Eh]  
004E53AB  add         ah,ch  
004E53AD  push        edx  
004E53AE  dec         esi  
004E53AF  add         dh,bh  
004E53B1  push        edx  
004E53B2  dec         esi  
004E53B3  add         byte ptr [eax],dl  
...

逆向工程角度 来看,这里其实根本不是可执行代码 ,而是你之前 jump table 的数据区域,也就是:

复制代码
0x004E53A4 往后的一大块,存的是 case 分支的地址:
c8 52 4e 00  da 52 4e 00  ec 52 4e 00  fe 52 4e 00  ...

为什么被误当成指令

  1. 反汇编工具是线性扫描

    工具看到 0x004E53A4 就按照默认规则当成指令开始解析,于是把 4 字节地址解析成 enter 0x4E52,0,紧跟的 4 字节又被解析成 ficom dword ptr [edx+4Eh]......

    其实这些 4 字节数据本来是 0x004E52C8, 0x004E52DA......,只是jump table 的 case 地址

  2. x86 可变长度指令造成的歧义

    x86 指令长度不固定,反汇编器没有上下文时,会把任意 4/8 字节数据解析成可能的指令组合 → 导致"乱七八糟"的 asm 看起来像真正指令,其实完全是伪指令

  3. 典型特征

    • 地址紧跟在 jmp [reg*4 + table_addr] 后面

    • 每 4 字节整齐排列(符合 32-bit 地址大小)

    • 与周围真实可执行指令格式不符(push/call/add esp)

🔑 经验公式:看到 jmp 间接跳转后,紧跟的一片"奇怪指令",几乎百分之百是 jump table 数据,不是真指令。


如何正确处理

  1. 告诉反汇编工具这是数据

    • IDA/IDA Pro:手动标记 define dwordarray of dword

    • Ghidra/ghidra-like:右键选择 Data → Array of DWords

  2. 手动对照 jump table 数据

asm 复制代码
0x004E53A4:  c8 52 4e 00  -> 0x004E52C8  (case 1)
0x004E53A8:  da 52 4e 00  -> 0x004E52DA  (case 2)
0x004E53AC:  ec 52 4e 00  -> 0x004E52EC  (case 3)
...
  1. 理解执行逻辑
  • jmp [edx*4 + 0x004E53A4] 执行时,CPU 不会执行这些字节,它会把 edx*4 + base 读出 32-bit 数值 → 当作目标地址 → 跳过去执行 case 分支。

🔹总结

  • 004E53A4 enter 4E52h,0 ... 并不是汇编指令,是 jump table 的 case 地址数据

  • 反汇编工具误当作指令 → 产生伪装"奇怪 asm"

  • 正确做法:把它标记为 数据数组,对应 case 地址

  • 这也是你在 jump table 逆向时最常遇到的坑

小经验:凡是 jump table 后看到"乱七八糟"的指令,第一反应应该是"这里是数据,不是指令",不要试图理解成真正的 CPU 指令。


如果你愿意,我可以帮你画一张跳转表数据 → case 地址 → 分支汇编 → switch 统一出口的可视化图,把这些"伪指令数据"映射成真正的逻辑路径,这样一眼就能理解整个 switch 的执行流程。

你希望我画吗?

相关推荐
CHANG_THE_WORLD18 小时前
1.单行 if 条件语句反汇编逆向分析
if语句·反汇编·if 语句反汇编
CHANG_THE_WORLD20 小时前
2.if else 语句特征
反汇编·if else
2401_858286115 个月前
CD64.【C++ Dev】多态(3): 反汇编剖析单继承下的虚函数表
开发语言·c++·算法·继承·面向对象·虚函数·反汇编
bcbobo21cn6 个月前
OllyDbg技巧学习
ollydbg·反汇编·二进制代码·函数体
2401_858286111 年前
动态内存管理练习题的反汇编代码分析(底层)
汇编·visualstudio·指针·vs·结构体·寄存器·反汇编
NovFif2 年前
【加密与解密(第四版)】第十五章笔记
开发语言·笔记·安全·编程语言·反汇编
NovFif2 年前
【加密与解密(第四版)】第六章笔记
开发语言·安全·系统安全·密码学·编程语言·反汇编
NovFif2 年前
【加密与解密(第四版)】第十七章笔记
windows·笔记·安全·系统安全·反汇编
剁椒排骨2 年前
BUUCTF靶场 [reverse]easyre、reverse1、reverse2
网络安全·数据分析·ida·逆向·ctf·reverse·反汇编