最近在學(xué)蒸米的《一步一步學(xué)ROP之linux_x86篇》渗常,內(nèi)容寫的很詳細(xì),個(gè)人學(xué)到了很多屡限,但同時(shí)學(xué)的過(guò)程中也有很多疑惑兼耀,然后文章有些地方也有些小錯(cuò)誤压昼,所以在這里自己再補(bǔ)充并修正一些方便自己日后回顧學(xué)習(xí)。如果有冒犯到博主在這里說(shuō)聲抱歉瘤运。(如侵權(quán)刪)
原文章地址:https://yq.aliyun.com/articles/58699
一窍霞、序
ROP的全稱為Return-oriented programming(返回導(dǎo)向編程),這是一種高級(jí)的內(nèi)存攻擊技術(shù)可以用來(lái)繞過(guò)現(xiàn)代操作系統(tǒng)的各種通用防御(比如內(nèi)存不可執(zhí)行和代碼簽名等)拯坟。雖然現(xiàn)在大家都在用64位的操作系統(tǒng)但金,但是想要扎實(shí)的學(xué)好ROP還是得從基礎(chǔ)的x86系統(tǒng)開始,但看官請(qǐng)不要著急郁季,在隨后的教程中我們還會(huì)帶來(lái)linux_x64以及android (arm)方面的ROP利用方法冷溃,歡迎大家繼續(xù)學(xué)習(xí)。
小編備注:文中涉及代碼可在文章最后的github鏈接找到梦裂。
二似枕、Control Flow Hijack 程序流劫持
比較常見(jiàn)的程序流劫持就是棧溢出贮聂,格式化字符串攻擊和堆溢出了槽卫。通過(guò)程序流劫持,攻擊者可以控制PC指針從而執(zhí)行目標(biāo)代碼拦键。為了應(yīng)對(duì)這種攻擊,系統(tǒng)防御者也提出了各種防御方法毅往,最常見(jiàn)的方法有DEP,NX(堆棧不可執(zhí)行)牵咙,ASLR(內(nèi)存地址隨機(jī)化),Stack Protector(棧保護(hù))等攀唯〗嘧溃可以在linux下使用命令
checksec + 目標(biāo)文件
查看文件所使用的防御
但是如果上來(lái)就部署全部的防御,初學(xué)者可能會(huì)覺(jué)得無(wú)從下手侯嘀,所以我們先從最簡(jiǎn)單的沒(méi)有任何保護(hù)的程序開始另凌,隨后再一步步增加各種防御措施,接著再學(xué)習(xí)繞過(guò)的方法戒幔,循序漸進(jìn)吠谢。
首先來(lái)看這個(gè)有明顯緩沖區(qū)溢出的程序:
這里我們用
gcc -fno-stack-protector -z execstack -o level1 level1.c
這個(gè)命令編譯程序。-fno-stack-protector和-z execstack這兩個(gè)參數(shù)會(huì)分別關(guān)掉Stack Protector和NX(linux下開啟了NX的以及windows下開啟了DEP的程序堆棧是不可執(zhí)行的诗茎,linux下的程序默認(rèn)編譯時(shí)是開啟了NX的)工坊。同時(shí)我們?cè)趕hell中執(zhí)行:
這幾個(gè)指令。執(zhí)行完后我們就關(guān)掉整個(gè)linux系統(tǒng)的ASLR保護(hù)敢订。
接下來(lái)我們開始對(duì)目標(biāo)程序進(jìn)行分析王污。首先我們先來(lái)確定溢出點(diǎn)的位置,這里我推薦使用pattern.py這個(gè)腳本來(lái)進(jìn)行計(jì)算楚午。我們使用如下命令:
來(lái)生成一串測(cè)試用的150個(gè)字節(jié)的字符串:
隨后我們使用gdb ./level1調(diào)試程序昭齐。
我們可以得到內(nèi)存出錯(cuò)的地址為0x37654136。隨后我們使用命令:
就可以非常容易的計(jì)算出PC返回值的覆蓋點(diǎn)為140個(gè)字節(jié)矾柜。我們只要構(gòu)造一個(gè)”A”*140+ret字符串阱驾,就可以讓pc執(zhí)行ret地址上的代碼了。
接下來(lái)我們需要一段shellcode把沼,可以用msf生成啊易,或者自己反編譯一下吁伺。
這里我們使用一段最簡(jiǎn)單的執(zhí)行execve ("/bin/sh")命令的語(yǔ)句作為shellcode饮睬。
溢出點(diǎn)有了,shellcode有了篮奄,下一步就是控制PC跳轉(zhuǎn)到shellcode的地址上:
[shellcode][“AAAAAAAAAAAAAA”….][ret]
^------------------------------------------------|
對(duì)初學(xué)者來(lái)說(shuō)這個(gè)shellcode地址的位置其實(shí)是一個(gè)坑捆愁。因?yàn)檎5乃季S是使用gdb調(diào)試目標(biāo)程序,然后查看內(nèi)存來(lái)確定shellcode的位置窟却。但當(dāng)你真的執(zhí)行exp的時(shí)候你會(huì)發(fā)現(xiàn)shellcode壓根就不在這個(gè)地址上昼丑!這是為什么呢?原因是gdb的調(diào)試環(huán)境會(huì)影響buf在內(nèi)存中的位置夸赫,雖然我們關(guān)閉了ASLR菩帝,但這只能保證buf的地址在gdb的調(diào)試環(huán)境中不變,但當(dāng)我們直接執(zhí)行./level1的時(shí)候,buf的位置會(huì)固定在別的地址上呼奢。怎么解決這個(gè)問(wèn)題呢宜雀?
最簡(jiǎn)單的方法就是開啟core dump這個(gè)功能。
開啟之后握础,當(dāng)出現(xiàn)內(nèi)存錯(cuò)誤的時(shí)候辐董,系統(tǒng)會(huì)生成一個(gè)core dump文件在tmp目錄下。然后我們?cè)儆胓db查看這個(gè)core文件就可以獲取到buf真正的地址了禀综。
因?yàn)橐绯鳇c(diǎn)是140個(gè)字節(jié)简烘,再加上4個(gè)字節(jié)的ret地址,我們可以計(jì)算出buffer的地址為$esp-144定枷。通過(guò)gdb的命令 “x/10s $esp-144”孤澎,我們可以得到buf的地址為0xbffff290。
OK欠窒,現(xiàn)在溢出點(diǎn)亥至,shellcode和返回值地址都有了,可以開始寫exp了贱迟。寫exp的話姐扮,我強(qiáng)烈推薦pwntools這個(gè)工具,因?yàn)樗梢苑浅7奖愕淖龅奖镜卣{(diào)試和遠(yuǎn)程攻擊的轉(zhuǎn)換衣吠。本地測(cè)試成功后只需要簡(jiǎn)單的修改一條語(yǔ)句就可以馬上進(jìn)行遠(yuǎn)程攻擊茶敏。
最終本地測(cè)試代碼如下:
執(zhí)行exp:
接下來(lái)我們把這個(gè)目標(biāo)程序作為一個(gè)服務(wù)綁定到服務(wù)器的某個(gè)端口上,這里我們可以使用socat這個(gè)工具來(lái)完成缚俏,命令如下:
隨后這個(gè)程序的IO就被重定向到10001這個(gè)端口上了(ctrl+c關(guān)閉)惊搏,并且可以使用 nc 127.0.0.1 10001來(lái)訪問(wèn)我們的目標(biāo)程序服務(wù)了。
因?yàn)楝F(xiàn)在目標(biāo)程序是跑在socat的環(huán)境中忧换,exp腳本除了要把p = process('./level1')換成p = remote('127.0.0.1',10001) 之外恬惯,ret的地址還會(huì)發(fā)生改變。解決方法還是采用生成core dump的方案亚茬,然后用gdb調(diào)試core文件獲取返回地址酪耳。然后我們就可以使用exp進(jìn)行遠(yuǎn)程溢出啦!
三刹缝、Ret2libc – Bypass NX 通過(guò)ret2libc繞過(guò)NX防護(hù)
現(xiàn)在我們把NX打開碗暗,依然關(guān)閉stack protector和ASLR。編譯方法如下:
gcc -fno-stack-protector -o level1 level1.c
這時(shí)候我們?nèi)绻褂胠evel1的exp來(lái)進(jìn)行測(cè)試的話梢夯,系統(tǒng)會(huì)拒絕執(zhí)行我們的shellcode言疗。如果你通過(guò)sudo cat /proc/[pid]/maps查看,你會(huì)發(fā)現(xiàn)level1的stack是rwx的颂砸,但是level2的stack卻是rw的噪奄。
level1: bffdf000-c0000000 rw-p 00000000 00:00 0 [stack]
level2: bffdf000-c0000000 rwxp 00000000 00:00 0 [stack]
那么如何執(zhí)行shellcode呢死姚?我們知道level2調(diào)用了libc.so,并且libc.so里保存了大量可利用的函數(shù)勤篮,我們?nèi)绻梢宰尦绦驁?zhí)行system(“/bin/sh”)的話知允,也可以獲取到shell。既然思路有了叙谨,那么接下來(lái)的問(wèn)題就是如何得到system()這個(gè)函數(shù)的地址以及”/bin/sh”這個(gè)字符串的地址温鸽。
如果關(guān)掉了ASLR的話,system()函數(shù)在內(nèi)存中的地址是不會(huì)變化的手负,并且libc.so中也包含”/bin/sh”這個(gè)字符串涤垫,并且這個(gè)字符串的地址也是固定的。那么接下來(lái)我們就來(lái)找一下這個(gè)函數(shù)的地址竟终。這時(shí)候我們可以使用gdb進(jìn)行調(diào)試蝠猬。然后通過(guò)print和find命令來(lái)查找system和”/bin/sh”字符串(查找/bin/sh的時(shí)候直接用find "/bin/sh"命令)的地址。
我們首先在main函數(shù)上下一個(gè)斷點(diǎn)统捶,然后執(zhí)行程序榆芦,這樣的話程序會(huì)加載libc.so到內(nèi)存中,然后我們就可以通過(guò)”print system”這個(gè)命令來(lái)獲取system函數(shù)在內(nèi)存中的位置喘鸟,隨后我們可以通過(guò)” print __libc_start_main”這個(gè)命令來(lái)獲取libc.so在內(nèi)存中的起始位置匆绣,接下來(lái)我們可以通過(guò)find命令來(lái)查找”/bin/sh”這個(gè)字符串。這樣我們就得到了system的地址0xb7e5f460以及"/bin/sh"的地址0xb7f81ff8什黑。下面我們開始寫exp:
要注意的是system()后面跟的是執(zhí)行完system函數(shù)后要返回地址崎淳,接下來(lái)才是”/bin/sh”字符串的地址。因?yàn)槲覀儓?zhí)行完后也不打算干別的什么事愕把,所以我們就隨便寫了一個(gè)0xdeadbeef作為返回地址拣凹。下面我們測(cè)試一下exp:
OK。測(cè)試成功恨豁。
四嚣镜、ROP– Bypass NX and ASLR 通過(guò)ROP繞過(guò)NX和ASLR防護(hù)
接下來(lái)我們打開ASLR保護(hù)。
sudo -s
echo 2 > /proc/sys/kernel/randomize_va_space
exit
現(xiàn)在我們?cè)倩仡^測(cè)試一下level2的exp橘蜜,發(fā)現(xiàn)已經(jīng)不好用了菊匿。
如果你通過(guò)sudo cat /proc/[pid]/maps或者ldd查看,你會(huì)發(fā)現(xiàn)level2的libc.so地址每次都是變化的扮匠。
那么如何解決地址隨機(jī)化的問(wèn)題呢捧请?思路是:我們需要先泄漏出libc.so某些函數(shù)在內(nèi)存中的地址,然后再利用泄漏出的函數(shù)地址根據(jù)偏移量計(jì)算出system()函數(shù)和/bin/sh字符串在內(nèi)存中的地址棒搜,然后再執(zhí)行我們的ret2libc的shellcode。既然棧活箕,libc力麸,heap的地址都是隨機(jī)的。我們?cè)趺床拍苄孤冻鰈ibc.so的地址呢?方法還是有的克蚂,因?yàn)槌绦虮旧碓趦?nèi)存中的地址并不是隨機(jī)的闺鲸,如圖所示:
Linux內(nèi)存隨機(jī)化分布圖
所以我們只要把返回值設(shè)置到程序本身就可執(zhí)行我們期望的指令了。首先我們利用objdump來(lái)查看可以利用的plt函數(shù)和函數(shù)對(duì)應(yīng)的got表:
我們發(fā)現(xiàn)除了程序本身的實(shí)現(xiàn)的函數(shù)之外埃叭,我們還可以使用read@plt()和write@plt()函數(shù)摸恍。但因?yàn)槌绦虮旧聿](méi)有調(diào)用system()函數(shù),所以我們并不能直接調(diào)用system()來(lái)獲取shell赤屋。但其實(shí)我們有write@plt()函數(shù)就夠了立镶,因?yàn)槲覀兛梢酝ㄟ^(guò)write@plt ()函數(shù)把write()函數(shù)在內(nèi)存中的地址也就是write.got給打印出來(lái)。既然write()函數(shù)實(shí)現(xiàn)是在libc.so當(dāng)中类早,那我們調(diào)用的write@plt()函數(shù)為什么也能實(shí)現(xiàn)write()功能呢? 這是因?yàn)閘inux采用了延時(shí)綁定技術(shù)媚媒,當(dāng)我們調(diào)用write@plit()的時(shí)候,系統(tǒng)會(huì)將真正的write()函數(shù)地址link到got表的write.got中涩僻,然后write@plit()會(huì)根據(jù)write.got 跳轉(zhuǎn)到真正的write()函數(shù)上去缭召。(如果還是搞不清楚的話,推薦閱讀《程序員的自我修養(yǎng) - 鏈接逆日、裝載與庫(kù)》這本書)
因?yàn)閟ystem()函數(shù)和write()在libc.so中的offset(相對(duì)地址)是不變的嵌巷,所以如果我們得到了write()的地址并且擁有目標(biāo)服務(wù)器上的libc.so就可以計(jì)算出system()在內(nèi)存中的地址了。然后我們?cè)賹c指針return回vulnerable_function()函數(shù)室抽,就可以進(jìn)行ret2libc溢出攻擊晴竞,并且這一次我們知道了system()在內(nèi)存中的地址,就可以調(diào)用system()函數(shù)來(lái)獲取我們的shell了狠半。
使用ldd命令可以查看目標(biāo)程序調(diào)用的so庫(kù)噩死。隨后我們把libc.so拷貝到當(dāng)前目錄,因?yàn)槲覀兊膃xp需要這個(gè)so文件來(lái)計(jì)算相對(duì)地址:
最后exp如下:
這里對(duì)payload1解釋一下
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(got_write) + p32(4)
#p32(1) + p32(got_write) + p32(4)其實(shí)在調(diào)用write函數(shù)
#write用法:ssize_t write(int fd, const void *buf, size_t nbyte);
#fd:[文件描述符] fd=0時(shí)為輸入,fd=1時(shí)為輸出,fd=2為報(bào)錯(cuò)
#buf:指定的緩沖區(qū)神年,即[指針]已维,指向一段內(nèi)存單元;
#nbyte:要寫入文件指定的字節(jié)數(shù)已日;返回值:寫入文檔的字節(jié)數(shù)(成功)垛耳;-1(出錯(cuò))
#p32(1) + p32(got_write) + p32(4)相當(dāng)于傳進(jìn)去fd=1(輸出),buf= p32(got_write) 飘千,nbyte=4 最終就是為了打印出write()的地址
接著我們使用socat把level2綁定到10003端口:
最后執(zhí)行我們的exp:
五堂鲜、小結(jié)
本章簡(jiǎn)單介紹了ROP攻擊的基本原理,由于篇幅原因护奈,我們會(huì)在隨后的文章中會(huì)介紹更多的攻擊技巧:如何利用工具尋找gadgets缔莲,如何在不知道對(duì)方libc.so版本的情況下計(jì)算offset;如何繞過(guò)Stack Protector等霉旗。歡迎大家到時(shí)繼續(xù)學(xué)習(xí)痴奏。另外本文提到的所有源代碼和工具都可以從作者的github下載:https://github.com/zhengmin1989/ROP_STEP_BY_STEP
六蛀骇、參考文獻(xiàn)
The geometry of innocent flesh on the bone: return-into-libc without function calls (on the x86)
picoCTF 2013: https://github.com/picoCTF/2013-Problems
Smashing The Stack For Fun And Profit: http://phrack.org/issues/49/14.html
程序員的自我修養(yǎng)
ROP輕松談