本篇记录一些常见的系统防护手段(运行时保护策略),持续更新,未完待续
Windows
这是0day安全里面的图,可以看到在n年前Windows就有了多种保护机制,而且不同系统机制也不同,如今保护方式更是复杂,这或许也是ctf的pwn为Linux下的原因吧。
GS
DEP
ASLR
这是Kernel Version 6 开始采用的技术,就是XP后都支持了,它会使PE文件在每次加载到内存的其实地址都会随机变化(含代码),并且堆栈也会随机变化,它也需要软件支持此功能,编程工具和系统会默认开启此功能,可以关闭他们:
软件支持ASLR则一般都有.reloc节区,在PE中海油些其他的不同:
删除DllCharacteristics的Dll can move属性即可去掉ALSR功能:
直接使用CFF就可以去掉,不过作者说这样不好,应该用winhex这类工具,有利于技术提升。。。
Linux
stack canary
同Windows的GS,缓解措施:
- 变量重排:局部变量->数组->哨位->EBP值->RET值
- 哨位(探测仪)是随机不可测的
- 默认对有数组的函数实施保护
设置方式例:
1 | -fno-stack-protector |
对抗措施:
- fork下可以爆破
- 格式化串等可以读
- perror
ASLR
有三个选项开关:
- 0 - 表示关闭进程地址空间随机化。
- 1 - 表示将mmap的基址,stack和vdso页面随机化。
- 2 - 表示在1的基础上增加堆(heap)的随机化。
设置方式例:
1 | echo 0 >/proc/sys/kernel/randomize_va_space |
PIE
PIE指的是位置无关的可执行程序,用于生成位置无关的可执行程序,所谓位置无关的可执行程序,指的是,可执行程序的代码指令集可以被加载到任意位置,进程通过相对地址获取指令操作和数据,如果不是位置无关的可执行程序,则该可执行程序的代码指令集必须放到特定的位置才可运行进程。
1 | -no-pie |
- 在一些旧版本上伪随机熵不够可以使用爆破,即多次重复同样地址。
NX
data bss stack heap等不可执行,可使用如下开关参数:
1 | -z noexecstack |
FORTIFY_SOURCE
固定参数,主要用来防格式化串,它会让%12$s
在前12个参数没被用到时失效并且会组织%n
的对应地址在可写的区域上。使用-O2 -D_FORTIFY_SOURCE=2
打开(1表示编译时检查,2加了运行时溢出检查)。
RELRO
重定位区域只读,默认是部分只读,此时GOT表依然可写,当改为full时GOT也只读,重定位将会发生在程序开始前。可使用如下开关参数:
1 | -z now |
Sanitizer
一款安全检查工具,它含有多个子工具。
LeakSanitizer
检测内存泄漏的
MemorySanitizer
检测未初始化使用的
ThreadSanitizer
检测竞争与死锁的
AddressSanitizer
检测内存访问问题的,目前支持如下错误检测:
1 | Use after free (dangling pointer dereference) |
这里可以把错误大致分为两类:①越界访问②释放后访问。针对这两个问题,此工具的原理如下:
首先针对问题②:
- 编译时用自己运行时库函数替换掉
malloc
和free
,然后在被释放的内存里….下毒,并且在代码里的解指针操作前加上校验(此处可以优化,因为有的时候能保证访问是安全的,这时就不必要再检查了,如紧连着两次同属性访问同一地址在第一次检查后第二次就没有必要再检查了),若发现指向的内存有..毒就抛出异常。1
2
3
4if (IsPoisoned(address)) { //此处为检查代码
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address; - 怎么为有毒与无毒呢?可以对程序真正使用的内存进行映射(shandow内存)以标志内存是否可用,由于是按字节寻址,所以一字节对应标志位的一比特,但事实上AddressSanitizer并不是直接这样1byte对应1bit的,而是8bytes对应1byte,于是不同处在于:
1.8字节都没有中毒,对应的shandow字节为0
2.8字节都有中毒,对应的shandow字节为负数
3.前k字节没中毒,余下8-k字节中毒,则对应的shandow字节为8-k。因为malloc返回的内存都是8字节对齐的,所以shadow里的字节应该要么是0要么是负数,但是因为对齐填充的内存并不是用户申请的,所以它理应不能用,那么最小权限原则它不应该可用,例如malloc(1)
这里请求1字节,事实上返回了8字节可用而且不会出什么错误,但是根据程序的逻辑之应该用1字节,于是shadow对应的字节因该为8 - 1 = 7
- 有位置记录内存状态了,再将真实使用的内存与shadow内存相映射就可以完成上述的检查了:如Linux64的映射方式为:
1
2
3
4shadow_address = MemToShadow(address);
if (ShadowIsPoisoned(shadow_address)) {
ReportError(address, kAccessSize, kIsWrite);
}1
Shadow = (Mem >> 3) + 0x7fff8000;
- 于是malloc时相应的shadowMem是0,而free后shadowMem为负数,这样就解决了释放后的问题。
- 另外shadowMem本身是系统级的内存访问保护。
其实shadowMem的作用不止于此,它的实质是在应用层标志所有不可访问的内存,于是经过转化就能用来解决问题①,对于越界访问只要在变量周围加入加入不可访问的内存,那么出现越界访问就被转化为非法访问了。
1 | void foo() { |
事实上从上面的介绍它并不完美,对于释放后使用,当释放后再分配前使用明显是会检测出来的,但是当释放后又被再次分配,通过上面的检查它其实是不会被检测的,AddressSanitizer的实现方法就是将释放后的内存放入一个隔离队列,在未来一段时间内不再分配它,以此来增加被检测的可能性。另外它还有很多其他特性详参见官方文档。
使用
GCC 4.8版开始支持它,更多用法与问题可以访问wiki页
1 | #对于单文件 |
另外我也遇到这个问题了,解答笑了,哈哈哈哈哈哈哈哈哈~
1 | Q: Why didn't ASan report an obviously invalid memory access in my code? |
参考
https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm