目錄
- 課程教學
- C語言函數調用棧
- ret2text
- PWN工具
課程教學
課程鏈接:https://www.bilibili.com/video/BV1854y1y7Ro/?vd_source=7b06bd7a9dd90c45c5c9c44d12e7b4e6
課程附件: https://pan.baidu.com/s/1vRCd4bMkqnqqY1nT2uhSYw 提取碼: 5rx6
C語言函數調用棧
一個棧幀保存的是一個函數的狀態信息,父函數每調用一個子函數就會在函數調用棧中新增一個棧幀;
32 x86 esp
64 x86 rsp
ebp 棧底
esp 棧頂
stack frame pointer記錄上一個父函數的棧頂指針的值,便于恢復父函數;
發生棧溢出的地方在local variables【上圖是32位情況】
上圖是32位情況,在32位傳參時,子函數所用到的參數保存在父函數棧幀的末尾(并不是保存在自己的棧幀中),這里的arguments是子函數所用到的形參
父函數最末尾的字長保存父函數自己棧頂的值,如上面紅色的箭頭指向previous stack frame pointer,同理父函數的父函數也是一樣的
父函數(main)先把所要調用的子函數(sum)中的參數(1,2)逆序壓棧(壓入自己的棧幀),此后壓入return address【即子函數下一條指令的地址(return 0 的地址)】,在子函數執行后回到下一條指令的地址(執行return 0);在子函數結束后要返回父函數的棧幀,這意味在調用子函數時不能把父函數的棧幀丟棄,由此需要加入父函數的棧底指針加入
丟棄某塊數據不用,并不需要把這塊數據抹除,只需要標記成不是我所使用的范圍即可;這也是磁盤數據恢復的原理,除非有新來的數據復寫
主調函數 caller
被調函數 callee
遵循C語言函數調用規范,一般在開頭用push ebp以及mov ebp, esp,需要保存父函數棧底的狀態;結束會執行leave(恢復父函數的棧底)以及 ret(返回到父函數的下一條指令)
首先,主調函數也是有自己的父函數,將它的父函數的ebp壓入
接著把esp抬高到和ebp相同的位置
下一條指令為新的棧幀開辟局部變量的空間;這里是sub 0x10 , esp,即esp-16;為什么是減去?因為棧是反向增長(高到低)
將被調函數所用到的參數(1,2,3)反向壓入棧,即先壓入3,再壓入2,最后壓入1;
call 這條指令不等于jump,jump是一個跳轉指令;call 不僅會將eip移動到目標代碼的位置,還會在棧中自動保存下一條指令的地址【此時的return address就是23的位置】
此時進入被調函數,同理首先是push ebp和mov sep , ebp;先把主調函數的棧幀保存;注意此時父函數的ebp重新增加到棧里了,將esp的值賦給ebp;讓ebp抬高到新的棧幀的棧頂
執行實際操作,最后的運算結果保存在eax的寄存器中【默認情況下保存函數的返回值】
由于esp并沒有開辟局部變量的空間;為什么不是leave?leave就是 mov ebp,esp再pop ebp,這里只有pop ebp是因為子函數沒有任何局部變量,所以ebp在調用返回時已經在相應的位置了;pop這條指令總是把esp當前指向的位置,對應的一個字長的數據抬入到目標位置;所以pop ebp就會把esp向上抬一個字長,把esp本來指向父函數的值抬入ebp中;
ret 相當于pop eip,eip回到父函數的位置
主調函數情況其局部變量以及被調函數的相關參數
使用add這條指令清空數據;最后保存結果到eax中
值得注意的是,此時返回時esp和ebp并不在相同位置,所以需要leave,首先將esp的值變成ebp
返回父函數的父函數
以上過程需要非常熟練;棧還有很多其他工作的規則,以上是最基本的
ret2text
關注eip寄存器,其中return address存在eip中;當eip中寫入我們目標代碼的地址,程序的控制流便被劫持了
棧溢出是緩沖區溢出的一種
向局部變量(str[8])中寫入24字節數據,溢出到了關鍵結構
當我們拿到一個CTF PWN的題先通過checksec 看這個程序有什么保護措施
【x86架構小端序的可執行ELF】
通過IDA看c語言代碼
顯然漏洞明顯,讀入的數據長度不受限向上溢出
值得注意的是,此時雖然開辟了8字節,但是與ebp的距離是16字節(10h)
最好的方法還是動態調試,因為出題人可能以esp來尋址
通過gdb動態調試,直接run是沒什么意義的,先打斷點,例如b main【b 表示 break】,再r【r 表示run】;此時我們可以看到具體的信息
在程序執行到綠色箭頭位置時,所有寄存器的值;
eip此時main函數偏移26字節的指令,當前指針也是0x804856b
esp和ebp此時對應的值是一個很大的值,這是棧的地址(用戶空間的最高地址)
這是反匯編窗口
最上面的00:0000是棧頂(低地址),最下面的07:001c是棧底(高地址);gdb是反著顯示的
函數調用棧的關系
按n一直步過到漏洞位置
按s步進入這個函數
按n開始輸入,按照正常輸入8個A看什么情況
這里我們看棧里面的情況,輸入stack 看多少項(24項棧值)
esp和ebp之間就是當前執行的函數的棧幀,esp表示棧頂,ebp是棧底
ebp是前一個函數被保存在棧里的ebp的值,ebp再往高一個字長就是返回地址,我們的目標就是攻擊這個返回地址
我們此時可控制的區域是esp和ebp之間的位置,即buffer這個變量
這意味著只要我們一直寫,覆蓋返回地址即可達到攻擊效果
回到IDA我們可找到這個后門函數,執行系統命令,直接獲得shell的控制權,相當于在shell中打開shell
這意味著我們先寫20個字節A(ebp還有4個字節),再寫4個字節制定的地址就會把原本的地址覆蓋掉
我們要找到getshell的開始地址,通過雙擊IDA中getshell在匯編代碼中找到起始地址8048522
我們這里就可以寫腳本來獲得shell了,值得注意的是payload中不能直接加0x8048522【前面是字符串這里是整型】,所以需要p32來轉換
最終發送payload并且與之交互io.interactive(),即可獲得shell
打遠程只要用remote即可
但是實際情況下不一定有后門函數
一般系統調用這樣的代碼需要我們自己輸入
只要不是代碼的地方都不可執行
隨機化棧中的地址
所以放在Bss中居多,Bss用于存放全局變量的,如果這個全局變量是開辟緩存區可以輸入系統調用即可
比如上面這個情況
利用工具直接生成shellcode的機械碼,再io.send直接發送
PWN工具
IDA pro
f5 進入c程序
esc 返回上一個程序
白色的函數為已寫死的函數
粉色的函數要用的時候再去調
在Options的General中可調整一些設置,例如加入機械碼
這樣C語言代碼被拷貝到匯編代碼中
shift+f12 或者 shift+fn+f12打開一個字符串界面【在不知道mian函數位置,可通過程序所表示的字符串找程序的主函數】
\r把之前的文本在顯示時清空,io.recv()把所有的數據完全還原【這里我用的時io.recvline()一行一行接收】
這里的ZmxhZ3tuMHRfZjRzdGVyX3Q2YW5feTB1fQo=顯然是Base64,我們需要解碼
將其解碼