win10 x86/32位栈溢出

        上个月做了一个栈溢出执行cmd shell的例子(https://www.cnblogs.com/theseventhson/p/13933230.html),当时在代码中通过数组显式调用了shell函数(a[x] = (int)shell),即把shell函数的地址放在了数组中,只需要通过超(溢)长(出)数组把shell的地址写入函数返回地址(ebp+4)即可达到执行shell函数的目的。这次介绍一个更贴近实际业务的栈溢出例子,代码如下:

  乍一看,就是一个普通的计数程序,正常情况下用户输入10个数字,然后输入0表示结尾,最后打印用户输入数字的总和;怎么才能让hackMe函数被执行了?

#include <iostream>
#include <iomanip>

void HackMe()
{
    unsigned long long x = 0;
    for (int i = 0; true; i++)
    {
        if (i % 100000000 == 0)
        {
            system("cls");
            std::cout << "\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n";
            std::cout << "\n                你的电脑已经被拿下,所有文件都被加密! \n";
            std::cout << "\n                请缴纳10个比特币作为赎金解密所有文件\n";
            std::cout << "\n\\>正在传输硬盘数据....已经传输" << x++ << "个文件......\n\n";
            std::cout << "\n\\>             数据传输完成后将电脑会自动关机!\n";
            std::cout << "\n■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n";
        }
    }
}

int GetNum()
{
    int rt;
    std::cout << "请输入数字:";
    std::cin >> rt;
    return rt;
}

int count()
{
    int i=0;
    int total=0;
    int num[10]={0};
    do
    {
        num[i] = GetNum();
        total += num[i];
    } while (num[i++]);
    return total;
}

int main()
{
    std::cout << "\n                HackMe地址:"<< HackMe <<std::endl;
    std::cout << "\n[说明:最多输入10个数字,当输入0时代表输入结束]\n\n";
    std::cout << "\n总和为:" << count();
}

  1、这里从main开始的函数调用链:main->count->GetNum->std::out和std::in;这个程序是x86、32位的,所以理论上每个调用都可以考虑搞得栈溢出,跳转到hackMe执行代码;但实际分析时发现:能让用户输入的只有GetNum函数,所以先从这个函数开始分析,用IDA查看汇编代码如下:

text:00401110 sub_401110      proc near               ; CODE XREF: sub_401140:loc_401174↓p
.text:00401110
.text:00401110 var_4           = dword ptr -4
.text:00401110
.text:00401110                 push    ebp
.text:00401111                 mov     ebp, esp
.text:00401113                 push    ecx
.text:00401114                 push    offset unk_4032B4
.text:00401119                 mov     eax, ds:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::basic_ostream<char,std::char_traits<char>> std::cout
.text:0040111E                 push    eax
.text:0040111F                 call    sub_4014A0
.text:00401124                 add     esp, 8
.text:00401127                 lea     ecx, [ebp+var_4]
.text:0040112A                 push    ecx
.text:0040112B                 mov     ecx, ds:?cin@std@@3V?$basic_istream@DU?$char_traits@D@std@@@1@A ; std::basic_istream<char,std::char_traits<char>> std::cin
.text:00401131                 call    ds:??5?$basic_istream@DU?$char_traits@D@std@@@std@@QAEAAV01@AAH@Z ; std::basic_istream<char,std::char_traits<char>>::operator>>(int &)
.text:00401137                 mov     eax, [ebp+var_4]
.text:0040113A                 mov     esp, ebp
.text:0040113C                 pop     ebp
.text:0040113D                 retn
.text:0040113D sub_401110      endp

  用户的输入被放在ebp-4这里,然后再mov到eax作为函数的返回值;既然用输入放在epb-4,而ebp+4就是返回地址,是不是可以在这里输入一个超长的数字,比如12字节的数字,依次覆盖ebp-4、ebp、ebp+4了? 最终达到覆盖返回地址的效果?如下:

      

  实际结果是不行的,马上让输入第二个数字了,原因是啥了?

   理论上讲:用户输入保存在ebp-4,返回地址在ebp+4,那么需要构造12个字节的数据;这里hackMe的地址是E811000,那么输入的数据应该是0x0000000000000000E81000;如果输入是一个没有长度约束的字符串还好,用户可以为所欲为地输入;但这里的输入是std:cin>>rt,rt是int型,只有4字节,超过4字节的只取最低为的4字节,所以这里尽管输入了12字节,但只被读取了低位的4字节(感兴趣的小伙伴可以用OD、x32dbg调试看看:GetNum函数返回后,eax的值是多少);所以想通过这里的输入导致栈溢出是不太可能的,只能换个地方!

  2、第一个GetNum函数排除,顺藤摸瓜看看第二个count函数,ida查看的汇编代码如下:

.text:00401140 ; Attributes: bp-based frame
.text:00401140
.text:00401140 sub_401140      proc near               ; CODE XREF: _main+44↓p
.text:00401140
.text:00401140 var_34          = dword ptr -34h
.text:00401140 var_30          = dword ptr -30h
.text:00401140 var_2C          = dword ptr -2Ch
.text:00401140 var_28          = dword ptr -28h
.text:00401140 var_24          = dword ptr -24h
.text:00401140 var_20          = dword ptr -20h
.text:00401140 var_1C          = dword ptr -1Ch
.text:00401140 var_18          = dword ptr -18h
.text:00401140 var_14          = dword ptr -14h
.text:00401140 var_10          = dword ptr -10h
.text:00401140 var_C           = dword ptr -0Ch
.text:00401140 var_8           = dword ptr -8
.text:00401140 var_4           = dword ptr -4
.text:00401140
.text:00401140                 push    ebp
.text:00401141                 mov     ebp, esp
.text:00401143                 sub     esp, 34h
.text:00401146                 mov     [ebp+var_4], 0
.text:0040114D                 mov     [ebp+var_8], 0
.text:00401154                 xor     eax, eax
.text:00401156                 mov     [ebp+var_34], eax
.text:00401159                 mov     [ebp+var_30], eax
.text:0040115C                 mov     [ebp+var_2C], eax
.text:0040115F                 mov     [ebp+var_28], eax
.text:00401162                 mov     [ebp+var_24], eax
.text:00401165                 mov     [ebp+var_20], eax
.text:00401168                 mov     [ebp+var_1C], eax
.text:0040116B                 mov     [ebp+var_18], eax
.text:0040116E                 mov     [ebp+var_14], eax
.text:00401171                 mov     [ebp+var_10], eax
.text:00401174
.text:00401174 loc_401174:                             ; CODE XREF: sub_401140+64↓j
.text:00401174                 call    sub_401110
.text:00401179                 mov     ecx, [ebp+var_4]
.text:0040117C                 mov     [ebp+ecx*4+var_34], eax
.text:00401180                 mov     edx, [ebp+var_4]
.text:00401183                 mov     eax, [ebp+var_8]
.text:00401186                 add     eax, [ebp+edx*4+var_34]
.text:0040118A                 mov     [ebp+var_8], eax
.text:0040118D                 mov     ecx, [ebp+var_4]
.text:00401190                 mov     edx, [ebp+ecx*4+var_34]
.text:00401194                 mov     [ebp+var_C], edx
.text:00401197                 mov     eax, [ebp+var_4]
.text:0040119A                 add     eax, 1
.text:0040119D                 mov     [ebp+var_4], eax
.text:004011A0                 cmp     [ebp+var_C], 0
.text:004011A4                 jnz     short loc_401174
.text:004011A6                 mov     eax, [ebp+var_8]
.text:004011A9                 mov     esp, ebp
.text:004011AB                 pop     ebp
.text:004011AC                 retn
.text:004011AC sub_401140      endp
.text:004011AC

  代码不多,分析的时候养成好习惯,画个栈图,一切都清晰明了了(顺便说一下自己的观点:数据是核心,所有的代码都是为数据服务的):刚开始esp-34h,相当于分配了13个字节的栈空间;从栈顶开始一次分配10个字节存储数组的10个元素;再接着是临时变量(也就是ebp-ch,用来临时存储输入的数据,后续用于和0比较,看看输入是否结束)、total(也就是ebp-8)、i(也就是ebp-4),然后是ebp本身,再往下就是ebp+4、也就是count函数调用前的返回地址了,整个栈空间如下:

      

  用调试器能直观看到栈的变化情况:

     

     这么来看就很清晰了:用户的输入从栈顶开始保存,第15个就是返回地址,那么一直输入直到第15的时候把hackMe函数的返回地址输入是不是就行了?刚开始想的就是这么简单,从1开始挨个输入,直到输入到14,寄存器和堆栈变成了这样:ebp+4被改成了E,而不是我们预想的hackMe返回地址,后面直接导致著名的access_violation异常.........

  

   重新输入,每步都调试,终于发现问题所在:(1)第11个数字存放在epb-c(这里原本是临时存放输入的空间,下面用来和0比较的,看看是不是结束了),第12个数字放在ebp-8(这里原本存放total总和),第13个数字放在ebp-4(这里原本存放循环次数i),这3个数字会通过ebp互相使用(详情见汇编代码),不能乱填;(2)界面刚开始打印出来的hackMe地址是16进制的,但std::cin接受输入是10进制的,如果强行输入16进制的地址,遇到字母就会被截断,造成地址输入错误;所以需要先把16进制的地址换成10进制后再输入,最终的效果如下:

       第14个数字输入hackMe的返回地址:

结果:hackMe函数已被执行:

 

最后总结一下注意事项:

1、编译的时候必须是32位的,因为64位汇编函数调用约定不同:32位所有参数都通过栈push传递,64位前4个参数通过rcx、rdx、r8、r9传递,第5个参数才通过栈push传递;

2、编译时禁止优化:

     

     禁止安全检查:

   

 3、std:cout打印的是变量的默认类型,比如hackMe地址是16进制的,打印也是16进制;

       同理:std:cin输入也是按照变量类型读取的。这里输入类型是int,所以接受也按照int类型接受,上面的地址要先转成10进制再输入;

4、这个例子告诉我们:用户所有的输入都不能相信,接收到以后都必须过滤,检查有没有超长或非法输入。前段时间就又爆出了“bad neighbor”漏洞:https://www.cnblogs.com/theseventhson/p/14004712.html  也是驱动模块接受到数据后未检查长度和内容、导致内核代码被覆盖导致的;

posted @ 2020-12-24 17:06  第七子007  阅读(479)  评论(0编辑  收藏  举报