前两天跟着《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]
推论:
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类型无关。这就是多态的汇编实质