极为详细:双重释放漏洞调试分析
看雪论坛作者ID:LarryS
1
简介
对于这种情况,我根据函数调用栈帧进行了详细地调试,分析了产生上述结果的原因。经过此次调试学习,对于heap的结构以及windows引入的安全机制有了更深的理解,同时也学习到了更多调试内存异常的方法,相信对于之后的漏洞调试分析会有很大帮助。
2
一个思维误区
3
双重释放为何会引发异常
3.1 实验代码
#include <stdio.h>
#include "windows.h"
int main (int argc, char *argv[])
{
void *p1,*p2,*p3;
char* test_str = "aaaaaaaa";
//__asm int 3
p1 = (char *)malloc(100);
printf("Alloc p1:%p\n",p1);
strncpy(p1, test_str, strlen(test_str));
p2 = (char *)malloc(100);
printf("Alloc p2:%p\n",p2);
strncpy(p2, test_str, strlen(test_str));
p3 = (char *)malloc(100);
printf("Alloc p3:%p\n",p3);
strncpy(p3, test_str, strlen(test_str));
printf("Free p1\n");
free(p1);
printf("Free p3\n");
free(p3);
printf("Free p2\n");
free(p2);
printf("Double Free p2\n");
free(p2);
//printf("Triple Free p2\n");
//free(p2);
return 0;
}
3.2 追踪
(b7c.830): Access violation - code c0000005 (!!! second chance !!!)
eax=00000dac ebx=002d0678 ecx=00000665 edx=002d0564 esi=002d0678 edi=002d0000
eip=7792434c esp=0018fdbc ebp=0018fde4 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
ntdll!RtlpCoalesceFreeBlocks+0x47c:
7792434c 66894cc604 mov word ptr [esi+eax*8+4],cx ds:002b:002d73dc=????
0:000> kb
ChildEBP RetAddr Args to Child
0018fde4 77923407 0000007f 002d06f0 0018feac ntdll!RtlpCoalesceFreeBlocks+0x47c
0018fedc 779232f2 002d06f0 002d06f8 002d06f8 ntdll!RtlpFreeHeap+0x1f4
0018fefc 772014d1 002d0000 00000000 002d06f8 ntdll!RtlFreeHeap+0x142
0018ff10 004011a7 002d0000 00000000 002d06f8 kernel32!HeapFree+0x14
0018ff2c 00401138 002d06f8 00408030 002d0770 double_free!free+0x66
0018ff48 004014c2 00000001 002d0e38 002d0e90 double_free!main+0x138 [C:\Users\test\Documents\ldzz\double_free\double_free.c @ 33]
0018ff88 77203677 7efde000 0018ffd4 77929d72 double_free!mainCRTStartup+0xb4
0018ff94 77929d72 7efde000 7fa803bd 00000000 kernel32!BaseThreadInitThunk+0xe
0018ffd4 77929d45 0040140e 7efde000 00000000 ntdll!__RtlUserThreadStart+0x70
0018ffec 00000000 0040140e 7efde000 00000000 ntdll!_RtlUserThreadStart+0x1b
0:000> !heap -x 2d06f8
List corrupted: (Blink->Flink = 002d3178) != (Block = 002d0680)
HEAP 002d0000 (Seg 002d0000) At 002d0678 Error: block list entry corrupted
HEAP 002d0000 (Seg 002d0000) At 002d73d8 Error: invalid block size
Entry User Heap Segment Size PrevSize Unused Flags
-----------------------------------------------------------------------------
002d0678 002d0680 002d0000 002d0000 6d60 108 0 free
0:000> !address 2d73d8
Failed to map Heaps (error 80004005)
Usage: <unclassified>
Allocation Base: 002d0000
Base Address: 002d5000
End Address: 002e0000
Region Size: 0000b000
Type: 00020000 MEM_PRIVATE
State: 00002000 MEM_RESERVE
Protect: 00000000
0:000> !heap -a -h 2d0000
Index Address Name Debugging options enabled
2: 002d0000
Segment at 002d0000 to 002e0000 (00005000 bytes committed)
Flags: 00001003
ForceFlags: 00000001
Granularity: 8 bytes
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200
......
(b7c.830): Access violation - code c0000005 (!!! second chance !!!)
eax=00000dac ebx=002d0678 ecx=00000665 edx=002d0564 esi=002d0678 edi=002d0000
eip=7792434c esp=0018fdbc ebp=0018fde4 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
ntdll!RtlpCoalesceFreeBlocks+0x47c:
7792434c 66894cc604 mov word ptr [esi+eax*8+4],cx ds:002b:002d73dc=????
3.3 溯源
0:000> p
eax=000306f8 ebx=7efde000 ecx=0040a110 edx=0008e3b8 esi=00000000 edi=00000000
eip=0040111e esp=0018ff28 ebp=0018ff48 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
double_free!main+0x11e:
0040111e e825000000 call double_free!free (00401148)
0:000> dd esp L1
0018ff28 000306f8
0:000> bp RtlpCoalesceFreeBlocks
0:000> g
Breakpoint 0 hit
eax=0018fea0 ebx=00000000 ecx=779bef0f edx=00000000 esi=000306f0 edi=00030000
eip=779230cf esp=0018fddc ebp=0018fed0 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ntdll!RtlpCoalesceFreeBlocks:
779230cf 8bff mov edi,edi
0:000> dd esp L5
0018fddc 77923407 00030000 000306f0 0018fea0
0018fdec 00000000
PHEAP_FREE_ENTRY
RtlpCoalesceFreeBlocks(
IN PHEAP Heap,
IN PHEAP_FREE_ENTRY FreeBlock,
IN OUT PULONG FreeSize,
IN BOOLEAN RemoveFromFreeList
);
0:000> dd 18fea0 L1
0018fea0 0000000f
0:000> dd 306f0 L2
000306f0 d501000f 08000028
ntdll!RtlpCoalesceFreeBlocks:
779230cf 8bff mov edi,edi
779230d1 55 push ebp
779230d2 8bec mov ebp,esp
779230d4 83ec1c sub esp,1Ch
779230d7 53 push ebx
779230d8 8b5d0c mov ebx,dword ptr [ebp+0Ch] // 第二个参数306f0 要释放的堆块
779230db 0fb74304 movzx eax,word ptr [ebx+4] // 相邻的上一个堆块的大小
779230df 56 push esi
779230e0 57 push edi
779230e1 8b7d08 mov edi,dword ptr [ebp+8] // 第一个参数 30000
779230e4 0fb74f54 movzx ecx,word ptr [edi+54h] // Heap->Encoding.PreviousSize 编码值
779230e8 33c1 xor eax,ecx // Vista之后引入了对heap_entry的编码,这里在进行解码
779230ea c1e003 shl eax,3 // size是以8字节为单位的,这里*8,得到字节数0x78
779230ed 8bf3 mov esi,ebx
779230ef 2bf0 sub esi,eax // 0x306f0-0x78=0x30678,就是p1的位置
779230f1 3bf3 cmp esi,ebx
779230f3 7417 je ntdll!RtlpCoalesceFreeBlocks+0x481 (7792310c)
779230f5 8b474c mov eax,dword ptr [edi+4Ch] // Heap->EncodeFlagMask
779230f8 8bc8 mov ecx,eax
779230fa c1e914 shr ecx,14h
779230fd 224f52 and cl,byte ptr [edi+52h] // Heap->Encoding.Flags
77923100 324e02 xor cl,byte ptr [esi+2] // pre_block->Flags 获得解码后的前一个堆块的Flags值
77923103 f6c101 test cl,1 // 检查是不是空闲
77923106 0f84f5100000 je ntdll!RtlpCoalesceFreeBlocks+0x41 (77924201)
77924225 8b560c mov edx,dword ptr [esi+0Ch] // pre_block->FreeList.Blink
77924228 8d4608 lea eax,[esi+8]
7792422b 8b08 mov ecx,dword ptr [eax] // pre_block->FreeList.Flink
7792422d 894df4 mov dword ptr [ebp-0Ch],ecx
77924230 8b4904 mov ecx,dword ptr [ecx+4] // pre_block->FreeList.Flink -> Blink
77924233 8955ec mov dword ptr [ebp-14h],edx
77924236 8b12 mov edx,dword ptr [edx] // pre_block->FreeList.Blink -> Flink
77924238 3bd1 cmp edx,ecx // 这里正常应该是相等的,都指向前一个堆块
0:000> bp 7792430b
0:000> g
Breakpoint 1 hit
eax=00030138 ebx=000306f0 ecx=00033178 edx=00030564 esi=00030678 edi=00030000
eip=7792430b esp=0018fdb0 ebp=0018fdd8 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!RtlpCoalesceFreeBlocks+0x334:
7792430b 8b45f4 mov eax,dword ptr [ebp-0Ch] ss:002b:0018fdcc=00033178
7792430b 8b45f4 mov eax,dword ptr [ebp-0Ch] // pre_block->FreeList.Flink
7792430e 8b4dec mov ecx,dword ptr [ebp-14h] // pre_block->FreeList.Blink
77924311 8901 mov dword ptr [ecx],eax
77924313 894804 mov dword ptr [eax+4],ecx // 上面这两句把上一堆块(p1)从双向链表卸下来了
77924316 f6460208 test byte ptr [esi+2],8 // 检查p1的flags是否设置了0x8
7792431a 0f859ac10100 jne ntdll!RtlpCoalesceFreeBlocks+0x349 (779404ba) // 设置就跳转
77924320 8a4602 mov al,byte ptr [esi+2] // 未设置8,执行到这里
77924323 a804 test al,4 // 检查p1的flags值是否设置了0x4
77924325 0f859f5d0500 jne ntdll!RtlpCoalesceFreeBlocks+0x3d9 (7797a0ca) // 设置就跳转
0x01 Indicates that the allocation is being used by the application or the heap manager
0x04 Indicates whether the heap block has a fill pattern associated with it
0x08 Indicates that the heap block was allocated directly from the virtual memory manager
0x10 Indicates that this is the last heap block prior to an uncommitted range
7792432b 0fb70e movzx ecx,word ptr [esi] // p1的大小
7792432e 8b4510 mov eax,dword ptr [ebp+10h] // 函数的第三个参数 *FreeSize 0018fea0,里面保存的是释放堆块的大小
77924331 c6460200 mov byte ptr [esi+2],0 // pre_block->Flags = 0
77924335 c6460700 mov byte ptr [esi+7],0 // pre_block->UnusedBytes = 0
77924339 0108 add dword ptr [eax],ecx // 释放堆块大小 + p1的大小,两个堆块合并后的大小
7792433b 668b08 mov cx,word ptr [eax]
7792433e 66890e mov word ptr [esi],cx // 更新p1的大小为合并后的大小
77924341 668b08 mov cx,word ptr [eax]
77924344 66334f54 xor cx,word ptr [edi+54h] // 这里在对大小的数值进行编码
77924348 8b00 mov eax,dword ptr [eax]
7792434a 8bde mov ebx,esi
7792434c 66894cc604 mov word ptr [esi+eax*8+4],cx // 注意这里
// esi: 指向p1堆块
// eax: 更新后未编码的p1堆块大小
3.4 总结一下
4
继续溯源 - 出现三种结果的原因
7792313a 8b474c mov eax,dword ptr [edi+4Ch] // Heap->EncodeFlagMask
7792313d c1e814 shr eax,14h
77923140 224752 and al,byte ptr [edi+52h] // Heap->Encoding.Flags
77923143 324602 xor al,byte ptr [esi+2] // next_block->Flags
77923146 a801 test al,1 // 检查是不是空闲堆块
77923148 0f855f010000 jne ntdll!RtlpCoalesceFreeBlocks+0x91d (779232ad)
while ( ((next_block->Flags ^ Heap->Encoding.Flags & (Heap->EncodeFlagMask >> 20)) & 1) == 0 )// 验证flags数值,这一数值位于next_block的前四个字节中
{
if ( Heap->EncodeFlagMask )
{
next_block->InterceptorValue ^= Heap->Encoding.InterceptorValue;// 解码操作,这里会改变next_block前四个字节的数据
if ( next_block->SmallTagIndex != (next_block->Flags ^ (LOBYTE(next_block->Size) ^ HIBYTE(next_block->Size))) )
RtlpAnalyzeHeapFailure(Heap, next_block, 0);
}
if ( !RemoveFromFreeList )
goto LABEL_11; // 这里就跳转了
v68 = pre_block_->FreeList.Flink;
v52 = v68->Blink;
v72 = pre_block_->FreeList.Blink;
v53 = v72->Flink;
if... // 这里在进行堆块合并,程序执行不到这里
RtlpLogHeapFailure(12, Heap, &pre_block_->FreeList, v52, v53, 0);
LABEL_186:
RemoveFromFreeList = 0;
LABEL_11:
v66 = next_block->FreeList.Flink;
v11 = v66->Blink;
v69 = next_block->FreeList.Blink;
v12 = *v69;
if... // 验证双向链表指针,一定会失败
}
5
关于p2 self size的疑问
5.1 调试
0:000> ba r4 306f0
0:000> dd 306f0 L2
000306f0 23300bc3 08000028
0:000> g
Breakpoint 0 hit
eax=2d310bcc ebx=00000000 ecx=00030000 edx=00000003 esi=000306f0 edi=00030000
eip=77928107 esp=0018fdf0 ebp=0018fed0 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!RtlpFreeHeap+0x166:
77928107 8a4602 mov al,byte ptr [esi+2] ds:002b:000306f2=01
0:000> dd 306f0 L2
000306f0 0e01000f 08000028
779233ef 0fb706 movzx eax,word ptr [esi] // 获取RtlpCoalesceFreeBlocks 的第三个参数FreeSize
779233f2 8945d0 mov dword ptr [ebp-30h],eax
779233f5 f6474080 test byte ptr [edi+40h],80h
779233f9 7514 jne ntdll!RtlpFreeHeap+0x1fc (7792340f)
779233fb 53 push ebx
779233fc 8d45d0 lea eax,[ebp-30h]
779233ff 50 push eax
77923400 56 push esi
77923401 57 push edi
77923402 e8c8fcffff call ntdll!RtlpCoalesceFreeBlocks (779230cf)
0:000> bp free
0:000> bl
0 e 000306f0 r 4 0001 (0001) 0:****
1 e 00401148 0001 (0001) 0:**** double_free!free
0:000> g
Breakpoint 1 hit
eax=000306f8 ebx=7efde000 ecx=0040a110 edx=0008e3b8 esi=00000000 edi=00000000
eip=00401148 esp=0018ff24 ebp=0018ff48 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
double_free!free:
00401148 55 push ebp
0:000> g
Breakpoint 0 hit
eax=2d310bcc ebx=00000000 ecx=00030000 edx=00000003 esi=000306f0 edi=00030000
eip=77928107 esp=0018fdf0 ebp=0018fed0 iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
ntdll!RtlpFreeHeap+0x166:
77928107 8a4602 mov al,byte ptr [esi+2] ds:002b:000306f2=30
0:000> dd 306f0 L2
000306f0 23300bc3 08000028
77928105 3106 xor dword ptr [esi],eax // 这里在进行“解码”
77928107 8a4602 mov al,byte ptr [esi+2] // FreeBlock->Flags
7792810a 324601 xor al,byte ptr [esi+1] // HIBYTE(FreeBlock->Size)
7792810d 3206 xor al,byte ptr [esi] // LOBYTE(FreeBlock->Size)
7792810f 384603 cmp byte ptr [esi+3],al // FreeBlock->SmallTagIndex
77928112 0f84b9b2ffff je ntdll!RtlpFreeHeap+0x17b (779233d1) // 验证通过,进行跳转
77928118 e987280500 jmp ntdll!RtlpFreeHeap+0x173 (7797a9a4)
if ( FreeBlock->SmallTagIndex != (LOBYTE(FreeBlock->Size) ^ (HIBYTE(FreeBlock->Size) ^ FreeBlock->Flags)) )
779beea8 8a4802 mov cl,byte ptr [eax+2] // FreeBlock->Flags
779beeab 324801 xor cl,byte ptr [eax+1] // HIBYTE(FreeBlock->Size)
779beeae 3208 xor cl,byte ptr [eax] // LOBYTE(FreeBlock->Size)
779beeb0 884803 mov byte ptr [eax+3],cl // 重新设置FreeBlock->SmallTagIndex
779beeb3 8b4f50 mov ecx,dword ptr [edi+50h]
779beeb6 3108 xor dword ptr [eax],ecx ds:002b:000306f0=f8300bc3 // 重新对前四个字节进行编码
0:000> dd 306f0 L2
000306f0 f8300bc3 08000028
0:000> p
eax=000306f0 ebx=d90000d9 ecx=2d310bcc edx=00000003 esi=00000000 edi=00030000
eip=779beeb8 esp=0018fda4 ebp=0018fddc iopl=0 nv up ei ng nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000286
ntdll!RtlpAnalyzeHeapFailure+0x209:
779beeb8 8b4de4 mov ecx,dword ptr [ebp-1Ch] ss:002b:0018fdc0=00000000
0:000> dd 306f0 L2
000306f0 d501000f 08000028
5.2 小总结
p2第一次释放:23300bc3 -> 0e01000f 原因:解码操作,可以通过验证
p2第二次释放:0e01000f -> 23300bc3 原因:解码操作,无法通过验证
23300bc3 -> f8300bc3 原因:验证失败,调用RtlpAnalyzeHeapFailure,进行修复
f8300bc3 -> d501000f 原因:RtlpAnalyzeHeapFailure在修复后重新进行了编码
p2第三次释放:d501000f -> f8300bc3 原因:解码操作,可以通过验证
6
总结
p2第一次释放:
一切正常,p1, p2, p3以及后方空闲堆块合并成一个大的空闲堆块P,此时P具有正确的堆块大小S
p2第二次释放:
RtlpFreeHeap:解码操作后,p2具有错误的self size,且前四个字节无法通过验证,调用->
RtlpAnalyzeHeapFailure:修复前四个字节以通过验证,并重新进行了编码,并没有再次进行解码,直接调用->
RtlpCoalesceFreeBlocks:该函数的FreeSize参数直接从p2起始位置获取,经过修正后,p2的self size正常
空闲堆块P具有了新的堆块大小 S' = S + p2.self_size
此时S'已经不正确,但并未超过可访问范围
特殊情况:如果根据S'计算得到的堆块位置处,解码前后进行空闲堆块判断的结果都是“空闲”的,那么程序会陷入无限循环中。
p3第三次释放:
RtlpFreeHeap:解码操作后,p2具有错误的self size。但由于之前的修正操作,前四个字节可以通过验证,调用->
RtlpCoalesceFreeBlocks:此时p2的self size是一个很大的错误的值
空闲堆块P具有了新的堆块大小 S'' = S' + p2.self_size
由于p2.self_size过大,S''超过了可访问范围,触发异常
7
一个不成熟的小想法
8
参考资料
2、VC6 Release下,生成pdb文件(https://www.cnblogs.com/vcerror/p/4289211.html)
3、Heap Corruption: A Case Study (强烈推荐阅读!)(https://bbs.pediy.com/thread-268866.htm)
4、Vista 数据结构(https://www.nirsoft.net/kernel_struct/vista/)
5、heap.c源码(https://github.com/ZoloZiak/WinNT4/blob/master/private/ntos/rtl/heap.c)
6、Low-fragmentation Heap(https://docs.microsoft.com/en-us/windows/win32/memory/low-fragmentation-heap)
7、Understanding the Low Fragmentation Heap(http://illmatics.com/Understanding_the_LFH_Slides.pdf)
8、<Advanced windows debugging>(https://cdn.ttgtmedia.com/searchWinDevelopment/downloads/advanced_windows_debugging.pdf)
看雪ID:LarryS
https://bbs.pediy.com/user-home-600394.htm
# 往期推荐
1. 新人PWN入坑总结
4. V8利用初探 2019 StarCTF oob 复现分析
5. 新人PWN入坑总结
6. 数据库注入wp分析心得
球分享
球点赞
球在看
点击“阅读原文”,了解更多!