在逆向工程中,分析汇编代码中的 if
语句是一项常见任务,因为条件分支是程序控制流的重要组成部分。在高级语言(如 C/C++、Java)中,if
语句用于控制程序逻辑的分支。在汇编层面,if
语句的逻辑通常会被编译器转换为条件跳转指令,如 JE
(Jump if Equal)、JNE
(Jump if Not Equal)、JG
(Jump if Greater)、JL
(Jump if Less)等。
if
语句在汇编中的表示
if (a == b) {
// Code block A
} else {
// Code block B
}
在汇编语言中,它可能被编译为以下形式:
cmp eax, ebx ; 比较 a 和 b(假设它们分别存储在 eax 和 ebx 寄存器中)
je label_true ; 如果相等,则跳转到 label_true(执行代码块 A)
; 代码块 B 的指令
jmp label_end ; 如果不相等,则跳转到代码块的末尾
label_true:
; 代码块 A 的指令
label_end:
; 继续执行后面的指令
分析 if 语句的步骤
①查找比较指令(如 CMP
、TEST
等):
条件语句通常涉及到一个比较操作,例如 CMP
(比较)或 TEST
(测试)指令。它们会设置处理器的标志寄存器(flags),为接下来的条件跳转指令做准备。
②识别条件跳转指令:
比较指令之后,通常会有一条条件跳转指令(如 JE
、JNE
、JG
、JL
等)。这些指令的操作取决于之前比较指令的结果。
例如:
JE(Jump if Equal)——如果两个值相等,则跳转。
JNE(Jump if Not Equal)——如果两个值不相等,则跳转。
③确定条件跳转的目标地址(跳转的代码块):
跳转指令的目标地址通常是代码块 A 的开始地址。如果条件为真(例如 a == b
),程序执行跳转到代码块 A。否则,程序继续执行代码块 B。
④确定 if
语句的结构:
跳转指令将引导你到代码块 A 或 B,继续分析这些代码块,以理解 if
语句的逻辑。
⑤分析无条件跳转指令(如 JMP
):
如果有 else
语句(即代码块 B),则代码块 A 通常会在末尾包含一个无条件跳转指令(如 JMP
),跳到 if
语句的结束部分。
逆向分析实例
假设有一个简单的 if
语句如下:
#include <stdio.h>
#include <stdlib.h>
int main() {
int nFlagA = 0;
int nFlagB = 0;
scanf("%d", &nFlagA);
scanf("%d", &nFlagB);
if (nFlagA == 10 && nFlagB == 20) {
printf("Flag = %d", nFlagB);
}
else if (nFlagA == 10 || nFlagB == 20) {
printf("FlagA ==10 || nFlagB == 20 (nFlagA=%d,nFlagB=%d)", nFlagA, nFlagB);
}
else
{
printf("Flag!=10 && FlagB !=20");
}
system("pause");
return 0;
}
此时使用Visual Studio
对该代码进行处理,并生成exe
文件,对应的编译配置为Debug-x86
;
编译完成后,将exe
文件拖入IDA(32-bit)
中进行分析:
此时我们尝试再函数列表中搜索main
函数,查看IDA
是否帮咱们分析出该程序的main
函数:
这个时候可以发现main
函数被成功分析出来,那么这个时候就无需再使用特征去动态调试定位main函数,可以直接对该程序的反汇编代码进行分析了;从反汇编代码中可以看红框中的代码是一个典型的编译器生成的程序入口函数样板代码,涉及到一些栈帧设置、安全检查和初始化的过程(这部分内容不是本篇文章的重点)。所以可以直接跳过(这边截取出来,有兴趣的兄弟可以看看是个什么事):
对于本文章的内容重点则是在红线以下的这部分内容:
Part.1
以上是IDA分析出来的第一部分反汇编代码;我们这个时候来做一个解析:
mov [ebp+var_C], 0
mov [ebp+var_18], 0
push offset aFlaga ; "FlagA = "
call sub_41104B
add esp, 4
lea eax, [ebp+var_C]
push eax
push offset aD ; "%d"
call sub_4110A0
add esp, 8
mov [ebp+var_C], 0
:将栈上偏移量 var_C
(局部变量)的值设置为 0
。这可能是一个名为 FlagA
的变量。
mov [ebp+var_18], 0
:将栈上偏移量 var_18
(局部变量)的值设置为 0
。这可能是一个名为 FlagB
的变量。
push offset aFlaga
:将字符串 "FlagA = "
的地址压入栈中。call sub_41104B
:调用函数 sub_41104B
;这个函数可能是一个打印函数,用于输出栈上提供的字符串。现在要确定该函数是否为打印函数可以参考如下方法:
①确定这个函数()首先我们可以在IDA中双击该函数,查看该函数中的反汇编代码构成:
初次双击发现函数中进行进行了跳转,我们接着跟进,查看是否存在函数特征;
未发现明显特征,接着寻找call,继续跟进。但是这边要注意这种带有Check的函数就没有跟进的必要了,因为这些是函数在运行过程中需要做的一些检查,这个不是我们关注的重点;
这个时候我们锁定了sub_4111B8
函数,跟进查看,发现还是一个跳转;
继续跟进函数,可以发现一个非常明显的printf函数特征:
所以基本上可以断定该函数就是一个printf
函数;但是如果跟进了好几层都没有看到具体的函数特征建议还是换个方法吧。
②第二种方法就是此处可以根据程序的运行情况进行判断:
可以看到此处窗口显示打印了一行FlagA =
,所以基本可以判断sub_41104B
就是printf
函数。
③如果想要更加严谨一点,此处我们可以通过使用x86dbg
进行动态调试去确定该函数;
首先要针对该程序的main函数,这边可以根据特征定位main函数的方法进行定位(不会的可以看看笔者之前定位main函数相关文章):
根据特征定位函数位置:
mov [ebp+var_C], 0
mov [ebp+var_18], 0
push offset aFlaga ; "FlagA = "
call sub_41104B
此时我们将程序运行至call
的上条代码,可以看到此时窗口中并没有任何字样:
接着我们将代码运行至call后,此时可以看到窗口中出现FlagA=
字符串:
这三种方法读者在使用时可以根据当前分析的程序的特点自行进行选择,如果有更方便的方法希望师傅们在评论区不多多指教。
在确定经过这些判断后,我们就可以断定该函数是一个输出函数,此处我们就以printf()进行指代,并可以在IDA中对这个函数进行重命名:
右击函数名--> Rename --> 新函数名
这个时候IDA就会将该函数进行全局重命名。
add esp, 4
:函数调用后,清理栈上的参数,释放 4 字节的空间(即一个 push
的大小)。
lea eax, [ebp+var_C]
:将局部变量 var_C
(FlagA)的地址加载到 EAX
寄存器中。
push eax
:将 EAX
(即 FlagA
的地址)压入栈中。
push offset aD
:将格式字符串 "%d"
的地址压入栈中。
call sub_4110A0
:调用函数 sub_4110A0
。这个函数可能是一个格式化输出/输入函数;此处也可以根据程序的运行情况进行判断;
可以看到此时该函数正在等待用户输入,所以可以断定sub_4110A0
函数为scanf
函数;或者此处我们也可以通过动态调试去确定该函数是否为输入函数,这边我们根据特征定位函数:
lea eax, [ebp+var_C]
push eax
push offset aD ; "%d"
call sub_4110A0
在这边我们也是将程序运行至call后面,但是在程序运行至call处,发现怎么按F8都无法往下执行;这个时候我们基本可以确定这个是一个输入函数,程序无法往下运行是因为窗口在等我们进行输入,如果这个时候我们输入数值,就会发现程序能够继续运行:
至此我们可以确定此处call调用的函数为输入函数,此处我们以scanf
指代。这边插播一个小插曲,在我们输入数值后如果想要修改变量中的值,则可以右击变量:
选择在内存中转到(F) --> 地址A:变量地址
这个时候内存窗口中就会为我们呈现变量在内存中的位置和值:此处为14h也就是我们刚才输入的20
双击这个值会跳出以下窗口:
在这个窗口中我们可以对变量的值进行修改。
OK,回到正题。在确定函数是scanf
后我们也是可以在IDA中对该函数进行重命名。并且在此处我们还可以确定一点:[ebp+var_C]
就是FlagA变量。最后add esp, 8
:平栈操作,在函数调用后清理栈上的两个参数(共 8 字节)。
实际上至此我们可以总结以下上半部分代码做了哪些事:
①定义了[ebp+var_C]和[ebp+var_18]两个变量,并将其初始值赋值为0;且由程序的输出中我们可以看到此时[ebp+var_C]变量名为FlagA。
②使用了printf函数打印了字符串"FlagA = ";并进行了平栈操作。
③将变量FlagA的地址以及格式化字符"%d"传入栈中,使用了scanf函数,接收用户输入的值,并将值复制给FlagA,进行了平栈操作。
这个时候再去分析下面的代码就清晰很多了:
其实上半部分代码的分析后,我们将两确定了scanf
和printf
两个函数后,基本上下半部分的代码就可以一眼看出来做了哪些事了。
push offset aFlagb ; "FlagB = "
call printf
add esp, 4
lea eax, [ebp+var_18]
push eax
push offset aD ; "%d"
call scanf
add esp, 8
我们也可以总结以下半部分代码做了哪些事:
①使用了printf函数打印了字符串"FlagB = ";并进行了平栈操作。
②将变量`[ebp+var_18]`的地址以及格式化字符`%d`传入栈中,调用scanf函数,接收用户输入的值,并将值复制给FlagB,进行了平栈操作。
在这边也是可以断定[ebp+var_18]
代表的就是变量FlagB.
Part.2:
我们接着来说以下part.2部分:
cmp [ebp+FlagA], 0Ah
jnz short loc_4119BB
cmp [ebp+var_C], 0Ah
:比较 FlagA
(var_C
)的值与 0Ah
(十进制的 10)。
jnz short loc_4119BB
:如果 FlagA
不等于 10
,则跳转到 loc_4119BB
。如果 FlagA
等于 10
,则继续顺序执行下面的代码。
①在这边我们就先来看一下 FlagA
不等于 10
时,程序会做什么事情:这个时候我们就双击loc_4119BB
调转到该分支:
cmp [ebp+FlagA], 0Ah
jz short loc_4119C7
在这里可以看到这个分支中做了一个比较迷惑的操作:又判断了一次FlagA
是否等于0Ah(10)
;此时的FlagA
已经绝对不可能等于10了,所以我们直接不进行跳转,接着往下执行,代码如下:
cmp [ebp+FlagB], 14h
jnz short loc_4119DE
此时代码会先判断FlagB
是否等于14h(20)
,若不等于20跳转至loc_4119DE
分支,若等于20则继续执行;这边的两种结果我们继续分析:
1>Flag A != 10 && FlagB != 20:
这中情况执行分支loc_4119DE
:
push offset aFlag10Flagb20 ; "Flag!=10 && FlagB !=20"
call printf
add esp, 4
此时程序打印字符串:"Flag!=10 && FlagB !=20"。
执行的语句:printf("Flag!=10 && FlagB !=20");
2>Flag A != 10 && FlagB = 20:
这种情况继续执行:
mov eax, [ebp+FlagB]
push eax
mov ecx, [ebp+FlagA]
push ecx
push offset aFlaga10Nflagb2 ; FlagA ==10 || nFlagB == 20 (nFlagA=%d,nFlagB=%d)'
call printf
add esp, 0Ch
jmp short loc_4119EB
此时程序将FlagB、FlagA和字符串FlagA ==10 || nFlagB == 20 (nFlagA=%d,nFlagB=%d)'
压入栈中(如果这边觉得FlagB和FlagA栈中的顺序有问题的读者,可以再去翻翻我汇编部分中的相关文章去进行查阅);接着调用printf
函数;
该条件执行的内容为:"printf(FlagA ==10 || nFlagB == 20 (nFlagA=%d,nFlagB=%d),FlagA,FlagB);"
其实到这边我们基本上可以断定这边是一个if选择语句结构了;Flag!=10的情况分析完毕后,我们接着来看一下Flag=10的情况。
②FlagA=10
时继续执行的代码为:
cmp [ebp+FlagB], 14h
jnz short loc_4119BB
在此处接着判断了FlagB
是否为20;
1>若不等于20则转到loc_4119BB
:
cmp [ebp+FlagA], 0Ah
jz short loc_4119C7
而这个分支我们在上面已经分析过一次了,它又做了一次判断,FlagA是否为10;此时FlagA已经确定为10了,所以直接跳转到loc_4119C7
分支;
这个分支上面已经分析过了,所以我们这边直接写结论:
FlagA=10 && FlagB!=20
该条件执行的内容为:"printf(FlagA ==10 || nFlagB == 20 (nFlagA=%d,nFlagB=%d),FlagA,FlagB);"
2>若FlagB
为20;则直接向下运行
mov eax, [ebp+FlagB]
push eax
push offset aFlagD ; "Flag = %d"
call printf
add esp, 8
jmp short loc_4119EB
此处会将FlagB
和字符串"Flag = %d"压入栈中,并调用printf函数进行打印;打印处FlagB的值,也就是20;所以最后的结果为
FlagA =10 && FlagB=20
执行的语句为:printf("Flag = %d",FlagB)
Part.3
最后这些分支都会无条件跳转至最后一部分:
在这里我们也需要画一个范围;红线以下的各种check
在这边我们并不用关注,我们只需要关注红线上的内容:
loc_4119EB:
mov esi, esp
push offset Command ; "pause"
call ds:system
add esp, 4
这部分其实就非常明显了,就是一个system("pause");
。
至此,程序的逆向分析结束,最后对分析的结果做一个总结:
1.程序定义了两个变量FlagA和FlagB
2.打印了字符串FlagA=
3.给FlagA赋值
4.打印了字符串FlagB=
5.给FlagB赋值
6.判断:FlagA=10 && FlagB =20 =>执行语句printf("Flag = %d",FlagB)
FlagA=10 || FlagB =20 =>执行语句printf(FlagA ==10 || nFlagB == 20 (nFlagA=%d,nFlagB=%d),FlagA,FlagB);
FlagA!=10 && FlagB!=20 =>执行语句printf("Flag!=10 && FlagB !=20");
7.system("pause");
在本篇文章中,我们深入探讨了C/C++中if
语句的逆向分析过程,从汇编代码的结构入手,通过具体的案例和调试,我们可以清晰地看到如何通过分析条件跳转、比较指令以及对应的逻辑分支来还原出源代码中的if
逻辑。掌握这些技巧,不仅可以帮助我们更好地理解程序的控制流程,也能为逆向工程实践中的静态分析和漏洞挖掘提供有力支持。希望这篇文章能帮助你在逆向工程的道路上更进一步。感谢你的阅读与关注,我们下篇文章再见!
原文始发于微信公众号(风铃Sec):C/C++逆向:if语句逆向分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论