干货!一文看懂漏洞攻击那些事儿
The following article is from 小白学黑客 Author 小白
二进制漏洞的攻防对抗经过几十年的发展,涌现出了形形色色的攻击手法,也催生了一个又一个的防御手段,今天这篇文章来简单科普回顾一下这几十年的攻防对抗历史。
第一回合:栈溢出攻击 vs GS/Stack Canary + ASLR
栈溢出攻击,也叫缓冲区溢出攻击,它的名头即使是不做安全的朋友估计也听说过。
这种攻击手法,曾经风靡一时。
因为函数调用的过程中,涉及到函数栈帧的压入弹出,也会将调用处的返回地址压入栈中,这就导致了一些情况下,通过操作位于栈中的局部变量,可能会覆盖、篡改掉栈中的返回地址。
攻击者经过精确计算,可以精准控制写入的数据,覆盖返回地址,从而待函数返回时劫持执行流。
早期的C函数库中大量关于字符串的操作函数都是不安全的,都没有检查缓冲区的长度,这种问题非常严重。
后来各家编译器都推出了更安全版本的字符串操作函数,增加了对缓冲区长度的检查。
栈溢出攻击能够得逞的根本原因,在于栈中的返回地址被破坏了之后,程序未能感知到,错把攻击者填充的地址,当成正常的返回地址取出使用。
基于这个特点,微软的VC++编译器、Linux平台的GCC/G++编译器都引入了一个相同的应对手段:在栈中做记号。
通过在返回地址之前放置一个标记,在函数返回之际,取出栈中的返回地址之前,将这一标记取出来,做一个安全校验,如果校验通过,说明返回地址安全,否则则说明栈数据被破坏,程序立即终止。
在VC++中,这一技术叫GS(Guard of Stack),在GCC/G++上,这一技术叫做Stack Canary。
后来,攻击者更换攻击目标,盯上了C++的虚函数表中的函数指针、栈中的异常处理函数指针等等,通过覆盖它们来劫持执行流。
于是,一个新的防御技术又出现了,它就是:地址空间随机化ASLR。
攻击者能成功覆盖函数指针的前提是它们能够精确计算需要覆盖的目标在内存中哪个位置,而在之前的系统中,程序模块加载的地址都是固定的,也就是每次启动,可执行文件、动态链接库文件都是加载到了同一个位置,这就为预测带来了方便。
ASLR的出现,改变了这一历史,将各个模块加载到内存中的地址随机化,每次都不一样,攻击者就很难预测了。
第二回合:堆喷射+堆溢出 vs DEP
ASLR的出现极大的增加了攻击的难度,攻击者很难填写正确的shellcode地址来劫持返回地址了。于是,一项新的技术出现了,这就是Heap Spary 堆喷射技术。
这项技术常用于针对IE浏览器中的攻击,使用Javascript分配大量内存,占据内存空间关键地址(如0x0A0A0A0A、0x0C0C0C0C、0x0D0D0D0D等),然后使用大量滑板指令nop填充堆区,最后劫持EIP执行shellcode。
看上去有点复杂,简单来说就是:我分配大量内存,把上面那些特殊的地址占据,然后在里面填充nop指令,在最后再放恶意代码,然后劫持跳转到那些特殊地址,最后通过一大片nop都能滑行到恶意代码中。
这种行为大面积分配堆内存,往里面写入nop指令,动作有点像喷射,所以叫堆喷射。
魔高一尺,道高一丈,无论是栈溢出还是堆喷射,其本质都是把shellcode放到栈或者堆里,然后劫持EIP去执行。但本质上,栈和堆是存放数据的地方,而非存放指令的地方。
于是,抓住这个特点,一个新的防御手段又又出现了!
DEP:数据执行保护,釜底抽薪,让栈和堆所在的内存页面不可执行!
x64架构下,页表PTE中有一个标记NX/XD位,指示该页面是否可执行,当CPU检测到执行了带有该标记的页面中的代码,则抛异常,终止程序。
第三回合:ROP vs CFG
有了ASLR和DEP是不是就可以高枕无忧了呢?
黑客的脑子总是很聪明,那里有压迫,哪里就有反抗。
既然攻击者填充的代码不允许执行,那就用目标程序自己的代码吧!
什么意思?
所有的程序,归根到底都是由一条条CPU指令构成的,如果攻击者的指令不是它们自己写的,而是来自于正常的程序片段呢?
黑客很聪明啊,它们把需要完成的核心功能拆解成一条条的指令,然后去正常的程序和系统库中寻找这些现成的指令片段,一个个拼凑起来,就完成了自己的功能。
这种技术叫做:面向返回编程ROP。
ROP的核心思想:攻击者扫描已有的动态链接库和可执行文件,提取出可以利用的指令片段(gadget),这些指令片段均以ret指令结尾,即用ret指令实现指令片段执行流的衔接。
这一招是不是很聪明,真是让人拍案叫绝!
不得不说这一招威力真的很大,面对这种攻击,微软从Windows 8.1搞出了一个叫CFG的技术。
CFG是Control Flow Guard的缩写,就是控制流保护,它是一种编译器和操作系统相互配合的防护手段,目的在于防止不可信的间接调用。
CFG通过在编译和链接期间,记录下所有的间接调用信息,并把他们记录在最终的可执行文件中,并且在所有的间接调用之前插入额外的校验,当间接调用(通过指针调用,如 call dword ptr eax,而不是通过函数名的普通调用)的地址被篡改时,会触发异常。
如上图中,在call [ebp+var_14] 之前插入的 __guard_check_icall_fptr 调用就属于安全校验。
简单来说,编译器在代码编译的时候就会把所有合法的间接调用地址记录在案,并悄悄的在代码中所有进行间接调用的地方插入校验,一旦发现要调用的目标不在记录中,则认为这是一个非法的调用行为,立刻Crash掉。
这一招杀敌一千,自损一百,所有间接调用都加入了检验,对程序执行性能的影响无法忽略。
总结
二进制漏洞攻防你来我往,手法越来越刁钻,了解这些技术,会让你对程序执行的原理有更本质的认识。未来会朝着什么方向发展,你怎么看呢?欢迎评论区留言交流。
好了,以上就是这篇文章的全部内容了,写作不易,大家看到这里来个点赞分享吧~谢谢啦~
点此查看详情
1、用AI「复活」逝者:「深度怀旧」项目火遍推特,我们也用民国老照片试了下
3、卸载 PyCharm!这才是 Python 小白的最理想的 IDE
识别关注我们
了解更多精彩内容
点分享
点点赞
点在看