漏洞利用原理(初级)

堆的工作原理

Windows堆的历史

微软操作系统堆管理机制的发展大致可以分为三个阶段(Win32):

  • Windows 2000~Windows XP SP1:堆管理只考虑完成分配任务和性能因素,丝毫没有考虑安全因素
  • Windows XP 2~Windows 2003:加入了安全因素,比如修改了块首的格式并加入安全cookie,双向链表结点在删除时会做指针验证等。
  • Windows Visa~Windows 7:不论在堆分配效率上还是安全与稳定性上,都是堆管理算法的一个里程碑。

这里主要关注Windows 2000~Windows XP SP1。

堆与栈的区别

栈的特点:

  • 在程序设计时已经规定好怎么使用,使用多少内存空间
  • 在使用时不需要额外的申请操作,系统栈会根据函数中的变量声明自动在函数栈帧中给其预留
  • 栈空间由系统维护,分配、回收都由系统来完成,最终达到栈平衡

堆的特点:

  • 堆是一种在程序运行时动态分配的内存,需要在程序运行时参考用户的反馈
  • 堆的使用需要程序员进行专门的申请,如:C——malloc函数、C++——new函数等,且申请可能失败
  • 一般用一个堆指针来使用申请得到的内存,读、写、释放都通过这个指针完成
  • 使用完毕后需将堆指针传给堆释放函数(free、delete)回收内存,否则会造成内存泄漏
堆内存栈内存
典型用例动态增长的链表等数据结构函数局部数组
申请方式需要函数申请,通过返回指针使用在程序中直接声明即可
释放方式需要将指针传给释放函数函数返回时,系统自动回收
管理方式需要管理员申请与释放申请与释放均由系统自动完成,达到栈区平衡
所处位置变化范围很大 0x0012XXXX
增长方向由内存低地址向高地址排列(不考虑碎片等情况)由内存高地址向低地址增加

堆的数据结构与管理策略

现在操作系统的堆数据结构:

  • 堆块
    出于性能考虑,堆区内存按不同大小组织成块,以堆块为单位进行标识。
    • 块首:堆块头8个字节,用于标识堆块信息
    • 块身:紧跟在块首后的部分,也是最终分配的数据区

空闲堆块结构:
在这里插入图片描述
占用堆块结构:
在这里插入图片描述
堆管理系统返回的指针一般指向块身的起始位置。

  • 堆表
    堆表一般位于堆区的起始位置,用于索引堆区中所有堆块的重要信息。
    在Windows中,占用态的堆块被使用它的程序索引,堆表只索引所有空闲态的堆块,最重要的堆表包括两个:
    • 空闲双向链表Freelist(空表)
      空闲堆块的块首中包含一堆重要的指针,用于将空闲堆块组织成双向链表,按照堆块的大小,空表总共分为128条:
      空表
    • 快速单向链表Lookaside(快表)
      快表是Windows用来加速堆块分配而采用的一种堆表,从来不会发生堆块合并(其中空闲块块首被设置为占用态,防止堆块合并)。
      快表也128条,总是被初始化为空,且每条快表最多4个结点,很快会被填满。
      在这里插入图片描述

堆中的操作:

  • 堆块分配
    • 快表分配:寻找大小匹配的空闲堆块—>将其状态改为占用态—>将其从堆表中“卸下”—>返回一个指向堆块块身的指针
    • 普通空表分配:首先寻找最优的空闲块分配,若失败寻找次优的
    • 零号空表(free[0])分配:由于零号空表的空间表为升序的,故先查找最后一个是否满足,如果满足再正向搜索
    • “找零钱”现象:当出现次优分配时,会先从大块中按请求“割”出一块,然后将剩下部分重新标注块首链入空表。由于快表只有在精确匹配时才会分配,故不存在“找钱”现象。
  • 堆块释放
    释放堆块为将堆块状态改为空闲,链入相应的堆表。
  • 堆块合并
    堆块合并由堆管理系统自动完成。
    经过反复的申请与释放,堆区会产生很对内存碎片,会进行堆块合并操作:将两个相邻的块从空闲链表中“卸下”—>合并堆块—>调整合并后大块的块首信息—>重新链入空闲链表。

在具体进行堆块分配和释放时,根据操作内存的不同策略也不同:

分配释放
小块(SIZE<1KB)快表分配(失败)—>普通空表分配(失败)—>堆缓存(heap cache)分配(失败)—>零号空表分配(失败)—>进行内存紧缩后再尝试分配(失败)—>返回NULL优先链入快表(失败)—>链入相应的空表
大块(1KB<=SIZE<512KB)堆缓存分配(失败)—>使用free[0]中的大块进行分配优先放入堆缓存(失败)—>链入freelist[0]
巨块(SIZE>=512KB)巨块非常罕见,用到虚分配方法直接释放,无堆表操作

在堆中漫游

堆分配函数之间的调用关系

Windows平台下的堆管理架构:
在这里插入图片描述
所有的堆分配函数最终都将使用位于nydll.dll中的RtlAllocateHeap()函数进行分配,如下图所示:
在这里插入图片描述

堆的调式方法

用于调试的代码:

#include <windows.h>
#include <Winbase.h>
int main(){
	HLOCAL h1,h2,h3,h4,h5,h6;
	HANDLE hp;
	hp=HeapCreat(0,0x1000,0x10000);     //创建一个只有调用进程才能访问的私有堆,参数为:堆的可选属性,堆的初始大小,堆的最大大小,单位为Bytes。
	__asm int 3           //避免程序检测出调试器,加入一个人工断点
	
	h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,3);     //在指定的堆上分配内存,参数为:要分配堆的句柄,堆分配时的可选参数,要分配堆的字节数
	h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,5);     //可选参数包括:HEAP_GENERATE_EXCEPTIONS——分配错误将会抛出异常,而不是返回NULL;HEAP_NO_SERIALIZE——不使用连续存取;HEAP_ZERO_MEMORY——将分配的内存全部清零
	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);              //释放堆内存
	HeapFree(hp,0,h3);              
	HeapFree(hp,0,h5);              
	
	HeapFree(hp,0,h4);              
	
	return 0;
}

调试态堆管理策略和常态堆管理策略的差异:

  • 调试堆不使用快表,只用空表分配
  • 所有堆块都加上多余的16字节尾部来防止溢出(防止程序溢出),包括8个字节的0xAB和8个字节的0x00
  • 块首的标志位不同

识别堆表

堆表中包含的信息依次为:段表索引、虚表索引、空表使用标识和空表索引区0x178)。

一个堆初始化时:

  • 只有一个空闲状态的大块,称为“尾块”
  • 位于堆偏移0x0688处(启用快表后此位置为快表)
  • Freelist[0]指向“尾块”
  • 除零号空表索引外,其余各项索引都指向自己

在这里插入图片描述
在这里插入图片描述

堆块的分配

  • 堆块的大小包括了块首在内,所以申请32字节会分配32+8=40字节
  • 堆块的单位为8字节
  • 初始情况下,快表和空表都为空,只能从“尾块”进行“次优块”
  • 随着次优分配,会从“尾块”切走一些小块,并修改“尾块”的size信息
堆句柄请求字节实际分配(字节)
h132*8=16
h252*8=16
h362*8=16
h482*8=16
h5194*8=32
h6244*8=32

在这里插入图片描述

堆块的释放

根据实际分配的堆单位,h1、h3对应的堆块被链入freelist[2],h5被链入freelist[4]。
在这里插入图片描述
在这里插入图片描述

堆块的合并

当h4释放时,由于h3、h4和h5相邻,会发生堆块合并操作,合并后为2+2+4=8个堆单位,被链入Freelist[8]。
在这里插入图片描述
在这里插入图片描述

快表的使用

示例代码:

#include <stdio.h>
#include <windows.h>
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);
}

在这里插入图片描述
此时堆偏移0x0688处为快表。

  • 初始时的快表为空的
    在这里插入图片描述

堆溢出利用(上)——DWORD SHOOT

链表“拆卸”中的问题

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

正常的拆卸过程中的过程:
在这里插入图片描述
当堆溢出发生时,非法数据可以淹没下一个堆块的块首:
在这里插入图片描述

“DWORD SHOOT”示例

示例代码:

#include <windows.h>
#include <Winbase.h>
int 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
	
	HeapFree(hp,0,h1);
	HeapFree(hp,0,h3);
	HeapFree(hp,0,h5);
	
	h1=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
	return 0;
}

三次释放后空闲双向链表为:
在这里插入图片描述

堆溢出利用(下)——代码植入

DWORD SHOOT的利用方法

DWORD SHOOT的常用目标(win XP SP1之前):

  • 内存变量:影响程序执行的重要标志变量
  • 代码逻辑
  • 函数返回地址
  • 攻击异常处理:S.E.H、F.V.E.H、进程环境块(P.E.B)中的U.E.F、线程环境块(T.E.B)中存放的第一个S.E.H指针(T.E.H)
  • 函数指针
  • P.E.B(进程环境块)中线程同步函数的入口地址:每个进程的P.E.B中都存放着一对同步函数指针,指向RtlEnterCriticalSection()RtlLeaveCriticalSection(),并且在进程退出时会被ExitProcess()调用。

狙击P.E.B中RtlEnterCriticalSection()的函数指针

地址:

  • RtlEnterCriticalSection():P.E.B中偏移0x20处,0x7FFDF020
  • RtlLeaveCriticalSection():P.E.B中偏移0x24处,0x7FFDF024

实验程序:

#include <windows.h>
char shellcode[]="\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);    //申请200字节空间
	__asm int 3
	memcpy(h1,shellcode,0x200)                //内存拷贝函数,参数为:存储的位置,存储的数据以及存储数据的大小,多出的数据会覆盖尾块的块首
	h2=HeapAlloc(hp,HEAP_ZERO_MEMORY,8);      //分配时导致DWORD SHOOT
	return 0;
}

在这里插入图片描述
shellcode设置:

  • 首先用\x90以及前面的failwest的shellcode填满h1的200个字节
  • 然后拼接下一空闲块(尾块)的8字节正常块首信息
  • 前向指针是“子弹”,这里使用shellcode的起始地址
  • 后向指针是“目标”,这里填入P.E.B中的RtlEnterCriticalSection()0x7FFDF020

为防止shellcode中的函数调用RtlEnterCriticalSection()

  • 先记录0x7FFDF020处的值,例如:0x7C920100
  • 在shellcode一开始进行修复:
mov eax,0x7FFDF020
mov ebx,0x7C920100
mov [eax],ebx

堆溢出利用的注意事项

  • 调试堆与正常堆的区别
  • 在shellcode中修复环境
    简单的修复堆区的做法:
    • 在堆区偏移0x28的地方存放着堆区所有空闲块的总和TotalFreeSize
    • 把一个较大块的标识大小的字节修改为TotalFreeSize
    • 把该块flag设置为0x10
    • 把freelist[0]的前后向指针指向这个堆块
  • 定位shellcode的跳板
  • DWORD SHOOT后的“指针反射”现象
int remove(ListNode * node){
	node->blink->flink=node->flink;
	node->flink->blink=node->blink;     //指针反射,会将目标地址写入shellcode起始偏移4个字节的地方
	return 0;
}
参考文献

《0day安全:软件漏洞分析技术》

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值