windows和linux堆管理机制虽然呈现给用户的效果是一样的,大体思路也是差不太多,但是底层实现逻辑大相径庭,很多地方和glibc的ptmalloc差别很大。网上资料零零散散,而且都是通过逆向手段分析,所以每个版本资料还多少有些差异,在这里对windows堆管理机制做个归纳,学习一下。
接口在glibc中,通常我们调用的分配函数就是malloc、calloc、realloc,但是这三个函数本质都差不多,本体还是malloc函数的逻辑。
在windows中,堆的分配函数就比较多了这里我们逐一介绍一下。
函数原型参数说明HeapAlloc(HANDLE hHeap, DWORD dwFlags, size_t dwSize)hHeap为进程堆开始位置,flag就是标志,size就是大小。内存是指定位置开始分配,且分配的内存不可移动。对应的释放函数是HeapFreeGlobalAlloc(UINT uFlags, size_t dwBytes)uflag标志信息:GMEM_FIXED分配固定内存,返回一个指针;GMEM_MOVABLE分配活动内存,返回内存对象句柄,这个句柄可以利用GlobalLock转化为指针。从全局堆中分配内存,相应的释放函数是GlobalFreeLocalAlloc(UINT uFlags,size_t dwBytes)参数同GlobalAlloc对应的释放函数为LocalFreeVirtualAlloc(LPVOID lpAddress, size_t dwSize,DWORD flAllocationType,DWORD flProtect)意义和参数名一样对应的释放函数为VirtualFreemalloc和linux一样freeHeapCreate(DWORD flOptions , DWORD dwInitialSize , DWORD dwMaxmumSize);flOptions:堆的可选属性。这些标记影响以后对这个堆的函数操作,函数有:HeapAlloc , HeapFree , HeapReAlloc , HeapSize .wInitialSize:堆的初始大小,单位为Bytes。这个值决定了分配给堆的初始物理空间大小。这个值将向上舍入知道下个page boundary(页界)。若需得到主机的页大小,使用GetSystemInfo 函数。dwMaxmumSize:如果该参数是一个非零的值,它指定了这个堆的最大大小,单位为Bytes。用来创造一块堆区域从接口信息可以看出来,windows和linux堆的一个很大的不同点就是windows的堆有很多,linux的话都在一个区域里。
另外,globalalloc和localalloc在现代的win32以后的版本中没有区别,这两个函数刚开始是在16位windows中使用有区别的。在win32中每个程序都有一个自己的缺省堆,所以全局堆和局部堆在win32中都指向这个缺省堆,这俩没区别,甚至释放函数都可以混着用。等效于heapAlloc(GetProcessHeap(),flag,size)
malloc函数虽然不像其他的函数那样指明了堆区,但是实际上windows中malloc函数在初始化的时候自己HeapCreate了一段堆内存区域供他使用。每个模块的malloc都有自己的堆区域,所以不能一个dllfree掉另一个dll的堆指针。
概览windows堆管理机制较之于linux比较复杂,管理机制也分好几套。
UWP即Windows通用应用平台,Windows 10中的Universal Windows Platform简称。UWP不同于传统pc上的exe应用,可以在所有Windows10设备上运行。UWP应用程序进程至少包括三个堆:(1) 默认堆(2) 用于向进程的会话Csrss.exe实例传递大参数的共享堆。这是由CsrClientConnectToServer函数创建的,该函数在Ntdll.dll完成的进程初始化早期执行。(3) 由Microsoft C运行库创建的堆。该堆是由C/C++内存分配函数(如Maloc、Free、等)内部使用的堆。
在Windows10和服务器2016之前,只有一种堆类型,我们称之为NT堆。Windows 10引入了一种称为段堆(segment heap)的新堆类型。这两种堆类型包括公共元素,但结构和实现方式不同。默认情况下,所有UWP应用程序和某些系统进程都使用段堆,而所有其他进程都使用NT堆。这可以在注册表中更改。
大部分场合默认使用的堆都是NT heap
,segment heap
通常会在winapp或者某些特殊的进程(核心进程)中会使用到。
而在NT heap中又分为前端管理和后端管理两套不同的堆分配管理策略。
而windows程序的堆又分为两种:
第一种叫做processheap,它包括两个部分,一个是default heap,其地址信息回存放于_PEB中,在调用malloc等函数的时候会用到。第二个是crtheap,但是其本质一样是default,封装了一些别的信息,存放于crt_heap中。
第二种叫做private heap,也就是我们通过HeapCreate创建的堆。
NT堆大体流程
大体流程就是windows app调用msvcrt140.dll函数中的形如malloc、free等函数后,会调用kernel32.dll中的堆管理api,接着调用ntdll中的管理机制。
这里的管理机制中,LFH就是前端管理的核心,那么整个流程具体来说就是如下的逻辑:
(1) 小于或等于16368字节,使用LFH分配器。这与NT堆的逻辑类似。如果LFH还没有启动,那么将使用可变大小(VS)分配器。(2) 对于小于或等于128 KB的大小(不由LFH提供服务),使用VS分配器。VS和LFH分配器都使用后端根据需要创建所需的堆子段。(3) 大于128 KB且小于或等于508 KB的分配由堆后端直接提供服务。(4) 大于508kb的分配直接调用内存管理器(VirtualAlloc),因为这些分配非常大,因此使用默认的64kb分配粒度(并舍入到最接近的页面大小)就足够了。
如果LFH没有启用,那么就直接调用后端堆管理机制。
启用LFH后,第一次申请或者LFH内部空间不够时会从后端堆中申请一段大空间来使用。
如果LFH搞定了申请,那么直接由LFH返回,不调用后端。
可以看出前端分配器就有点类似于linux中的fastbin。
这里要说明一下,在之前的windows版本中,前端分配器并不是LFH,而是look aside表,也就是0day一书中提到的快表,但是windows10中已经不适用lookaside了。
数据结构由前面的内容可以看出来windows有很多的堆,从linux的管理机制中,我们知道每个堆都由一个重要的数据结构malloc_state来管理,这些个mallocstate就称之为arena,主线程叫main_arena,别的叫thread_arena,这些个数据结构由指针链接形成链表。
那么在windows的堆管理机制中,同样也需要类似于arena这样的结构体。但是不同于linux,每个这样的堆管理结构体是存放于每个堆段的头部,并不是在某些dll的数据段中。
这个数据结构就称之为_HEAP,长这个样子:
+0x000 Segment : _HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x008 SegmentSignature : Uint4B //用来判断NT还是Segment
+0x00c SegmentFlags : Uint4B
+0x010 SegmentListEntry : _LIST_ENTRY
+0x018 Heap : Ptr32 _HEAP
+0x01c BaseAddress : Ptr32 Void
+0x020 NumberOfPages : Uint4B
+0x024 FirstEntry : Ptr32 _HEAP_ENTRY
+0x028 LastValidEntry : Ptr32 _HEAP_ENTRY
+0x02c NumberOfUnCommittedPages : Uint4B
+0x030 NumberOfUnCommittedRanges : Uint4B
+0x034 SegmentAllocatorBackTraceIndex : Uint2B
+0x036 Reserved : Uint2B
+0x038 UCRSegmentList : _LIST_ENTRY
+0x040 Flags : Uint4B
+0x044 ForceFlags : Uint4B
+0x048 CompatibilityFlags : Uint4B
+0x04c EncodeFlagMask : Uint4B //用来表示是否encode header
+0x050 Encoding : _HEAP_ENTRY //用来encode的cookie
+0x058 Interceptor : Uint4B
+0x05c VirtualMemoryThreshold : Uint4B
+0x060 Signature : Uint4B
+0x064 SegmentReserve : Uint4B
+0x068 SegmentCommit : Uint4B
+0x06c DeCommitFreeBlockThreshold : Uint4B
+0x070 DeCommitTotalFreeThreshold : Uint4B
+0x074 TotalFreeSize : Uint4B
+0x078 MaximumAllocationSize : Uint4B
+0x07c ProcessHeapsListIndex : Uint2B
+0x07e HeaderValidateLength : Uint2B
+0x080 HeaderValidateCopy : Ptr32 Void
+0x084 NextAvailableTagIndex : Uint2B
+0x086 MaximumTagIndex : Uint2B
+0x088 TagEntries : Ptr32 _HEAP_TAG_ENTRY
+0x08c UCRList : _LIST_ENTRY
+0x094 AlignRound : Uint4B
+0x098 AlignMask : Uint4B
+0x09c VirtualAllocdBlocks : _LIST_ENTRY
+0x0a4 SegmentList : _LIST_ENTRY
+0x0ac AllocatorBackTraceIndex : Uint2B
+0x0b0 NonDedicatedListLength : Uint4B
+0x0b4 BlocksIndex : Ptr32 Void //用来管理后端的chunk列表
+0x0b8 UCRIndex : Ptr32 Void
+0x0bc PseudoTagEntries : Ptr32 _HEAP_PSEUDO_TAG_ENTRY
+0x0c0 FreeLists : _LIST_ENTRY //用来管理后端所有的freechunk的链表
+0x0c8 LockVariable : Ptr32 _HEAP_LOCK
+0x0cc CommitRoutine : Ptr32 long
+0x0d0 StackTraceInitVar : _RTL_RUN_ONCE
+0x0d4 CommitLimitData : _RTL_HEAP_MEMORY_LIMIT_DATA
+0x0e4 FrontEndHeap : Ptr32 Void //指向前端堆的结构
+0x0e8 FrontHeapLockCount : Uint2B
+0x0ea FrontEndHeapType : UChar
+0x0eb RequestedFrontEndHeapType : UChar
+0x0ec FrontEndHeapUsageData : Ptr32 Wchar //指向前端管理chunk的列表
+0x0f0 FrontEndHeapMaximumIndex : Uint2B
+0x0f2 FrontEndHeapStatusBitmap : [257] UChar
+0x1f4 Counters : _HEAP_COUNTERS
+0x250 TuningParameters : _HEAP_TUNING_PARAMETERS
其中比较重要的字段意义都写在了注释中。
在linux中,堆是由一个个chunk构成的,在windows中也一样,也是由一个个堆块构成。
这样一个堆块的结构,称之为_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
在老外逆出来的c版本中是这样的:
//0x10 bytes (sizeof)
struct _HEAP_ENTRY
{
union
{
struct _HEAP_UNPACKED_ENTRY UnpackedEntry; //0x0
struct
{
VOID* PreviousBlockPrivateData; //0x0
union
{
struct
{
USHORT Size; //0x8
UCHAR Flags; //0xa
UCHAR SmallTagIndex; //0xb
};
struct
{
ULONG SubSegmentCode; //0x8
USHORT PreviousSize; //0xc
union
{
UCHAR SegmentOffset; //0xe
UCHAR LFHFlags; //0xe
};
UCHAR UnusedBytes; //0xf
};
ULONGLONG CompactHeader; //0x8
};
};
struct _HEAP_EXTENDED_ENTRY ExtendedEntry; //0x0
struct
{
VOID* Reserved; //0x0
union
{
struct
{
USHORT FunctionIndex; //0x8
USHORT ContextValue; //0xa
};
ULONG InterceptorValue; //0x8
};
USHORT UnusedBytesLength; //0xc
UCHAR EntryOffset; //0xe
UCHAR ExtendedBlockSignature; //0xf
};
struct
{
VOID* ReservedForAlignment; //0x0
union
{
struct
{
ULONG Code1; //0x8
union
{
struct
{
USHORT Code2; //0xc
UCHAR Code3; //0xe
UCHAR Code4; //0xf
};
ULONG Code234; //0xc
};
};
ULONGLONG AgregateCode; //0x8
};
};
};
};
这里的话主要是因为一个chunk(也就是_HEAP_ENTRY,这么叫方便些)有不同的状态,所以就union一下。
那么具体来说,一个chunk有三种状态:使用(allocated)、释放(free)、虚拟(virtual alloc)(mmap出来的chunk)。
使用状态(inuse):
偏移&名称大小意义0x0: PreviousBlockPrivateData8bytes前一个chunk的数据,由于需要0x10对其所以算在头部0x8: Size2bytes本chunk的大小,这里的大小是 real_size >> 40xa: Flag1byte表示当前chunk是否inuse0xb: smallTagIndex1byte前三个byte(size和flag)做xor后的值,验证作用0xc: PreviousSIze2bytes表示前一个chunk的size,同样也是右移4位后的值0xe: SegmentOffset1byte某些情况下用来找segment0xf: UnusedBytes1byte记录malloc后所剩的chunk大小,可用来判断是前端或者后端0x10: userdata(Size > 40xa: Flag1byte表示当前chunk是否inuse0xb: smallTagIndex1byte前三个byte(size和flag)做xor后的值,验证作用0xc: PreviousSIze2bytes表示前一个chunk的size,同样也是右移4位后的值0xe: SegmentOffset1byte某些情况下用来找segment0xf: UnusedBytes1byte恒为00x10:flink8bytes指向list中后一个chunk0x18: blink8bytes指向list中前一个chunkvirtualAlloc状态:
偏移&名称大小意义0x0: flink8bytes双向链表指针0x8: blink8bytes双向链表指针0x10: size2bytes这里的size是unusedsize,且没有进行移位0x12: flag1byte0x13: smallTagIndex1byte0x14: PreviousSIze2bytes0x16: SegmentOffset1byte0x17:UnusedBytes1byte恒为4这里的virtualalloc的chunk状态可能有些勘误,因为网上关于这里的资料比较少。
这里要说明一下,关于chunk头部的验证:
在之前的_HEAP结构体中有一个encoding字段,这个cookie就是为了加密头部来用的,具体来说就是xor一下,所以在对chunk进行操作的时候会验证其有效性。同时SmallTagIndex也会对flag和size做一个验证。
free_list
这个是在_HEAP中的一个指针,指向的是free的chunk的链表,双向有序链表。在一个chunk被释放后,会插入到这个list中(类似于unsortedbin)
BlocksIndex
这个指针的结构是_HEAP_LIST_LOOKUP。
这一结构体长这个样子:
//0x38 bytes (sizeof)
struct _HEAP_LIST_LOOKUP
{
struct _HEAP_LIST_LOOKUP* ExtendedLookup; //0x0 指向下一个lookup,通常chunk会更大
ULONG ArraySize; //0x8 管理的最大chunk大小(右移4位后)
ULONG ExtraItem; //0xc
ULONG ItemCount; //0x10 当前管理的chunk数
ULONG OutOfRangeItems; //0x14 超出该结构体管理的chunk数量
ULONG BaseIndex; //0x18 该结构管理的chunk的起始index(listhint中)
struct _LIST_ENTRY* ListHead; //0x20 指向freelist的head
ULONG* ListsInUseUlong; //0x28 判断listhint中是否有合适大小的chunk, 是一个bitmap
struct _LIST_ENTRY** ListHints; //0x30 用来指向对应大小chunk的array,0x10递增
};
这两个链表之间的关系如下图(摘自angelboy的slide)
可以看到,所有的chunk都是存储在freelist中,而blockindex用来定位这些在freelist中的chunk的位置,快速找到合适大小的chunk。
NT后端管理机制管理机制无非就是申请和释放的逻辑。
申请申请时,分为三种情况:
1.Size4
+0x002 SizeIndex : UChar //使用大小>>4
+0x003 UseAffinity : Pos 0, 1 Bit
+0x003 DebugFlags : Pos 1, 2 Bits
+0x003 Flags : UChar
_HEAP_LOCAL_SEGMENT_INFO
ntdll!_HEAP_LOCAL_SEGMENT_INFO
+0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA//对应 _LFH_HEAP->LocalData ,便于从 SegmentInfo 找回 _LFH_HEAP
+0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT//对应已分配的Subsegment,用于管理userblock记录剩余多少chunk、最大分配书等等
+0x010 CachedItems : [16] Ptr64 _HEAP_SUBSEGMENT//_HEAP_SUBSEGMENT array
//存放对应此SegmentInfo且还有可以分配chunk给user的Subsegment
//当ActiveSubsegment⽤完时,将从这里填充,并置换掉ActiveSubsegment
+0x090 SListHeader : _SLIST_HEADER
+0x0a0 Counters : _HEAP_BUCKET_COUNTERS
+0x0a8 LastOpSequence : Uint4B
+0x0ac BucketIndex : Uint2B
+0x0ae LastUsed : Uint2B
+0x0b0 NoThrashCount : Uint2B
其中,cachedItems比较重要,其结构体为_HEAP_SUBSEGMENT:
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO//指向对应的_HEAP_LOCAL_SEGMENT_INFO
+0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER //记录要分配出去的chunk所在位置,开头存储一些metadata来管理这些chunk
+0x010 DelayFreeList : _SLIST_HEADER
+0x020 AggregateExchg : _INTERLOCK_SEQ //用来管理对应的userblock中还有多少freedchunk,LFH以此来判断是否从此userblock中分配
+0x024 BlockSize : Uint2B //此userblock中每个chunk的大小
+0x026 Flags : Uint2B
+0x028 BlockCount : Uint2B //此userblock中chunk的总数
+0x02a SizeIndex : UChar //该userblock对应的sizeindex
+0x02b AffinityIndex : UChar
+0x024 Alignment : [2] Uint4B
+0x02c Lock : Uint4B
+0x030 SFreeListEntry : _SINGLE_LIST_ENTRY
_INTERLOCK_SEQ
ntdll!_INTERLOCK_SEQ
+0x000 Depth : Uint2B //该userblock剩余freechunk的数量
+0x002 Hint : Pos 0, 15 Bits
+0x002 Lock : Pos 15, 1 Bit
+0x002 Hint16 : Uint2B
+0x000 Exchg : Int4B
_HEAP_USERDATA_HEADER
ntdll!_HEAP_USERDATA_HEADER
+0x000 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x000 SubSegment : Ptr64 _HEAP_SUBSEGMENT //指回对应的_HEAP_SUBSEGMENT
+0x008 Reserved : Ptr64 Void
+0x010 SizeIndexAndPadding : Uint4B
+0x010 SizeIndex : UChar
+0x011 GuardPagePresent : UChar
+0x012 PaddingBytes : Uint2B
+0x014 Signature : Uint4B
+0x018 EncodedOffsets : _HEAP_USERDATA_OFFSETS //用于检查Userdata的头部字段
+0x020 BusyBitmap : _RTL_BITMAP_EX //bitmap,用来记录使用的chunk
+0x030 BitmapData : [1] Uint8B
其中的EncodingOffset字段就是个验证,在USERBLOCK初始化时会生成这个数值作为验证用,其数值具体来说是以下四个值的xor:
(sizeof(userblock header)) | (blockunit*0x10 > 4 ((chunk address) - (UserBlock address)) 0x10
的时候,置位_HEAP->CompatibilityFlag |= 0x20000000
,下一次Allocate
就会对LFH进行初始化:
首先会ExtendFrontENdUsageData,也就是将这个数值增大,然后增加更大的_HEAP->BlocksIndex,因为这里_HEAP->BlocksIndex可以理解为一个_HEAP_LIST_LOOKUP结构的单向链表(参考上面Back-End的解释),且默认初始情况下只存在一个管理比较小的(0x0 ~ 0x80)的chunk的_HEAP_LIST_LOOKUP,所以这里会扩展到(0x80 ~ 0x400),即在链表尾追加一个管理更大chunk的_HEAP_LIST_LOOKUP结构体结点。
在 FrontEndHeapUsageData 写上对应的index,此时 enable LFH 范围变为 (idx: 0-0x400)
FrontEndHeapUsageData中分为两部分:对应用于判断LFH是否需要初始化的map以及已经enable LFH的chunk size (例如enable malloc 0x50大小的chunk,则写入0x50>>4=5)
原BlocksIndex进行扩展,即新建一个BlocksIndex,写入原BlocksIndex->ExtendedLookup,进行扩展
建立并初始化_HEAP->FrontEndHeap(通过mmap),即初始化_LFH_HEAP的一些metadata。
建立并初始化_LFH_HEAP->SegmentInfoArrays[x],在SegmentInfoArrays[BucketIndex]处填上对应的_HEAP_LOCAL_SEGMENT_INFO结构体指针。
在初始化后,从LFH分配内存的逻辑为:
1.先看ActiveSubSegment中是否有可以分配的chunk,这个是否有的判断标准就是ActiveSubSegment->AggregateExchg->depth
2.如果没有就从CachedItem中找,找到的话会把ActiveSubSegment换成CachedItem中的SubSegment
到了这一步时,LFH分配器就找到了UserBlock,UserBlock中有很多的chunk可以供用户使用,LFH选取chunk的标准如下:
1.首先从RtlpLowFragHeapRandomData中下标为x处取一个值,这个名字很长的数组是一个长度为256byte的元素大小范围为0-0x7f的随机数数组,每次取,x都会自增1,如果x超过了256,那么x = rand()%256.
2.最终获取的index为RtlLowFragHeapRandomData[x]*maxidx >> 7,检查bitmap是否为0,如果冲突了的话就往后找最近的
3.检查(unused byte & 0x3f)!=0(表示chunk是free的)
4.最后设置index(chunk头部中的previoussize)和unusedbyte返回给用户。
释放
1.将unused位改成0x80
2.根据头部中的字段找到userblock,然后找会Subsegment,根据index设置bitmap
3.更新ActiveSubSegment->AggregateExchg
4.如果释放的chunk不属于当前的ActiveSubSegment就看一下能不能放到cachedItems中,可以就放进去。
利用方式 地址问题先不考虑如何利用的事,首先关注最基本的问题,要泄漏什么地址?地址在哪?
假设我们有了任意内存地址读写,那么我们就需要泄漏一些关键的函数地址,比如说system,以及攻击的目标点,比如栈地址。
不同于linux,windows有一堆dll函数库。
这里,根据angelboy的slide,需要泄漏的地址为kernelbase以及stackaddress,这两个地址在kernel32.dll。
那么如何泄漏ntdll呢?_HEAP_LOCK相关的信息会指向ntdll,具体来说,就是_HEAP->LockVariable.Lock以及CriticalSection->DebugInfo
在ntdll!PebLdr中,_PEB_LDR_DATA可以找到所有dll的位置。
同样可以从IAT表中找到kernel32,不过需要先泄漏binary的地址。
在KERNELBASE!BasepFilterInfo中,会有大概率包含stack的指针,这个主要是因为内存没有初始化。
如果这个上面没有想要的地址,可以从PEB向后算一个page,通常会是TEB上,这上面也会有stack的地址信息。
攻击的话,angelboy提出的方式就是泄漏地址,然后攻击栈写rop或者shellcode。
后端利用方式 unlink和linux中的unlink很像(都是双向链表的节点移除),但是绕过条件和linux不同,因为头部的信息不同,需要对一些encode的字段构造一下。还有就是flink和blink指向的是userdata部分。
具体构造就是p -> fd = &p-8, p->bk = &p.
前端利用方式angelboy同样是只是草草的介绍了下如果有了uaf的话,如何绕过随机在LFHuserblock中分配到指定chunk的方式,具体来说就是填满其他的,下一次肯定就会落到目标点。那么有了uaf之后呢,劫持哪些指针劫持到哪里并没有说明。所以这里的话还需要后续调试的时候整理。
具体怎么攻击才叫合理?哪些攻击面呢?由于Angelboy给的利用方式太少,而且比较笼统局限,所以我又参考了别的资料,想找到一些类似于linux堆利用手法的攻击方式。
然而现实打了我一巴掌,根据冠城大佬的ppt,在windows中,想通过攻击堆的头部或者其他字段来进行getshell几乎不可能,因为windows堆的防御机制十分严格。堆中比较合理的攻击手法似乎就只有unlink或者其他形式的修改函数指针的方式。
戳“阅读原文”体验免费靶场!