Windows保护机制

数据保护

(DEP) 是一项安全功能,可有助于防止计算机受到病毒和其它安全威胁的损坏。 有害程序可能尝试从为Windows 和其它授权程序保留的系统内存位置运行(也称为执行)代码,以攻击Windows。 这些类型的攻击可能会损害您的程序和文件

还有其他的:

https://xineting.github.io/2018/11/03/windows%E4%BF%9D%E6%8A%A4%E6%9C%BA%E5%88%B6/

DEP全称Data Execution Prevention,是Windows平台的一个说法。早年的DEP分硬件DEP和软件DEP,软件DEP实际上是指SafeSEH。现在大部分提到DEP都是指硬件DEP,也叫NX(编译选项/NXCOMPAT)。

image-20230528162837575

Linux上只有硬件DEP(因为异常处理并非SEH机制),但Linux上一般不用DEP来描述,而是仅仅用NX。

NX的作用是防止数据页上的数据被当成代码来执行,x86是通过PAE的扩充位来标志PTE内存页是否具有可执行权限。开启了NX后,数据页(首当其冲的就是堆页和堆栈页)就不再具有可执行权限。X64因为本身就富余扩展位,所以无需PAE即支持NX,原理一致。

比较老的CPU可能不支持PAE(设计上就没有扩展的4根地址总线),所以DEP也就形同虚设。

image-20230528162937978

知道有这个DEP就暂时就这样吧

ASLR

ASLR全称Address Space Layout Randomization. 本质上是一种技术思想。应用程序的虚拟内存空间中有着堆、栈、共享库、PE映像等等模块,这些模块的地址是固定的。而因为其固定性,很多包含硬编码合适的地址的exp都一度相当稳定。

ASLR旨在把各种模块的地址在程序加载期间随机化处理,让exp的开发者无法硬编码地址。对不同的平台来说,ASLR的实现大同小异,但因为受限于平台原本加载方式的设计,所以表现上有些差异。

以Windows和Linux为例:

类似DEP,Windows的ASLR是“系统支持 + 程序链接选项开启”。

/dynamicbase一旦开启,程序每次启动时heap和stack、PEB、TEB都会变化,每次重启系统其PE映像也会发生变化 。

Linux的ASLR则是“系统支持 + PIE”,ASLR有0/1/2三个级别,0表示未开启,1表示随机化stack、libraries,2进一步随机化heap。

如果elf编译时使用了-fPIE选项(gcc),则ELF被视为特殊的so,加载时也会随机化基址。

image-20230528163251474

未开启ASLR,PE映像加载和PE文件头一致,stack每次都相同,heap每次则不同。

image-20230528163305425

开启ASLR,PE映像加载和PE文件头不一致会重定向,每次运行stack和heap都不同,而系统重启前PEImage都相同。

DEP/NX对抗

正面刚系列:

Ret2Libc

核心思想在于我不自己写shellcode,我通过布置栈上的参数去call已有的函数,此时ret不再是回到data页(比如栈),而是跳到某个函数(比如system())。

ROP(Return Oriented Program)

ROP链实现原生的功能,由各种以ret [n]结尾的代码片段组成,称为gadget,然后通过控制调用链和栈上参数,step by step。

Windows因为历史原因,提供了各种Ring3层API来设置DEP的开闭,也提供VirtualProtect等一干Ring3层API来设置数据内存页面的可执行位。于是可以ROP + data页执行

攻击未开启DEP的模块(当前常见的比如浏览器中的JIT)

ASLR+PIE对抗

Windows

攻击未启用ASLR的模块

对x86来说,由于ASLR只随机化高16位,可以仅覆盖低16位的地址

利用堆喷射(Heap Spray)到稳定地址(0x0c0c0c0c/0x06060606)

Leak Info泄露出模块基址

Linux

未开启PIE

Ret2plt

Got表劫持

Stack-pivot

Leak info泄露出模块基址

开启PIE

Leak info泄露出模块基址

Linux保护机制

Linux ELF文件的保护主要有四种:Canary、NX、PIE、RELRO

Canary

技术上表示最先的测试的意思。所以大家都用Canary来搞最先的测试。Stack Canary表示栈的报警保护。

在函数返回值之前添加的一串随机数(不超过机器字长)(也叫做cookie),末位为/x00(提供了覆盖最后一字节输出泄露Canary的可能),如果出现缓冲区溢出攻击,覆盖内容覆盖到Canary处,就会改变原本该处的数值,当程序执行到此处时,会检查Canary值是否跟开始的值一样,如果不一样,程序会崩溃,从而达到保护返回地址的目的。

image-20230528170616384

总的来说,Canary参数表示着对栈的保护,防止栈溢出的一种保护,即在栈靠近栈底某个位置设置初值,防止栈溢出的一种保护。

GCC用法:

  • gcc -o test test.c // 默认情况下,不开启Canary保护
  • gcc -fno-stack-protector -o test test.c //禁用栈保护
  • gcc -fstack-protector -o test test.c //启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码
  • gcc -fstack-protector-all -o test test.c //启用堆栈保护,为所有函数插入保护代码

总的来说:

  • -fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)

NX

NX即No-eXecute(不可执行)的意思,NX(DEP)的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常,而不是去执行恶意指令。

正常在栈溢出时通过跳转指令跳转至shellcode,但是NX开启后CPU会对数据区域进行检查,当发现正常程序不执行,并跳转至其他地址后会抛出异常,接下来不会继续执行shellcode,而是去转入异常处理,处理后会禁止shellcode继续执行

GCC用法

  • gcc -o test test.c // 默认情况下,开启NX保护

  • gcc -z execstack -o test test.c // 禁用NX保护

  • gcc -z noexecstack -o test test.c // 开启NX保护

  • -z execstack / -z noexecstack (关闭 / 开启)

PIE(ASLR)

一般情况下NX(Windows平台上称为DEP)和地址空间分布随机化(PIE/ASLR)(address spacelayout randomization)会同时工作。内存地址随机化机制有三种情况:

0 - 表示关闭进程地址空间随机化。

1 - 表示将mmap的基地址,栈基地址和.so地址随机化

2 - 表示在1的基础上增加heap的地址随机化

该保护能使每次运行的程序的地址都不同,防止根据固定地址来写exp执行【payload】攻击。

可以防止Ret2libc方式针对DEP的攻击。ASLR和DEP配合使用,能有效阻止攻击者在堆栈上运行恶意代码

liunx下关闭PIE的命令如下

  • sudo -s echo 0 > /proc/sys/kernel/randomize_va_space

GCC用法:

  • gcc -o test test.c // 默认情况下,不开启PIE
  • gcc -fpie -pie -o test test.c // 开启PIE,此时强度为1
  • gcc -fPIE -pie -o test test.c // 开启PIE,此时为最高强度2
  • gcc -fpic -o test test.c // 开启PIC,此时强度为1,不会开启PIE
  • gcc -fPIC -o test test.c // 开启PIC,此时为最高强度2,不会开启PIE

总:

  • -no-pie / -pie (关闭 / 开启)

RELRO

Relocation Read-Only (RELRO) 可以使程序某些部分成为只读的。它分为两种:Partial RELRO 和 FullRELRO,即:部分RELRO 和 完全RELRO。

部分RELRO 是 GCC 的默认设置,几乎所有的二进制文件都至少使用部分RELRO。这样仅仅只能防止全局变量上的缓冲区溢出从而覆盖 GOT。

完全RELRO 使整个 GOT 只读,从而无法被覆盖,但这样会大大增加程序的启动时间,因为程序在启动之前需要解析所有的符号。

在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。所以在安全防护的角度应尽量减少可写的存储区域

RELRO会设置符号重定向表格为只读或者程序启动时就解析并绑定所有动态符号,从而减少对GOT表的攻击。如果RELRO为Partial RELRO,就说明对GOT表具有写权限

主要用来保护重定位表段对应数据区域,默认可写

Partial RELRO:.got不可写,got.plt可写

Full RELRO:.got和got.plt不可写

got.plt可以简称为got表

GCC用法

  • gcc -o test test.c // 默认情况下,是Partial RELRO

  • gcc -z norelro -o test test.c // 关闭,即No RELRO

  • gcc -z lazy -o test test.c // 部分开启,即Partial RELRO

  • gcc -z now -o test test.c // 全部开启

  • -z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)

FORTIFY

fortify是轻微的检查,用于检查是否存在缓冲区溢出的错误。适用于程序采用大量的字符串或者内存操作函数,如:


  • memcpy()
1
void *memcpy(void *str1, const void *str2, size_t n)

从存储区str2复制n个字符到存储区str1

参数:

  • str1 – 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针

  • str2 – 指向要复制的数据源,类型强制转换为 void* 指针

  • n – 要被复制的字节数

  • 返回值:该函数返回一个指向目标存储区 str1 的指针


  • memset()
1
void *memset(void *str, int c, size_t n)

复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符

参数:

  • str – 指向要填充的内存块

  • c – 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式

  • n – 要被设置为该值的字节数

  • 返回值:该值返回一个指向存储区 str 的指针


  • strcpy()
1
char *strcpy(char *dest, const char *src)

把 src 所指向的字符串复制到 dest,容易出现溢出

参数:

  • dest – 指向用于存储复制内容的目标数组

  • src – 要复制的字符串

  • 返回值:该函数返回一个指向最终的目标字符串 dest 的指针


  • stpcpy()
1
extern char *stpcpy(char *dest,char *src)

把src所指由NULL借宿的字符串复制到dest所指的数组中

说明:src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串返回指向dest结尾处字符(NULL)的指针


  • strncpy()
1
char *strncpy(char *dest, const char *src, size_t n)

把 src 所指向的字符串复制到 dest,最多复制 n 个字符。当 src 的长度小于 n 时,dest 的剩余部分将用空字节填充

参数:

  • dest – 指向用于存储复制内容的目标数组

  • src – 要复制的字符串

  • n – 要从源中复制的字符数

  • 返回值:该函数返回最终复制的字符串


  • strcat()
1
char *strcat(char *dest, const char *src)

把 src 所指向的字符串追加到 dest 所指向的字符串的结尾

参数:

  • dest – 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串

  • src – 指向要追加的字符串,该字符串不会覆盖目标字符串

  • 返回值:


  • strncat()
1
char *strncat(char *dest, const char *src, size_t n)

把 src 所指向的字符串追加到 dest 所指向的字符串的结尾,直到 n 字符长度为止

参数:

  • dest – 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串,包括额外的空字符

  • src – 要追加的字符串

  • n – 要追加的最大字符数

  • 返回值:该函数返回一个指向最终的目标字符串 dest 的指针


  • sprintf():PHP
1
sprintf(format,arg1,arg2,arg++)

arg1、arg2、++ 参数将被插入到主字符串中的百分号(%)符号处。该函数是逐步执行的。在第一个 % 符号处,插入 arg1,在第二个 % 符号处,插入 arg2,依此类推

参数:

  • format – 必需。规定字符串以及如何格式化其中的变量

  • arg1 – 必需。规定插到 format 字符串中第一个 % 符号处的参

  • arg2 – 可选。规定插到 format 字符串中第二个 % 符号处的参数

  • arg++ – 可选。规定插到 format 字符串中第三、四等等 % 符号处的参数

  • 返回值:返回已格式化的字符串


  • snprintf()
1
int snprintf ( char * str, size_t size, const char * format, ... )

设将可变参数(…)按照 format 格式化成字符串,并将字符串复制到 str 中,size 为要写入的字符的最大数目,超过 size 会被截断

参数:

str – 目标字符串

  • size – 拷贝字节数(Bytes)如果格式化后的字符串长度大于 size

  • format – 格式化成字符串

  • 返回值:如果格式化后的字符串长度小于等于 size,则会把字符串全部复制到 str 中,并给其后添加一个字符串结束符 \0。 如果格式化后的字符串长度大于 size,超过 size 的部分会被截断,只将其中的(size-1) 个字符复制到 str 中,并给其后添加一个字符串结束符 \0,返回值为欲写入的字符串长度


  • vsprintf():PHP
1
vsprintf(format,argarray)

与 sprintf() 不同,vsprintf() 中的参数位于数组中。数组元素将被插入到主字符串中的百分号(%)符号处。该函数是逐步执行的

参数:

  • format – 必需。规定字符串以及如何格式化其中的变量

  • argarray – 必需。带有参数的一个数组,这些参数会被插到 format 字符串中的 % 符号处

  • 返回值:以格式化字符串的形式返回数组值


  • vsnprintf()
1
int vsnprintf (char * s, size_t n, const char * format, va_list arg )

将格式化数据从可变参数列表写入大小缓冲区

如果在printf上使用格式,则使用相同的文本组成字符串,但使用由arg标识的变量参数列表中的元素而不是附加的函数参数,并将结果内容作为C字符串存储在s指向的缓冲区中 (以n为最大缓冲区容量来填充)。如果结果字符串的长度超过了n-1个字符,则剩余的字符将被丢弃并且不被存储,而是被计算为函数返回的值。在内部,函数从arg标识的列表中检索参数,就好像va_arg被使用了一样,因此arg的状态很可能被调用所改变。在任何情况下,arg都应该在调用之前的某个时刻由va_start初始化,并且在调用之后的某个时刻,预计会由va_end释放

参数:

  • s – 指向存储结果C字符串的缓冲区的指针,缓冲区应至少有n个字符的大小

  • n – 在缓冲区中使用的最大字节数,生成的字符串的长度至多为n-1,为额外的终止空字符留下空,size_t是一个无符号整数类型

  • format – 包含格式字符串的C字符串,其格式字符串与printf中的格式相同

  • arg – 标识使用va_start初始化的变量参数列表的值

  • 返回值:如果n足够大,则会写入的字符数,不包括终止空字符。如果发生编码错误,则返回负数。注意,只有当这个返回值是非负值且小于n时,字符串才被完全写入


  • gets():
1
char *gets(char *str)

从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定

参数:str – 这是指向一个字符数组的指针,该数组存储了 C 字符串 返回值:如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回NULL

GCC用法

gcc -D_FORTIFY_SOURCE=1 仅仅只在编译时进行检查(尤其是#include <string.h>这种文件头)

gcc -D_FORTIFY_SOURCE=2 程序执行时也会进行检查(如果检查到缓冲区溢出,就会终止程序)

在-D_FORTIFY_SOURCE=2时,通过对数组大小来判断替换strcpy、memcpy、memset等函数名,从而达到防止缓冲区溢出的作用