逆向工程初探之循环结构的判别
逆向了几个样本之后,现在对于循环结构的判别有所了解
循环结构分为三种
while 先比较判断,后执行循环体
do while 先执行循环体,后比较判断
for 先初始化,再比较判断,最后执行循环体。
While
while 在执行循环语句块之前,必须要进行条件判断,根据比较结果再选择是否执行循环语句块。
#include<iostream>
#include<windows.h>
#include<tchar.h>
using namespace std;
int main()
{
int nSum = 0;
int nIndex = 0;
int nCount = 10;
while (nIndex <= nCount)
{
nSum += nIndex;
nIndex++;
}
}
这里使用了vs2015进行反汇编 与正常的汇编语句可能有出入
while语句就是先用一个jg指令,如果不符合条件就向下跳转,越过循环,然后每执行一次循环语句块,jmp跳转至while开头,然后再进行比较
int main()
{
00BF16D0 55 push ebp
00BF16D1 8B EC mov ebp,esp
00BF16D3 81 EC E4 00 00 00 sub esp,0E4h
00BF16D9 53 push ebx
00BF16DA 56 push esi
00BF16DB 57 push edi
00BF16DC 8D BD 1C FF FF FF lea edi,[ebp-0E4h]
00BF16E2 B9 39 00 00 00 mov ecx,39h
00BF16E7 B8 CC CC CC CC mov eax,0CCCCCCCCh
00BF16EC F3 AB rep stos dword ptr es:[edi]
int nSum = 0;
00BF16EE C7 45 F8 00 00 00 00 mov dword ptr [nSum],0
int nIndex = 0;
00BF16F5 C7 45 EC 00 00 00 00 mov dword ptr [nIndex],0
int nCount = 10;
00BF16FC C7 45 E0 0A 00 00 00 mov dword ptr [nCount],0Ah
while (nIndex <= nCount)
00BF1703 8B 45 EC mov eax,dword ptr [nIndex]
00BF1706 3B 45 E0 cmp eax,dword ptr [nCount]
00BF1709 7F 14 jg main+4Fh (0BF171Fh)
{
nSum += nIndex;
00BF170B 8B 45 F8 mov eax,dword ptr [nSum]
00BF170E 03 45 EC add eax,dword ptr [nIndex]
00BF1711 89 45 F8 mov dword ptr [nSum],eax
nIndex++;
00BF1714 8B 45 EC mov eax,dword ptr [nIndex]
00BF1717 83 C0 01 add eax,1
00BF171A 89 45 EC mov dword ptr [nIndex],eax
}
00BF171D EB E4 jmp main+33h (0BF1703h)
}
00BF171F 33 C0 xor eax,eax
00BF1721 5F pop edi
00BF1722 5E pop esi
00BF1723 5B pop ebx
00BF1724 8B E5 mov esp,ebp
00BF1726 5D pop ebp
00BF1727 C3 ret
总结
WHILE_BEGIN:
jxx WHILE_END; 条件成立跳转到循环语句块结尾处
······ ;循环语句块
jmp WHILE_BEGIN;跳转到取出条件比较数据处
WHILE_END:
do while
do while 的工作流程清晰,识别起来相对简单。先执行语句块,再进行比较。当条件成立时,会继续执行语句块。
我们可以用goto语句来模拟do循环,这样会加深我们对与do while的理解
#include<iostream>
#include<windows.h>
#include<tchar.h>
using namespace std;
int main()
{
int nSum = 0;
int nIndex = 0;
int nCount = 10;
GOTO:
nSum += nIndex;
nIndex++;
if (nIndex <= nCount)
{
goto GOTO;
}
}
汇编
002116D0 push ebp
002116D1 mov ebp,esp
002116D3 sub esp,0E4h
002116D9 push ebx
002116DA push esi
002116DB push edi
002116DC lea edi,[ebp-0E4h]
002116E2 mov ecx,39h
002116E7 mov eax,0CCCCCCCCh
002116EC rep stos dword ptr es:[edi]
int nSum = 0;
002116EE mov dword ptr [nSum],0
int nIndex = 0;
002116F5 mov dword ptr [nIndex],0
int nCount = 10;
002116FC mov dword ptr [nCount],0Ah
GOTO:
nSum += nIndex;
00211703 mov eax,dword ptr [nSum]
00211706 add eax,dword ptr [nIndex]
00211709 mov dword ptr [nSum],eax
nIndex++;
0021170C mov eax,dword ptr [nIndex]
0021170F add eax,1
00211712 mov dword ptr [nIndex],eax
if (nIndex <= nCount)
00211715 mov eax,dword ptr [nIndex]
00211718 cmp eax,dword ptr [nCount]
0021171B jg GOTO+1Ch (021171Fh)
{
goto GOTO;
0021171D jmp GOTO (0211703h)
等效do while
#include<iostream>
#include<windows.h>
#include<tchar.h>
using namespace std;
int main()
{
int nSum = 0;
int nIndex = 0;
int nCount = 10;
do
{
nSum += nIndex;
nIndex++;
} while (nIndex <= nCount);
}
006D16D0 push ebp
006D16D1 mov ebp,esp
006D16D3 sub esp,0E4h
006D16D9 push ebx
006D16DA push esi
006D16DB push edi
006D16DC lea edi,[ebp-0E4h]
006D16E2 mov ecx,39h
006D16E7 mov eax,0CCCCCCCCh
006D16EC rep stos dword ptr es:[edi]
int nSum = 0;
006D16EE mov dword ptr [nSum],0
int nIndex = 0;
006D16F5 mov dword ptr [nIndex],0
int nCount = 10;
006D16FC mov dword ptr [nCount],0Ah
do
{
nSum += nIndex;
006D1703 mov eax,dword ptr [nSum]
006D1706 add eax,dword ptr [nIndex]
006D1709 mov dword ptr [nSum],eax
nIndex++;
006D170C mov eax,dword ptr [nIndex]
006D170F add eax,1
006D1712 mov dword ptr [nIndex],eax
} while (nIndex <= nCount);
006D1715 mov eax,dword ptr [nIndex]
006D1718 cmp eax,dword ptr [nCount]
006D171B jle main+33h (06D1703h)
在这两种等效的方法中,我们要区别的是
if语句和do while 都会有一个比较的过程,但是区别在于 if语句跳转条件是与判断语句相反的,而且是一个向下跳转的过程。
而do while 语句跳转语句与判断语句一致,是一个向上跳转的过程。
这样if与do while就很好区分了
总结
DO_BEGIN:
······ ;循环语句块
;影响标记位的指令
jxx DO_BEGIN ;向上跳转
如果遇到以上代码块,即可判断此循环为do while ,且do while 只需要一次比较语句。
for
for 循环是三种循环结构中最复杂的一种。for循环由赋初值、设置循环条件、设置循环步长这三条语句组成。而由于for循环更加符合人类的思维方式,在循环中使用频率最高,所以掌握for循环在逆向中比较重要。
#include<iostream>
#include<windows.h>
#include<tchar.h>
using namespace std;
int main()
{
int nSum = 0;
int nIndex = 0;
int nCount = 10;
for (nIndex=0;nIndex<=nCount;nIndex++)
{
nSum += nIndex;
}
}
004D16D0 push ebp
004D16D1 mov ebp,esp
004D16D3 sub esp,0E4h
004D16D9 push ebx
004D16DA push esi
004D16DB push edi
004D16DC lea edi,[ebp-0E4h]
004D16E2 mov ecx,39h
004D16E7 mov eax,0CCCCCCCCh
004D16EC rep stos dword ptr es:[edi]
int nSum = 0;
004D16EE mov dword ptr [nSum],0
int nIndex = 0;
004D16F5 mov dword ptr [nIndex],0
int nCount = 10;
004D16FC mov dword ptr [nCount],0Ah
for (nIndex=0;nIndex<=nCount;nIndex++)
004D1703 mov dword ptr [nIndex],0 //初始化计数器变量
004D170A jmp main+45h (04D1715h) //跳转至04D1715,跳过步长
004D170C mov eax,dword ptr [nIndex]
004D170F add eax,1 //步长加一
004D1712 mov dword ptr [nIndex],eax
004D1715 mov eax,dword ptr [nIndex]
004D1718 cmp eax,dword ptr [nCount] //条件比较
004D171B jg main+58h (04D1728h)
{
nSum += nIndex;
004D171D mov eax,dword ptr [nSum]
004D1720 add eax,dword ptr [nIndex]
004D1723 mov dword ptr [nSum],eax
}
004D1726 jmp main+3Ch (04D170Ch) //向上跳转
}
for循环在debug下的调试有三个跳转指令,为什么要设计这么复杂,因为要满足在调试时的单步调试,所以进行了一一对应。
总结:
mov i,xxx
jmp FOR_CMP ;跳到循环条件判定
FOR_STEP:
修改循环变量
MOV
ADD
MOV
FOR_CMP: ;条件判定
mov
cmp
jxx FOR_END:
JMP FOR_STEP ;向上跳转,修改流程回到步长计算部分
FOR_END;
遇到以上代码块,就可以判定它为一个for循环结构。这种结构被赋初值后,利用jmp跳过第一次步长计算。然后,可以通过是for循环独有的,在计数器变量被赋初值后,利用jmp跳过第一次步长计算。然后,可以通过三个跳转指令还原for循环的各个组成部分;第一个jmp指令之前的代码为初始化部分;从第一个jmp指令到循环条件比较处之间的代码为步长计算部分;在条件跳转指令jxx之后寻找一个jmp指令,这个jmp指令必须是向上跳转的,且其目标是到步长计算的位置,在jxx和这个jmp之间的代码为循环语句块。
参考书籍
《C反汇编与逆向分析技术揭秘》