逆向-循环结构

Posted 嘻嘻兮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了逆向-循环结构相关的知识,希望对你有一定的参考价值。

对于循环结构而言,主要就三种,do-while循环,while循环,for循环,这三种循环,在debug下其特点还是比较明显的,在release下的话可以说基本上都被优化为do-while循环(效率高),所以说release下的循环,根据其汇编代码我们只能做等价的还原。

下面先来看do-while循环

int main(int argc, char* argv[])

    int i = 0;
    int sum = 0;
    do 
    
        sum += i;
        ++i;
     while (i < argc);
    printf("%d\\r\\n",sum);
    return 0;

对应的汇编代码

9:        int i = 0;
0040D718 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0
10:       int sum = 0;
0040D71F C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
11:       do
12:       
13:           sum += i;
0040D726 8B 45 F8             mov         eax,dword ptr [ebp-8]
0040D729 03 45 FC             add         eax,dword ptr [ebp-4]
0040D72C 89 45 F8             mov         dword ptr [ebp-8],eax
14:           ++i;
0040D72F 8B 4D FC             mov         ecx,dword ptr [ebp-4]
0040D732 83 C1 01             add         ecx,1
0040D735 89 4D FC             mov         dword ptr [ebp-4],ecx
15:        while (i < argc);
0040D738 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D73B 3B 55 08             cmp         edx,dword ptr [ebp+8]
0040D73E 7C E6                jl          main+26h (0040d726)  小于则跳转

可以发现,do-while循环的汇编代码和高级代码的逻辑真的是一模一样,对于其跳转的逻辑也是一样的(if相反),因为对于循环而言,条件满足则进行循环,这里和汇编的逻辑也是一样的,条件满足进行跳转。

对于Do-While循环的大体架构如下:

DO_BEGIN:
	//.... 中间循环体
jxx DO_BEGIN  //这里是一个减量地址
DO_END:

这里do-while循环的release版本与debug结构是类似的,所以这里就不分析了。

下面再来看while循环

int main(int argc, char* argv[])

	int i = 0;
    int sum = 0;
    while (i < argc)
    
        sum += i;
        ++i;
     
    printf("%d\\r\\n",sum);
    return 0;

对应的反汇编代码

9:        int i = 0;
0040D718 C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0
10:       int sum = 0;
0040D71F C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
11:       while (i < argc)
0040D726 8B 45 FC             mov         eax,dword ptr [ebp-4]
0040D729 3B 45 08             cmp         eax,dword ptr [ebp+8]
0040D72C 7D 14                jge         main+42h (0040d742)  //先进行比较,反条件
12:       
13:           sum += i;
0040D72E 8B 4D F8             mov         ecx,dword ptr [ebp-8] //循环体部分
0040D731 03 4D FC             add         ecx,dword ptr [ebp-4]
0040D734 89 4D F8             mov         dword ptr [ebp-8],ecx
14:           ++i;
0040D737 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D73A 83 C2 01             add         edx,1
0040D73D 89 55 FC             mov         dword ptr [ebp-4],edx
15:       
0040D740 EB E4                jmp         main+26h (0040d726) //往上跳到比较部分

这里的话主要就是while部分的比较是和逻辑是相反的,为什么只要想想if语句即可,和其同理。

对于while循环的主体架构

WHILE_BEGIN:
    jxx WHILE_END
    //...循环体
    jmp WHILE_BEGIN  //减量跳是while,增量跳说明是if else
WHILE_END:

下面看一下while循环的release版本

.text:00401008                 test    edx, edx
.text:0040100A                 jle     short loc_401013 //先进行if比较
.text:0040100C//下面就是do-while循环结构
.text:0040100C loc_40100C:                             ; CODE XREF: _main+11↓j
.text:0040100C                 add     ecx, eax
.text:0040100E                 inc     eax
.text:0040100F                 cmp     eax, edx
.text:00401011                 jl      short loc_40100C
.text:00401013
.text:00401013 loc_401013:                             ; CODE XREF: _main+A↑j

可以发现,对于while循环,其release先使用了if语句判断,然后里面套了一个do-while循环,高级对应代码如下

    if(argc > 0) //i已经初始化为0,只要argc大于0,第一次肯定就能满足i < argc
    
        do 
        
            sum += i;
            ++i;
         while (i < argc);
    

 

最后再来看一下for循环

int main(int argc, char* argv[])

    int sum = 0;
    for(int i=0;i < argc;++i)
        sum += i;
    printf("%d\\r\\n",sum);
    return 0;

对应的汇编代码

10:       for(int i=0;i < argc;++i)
0040D71F C7 45 F8 00 00 00 00 mov         dword ptr [ebp-8],0
0040D726 EB 09                jmp         main+31h (0040d731) //初始化后先跳转到比较部分
//步长部分,也就是 i++
0040D728 8B 45 F8             mov         eax,dword ptr [ebp-8]
0040D72B 83 C0 01             add         eax,1
0040D72E 89 45 F8             mov         dword ptr [ebp-8],eax
//这里是比较部分
0040D731 8B 4D F8             mov         ecx,dword ptr [ebp-8]
0040D734 3B 4D 08             cmp         ecx,dword ptr [ebp+8]
0040D737 7D 0B                jge         main+44h (0040d744)
11:           sum += i;
//循环的主体
0040D739 8B 55 FC             mov         edx,dword ptr [ebp-4]
0040D73C 03 55 F8             add         edx,dword ptr [ebp-8]
0040D73F 89 55 FC             mov         dword ptr [ebp-4],edx
0040D742 EB E4                jmp         main+28h (0040d728)  //往上跳转到步长部分
12:       printf("%d\\r\\n",sum);
0040D744 8B 45 FC             mov         eax,dword ptr [ebp-4]

可以发现,debug版下的代码和for的逻辑基本一致,这里需要注意的是在第一次初始化完毕后,需要先使用一个jmp跳过步长部分,进行循环比较,因为在我们的for逻辑里面第一次是不进行i++的,而后面每次循环主体完毕后在进行i++,在进行比较,所以步长部分设计在比较部分的上面是合理的,只是第一次比较特殊而已。

for循环的主体架构

FOR_INTI:
    //... 初始化
    jmp FOR_CMP
FOR_STEP:
    //...步长部分
FOR_CMP:
    //..比较部分
    jxx FOR_END
    //...循环体
    //...
    jmp FOR_STEP
FOR_END

对于for循环的release版本,这里就不分析了,因为其实for循环和while循环两者的执行逻辑上而言几乎是一样的,所以其release版本的优化会和上面while版本一致。

对于循环语句而言,这里需要解释的并没有太多,因为基本上分析的都是一些很基础的情况,对于正向对循环逻辑结构很熟悉的同学,因为其汇编代码的循环逻辑是一致的,所以很容易就看懂。但是在真实情况下,很多时候可能并不是那么的好还原,这还是需要经验的积累。

下面再来看一下在release下会遇到的优化手段

首先来看第一种,代码外提的优化,这种优化属于自身代码写的并不是那么高效造成的,看下面的例子。

int main(int argc, char* argv[])

    int i = 0;
    int sum = 0;
    while (i < argc * 22)  // 理论上argc * 22会在每次循环的时候需要乘
    
        sum += i;
        ++i;
     
    printf("%d\\r\\n",sum);
    return 0;

下面我们来看一下release版的汇编代码

.text:00401009                 lea     esi, [eax+eax*4] //esi = argc * 5
.text:0040100C                 lea     eax, [eax+esi*2] //eax = argc * 11
.text:0040100F                 pop     esi
.text:00401010                 shl     eax, 1 //相当于*2, eax = argc * 22
//下面为优化后的循环
.text:00401012                 test    eax, eax
.text:00401014                 jle     short loc_40101D
.text:00401016
.text:00401016 loc_401016:                             ; CODE XREF: _main+1B↓j
.text:00401016                 add     edx, ecx
.text:00401018                 inc     ecx
.text:00401019                 cmp     ecx, eax  //eax一直都是上面计算出来的定值
.text:0040101B                 jl      short loc_401016
.text:0040101D
.text:0040101D loc_40101D:                             ; CODE XREF: _main+14↑j

可以发现,如果这里的结果是一个定值,那么就根本不需要每次循环的时候都去计算,可以外提到外面,上面的汇编代码还原

    int eax = argc * 22; //代码外提,先计算结果
    if(argc > 0)
    
        do 
        
            sum += i;
            ++i;
         while (i < eax);
    

好了,再来扩展一下,如果此时不是一个基本运算,而是一个函数会是如何呢?

    while (i < strlen("hello world"))

这里的话,对于函数而言,编译器是不会外提优化的,因为编译器并不能保证一个函数的参数是常量但其返回值肯定都是一样,比如说使用了下面这个函数

rand()

如果使用了随机种子相关的函数,那么返回值就不确定了,所以编译器对函数情况不会做外提优化。

OK,下面再来看一种,强度削弱优化

int main(int argc, char* argv[])

    int sum = 0;
    int n = 0;
    scanf("%d",&n);
    while(argc <= 100)
    
        sum = argc * n;
        argc++;
    
    printf("%d\\r\\n",sum);
    return 0;

来看一下对应的反汇编代码

.text:0040101F                 cmp     edi, 100
.text:00401022                 jg      short loc_40103B
.text:00401024                 mov     edx, [esp+0Ch+var_4]
.text:00401028                 mov     ecx, 101
.text:0040102D                 mov     eax, edx
.text:0040102F                 imul    eax, edi //注意这里的乘法是在循环外
.text:00401032                 sub     ecx, edi
.text:00401034
.text:00401034 loc_401034:                             ; CODE XREF: _main+39↓j
.text:00401034                 mov     esi, eax
.text:00401036                 add     eax, edx
.text:00401038                 dec     ecx
.text:00401039                 jnz     short loc_401034
.text:0040103B
.text:0040103B loc_40103B:
.text:0040103B                 push    esi //esi为结果

对于上面的汇编代码中,我们先不管其内容如何优化,后面在分析,我们至少可以发现在源代码循环中我们使用了乘法,而此时汇编代码中却没有看见乘法(循环中),所以强度削弱,也就是使用一些低周期指令替换高周期指令,所以上面的是使用了加法替代了乘法。为什么可以替换呢,其主要原因看下面

sum = argc * n; //这里是直接=,说明这个循环最后结果就是最后一个循环的argc*n

注意上面的代码中sum的值是=,而不是+=,如果是=,那么其结果可以说是100*n,虽然看似直接输出即可,当时编译器并没有直接省去循环。对于循环而言,其需要循环(100-argc)次,那么假设每次都加n,那么最终结果为(100-argc)*n,对于其结果是不是还差argc*n,所以这部分就是上面汇编代码中出现的imul指令,先计算出argc*n,然后每次加上n,那么sum的最终结果肯定是100*n。

对应的上面的等价高级代码还原

int main(int argc, char* argv[])

	int n = 0;
    int sum = 0;
    scanf("%d",&n);
    //也就是只要满足argc加足100*n即可
    int esi;
    if(argc >= 100)
    
        sum = argc * n;
        //使用101去减主要是当argc等于100时,如果使用100去减那么下面就不能使用do-while结构,而是需要使用while
        int ecx = 101 - argc; 
        do
        
            esi = sum; //这里使用了esi记录前一次的结果,因为上面使用101去减,所以理论上会多一次
            sum = sum + n; //最后多+的一次n相当于会废弃,因为结果输出的是esi
            ecx--;
        while(ecx != 0);
    
    printf("%d\\r\\n",esi); //最后只要输出前一次的结果即可
    return 0;

 

以上是关于逆向-循环结构的主要内容,如果未能解决你的问题,请参考以下文章

iOS逆向:循环选择指针(下)

带有空主体的while循环检查易失性整数-这是啥意思?

C语言基础:循环结构(循环类型(while,do...while,for,嵌套循环),循环控制语句(break,continue,goto),无线循环(死循环))

循环语句(forwhile)(一)

数据结构与算法之排序插入排序 ——in dart

如何提取for循环的主体? [复制]