• 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 << "
    ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    ";
                std::cout << "
                    你的电脑已经被拿下,所有文件都被加密! 
    ";
                std::cout << "
                    请缴纳10个比特币作为赎金解密所有文件
    ";
                std::cout << "
    \>正在传输硬盘数据....已经传输" << x++ << "个文件......
    
    ";
                std::cout << "
    \>             数据传输完成后将电脑会自动关机!
    ";
                std::cout << "
    ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
    ";
            }
        }
    }
    
    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 << "
                    HackMe地址:"<< HackMe <<std::endl;
        std::cout << "
    [说明:最多输入10个数字,当输入0时代表输入结束]
    
    ";
        std::cout << "
    总和为:" << 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  也是驱动模块接受到数据后未检查长度和内容、导致内核代码被覆盖导致的;

  • 相关阅读:
    shell关闭指定进程
    linux tricks 之数据对齐。
    linux tricks 之VA系列函数.
    linux tricks 之 typeof用法.
    linux下notify机制(仅用于内核模块之间的通信)
    怎么判定一个mac地址是multicast还是unicast.
    linux tricks 之 ALIGN解析.
    fid解释
    c语言中宏定义#和 ##的作用:
    rebtree学习
  • 原文地址:https://www.cnblogs.com/theseventhson/p/14185096.html
Copyright © 2020-2023  润新知