在逆向分析中,循环语句通常会以特定的汇编模式或结构体现出来。常见的循环语句包括 for
循环、while
循环和 do-while
循环。由于不同的编译器会根据代码优化的级别生成不同的汇编代码,分析循环的模式也可能会有所不同。以下是三种常见循环语句的汇编分析要点:
1. for循环的逆向分析
典型的 C 代码:
for (int i = 0; i < n; i++) {// 循环体
}
汇编特征:
初始化:i = 0 通常表现为一个寄存器赋初始值。 条件检查:比较寄存器与终止条件(如 n),可能使用跳转指令如 JLE、JGE。 循环体:位于条件检查后的指令块。 递增:通过 ADD 或 INC 操作来递增寄存器的值。
汇编示例:
mov eax, 0 ; 初始化 i = 0
cmp eax, n ; 比较 i 和 n
jge end_loop ; 如果 i >= n,跳转到 end_loop
loop_body: ; 循环体开始
; (循环体指令)
add eax, 1 ; 递增 i
cmp eax, n ; 再次比较 i 和 n
jl loop_body ; 如果 i < n,跳回到 loop_body
end_loop: ; 结束
逆向分析技巧:
找到初始化的代码片段,如给寄存器赋值的操作;寻找条件比较部分,通常使用 CMP 或 TEST,接着跟踪跳转指令(如 JLE, JL),确定循环的边界。递增操作(ADD, INC)通常位于循环体的末尾或条件检查前。
2.while 循环的逆向分析
典型的 C 代码:
while (i < n) {// 循环体
}
汇编特征:
条件检查:在进入循环体之前,首先比较寄存器的值,若不满足条件则直接跳出循环。 循环体:条件检查通过后,执行循环体代码。 递增:递增通常在循环体内完成,然后再次检查条件。
汇编示例:
cmp eax, n ; 比较 i 和 n
jge end_while ; 如果 i >= n,跳转到 end_while
while_body: ; 循环体开始
; (循环体指令)
add eax, 1 ; 递增 i
cmp eax, n ; 比较 i 和 n
jl while_body ; 如果 i < n,跳回到 while_body
end_while: ; 结束
当条件和索引的初始值都确定的情况下,编译器可以判断第一次会不会执行
逆向分析技巧:
条件检查通常位于循环体之前,通过 CMP
和跳转指令控制,循环体代码位于条件检查之后,跟踪跳转位置可以定位循环体的结束。确定计数器或条件变量的更新位置,并检查跳转逻辑。
3.do-while 循环的逆向分析
典型的 C 代码:
do {// 循环体
} while (i < n);
汇编特征:
循环体:无条件执行一次循环体。 条件检查:循环体执行后检查条件,决定是否跳回循环。 跳转:条件满足时跳回到循环体的起始处。
汇编示例:
do_while_body: ; 循环体开始
; (循环体指令)
add eax, 1 ; 递增 i
cmp eax, n ; 比较 i 和 n
jl do_while_body ; 如果 i < n,跳回到 do_while_body
逆向分析技巧:
do-while
循环的特点是循环体在条件检查之前执行,因此逆向时首先识别无条件执行的代码块,条件检查位于循环体之后,查看 CMP
或 TEST
等操作判断是否跳回;循环体通常使用跳转指令(如 JL
, JG
)回到循环体开头。
逆向分析示例
下面是一个包含 for
循环、while
循环 和 do-while
循环的 C 代码示例,我们可以将它编译成可执行文件,使用IDA
或 x64dbg
进行逆向分析,观察它们对应的汇编代码差异。
#include <stdio.h>
int main() {int sum_for = 0;int sum_while = 0;int sum_do_while = 0;
// for 循环for (int i = 0; i < 5; i++) {sum_for += i;}
// while 循环int j = 0;while (j < 5) {sum_while += j;j++;}
// do-while 循环int k = 0;do {sum_do_while += k;k++;} while (k < 5);
printf("sum_for: %d\n", sum_for);printf("sum_while: %d\n", sum_while);printf("sum_do_while: %d\n", sum_do_while);system("pause");return 0;
}
此时使用Visual Studio
对该代码进行编译,生成exe
文件,对应的编译配置为Debug-x86
;本文只针对Debug-x86
程序进行分析,其他编译配置分析方式也大同小异。
静态分析:
将生成的程序载入IDA中进行静态分析
在为了不模糊重点,我们直接在Functon Window
中定位main
函数(关于定位main函数的各种方法有兴趣请查看前面的文章)。
接下去开始逐步对进行代码分析,正文代码从后线以下开始:
首先代码先初始化了四个局部变量var_8
、var_14
、var_20
、var_2C
,并将其值全部设置为0。
mov [ebp+var_8], 0
mov [ebp+var_14], 0
mov [ebp+var_20], 0
mov [ebp+var_2C], 0
jmp short loc_41185F
在初始化变量后进行了跳转,跳转的目标地址为loc_41185F
。
①for循环
可以看到在跳转到loc_41185F
时,中间跳过了三条指令,这三条指令做的就是自增的操作,我们就可以通过这个特征就判断出这段代码就是for
循环结构(该特征是for
循环和while
循环最明显的区别),接着我们来解析以下这个代码:
loc_411856: ; CODE XREF: _main+5E↓jmov eax, [ebp+var_2C]add eax, 1mov [ebp+var_2C], eax
loc_41185F: ; CODE XREF: _main+44↑jcmp [ebp+var_2C], 5jge short loc_411870mov eax, [ebp+var_8]add eax, [ebp+var_2C]mov [ebp+var_8], eaxjmp short loc_411856
跳转至loc_41185F
后:cmp [ebp+var_2C], 5
局部变量var_2C
先于5进行比较(可以看出来5就是这个循环的边界值),jge short loc_411870
若局部变量var_2C
大于等于5则跳转到loc_411870
地址(也就是跳出循环),若不大于则继续往下执行。 mov eax, [ebp+var_8]
将var_8
的值存入寄存器eax
,add eax, [ebp+var_2C]
将局部变量的var_2C
的值于eax
中的值相加,mov [ebp+var_8], eax
再将eax
中存储的两数之和存储至var_8
中。jmp short loc_411856
最后跳转至loc_411856
地址处执行局部变量var_2C
自增代码。自增完成后再执行loc_41185F
处指令。
总结:
初始化:var_2C 初始化为 0。 条件检查:每次循环开始时,比较 var_2C 是否小于 5。 递增计数器:在每次循环结束时,var_2C 增加 1。 累加操作:每次循环中,将 var_2C 的值加到 var_8 中。 循环终止:当 var_2C >= 5 时,跳出循环。
伪代码如下:
for(int var_2C = 0;var_2C < 5;var_2C++);
{var_8 += var_2C;
}
②while循环
根据上述代码可知第一个循环结束后跳出循环,来到地址loc_411870
进行执行,代码如下:
loc_411870: ; CODE XREF: _main+53↑jmov [ebp+var_38], 0
loc_411877: ; CODE XREF: _main+7F↓jcmp [ebp+var_38], 5jge short loc_411891mov eax, [ebp+var_14]add eax, [ebp+var_38]mov [ebp+var_14], eaxmov eax, [ebp+var_38]add eax, 1mov [ebp+var_38], eaxjmp short loc_411877
mov [ebp+var_38], 0
将局部变量 var_38
初始化为 0,这个变量可能是用作循环计数器。
cmp [ebp+var_38], 5
比较 var_38
的值和 5,var_38
是循环计数器。
jge short loc_411891
如果 var_38
的值大于或等于 5,则跳转到 loc_411891
,这意味着循环结束(跳出循环)。
mov eax, [ebp+var_14]
将局部变量 var_14
的值加载到 eax
寄存器中。var_14
可能是一个用于累加的变量。
add eax, [ebp+var_38]
将 var_38
(循环计数器)的值加到 eax
中。
mov [ebp+var_14], eax
将累加后的结果存回 var_14
,即更新了累加器。
mov eax, [ebp+var_38]
add eax, 1
mov [ebp+var_38], eax
后面三条指令就是对var_38
局部变量进行自增的操作。
最后jmp short loc_411877
无条件跳转到 loc_411877
,重新执行循环体,继续下一次迭代。
这段代码是一个典型的 while
循环,其中 var_38
是一个循环计数器,从 0 开始,直到计数器达到 5 时结束循环。循环中,var_38
的值不断累加到 var_14
中。
伪代码表示如下:
int var_38 = 0; // 计数器初始化
while (var_38 < 5) {var_14 += var_38; // 累加操作var_38++; // 计数器递增
}
③do-while循环
第二个循环结束后,根据上述代码可知跳入第三个循环loc_411891
(红线以上部分):
loc_411891: ; CODE XREF: _main+6B↑jmov [ebp+var_44], 0
loc_411898: ; CODE XREF: _main+9E↓jmov eax, [ebp+var_20]add eax, [ebp+var_44]mov [ebp+var_20], eaxmov eax, [ebp+var_44]add eax, 1mov [ebp+var_44], eaxcmp [ebp+var_44], 5jl short loc_411898
mov [ebp+var_44], 0
始化局部变量 var_44
为 0。var_44
作为循环计数器,用来控制循环执行的次数。
mov eax, [ebp+var_20]
将局部变量 var_20
的值加载到 eax
寄存器中。var_20
可能是一个用于累加操作的变量。
add eax, [ebp+var_44]
将 var_44
(计数器)的值加到 eax
中,累加操作。
mov [ebp+var_20], eax
将累加后的结果存回 var_20
,更新累加器。
mov eax, [ebp+var_44]
将计数器 var_44
的值加载到 eax
寄存器中。
add eax, 1
mov [ebp+var_44], eax
cmp [ebp+var_44], 5
后面三条指令则是局部变量var_44
(循环计数器)自增。
cmp [ebp+var_44], 5
比较 var_44
和 5,判断计数器是否小于 5。
jl short loc_411898
如果 var_44
小于 5,则跳转回 loc_411898
,继续循环。这是一个 "jump if less" 指令,意味着只要 var_44
小于 5,循环继续。
总结
这段代码实现了一个 do-while
循环,其中 var_44
作为循环计数器,从 0 开始,每次循环中都会将计数器的值累加到 var_20
,直到计数器达到 5 后,循环结束。
伪代码表示如下:
int var_44 = 0;
do {var_20 += var_44; // 累加操作var_44++; // 计数器递增
} while (var_44 < 5);
最后一部分代码则是分别打印第一个循环到第三个循环获得的值,并执行system(pause)
代码。
分析起来比较简单且并不是本文重点,所以就不再赘述了。
动态分析
动态分析代码与静态分析基本一致,在这我们将特征代码进行标注:
①for循环
②while循环
③do-while循环
动态分析代码与静态分析基本一致,这边就不再过多赘述了。
在逆向分析循环语句的过程中,通过仔细观察循环的初始化、条件判断和循环体的逻辑,我们能够准确识别出不同类型的循环结构,如 for
、while
和 do-while
。这些循环的识别不仅帮助我们理解程序的控制流,还为进一步的分析和优化提供了线索。无论是通过静态分析还是动态调试,掌握循环的逆向分析方法将大大提高我们对程序行为的洞察力,并为复杂程序的深入解析奠定基础。