堆溢出&异常处理&探究虚函数&多态的实质

前两天跟着《0day》堆溢出部分研究了一下一直不太清楚的堆溢出原理,不同于栈溢出,堆溢出的难度确实很大,但是理解了之后其实也不难,栈溢出后可以通过返回地址的修改控制EIP寄存器从而得到程序的控制权。但是堆区写入得都是纯数据啊,怎么可能能控制EIP寄存器呢?堆溢出是怎么获得程序控制权的呢,这是我一直困惑的一点。
看完之后恍然大悟啊,如果说栈溢出是主动控制EIP的话,堆溢出就是被动控制EIP,堆区写入的数据本身并不能被执行,但是写入的数据修改了关键变量后导致系统对改变量的处理出现了问题。具体来说就是堆区是个链表型的数据结构,所以卸下操作会修改前面那个块的后向指针和后面块的前项指针的值。我们只要控制溢出的数据把这两个指针换掉,就在系统卸下堆块的时候获得了一次修改指定地址为指定值的机会。这样利用思路就出来了,比如修改SEH存放处理函数地址的值指向Shellcode,修改虚函数表….

所以就引出了关于Windows异常处理流程的相关问题,这里推荐下0day安全这本书上关于此处的描述,还有两篇很不错的补充博客:Windows异常世界历险记(一)Windows异常世界历险记(二)

关于类的实例化在内存中的布局,虚函数表在内存中的布局这里通过几个实验简单研究下:
虚函数表的内存布局(书):
虚函数的实现

实验代码1:vc++6.0 debug编译

#include <iostream>
using namespace std;

class virtualtest{
public:
    char buf[20];
    virtualtest(){
        cout<<"virtualtest() Start!"<<endl;
    }
    void overflow(char *s){
        strcpy(buf,s);
        test();
    }
    virtual void test(void){
        cout<<"Test Start!"<<endl;
    }
};

int main(){
    cout<<"Main Start!"<<endl;
    virtualtest a1,*p1;
    char *p2=a1.buf-4;
    p1=&a1;
    cout<<"p1:"<<p1<<endl;
    cout<<"p2:"<<p2<<endl;
    a1.overflow("1234567890123456789");
    p1->overflow("A1A2A3A4A5A6A7A8A9A0A1A2A3A4A5A6A7A8");
    return 0;
}

放入OD调试分析:

00401590 >  55              push ebp                                 ; main()
00401591    8BEC            mov ebp,esp
00401593    83EC 60         sub esp,0x60
00401596    53              push ebx
00401597    56              push esi
00401598    57              push edi
00401599    8D7D A0         lea edi,dword ptr ss:[ebp-0x60]
0040159C    B9 18000000     mov ecx,0x18
004015A1    B8 CCCCCCCC     mov eax,0xCCCCCCCC
004015A6    F3:AB           rep stos dword ptr es:[edi]
004015A8    68 D2104000     push 1.004010D2
004015AD    68 68F04600     push offset 1.??_C@_0M@DLIL@Main?5Start?>
004015B2    68 90CE4700     push offset 1.std::couteginunsigned shor>
004015B7    E8 CEFCFFFF     call 1.0040128A                         ;cout<<"Main Start!"
004015BC    83C4 08         add esp,0x8
004015BF    8BC8            mov ecx,eax
004015C1    E8 1FFCFFFF     call 1.004011E5                         ;cout<<endl
004015C6    8D4D E8         lea ecx,dword ptr ss:[ebp-0x18]         ecx=ebp-0x18(12FF68)
004015C9    E8 AFFAFFFF     call 1.0040107D                         调用构造函数
004015CE    8D45 EC         lea eax,dword ptr ss:[ebp-0x14]         eax=ebp-0x14(12FF6C)
004015D1    83E8 04         sub eax,0x4                             
004015D4    8945 E0         mov dword ptr ss:[ebp-0x20],eax         p2=a1.buf-4=12FF68
004015D7    8D4D E8         lea ecx,dword ptr ss:[ebp-0x18]
004015DA    894D E4         mov dword ptr ss:[ebp-0x1C],ecx         p1=&a1=12FF68
004015DD    68 D2104000     push 1.004010D2
004015E2    8B55 E4         mov edx,dword ptr ss:[ebp-0x1C]

Main堆栈
推论:
1.main函数中构造的类的实例存储在main函数的堆栈区,实例的内存布局如前所说,第一个元素是一个指针,指向虚函数表,其后才是类的各个成员。p2指针=0012FF68指向了虚表指针的地址,p1=&a1=12FF68。所以a1的起始地址是指向了虚表指针的(操作系统并没有进行类似堆那样的隐藏处理)
2.在main函数中无论如何写0012FF6C开始的buf[20]都无法造成溢出覆盖低地址处的虚表指针0046F094
3.内存区查看下0046F094区域内存,可以看到倒序的004012C1,这个地址正是test函数的地址
虚表内存

004015E5    52              push edx                            
004015E6    68 64F04600     push offset 1.??_C@_03LFEC@p1?3?$AA@>; p1:
004015EB    68 90CE4700     push offset 1.std::couteginunsigned >
004015F0    E8 95FCFFFF     call 1.0040128A
004015F5    83C4 08         add esp,0x8
004015F8    8BC8            mov ecx,eax                         
004015FA    E8 42FAFFFF     call 1.00401041
004015FF    8BC8            mov ecx,eax                          ; p2:
00401601    E8 DFFBFFFF     call 1.004011E5
00401606    68 D2104000     push 1.004010D2
0040160B    8B45 E0         mov eax,dword ptr ss:[ebp-0x20]
0040160E    50              push eax                            
0040160F    68 60F04600     push offset 1.??_C@_03IOLN@p2?3?$AA@>; p2:
00401614    68 90CE4700     push offset 1.std::couteginunsigned >
00401619    E8 6CFCFFFF     call 1.0040128A
0040161E    83C4 08         add esp,0x8
00401621    50              push eax                             
00401622    E8 63FCFFFF     call 1.0040128A
00401627    83C4 08         add esp,0x8
0040162A    8BC8            mov ecx,eax                          
0040162C    E8 B4FBFFFF     call 1.004011E5

上面为输出p1指针和p2指针值的汇编代码

00401631    68 48F04600     push offset 1.??_C@_0BE@EIDM@1234567>;压入1234567890123456789
00401636    8D4D E8         lea ecx,dword ptr ss:[ebp-0x18]      ;ecx赋值为0012FF68
00401639    E8 74FCFFFF     call 1.004012B2                      

以上为a1.overflow(“1234567890123456789”);的汇编代码,首先将123….的字符串指针压栈,然后把实例的this指针赋值给ecx寄存器.之后是调用函数,这个函数内部有溢出漏洞,跟进去看下

00401710 >  55              push ebp
00401711    8BEC            mov ebp,esp
00401713    83EC 44         sub esp,0x44
00401716    53              push ebx
00401717    56              push esi
00401718    57              push edi
00401719    51              push ecx                              ;this指针(0012FF68)入栈
0040171A    8D7D BC         lea edi,dword ptr ss:[ebp-0x44]
0040171D    B9 11000000     mov ecx,0x11
00401722    B8 CCCCCCCC     mov eax,0xCCCCCCCC
00401727    F3:AB           rep stos dword ptr es:[edi]
00401729    59              pop ecx                              
0040172A    894D FC         mov dword ptr ss:[ebp-0x4],ecx         ;this指针赋值给[ebp-0x4]
0040172D    8B45 08         mov eax,dword ptr ss:[ebp+0x8]
00401730    50              push eax                               ;*s->"1234567890"
00401731    8B4D FC         mov ecx,dword ptr ss:[ebp-0x4]
00401734    83C1 04         add ecx,0x4
00401737    51              push ecx                               ;buf
00401738    E8 E3F30100     call 1.strcpyum_put<char,std::ostrea>  ;strcpy(buf,s)
0040173D    83C4 08         add esp,0x8
00401740    8B55 FC         mov edx,dword ptr ss:[ebp-0x4]
00401743    8B02            mov eax,dword ptr ds:[edx]           
00401745    8BF4            mov esi,esp
00401747    8B4D FC         mov ecx,dword ptr ss:[ebp-0x4]
0040174A    FF10            call dword ptr ds:[eax]                调用test()
0040174C    3BF4            cmp esi,esp
0040174E    E8 8DF30100     call 1.__chkespEINI@French?$AA@nuary>
00401753    5F              pop edi                              
00401754    5E              pop esi                              
00401755    5B              pop ebx                              
00401756    83C4 44         add esp,0x44
00401759    3BEC            cmp ebp,esp
0040175B    E8 80F30100     call 1.__chkespEINI@French?$AA@nuary>
00401760    8BE5            mov esp,ebp
00401762    5D              pop ebp                             
00401763    C2 0400         retn 0x4

调用test()=call eax,其中eax=edx=[ebp-0x4]=ecx=this
字符串赋值之后这里写图片描述
可以看出无论怎么溢出都不会影响虚函数表。这个程序不存在利用溢出劫持虚函数的漏洞!!
那么到底什么样的程序才有这个漏洞呢
如果把overflow本身的栈空间有字符串,且这个字符串存在溢出漏洞才可利用,例如:

void overflow(char *s){
        char buf1[20];
        strcpy(buf1,s);
        test();
    }

这里溢出这个buf1,就可以控制[ebp-0x4]的区域数值,此前这里存放着this指针(指向虚函数表地址的指针的地址),这样调用test()=call eax,其中eax=edx=[ebp-0x4]=ecx=this 这个调用链就在[ebp-0x4]处给劫持了,可以控制其指向我们构造的指针,而这个指针指向的地址存放着Shellcode的地址。这种虚函数劫持需要虚函数内部一个函数有溢出漏洞可以利用,函数栈空间示意图
【其他变量】
【this指针】
【开启GS后此处有Cookie】
【上个栈空间的EBP值】
【函数返回地址】
利用其他变量的溢出覆盖this指针地址,从而使得test()被劫持,这个方式是绕过GS保护的经典方式,因为GS保护只有在函数返回时才会检验,这里覆盖this指针后,调用test时还未返回,因此还未进行GS校验,Shellcode已经得以执行。


再试试没有虚函数的内存布局:
这里写图片描述
main中调用overflow汇编代码

00401631   .  68 48F04600   push offset 1.??_C@_0BE@EIDM@12345678901>
00401636   .  8D4D EC       lea ecx,dword ptr ss:[ebp-0x14]           ;有虚函数的话为[ebp-0x18]
00401639   .  E8 79FCFFFF   call 1.004012B7

无虚函数this指针指向第一个成员的地址,实例的开始地址也是第一个成员的地址
跟入overflow函数查看

00401710 >  55              push ebp
00401711    8BEC            mov ebp,esp
00401713    83EC 44         sub esp,0x44
00401716    53              push ebx
00401717    56              push esi
00401718    57              push edi
00401719    51              push ecx
0040171A    8D7D BC         lea edi,dword ptr ss:[ebp-0x44]
0040171D    B9 11000000     mov ecx,0x11
00401722    B8 CCCCCCCC     mov eax,0xCCCCCCCC
00401727    F3:AB           rep stos dword ptr es:[edi]
00401729    59              pop ecx                                  
0040172A    894D FC         mov dword ptr ss:[ebp-0x4],ecx
0040172D    8B45 08         mov eax,dword ptr ss:[ebp+0x8]           
00401730    50              push eax                                 
00401731    8B4D FC         mov ecx,dword ptr ss:[ebp-0x4]
00401734    51              push ecx
00401735    E8 D6F30100     call 1.strcpyum_put<char,std::ostreambuf>
0040173A    83C4 08         add esp,0x8
0040173D    8B4D FC         mov ecx,dword ptr ss:[ebp-0x4]
00401740    E8 C5F8FFFF     call 1.0040100A                               ;test()
00401745    5F              pop edi                                  
00401746    5E              pop esi                                  
00401747    5B              pop ebx                                  
00401748    83C4 44         add esp,0x44
0040174B    3BEC            cmp ebp,esp
0040174D    E8 7EF30100     call 1.__chkespEINI@French?$AA@nuary?3Fe>
00401752    8BE5            mov esp,ebp
00401754    5D              pop ebp                                 
00401755    C2 0400         retn 0x4

test不是虚函数时没有了test()=call eax,其中eax=edx=[ebp-0x4]=ecx=this这个调用链了,编译完就是硬编码的call xxx


测试为啥只有指针类型的实例才会有多态特性
测试代码:

#include <iostream>
using namespace std;

class virtualtest{
public:
    char buf[20];
    virtualtest(){
        cout<<"virtualtest() Start!"<<endl;
    }
    virtual void test(void){
        cout<<"Test Start!"<<endl;
    }
};

int main(){
    cout<<"Main Start!"<<endl;
    virtualtest a;
    virtualtest *p=&a;
    a.test();
    p->test();
    return 0;
}

汇编代码定位:

004011A6   .  8D4D E8       lea ecx,dword ptr ss:[ebp-0x18]          存放a实例的地址
004011A9   .  E8 70FEFFFF   call 1.0040101E                          构造函数构造a
004011AE   .  8D45 E8       lea eax,dword ptr ss:[ebp-0x18]          
004011B1   .  8945 E4       mov dword ptr ss:[ebp-0x1C],eax          virtualtest *p=&a;
004011B4   .  8D4D E8       lea ecx,dword ptr ss:[ebp-0x18]          this指针赋值给ecx
004011B7   .  E8 F8FEFFFF   call 1.004010B4                          a.test()
004011BC   .  8B4D E4       mov ecx,dword ptr ss:[ebp-0x1C]
004011BF   .  8B11          mov edx,dword ptr ds:[ecx]               
004011C1   .  8BF4          mov esi,esp
004011C3   .  8B4D E4       mov ecx,dword ptr ss:[ebp-0x1C]
004011C6   .  FF12          call dword ptr ds:[edx]                 p->test(); 

推论:非指针型实例在编译后,调用函数都是硬编码的,赋值给ecx的也是本实例的this指针。指针型都是类似于上面那个调用链,获取this指向的实例的虚表地址,然后再进行调用
再看多态的表现形式:
假设people是基类 man是people的派生类 people和man中都有eat(),且他是虚函数

此时people *p=new people;这时p指向people的this指针,那么指针类型的调用链就会到p指向的实例空间取this指针然后调用,那么调用的自然就是people的test()。但是如果people *p=new man,那么p会从man的那个实例中取this指针,所以会调用man的test(),而和指针本身是people类型无关。这就是多态的汇编实质

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值