Canary

canary用来检测栈溢出,程序正常的走完了流程,到函数执行完的时候,程序会再次从一个神奇的地方把canary的值取出来,和之前放在栈上的canary进行比较,如果因为栈溢出什么的原因覆盖到了canary而导致canary发生了变化则直接终止程序。canary的最低位恒为零,使得不存在截断问题。

格式化字符串

通过格式化字符串读取canary的值,然后在栈溢出的padding块把canary所在位置的值用正确的canary替换,从而绕过canary的检测。
或者直接任意地址写覆盖返回地址之类的也可以绕过。

针对fork的进程

对fork而言,作用相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也一样。那我们就可以逐位爆破,如果程序GG了就说明这一位不对,如果程序正常就可以接着跑下一位,直到跑出正确的canary。

ssp leak(Stack Smashing Protector )

如果canary被我们的值覆盖而发生了变化,程序会执行函数___stack_chk_fail()

___stack_chk_fail()源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void 
__attribute__ ((noreturn))
__stack_chk_fail (void) {
__fortify_fail ("stack smashing detected");
}

void
__attribute__ ((noreturn))
__fortify_fail (msg)
const char *msg; {
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n", msg, __libc_argv[0] ?: "<unknown>")
}
libc_hidden_def (__fortify_fail)

__libc_message 的第二个%s输出的是argv[0],argv[0]是指向第一个启动参数字符串的指针,会在栈中存放,只要能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。

Modify the TLS

正常情况下,canary取值是:
32 bits:

1
2
mov     eax, large gs:14h
mov [ebp+var_C], eax

64 bits:

1
2
mov     rax, fs:28h
mov [rbp+var_8], rax

而段寄存器fs && gs的定义是指向本线程的TLS结构

在vvar与 /lib/x86_64-linux-gnu/ld-2.23.so之间的一段空间,可以看到有写权限
64位一般在该段的起始地址+0x1700+0x28处 / 32位 +0x14

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
 vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r-xp 1000 0 /home/sirius/tikool/prac/canary/canary_test
0x600000 0x601000 r--p 1000 0 /home/sirius/tikool/prac/canary/canary_test
0x601000 0x602000 rw-p 1000 1000 /home/sirius/tikool/prac/canary/canary_test
0x7ffff7a0d000 0x7ffff7bcd000 r-xp 1c0000 0 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7bcd000 0x7ffff7dcd000 ---p 200000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dcd000 0x7ffff7dd1000 r--p 4000 1c0000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd1000 0x7ffff7dd3000 rw-p 2000 1c4000 /lib/x86_64-linux-gnu/libc-2.23.so
0x7ffff7dd3000 0x7ffff7dd7000 rw-p 4000 0
0x7ffff7dd7000 0x7ffff7dfd000 r-xp 26000 0 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7fda000 0x7ffff7fdd000 rw-p 3000 0
0x7ffff7ff7000 0x7ffff7ffa000 r--p 3000 0 [vvar]
0x7ffff7ffa000 0x7ffff7ffc000 r-xp 2000 0 [vdso]
0x7ffff7ffc000 0x7ffff7ffd000 r--p 1000 25000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffd000 0x7ffff7ffe000 rw-p 1000 26000 /lib/x86_64-linux-gnu/ld-2.23.so
0x7ffff7ffe000 0x7ffff7fff000 rw-p 1000 0
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]


x/10gx 0x7ffff7fda000+0x1700
0x7ffff7fdb700: 0x00007ffff7fdb700 0x00007ffff7fda010
0x7ffff7fdb710: 0x00007ffff7fdb700 0x0000000000000000
0x7ffff7fdb720: 0x0000000000000000 0x2928659c8989cd00
0x7ffff7fdb730: 0xb97e5185f9afb6be 0x0000000000000000
0x7ffff7fdb740: 0x0000000000000000 0x0000000000000000

PIE

PIE(position-independent executable, 地址无关可执行文件)技术是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术。同ASLR一样,应用了PIE的程序会在每次加载时都变换加载基址,从而使位于程序本身的gadget也失效。

partial write

由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。因此通过覆盖EIP的后8或16位 (按字节写入,每字节8位)就可以快速爆破或者直接劫持EIP。

leak libc addr

PIE影响的只是程序加载基址,并不会影响指令间的相对地址,因此我们如果能泄露出程序或libc的某些地址,我们就可以利用偏移来达到目的。这也是比较常用的方法。

vdso/vsyscall

在开启了ASLR的系统上运行PIE程序,就意味着所有的地址都是随机化的。然而在某些版本的系统中这个结论并不成立,原因是存在着一个神奇的vsyscall。(由于vsyscall在一部分发行版本中的内核已经被裁减掉了,新版的kali也属于其中之一。vsyscall在内核中实现,无法用docker模拟,因此任何与vsyscall相关的实验都改成在Ubuntu 16.04上进行,同时libc中的偏移需要进行修正)

关于vsyscall

1
简单地说,现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall

由于vsyscall地址的固定性,这个本来是为了节省开销的设置造成了很大的隐患,因此vsyscall很快就被新的机制vdso所取代。与vsyscall不同的是,vdso的地址也是随机化的,且其中的指令可以任意执行,不需要从入口开始,这就意味着我们可以利用vdso中的syscall来干一些坏事了。