C++逆向:循环结构的分析、识别与还原
编辑循环结构是程序设计的三大结构之一,搞清楚它在反汇编下的代码特征对于逆向工程来说是非常重要的。对于C/C++
来说,循环结构常见的有三种类型:for
,while
和do...while
。本文会分析这三种情况分别在VS2019
的MSVC
或VC6
的MSVC
采用速度最快的优化选项后的表现形式及其还原方法,当VC6
中的反汇编结果与VS2019
中的一致时,则不再赘述。
...
各种循环语句的分析
do...while
循环的分析
示例代码:
int main(int argc, char **argv)
{
int nSum = 0;
int i = 1;
do
{
nSum = nSum + i;
i++;
} while (i <= 100);
printf("%d\r\n", nSum);
return 0;
}
反汇编结果:
.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc = dword ptr 8
.text:00401040 argv = dword ptr 0Ch
.text:00401040
.text:00401040 xor ecx, ecx
.text:00401042 mov eax, 1
.text:00401047
.text:00401047 LOOP_BEGIN: ; CODE XREF: _main+D↓j
.text:00401047 add ecx, eax
.text:00401049 inc eax
.text:0040104A cmp eax, 64h ; 'd'
.text:0040104D jle short LOOP_BEGIN
.text:0040104F push ecx
.text:00401050 push offset _Format ; "%d\r\n"
.text:00401055 call _printf
.text:0040105A add esp, 8
.text:0040105D xor eax, eax
.text:0040105F retn
.text:0040105F _main endp
观察其循环部分的特征:
.text:00401047 LOOP_BEGIN: ; CODE XREF: _main+D↓j
.text:00401047 add ecx, eax
.text:00401049 inc eax
.text:0040104A cmp eax, 64h ; 'd'
.text:0040104D jle short LOOP_BEGIN
可以发现,其逻辑上是当eax <= 100
时就会继续执行循环体内部的代码,其逻辑与源代码的逻辑是一致的,无需对jcc
的条件进行取反。
对永真条件的测试代码:
int main(int argc, char **argv)
{
do
{
printf("always true\n");
} while (true);
return 0;
}
反汇编结果如下:
.text:00401060 loc_401060: ; CODE XREF: _main+2D↓j
.text:00401060 push offset aAlwaysTrue ; "always true\n"
.text:00401065 call _printf
.text:0040106A add esp, 4
.text:0040106D jmp short loc_401060
可以总结do...while
代码特征:只有一个跳转,且当比较后,条件跳转往上走的就是do...while
循环结构。如果不是条件跳转,而是无条件跳转jmp
的话,则表明是永真的循环。
还原时,只需要找到do...while
循环结构的边界:
jcc
跳转所在地址处是do...while
循环的下界,即:高级代码的右花括号}
的位置。jcc
跳转后面接的地址是do...while
循环的上界,即:高级代码的左花括号{
的位置
找到这两个位置后,将循环条件取正作为do...while
的循环条件;将标号和jcc
跳转所包裹的代码作为do...while
循环体内部的代码进行还原即可。
while
循环的分析
对于while
的分析,需要注意,此时需要临时关闭优化选项,否则编译器会将下述示例代码优化为同do...while
相同的结果。而对于优化为和do...while
相同结果的,则不再赘述。
while
的代码实例:
int main(int argc, char **argv)
{
int nSum = 0;
int i = 1;
while (i <= 100)
{
nSum = nSum + i;
i++;
};
printf("%d\r\n", nSum);
return 0;
}
反汇编结果:
.text:00401080 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401080 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401080
.text:00401080 nSum = dword ptr -8
.text:00401080 i = dword ptr -4
.text:00401080 argc = dword ptr 8
.text:00401080 argv = dword ptr 0Ch
.text:00401080 envp = dword ptr 10h
.text:00401080
.text:00401080 push ebp
.text:00401081 mov ebp, esp
.text:00401083 sub esp, 8
.text:00401086 mov [ebp+nSum], 0
.text:0040108D mov [ebp+i], 1
.text:00401094
.text:00401094 LOOP_BEGIN: ; CODE XREF: _main+2C↓j
.text:00401094 cmp [ebp+i], 64h ; 'd'
.text:00401098 jg short LOOP_END
.text:0040109A mov eax, [ebp+nSum]
.text:0040109D add eax, [ebp+i]
.text:004010A0 mov [ebp+nSum], eax
.text:004010A3 mov ecx, [ebp+i]
.text:004010A6 add ecx, 1
.text:004010A9 mov [ebp+i], ecx
.text:004010AC jmp short LOOP_BEGIN
.text:004010AE ; ---------------------------------------------------------------------------
.text:004010AE
.text:004010AE LOOP_END: ; CODE XREF: _main+18↑j
.text:004010AE mov edx, [ebp+nSum]
.text:004010B1 push edx
.text:004010B2 push offset _Format ; "%d\r\n"
.text:004010B7 call _printf
.text:004010BC add esp, 8
.text:004010BF xor eax, eax
.text:004010C1 mov esp, ebp
.text:004010C3 pop ebp
.text:004010C4 retn
.text:004010C4 _main endp
可以总结while
循环结构的代码特征:
- 有两处跳转,一处为条件跳转,另一处为绝对跳转。
- 条件跳转为下跳,绝对跳转为上跳
- 条件跳转处的条件为源代码中的循环条件的相反条件
上述特征符合高级语法while
的逻辑和汇编代码自身的逻辑。而在还原时,只需要找到其边界:
jcc
条件跳转为下跳,它是代码的上界,即:高级代码的左花括号{
的位置jmp
无条件跳转为上跳,它是代码的下界,即:高级代码的右花括号}
的位置
还原时,只需要找到这两个边界后,将jcc
条件取反后作为循环条件,两个边界中间的汇编代码还原为相对应的高级代码即可。
for
循环的分析
在对for
循环的分析时,也需要暂时关闭优化选项,理由同while
的分析。
示例代码:
int main(int argc, char **argv)
{
int nSum = 0;
int i = 1;
for (i = 1; i <= 100; ++i)
{
nSum = nSum + i;
}
printf("%d\r\n", nSum);
return 0;
}
反汇编结果:
.text:00401080 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401080 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401080
.text:00401080 nSum = dword ptr -8
.text:00401080 i = dword ptr -4
.text:00401080 argc = dword ptr 8
.text:00401080 argv = dword ptr 0Ch
.text:00401080 envp = dword ptr 10h
.text:00401080
.text:00401080 push ebp
.text:00401081 mov ebp, esp
.text:00401083 sub esp, 8
.text:00401086 mov [ebp+nSum], 0
.text:0040108D
.text:0040108D LOOP_BEGIN:
.text:0040108D mov [ebp+i], 1
.text:00401094 jmp short LOOP_CMP
.text:00401096 ; ---------------------------------------------------------------------------
.text:00401096
.text:00401096 LOOP_STEP: ; CODE XREF: _main+2E↓j
.text:00401096 mov eax, [ebp+i]
.text:00401099 add eax, 1
.text:0040109C mov [ebp+i], eax
.text:0040109F
.text:0040109F LOOP_CMP: ; CODE XREF: _main+14↑j
.text:0040109F cmp [ebp+i], 64h ; 'd'
.text:004010A3 jg short LOOP_END
.text:004010A5 mov ecx, [ebp+nSum]
.text:004010A8 add ecx, [ebp+i]
.text:004010AB mov [ebp+nSum], ecx
.text:004010AE jmp short LOOP_STEP
.text:004010B0 ; ---------------------------------------------------------------------------
.text:004010B0
.text:004010B0 LOOP_END: ; CODE XREF: _main+23↑j
.text:004010B0 mov edx, [ebp+nSum]
.text:004010B3 push edx
.text:004010B4 push offset _Format ; "%d\r\n"
.text:004010B9 call _printf
.text:004010BE add esp, 8
.text:004010C1 xor eax, eax
.text:004010C3 mov esp, ebp
.text:004010C5 pop ebp
.text:004010C6 retn
.text:004010C6 _main endp
可以观察到其有三个跳转,因此它的速度是会比while
和do...while
循环要慢的,因为它打断了流水线优化。还可以观察到,其明显的具有三个部分:
LOOP_INIT
部分LOOP_CMP
部分LOOP_STEP
部分
由于要符合高级语言for
的逻辑要求:
- 初始化部分只执行一次;
- 在执行循环体之前需要先判断一下循环条件;
- 在执行完循环体后再执行步长部分。
故可以总结代码特征为:
- 有三处跳转,一处条件跳转,两处绝对跳转。
- 条件跳转为下跳,跳转到循环体外部
- 第一处绝对跳转为下跳,跳转到
for
循环的比较部分 - 第二处绝对跳转为上跳,跳转到
for
循环的步长部分。
掌握了for
循环的代码特征后,还原也就简单了,只需要分别找到对应的几个部分进行还原即可。
关键字break
和continue
的分析
对于这两个部分的分析,我是打开O2
了的优化。
循环体内包含break
关键字
示例代码:
int main(int argc, char** argv)
{
for (int i = 1; i <= 100; ++i)
{
if (i == argc)
{
break;
}
printf("%d\r\n", i);
}
return 0;
}
反汇编结果:
.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc = dword ptr 8
.text:00401040 argv = dword ptr 0Ch
.text:00401040 envp = dword ptr 10h
.text:00401040
.text:00401040 push ebp
.text:00401041 mov ebp, esp
.text:00401043 push esi
.text:00401044 push edi
.text:00401045 mov edi, [ebp+argc]
.text:00401048 mov esi, 1
.text:0040104D nop dword ptr [eax]
.text:00401050
.text:00401050 LOOP_BEGIN: ; CODE XREF: _main+26↓j
.text:00401050 cmp esi, edi
.text:00401052 jz short LOOP_END
.text:00401054 push esi
.text:00401055 push offset _Format ; "%d\r\n"
.text:0040105A call _printf
.text:0040105F inc esi
.text:00401060 add esp, 8
.text:00401063 cmp esi, 64h ; 'd'
.text:00401066 jle short LOOP_BEGIN
.text:00401068
.text:00401068 LOOP_END: ; CODE XREF: _main+12↑j
.text:00401068 pop edi
.text:00401069 xor eax, eax
.text:0040106B pop esi
.text:0040106C pop ebp
.text:0040106D retn
.text:0040106D _main endp
可以发现,其已经将for
循环,优化成了do...while
的循环了。而在这个循环体内部,还有一个条件跳转:
.text:00401050 cmp esi, edi
.text:00401052 jz short LOOP_END
经过分析后,可以得出,其是跳转到循环体外部的,也就是符合了break
关键字的逻辑要求:结束循环,转到执行循环体外部的代码。
在还原时,可以根据出现在循环体内部的情况,来具体根据关键字的语义来还原。
循环体内包含continue
关键字
示例代码:
int main(int argc, char** argv)
{
for (int i = 1; i <= 100; ++i)
{
if (i == argc)
{
continue;
}
printf("%d\r\n", i) :
}
return 0;
}
反汇编的结果:
.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc = dword ptr 8
.text:00401040 argv = dword ptr 0Ch
.text:00401040 envp = dword ptr 10h
.text:00401040
.text:00401040 push ebp
.text:00401041 mov ebp, esp
.text:00401043 push esi
.text:00401044 push edi
.text:00401045 mov edi, [ebp+argc]
.text:00401048 mov esi, 1
.text:0040104D nop dword ptr [eax]
.text:00401050
.text:00401050 LOOP_BEGIN: ; CODE XREF: _main+26↓j
.text:00401050 cmp esi, edi
.text:00401052 jz short LOOP_STEP
.text:00401054 push esi
.text:00401055 push offset _Format ; "%d\r\n"
.text:0040105A call _printf
.text:0040105F add esp, 8
.text:00401062
.text:00401062 LOOP_STEP: ; CODE XREF: _main+12↑j
.text:00401062 inc esi
.text:00401063
.text:00401063 LOOP_CMP:
.text:00401063 cmp esi, 64h ; 'd'
.text:00401066 jle short LOOP_BEGIN
.text:00401068
.text:00401068 LOOP_END:
.text:00401068 pop edi
.text:00401069 xor eax, eax
.text:0040106B pop esi
.text:0040106C pop ebp
.text:0040106D retn
.text:0040106D _main endp
可以发现,其也是将其优化成了do...while
了。再进一步分析,可以得出一个类似于满足for
循环的语义的几个部分。在还原时可以按照自己的喜好和代码可读性,还原为任意一种循环即可。
在分析上述反汇编时,可以看到一处条件跳转:
.text:00401050 LOOP_BEGIN: ; CODE XREF: _main+26↓j
.text:00401050 cmp esi, edi
.text:00401052 jz short LOOP_STEP
其就是关键字continue
的语义:提前结束本次循环,进入下一次循环。
循环体内包含break
关键字造成的特殊优化现象
示例代码:
int main(int argc, char** argv)
{
for (int i = 1; i <= 100; ++i)
{
printf("%d", i);
break;
}
return 0;
}
反汇编结果:
.text:00401040 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401040 _main proc near ; CODE XREF: __scrt_common_main_seh+F5↓p
.text:00401040
.text:00401040 argc = dword ptr 8
.text:00401040 argv = dword ptr 0Ch
.text:00401040
.text:00401040 push 1
.text:00401042 push offset _Format ; "%d"
.text:00401047 call _printf
.text:0040104C add esp, 8
.text:0040104F xor eax, eax
.text:00401051 retn
.text:00401051 _main endp
可以发现,编译器并没有生成循环代码。因为编译器在编译阶段判断出了这个循环只需要执行一次,故可以将循环体内部的代码外提,然后省略掉不必要的循环。
关键字continue
和break
的还原方法总结
根据上述分析,如果一个条件跳转跳转到了循环体的外部,则它就是break
;如果一个条件跳转跳转到了循环体的循环起始位置,则它就是continue
。
总的优化原则
对于循环结构来说,最好是能尽量不打断流水线优化。所以常见的对于循环优化的方法有:
-
强度削弱,用指令周期少指令代替指令周期多的指令
-
减少分支,能优化成
do...while
就优化成do...while
的形式 -
代码外提
对于强度削弱不需要解释,那么为什么要能优化成do...while
的循环就优化成do...while
的形式呢?由于do...while
的特征是先执行一次循环体内的代码后,再进行判断,它与汇编上条件跳转成立的条件逻辑上是一致的,且根据上面分析,它只有一处向上的跳转,能够减少分支,这样就可以尽量少的影响到流水线优化,提高程序的执行效率。
而对于代码外提其实是有条件的,对于循环条件的检查部分,不能是循环体内被修改的值,只有满足这个条件的时候才会被外提。即:当比较的是固定值时,编译器就会将其自动代码外提;但是当它是一个黑盒的时候,就不能做到自动的代码外提了。
- 0
-
分享