C/C++逆向:switch语句逆向分析

在逆向分析中,switch语句会被编译器转化为不同的底层实现方式,这取决于编译器优化和具体的场景。常见的实现方式包括以下几种:

①顺序判断(if-else链):

编译器将switch语句转化为一系列的if-else语句。这种方式适用于case值较少的情况。

复制代码
反汇编代码表现为一系列的比较和条件跳转指令(如CMP和JNE/JE)。
每个case的值会与目标变量进行比较,匹配时跳转到对应的代码块执行。
②跳转表(jump table):

switch语句中的case值是连续或接近连续的整数时,编译器可能会生成一个跳转表。跳转表是一个包含多个指针的数组,根据目标变量的值选择跳转到不同的代码块。

复制代码
反汇编时表现为读取跳转表并跳转,例如使用JMP指令配合计算偏移。
通常会看到LEA(加载有效地址)指令和JMP指令配合使用,或者使用INDIRECT JMP,根据变量值计算偏移量并跳转。
③二分查找(binary search)

case值的范围较大且稀疏时,编译器可能会生成二分查找结构。编译器会按case值排序,并生成类似于二分查找树的结构来进行高效的判断。

复制代码
反汇编时表现为多个比较和有条件的跳转,通常是递归式的分支跳转。

假设有如下C代码:

cpp 复制代码
switch(x) {
    case 1:
        do_case1();
        break;
    case 2:
        do_case2();
        break;
    default:
        do_default();
        break;
}

其汇编代码可能会有以下几种表现形式:

①顺序判断:
cpp 复制代码
CMP EAX, 1
JE case1
CMP EAX, 2
JE case2
JMP default
②跳转表:
cpp 复制代码
MOV EAX, [EBP+var_x]  ; 获取x的值
CMP EAX, 2            ; 检查x的范围
JA default
JMP [jump_table + EAX*4]  ; 通过x的值在跳转表中选择跳转地址

jump_table中保存了各个case的代码地址。

二分查找
cpp 复制代码
CMP EAX, 2
JE case2
CMP EAX, 1
JE case1
JMP default

这种实现常见于case值不连续且较大的情况。在该文中说的三种表现方式中二分查找和顺序判断这两种方式相对应容易理解,接下去的内容就蜻蜓点水大概过一下,重点会将跳转表的表现形式做说明。

逆向分析示例

假设此时有一个简单的switch代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
​
int main() {
    int nFlag = 0;
    scanf("%d", &nFlag);
    switch (nFlag)
    {
    case 10:
        printf("nFlag = 10");
        break;
    case 11:
        printf("nFlag = 11");
        break;
    case 12:
        printf("nFlag = 12");
        break;
    case 254:
        printf("nFlag = 254");
        break;
    case 255:
        printf("nFlag = 255");
        break;
    case 256:ewZ3ews
        printf("nFlag = 256");
        break;
    case 454:
        printf("nFlag = 454");
        break;
    case 455:
        printf("nFlag = 455");
        break;
    case 456:
        printf("nFlag = 456");
        break;
    default:
        break;
    }
    system("pause");
    return 0;
}

此时使用Visual Studio对该代码进行编译,生成exe文件,对应的编译配置为Debug-x86;

Debug-x86程序分析
静态分析:

将生成的程序载入IDA中进行静态分析

Function Window中定位到main函数:

(关于定位main函数的各种方法请查看前面的文章),接下去开始逐步对进行代码分析:

cpp 复制代码
mov     [ebp+var_C], 0
lea     eax, [ebp+var_C]
push    eax
push    offset Format   ; "%d"
call    j__scanf
add     esp, 8

上述这个汇编代码片段的作用是调用scanf函数从用户那里读取一个整数,并将其存储在局部变量中。我们逐步分析这个代码:

mov [ebp+var_C], 0:这条指令将值 0 存储到局部变量 [ebp+var_C] 中,通常用于初始化变量 var_C,确保它在使用前被清零。[ebp+var_C]x86汇编中的一种内存寻址方式,用于访问栈上的局部变量。为了理解它的含义,需要了解栈帧(stack frame)和ebp寄存器在函数调用中的作用。

复制代码
①当函数被调用时,系统会为这个函数在栈上分配一段内存,称为栈帧。栈帧中存储着局部变量、函数参数以及保存的寄存器值等信息。
②ebp(Base Pointer,基指针)寄存器用于标记栈帧的基地址,即函数栈帧的一个固定参考点。
③在函数调用期间,ebp 通常不会改变,函数中的局部变量和参数都可以相对于 ebp 寄存器通过偏移量进行访问。

ebp+var_C 实际上表示函数栈帧中的一个局部变量的内存地址。在函数执行时,ebp 指向栈帧的底部,局部变量通常存储在 ebp 的下方,var_C 是这个局部变量的偏移量。[ebp+var_C]:表示访问存储在ebp 基地址加上 var_C 偏移量处的内存位置的值。接着我们继续解释代码:

lea eax, [ebp+var_C]:这条指令将局部变量 var_C 的地址加载到寄存器 eax 中。lea 指令用于获取某个变量的地址,而不是变量的值,因此 eax 现在保存的是变量 var_C 的地址。

push eax:将 eax(也就是变量 var_C 的地址)压入栈中。这是为了给后续的 scanf 函数准备参数,scanf 需要知道存储输入结果的变量地址。

push offset Format:将格式字符串的地址(这里是 "%d",表示整数格式)压入栈中。这是 scanf 函数的第二个参数,用来指定输入的数据类型。

call j__scanf:调用 scanf 函数,它会根据传入的格式字符串和变量地址,从用户那里读取输入并将值存储在 var_C 中。

add esp, 8:由于之前在栈中压入了两个 4 字节的参数(格式字符串的地址和变量的地址),scanf 调用后需要通过这条指令恢复栈平衡,将栈指针 esp 增加 8,清理掉这两个参数。

这个汇编代码大致等价于以下 C 代码:

cpp 复制代码
int var_C = 0;
scanf("%d", &var_C);

scanf函数调用完毕,后续代码如下:

cpp 复制代码
mov     eax, [ebp+var_C]
mov     [ebp+var_D4], eax
cmp     [ebp+var_D4], 0FFh
jg      short loc_4119C2
cmp     [ebp+var_D4], 0FFh
jz      loc_411A34
mov     ecx, [ebp+var_D4]
sub     ecx, 0Ah        ; switch 245 cases
mov     [ebp+var_D4], ecx
cmp     [ebp+var_D4], 0F4h
ja      def_4119BB      ; jumptable 004119BB default case, cases 13-253
                        ; jumptable 004119F1 default case, cases 257-453
mov     edx, [ebp+var_D4]
movzx   eax, ds:byte_411AF8[edx]
jmp     ds:jpt_4119BB[eax*4] ; switch jump  

这段汇编代码的主要作用是对局部变量 var_D4 进行一系列的操作和比较,并根据比较结果跳转到不同的代码位置。我们逐步解释代码的含义:

mov eax, [ebp+var_C]:将位于 [ebp+var_C] 的值(即局部变量 var_C 的值)加载到 eax 寄存器中。

mov [ebp+var_D4], eax:将 eax 中的值(即用户输入的 var_C 的值)存储到 [ebp+var_D4],即局部变量 var_D4 中。

cmp [ebp+var_D4], 0FFh:将 var_D4 的值与 0xFF(255 十进制)进行比较。

jg short loc_4119C2:如果比较结果为 "大于"(jg,Jump if Greater),则跳转到 loc_4119C2代码如下:

cpp 复制代码
loc_4119C2:                             ; CODE XREF: _main+5D↑j
                 mov     ecx, [ebp+var_D4]     
                 sub     ecx, 100h       ; switch 201 cases
                 mov     [ebp+var_D4], ecx
                 cmp     [ebp+var_D4], 0C8h
                 ja      def_4119BB      ; jumptable 004119BB default case, cases 13-253
                                         ; jumptable 004119F1 default case, cases 257-453
                 mov     edx, [ebp+var_D4]
                 movzx   eax, ds:byte_411C04[edx]
                 jmp     ds:jpt_4119F1[eax*4] ; switch jump

cmp [ebp+var_D4], 0FFh:再次比较 var_D4 的值与 0xFF,这与前面相同,继续进行比较。

jz loc_411A34:如果比较结果为 "相等"(jz,Jump if Zero),则跳转到 loc_411A34,代码如下。

cpp 复制代码
loc_411A34:                             ; CODE XREF: _main+69↑j
                push    offset aNflag255 ; "nFlag = 255"
                call    j__printf
                add     esp, 4
                jmp     short def_4119BB ; jumptable 004119BB default case, cases 13-253
                                        ; jumptable 004119F1 default case, cases 257-453

mov ecx, [ebp+var_D4]:将局部变量 var_D4 的值加载到 ecx 寄存器中。

sub ecx, 0Ah:将 ecx 中的值减去 0xA(十进制 10)。那么在这边为什么要减去10呢?其实在反汇编代码中,var_D4 的值被减去 0xA(10),这是与 switch-case 语句的基准值对齐的一部分优化。为了更清楚地解释这个过程,我们需要理解以下几个概念:

复制代码
    在高级语言中,switch-case 语句通常用于根据不同的值跳转到不同的代码分支。当代码被编译时,编译器会尝试优化 switch-case 的实现,特别是当 case 的值范围较密集时,编译器可能会选择使用 跳转表(jump table) 来提高效率。
    跳转表允许程序通过索引快速跳转到对应的分支,而不是通过一系列的 if-else 或 cmp 指令逐个比较所有 case 值。跳转表的索引通常是 case 值的偏移量,因此编译器会对 switch-case 的 case 值进行某种形式的调整,使得 case 的最小值成为跳转表的起始索引。

在此处的反汇编代码中,var_D4 被减去 0xA(10),这很可能是因为 switch-case 中的 case 值范围不是从 0 开始的;很有可能此时 switch-case 语句中 case 的最小值是 10,那么减去 0xA 就将输入值与 case 的最小值对齐,使其从 0 开始索引跳转表。接着回到汇编代码:

mov [ebp+var_D4], ecx:将减法操作后的结果(ecx 中的值)存储回局部变量 var_D4

cmp [ebp+var_D4], 0F4h:比较 var_D4 的新值和 0xF4(十进制 244)。

ja def_4119BBja 是无符号大于跳转指令(Jump if Above),即如果 var_D4 的值大于 0xF4,则跳转到 def_4119BB 处执行。(这个事实上就是默认分支比较简单有兴趣可以看一下,我们的重点在下面的跳转表)

cpp 复制代码
def_4119BB:                             ; CODE XREF: _main+88↑j
                                         ; _main+9B↑j ...
                 mov     esi, esp        ; jumptable 004119BB default case, cases 13-253
                                         ; jumptable 004119F1 default case, cases 257-453
                 push    offset Command  ; "pause"
                 call    ds:__imp__system
                 add     esp, 4
                 cmp     esi, esp
                 call    j___RTC_CheckEsp
                 xor     eax, eax
                 push    edx
                 mov     ecx, ebp
                 push    eax
                 lea     edx, dword_411AC8
                 call    j_@_RTC_CheckStackVars@8 ; _RTC_CheckStackVars(x,x)
                 pop     eax
                 pop     edx
                 pop     edi
                 pop     esi
                 pop     ebx
                 mov     ecx, [ebp+var_4]
                 xor     ecx, ebp
                 call    j_@__security_check_cookie@4 ; __security_check_cookie(x)
                 add     esp, 0D4h
                 cmp     ebp, esp
                 call    j___RTC_CheckEsp
                 mov     esp, ebp
                 pop     ebp
                 retn
 _main           endp

mov edx, [ebp+var_D4]:将局部变量 var_D4 的值加载到寄存器 edx 中。

!!movzx eax, ds:byte_411AF8[edx]:这条指令从地址 ds:byte_411AF8[edx] 处取出一个字节,并将其零扩展到 eax 寄存器中。

复制代码
①movzx 是 "移动并零扩展"(move with zero extension),它从内存中读取一个字节并将其扩展为 32 位存储在 eax 中,确保高位被清零。
②ds:byte_411AF8[edx] 表示从数据段 ds 的偏移地址 0x411AF8 开始,通过 edx 的值作为偏移量来访问一个字节数据。
ds:byte_411AF8[edx]解释:

这个部分指的是一个基于段寄存器和偏移量的内存访问。让我们逐步解析它:

ds:这是数据段寄存器(Data Segment)。

byte_411AF8:这是一个内存地址,表示一个位于 411AF8地址处的字节变量。此时411AF8地址存放的数据如下:

cpp 复制代码
byte_411AF8     db 0, 1, 2, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                                         ; DATA XREF: _main+94↑r
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4 ; indirect table for switch statement
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4
                 db 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3

这是一个查找表用于处理 switch-case 语句。表中大多数值是 4,这可能是查找表的默认值,用于表示跳转到 switch-case 的默认分支(default case)。特定位置上,例如第一个位置是 0,第二个位置是 1,第三个位置是 2,最后一个位置是 3,这些值对应于 switch-case 语句中的有效分支。

[edx]:表示用 edx 中的值作为索引来访问该数组中的具体字节。

到此我们就可以清楚的知道ds:byte_411AF8[edx]表达的含义:表示对 byte_411AF8 数组的访问,其中 edx 寄存器中的值用作数组的索引。在获取数组的对应索引后,执行下一条指令:

!!jmp ds:jpt_4119BB[eax*4]

这条指令是一个基于跳转表的间接跳转,结合跳转表的内容来看,它是通过 eax 中的值从跳转表中选择一个跳转目标,然后执行相应的代码分支(case)。

以下为跳转表jpt_4119BB的内容,在跳转表中存储了各个 switch-case 语句的跳转地址。每一行表示一个 dd 数据(dd 表示定义双字节,4 个字节),即一个内存地址。正因为跳转表中存储的case语句的跳转地址为dd类型,所以在索引需要乘以 4 来计算跳转表的偏移量。

cpp 复制代码
jpt_4119BB     dd offset loc_4119F8    ; DATA XREF: _main+9B↑r
               dd offset loc_411A07    ; jump table for switch statement
               dd offset loc_411A16
               dd offset loc_411A25
               dd offset def_4119BB

jmp ds:jpt_4119BB[eax*4]指令通过 eax 的值(在上条指令的查询表中获取)计算出的偏移量,跳转到跳转表中指定的地址。

例如,此时程序接收到的用户输入为254,此时我们根据代码做一个解析(解析放在注释~~~部分):

cpp 复制代码
mov     eax, [ebp+var_C]                          ~~~程序接收到254~~~
mov     [ebp+var_D4], eax
cmp     [ebp+var_D4], 0FFh
jg      short loc_4119C2
cmp     [ebp+var_D4], 0FFh
jz      loc_411A34
mov     ecx, [ebp+var_D4]
sub     ecx, 0Ah        ; switch 245 cases        ~~~254-10 = 244~~~
mov     [ebp+var_D4], ecx
cmp     [ebp+var_D4], 0F4h
ja      def_4119BB      ; jumptable 004119BB default case, cases 13-253
                        ; jumptable 004119F1 default case, cases 257-453
mov     edx, [ebp+var_D4]               ~~~244 => edx~~~~
movzx   eax, ds:byte_411AF8[edx]        ~~~以edx(244)为索引在查询表byte_411AF8中查询~~~
jmp     ds:jpt_4119BB[eax*4] ; switch jump          ~~~根据查询到的值在跳转表中做跳转~~~

这个时候我们来说以下程序以edx(244)为索引在查询表byte_411AF8中查询的结果:

查询到的结果为3;接着执行jmp ds:jpt_4119BB[eax*4]进行跳转(间接跳转),此时的跳转指令为jmp ds:jpt_4119BB[12],对照跳转表:

我们最后会跳转进入loc_411A25分支,此时该分支代码如下:

cpp 复制代码
loc_411A25:                             ; CODE XREF: _main+9B↑j
                                        ; DATA XREF: .text:jpt_4119BB↓o
                push    offset aNflag254 ; jumptable 004119BB case 254
                call    j__printf
                add     esp, 4
                jmp     short def_4119BB ; jumptable 004119BB default case, cases 13-253
                                        ; jumptable 004119F1 default case, cases 257-453

成功定位跳转至case 254分支并执行代码;其他根据查询表、跳转表进行跳转的分支跳转也是按照这个流程,这边就不再做其他赘述了。

动态分析

x86-Debug程序载入x86dbg中,进行动态分析:

右击汇编代码窗口,选择搜索->所有模块->字符串输入特征字符串(nFlag)定位到main函数,并开始分析:

此时持续按F8进行步过运行程序,直到运行到如下函数后就无法继续运行:

此时打开程序窗口可以看到此时程序正在等待用户输入,这个时候基本上可以断定switchre_x86-debug.4D10A0函数为scanf函数;存放用户输入的变量地址存放在eax寄存器中,此时输入254为例子查看程序的具体执行。

回车后程序继续执行;在接收到用户输入的数据后先与0xFF(255)进行比较:若大于255则跳转至switchre_x86-debug.4D19C2位置,若等于0xFF则跳转至switchre_x86-debug.4D1A34位置。(这些都比较简单,如果感觉看着吃力的话可以再去看看笔者之前的文章这边我们的重点还是放在Switch分支结构的跳转表表现形式)

因为此时我们输入的值为254所以既不大于也不等于0xFF(x64dbg可以在程序运行过程中修改内存中变量的值),接着运行以下指令。

将用输入的值传入ecx寄存器,接着减去A(10);接着与0xF4(244)进行比较,大于244则转入switchre_x86-debug.4D1A7D(默认分支),若不大于则将被减去10的用户输入数据转入edx;此时我们可以在寄存器窗口查看当前寄存器中的值。

接着进行地址索引(间接跳转),索引得到的值:0xF4+4D1AF8 = 4D1BEC此时我们地址索引的代码如下:

这个时候我们可以直接在内存窗口,输入ctrl + G进行值的查看;

可以看到此时内存中的值为03,其实从代码中我们也可以发现,程序会将地址索引到的值放入eax寄存器中:此时也可以查看寄存器窗口eax寄存器中的值为3:

接着查看下面的代码:最后执行jmp跳转指令,此时会进行地址索引(其实就是间接跳转),获取最后case的执行地址。

这个时候我们接着计算地址:(eax)3*4 + 4D1AE4 = 4D 1AF0(该地址是存储case代码地址的内存地址),我们接着在内存窗口中查看地址为4D 1AF0的值,因为是小端序存储,所以得到的值应该是:004D1A25。

这个地址就是case对应的地址位置,我们在汇编窗口进行定位,可以看到此时已经跳转至case 254分支。

其他的分支也是如此,此处就不做过多说明。

在本篇文章中,我们深入探讨了 switch-case 语句在汇编代码中的表现形式,特别是如何通过跳转表来优化分支跳转逻辑。通过分析具体的反汇编代码,我们能够清晰地理解编译器如何通过查找表和间接跳转提高执行效率。

复制代码
间接跳转是一种程序控制流机制,它通过一个变量(通常是寄存器或内存地址)中的值来决定程序的跳转目标,而不是直接跳转到一个明确指定的地址。与直接跳转不同,间接跳转的目标地址是在程序运行时动态确定的。

这不仅帮助我们更好地理解反汇编代码中的结构,也为我们逆向分析复杂程序提供了重要的工具和思路。希望通过这些案例,你能够对 switch-case 语句的反汇编分析有更加深入的理解。此外x64-release程序的逆向过程也与x86-debug相似,只不过release代码中有对程序进行优化,有兴趣也可以动手做做。

另外,请多多关注笔者的VX-公ZH-【风铃Sec】,你们的支持就是我更新的动力!

相关推荐
机器视觉知识推荐、就业指导8 分钟前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
宅小海17 分钟前
scala String
大数据·开发语言·scala
qq_3273427320 分钟前
Java实现离线身份证号码OCR识别
java·开发语言
锅包肉的九珍21 分钟前
Scala的Array数组
开发语言·后端·scala
心仪悦悦24 分钟前
Scala的Array(2)
开发语言·后端·scala
yqcoder1 小时前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
baivfhpwxf20231 小时前
C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)
开发语言·c#
许嵩661 小时前
IC脚本之perl
开发语言·perl
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
直裾1 小时前
Scala全文单词统计
开发语言·c#·scala