文章目录
- 源代码
- 汇编语句
- [深度逆向分析:连续 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] ; 跳转表访问
解读与思考:
-
sub ecx,1→ 编译器优化:最小 case 值是 1 → 转 0 基索引,这样 jump table 可以从 0 开始 -
cmp [ebp-8],11+ja default→ 上界检查,保证索引不会越界 -
jmp [edx*4 + 004E53A4h]→ 间接跳转到对应 case -
通过索引访问跳转表 → 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
分析:
-
每个 case 都 push 字符串 → call printf → restore esp
-
统一跳回
0x004E5395,对应 switch 尾部 → break 的实现 -
所有 case 结构完全一致 → 便于 jump table 映射
-
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)
-
var = 10 →
sub 1→ index = 9 -
cmp 9,11→ 不跳 default -
jmp [9*4 + 004E53A4]→[0x004E53C8]→ 0x004E535B → case 10 -
执行 case 10 → push "10\r\n" → call printf → add esp → jmp switch 尾部
-
完成 switch 执行 → 返回函数
6️⃣ 深入经验总结
识别 jump table switch 的特征:
-
索引计算 + 边界检查
-
sub/add min_case→ 转 0 基索引 -
cmp max_index + ja/jb→ default 分支
-
-
间接跳转表
-
jmp [reg*4 + table_addr]→ 核心跳转 -
紧跟跳转表的内存字节 → 4 字节 per case → 小端序
-
-
case 分支统一模式
- push 数据 → call → add esp → jmp 统一出口
-
default 分支
- 边界检查触发 → 独立执行
-
执行效率
- 跳转表 O(1),相比 if-else 链(O(n) cmp/jcc)性能更优
-
可视化推理
var -> index -> jump table -> case address -> case branch -> switch exit -
实战技巧
-
看到
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 ...
为什么被误当成指令
-
反汇编工具是线性扫描
工具看到
0x004E53A4就按照默认规则当成指令开始解析,于是把 4 字节地址解析成enter 0x4E52,0,紧跟的 4 字节又被解析成ficom dword ptr [edx+4Eh]......其实这些 4 字节数据本来是
0x004E52C8,0x004E52DA......,只是jump table 的 case 地址。 -
x86 可变长度指令造成的歧义
x86 指令长度不固定,反汇编器没有上下文时,会把任意 4/8 字节数据解析成可能的指令组合 → 导致"乱七八糟"的 asm 看起来像真正指令,其实完全是伪指令。
-
典型特征:
-
地址紧跟在
jmp [reg*4 + table_addr]后面 -
每 4 字节整齐排列(符合 32-bit 地址大小)
-
与周围真实可执行指令格式不符(push/call/add esp)
-
🔑 经验公式:看到 jmp 间接跳转后,紧跟的一片"奇怪指令",几乎百分之百是 jump table 数据,不是真指令。
如何正确处理
-
告诉反汇编工具这是数据
-
IDA/IDA Pro:手动标记
define dword或array of dword -
Ghidra/ghidra-like:右键选择
Data → Array of DWords
-
-
手动对照 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)
...
- 理解执行逻辑:
- 当
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 的执行流程。
你希望我画吗?