ASLR( Address Space Layout Randomization:地址空间布局随机化)
程序加载到内存后不使用默认的加载地址,将加载基址进行随机化,依赖重定位表进行地址修复。地址随机化之后,shellcode中固定的地址值将失效。
图-程序地址未随机化处理
开启/关闭软件地址随机化。
- struct IMAGE_NT_HEADERS NtHeader
- struct DLL_CHARACTERISTICS DllCharacteristics
- 1:开启地址随机化
- 0:关闭地址随机化
- WORD IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE : 1
- struct IMAGE_OPTIONAL_HEADER64 OptionalHeader
想要开启地址随机化,需要当前程序中存在重定位表,如果程序没有重定位表,及时将该位置置为1,也无法进行地址随机化处理。
图-程序没有重定位表项
图-手动开启程序重定位
图-重定位失败
DEP(Data Execution Protection:数据执行保护)
系统内存的分配与使用是符合页表机制的,具体的页表机制请自行百度。
每个页目录表和页表项都存在 基址与属性控制位,通过修改这些控制位,达到当前内存是否有执行、读、写等权限。
图-表项构成
windows 系统上可以调用 VirtualProtect 函数完成内存属性的修改操作。
BOOL VirtualProtect( LPVOID lpAddress, // 目标地址起始位置 DWORD dwSize, // 大小 DWORD flNewProtect, // 请求的保护方式 PDWORD lpflOldProtect // 保存老的保护方式 );
常量/值说明PAGE_EXECUTE启用对已提交页面区域的执行访问。PAGE_EXECUTE_READ启用对页面已提交区域的执行或只读访问。PAGE_EXECUTE_READWRITE启用对页面已提交区域的执行、只读或读/写访问权限。PAGE_EXECUTE_WRITECOPY启用对文件映射对象的映射视图执行、只读或复制写入访问。。。。。。 |
其他的属性权限请参考微软官网声明:
https://learn.microsoft.com/zh-cn/windows/win32/Memory/memory-protection-constants
在x64Dbg中,"内存布局"页面也能看到当前调试程序的内存属性分布信息。
图-内存布局界面
图-修改内存保护权限
图-修改权限
具体实现原理与上述说明一致,参考windows VirtualProtect 函数。
cannary保护
官方文献:
对于编译器识别为受缓冲区溢出问题影响的函数,编译器会在返回地址之前在堆栈上分配空间。调用该函数时,分配的空间中会加载一个安全 Cookie,在模块加载期间会对该 Cookie 进行一次计算。退出调用该函数时,在 64 位操作系统上展开帧的过程中,会调用帮助程序函数来确保 Cookie 的值依然相同。如果值不同,则指示可能已覆盖堆栈。 如果检测到不同的值,将终止进程。
下述情况不给予保护:
- 函数不包含缓冲区
- 函数使用无保护的关键字标记
- __declspec(safebuffers)
- 函数在第一个语句中包含内嵌汇编代码
- __declspec(naked)
- 缓冲区不是8字节类型且大小不大于4个字节
- 声明全部函数进行保护
- #pragma strict_gs_check(on)
缓冲区溢出安全检查对 GS 缓冲区执行。GS 缓冲区可以是以下对象之一:
- 大于 4 个字节、具有两个以上元素且元素类型不是指针类型的数组。
- 大小超过 8 个字节且不包含指针的数据结构。
- 使用 _alloca 函数分配的缓冲区。(alloca是在栈上申请空间)
- 包含 GS 缓冲区的任何类或结构。
接下来通过程序来观察该保护的操作流程。
图-GS保护设置界面
主要观察函数调用过程中,保护流程如何实现,测试使用的代码如下:
#include <stdio.h> #include <windows.h> void func() { printf("func\n"); } int main() { func(); system("pause"); return 0; }
开启GS保护:
01256AE0 | push ebp ; main.cpp:5 01256AE1 | mov ebp, esp ; 01256AE3 | sub esp, 0xEC ; 01256AE9 | push ebx ; 01256AEA | push esi ; 01256AEB | push edi ; 01256AEC | lea edi, dword ptr ss:[ebp - 0xEC] ; 01256AF2 | mov ecx, 0x3B ; 3B:';' 01256AF7 | mov eax, 0xCCCCCCCC ; 01256AFC | rep stosd ; 01256AFE | mov eax, dword ptr ds:[<___security_cookie>] ; eax = ___security_cookie 01256B03 | xor eax, ebp ; eax = 新栈底的异或值 01256B05 | mov dword ptr ss:[ebp - 0x4], eax ; 将亦或值插入栈中 01256B08 | mov byte ptr ss:[ebp - 0x28], 0x0 ; 初始化缓存空间 01256B0C | xor eax, eax ; 01256B0E | mov dword ptr ss:[ebp - 0x27], eax ; 01256B11 | mov dword ptr ss:[ebp - 0x23], eax ; 01256B14 | mov dword ptr ss:[ebp - 0x1F], eax ; 01256B17 | mov dword ptr ss:[ebp - 0x1B], eax ; 01256B1A | mov dword ptr ss:[ebp - 0x17], eax ; 01256B1D | mov dword ptr ss:[ebp - 0x13], eax ; 01256B20 | mov dword ptr ss:[ebp - 0xF], eax ; 01256B23 | mov word ptr ss:[ebp - 0xB], ax ; 01256B27 | mov byte ptr ss:[ebp - 0x9], al ; 01256B2A | push <cpp."func"> ; main.cpp:7, 12B8C88:"func"==L"畦据" 01256B2F | lea eax, dword ptr ss:[ebp - 0x28] ; 01256B32 | push eax ; 01256B33 | call cpp.12535F9 ; 调用strcpy函数 01256B38 | add esp, 0x8 ; 平衡堆栈 01256B3B | lea eax, dword ptr ss:[ebp - 0x28] ; main.cpp:8 01256B3E | push eax ; 01256B3F | push <cpp."%s\n"> ; 12B8C90:"%s\n"==L"猥\n" 01256B44 | call cpp.12533A6 ; 调用printf函数 01256B49 | add esp, 0x8 ; 01256B4C | push edx ; main.cpp:9 01256B4D | mov ecx, ebp ; 01256B4F | push eax ; 01256B50 | lea edx, dword ptr ds:[<>] ; 01256B56 | call cpp.1252668 ; _RTC_CheckStackVars检查数组是否越界 01256B5B | pop eax ; 01256B5C | pop edx ; 01256B5D | pop edi ; 01256B5E | pop esi ; 01256B5F | pop ebx ; 01256B60 | mov ecx, dword ptr ss:[ebp - 0x4] ; 取出异或cookie 01256B63 | xor ecx, ebp ; 尝试还原成旧的的cookie 01256B65 | call cpp.1252208 ; __security_check_cookie重新计算,检查ebp是否正确 01256B6A | add esp, 0xEC ; 01256B70 | cmp ebp, esp ; 01256B72 | call cpp.1252F69 ; 01256B77 | mov esp, ebp ; 01256B79 | pop ebp ; 01256B7A | ret ;
图-插入cookie
__security_check_cookie的原理实现如下:
0125F310 | cmp ecx, dword ptr ds:[<___security_cookie>] ;判断cookie是否还原成功,如果堆栈被覆盖篡改, ;那么将无法得到正确的___security_cookie 0125F316 | jne <cpp.failure> ;根据判断结果进行跳转 0125F318 | ret ; 0125F31A | jmp cpp.12526CC ;跳转到__report_gsfailure函数继续执行
如果强行进入__report_gsfailure函数执行,最终会停止在异常处理上。
0126BD00 | push ebp ; 0126BD01 | mov ebp, esp ; 0126BD03 | sub esp, 0x324 ; 0126BD09 | push 0x17 ; 0126BD0B | call cpp.1253473 ;_IsProcessorFeaturePresent 0126BD10 | test eax, eax ; 0126BD12 | je cpp.126BD1B ; 0126BD14 | mov ecx, 0x2 ; 0126BD19 | int 0x29 ;
图-异常缓冲区溢出
查看0x29号中断对应内容。可以看到函数调用处为0x00000000地址处。
1: kd> !idt 0x29 Dumping IDT: 29: 00000000
关闭GS保护,观察生成的汇编代码:
;func函数的汇编代码 00846AE0 | push ebp 00846AE1 | mov ebp, esp 00846AE3 | sub esp, 0xE8 00846AE9 | push ebx 00846AEA | push esi 00846AEB | push edi 00846AEC | lea edi, dword ptr ss:[ebp - 0xE8] 00846AF2 | mov ecx, 0x3A 00846AF7 | mov eax, 0xCCCCCCCC 00846AFC | rep stosd 00846AFE | mov byte ptr ss:[ebp - 0x24], 0x0 00846B02 | xor eax, eax 00846B04 | mov dword ptr ss:[ebp - 0x23], eax 00846B07 | mov dword ptr ss:[ebp - 0x1F], eax 00846B0A | mov dword ptr ss:[ebp - 0x1B], eax 00846B0D | mov dword ptr ss:[ebp - 0x17], eax 00846B10 | mov dword ptr ss:[ebp - 0x13], eax 00846B13 | mov dword ptr ss:[ebp - 0xF], eax 00846B16 | mov dword ptr ss:[ebp - 0xB], eax 00846B19 | mov word ptr ss:[ebp - 0x7], ax 00846B1D | mov byte ptr ss:[ebp - 0x5], al 00846B20 | push <cpp."func"> 00846B25 | lea eax, dword ptr ss:[ebp - 0x24] 00846B28 | push eax 00846B29 | call cpp.8435F9 00846B2E | add esp, 0x8 00846B31 | lea eax, dword ptr ss:[ebp - 0x24] 00846B34 | push eax 00846B35 | push <cpp."%s\n"> 00846B3A | call cpp.8433A6 00846B3F | add esp, 0x8 00846B42 | push edx 00846B43 | mov ecx, ebp 00846B45 | push eax 00846B46 | lea edx, dword ptr ds:[<>] 00846B4C | call cpp.842668 00846B51 | pop eax 00846B52 | pop edx 00846B53 | pop edi 00846B54 | pop esi 00846B55 | pop ebx 00846B56 | add esp, 0xE8 00846B5C | cmp ebp, esp 00846B5E | call cpp.842F69 00846B63 | mov esp, ebp 00846B65 | pop ebp 00846B66 | ret
其中缺少了cookie的异或、插入、检验等操作。
RELRO (ReLocation Read-Only)
程序加载到内存中时,会解析当前程序结构,并将当前程序所需的动态库加载到内存中。最终修复程序与动态库之间的地址关联关系,形成一种调用、被调用的关系。
其中Linux程序依赖的就是GOT表,效果类似windows上的IAT表(导入地址表)。
同理,Linux上为了程序的运行效率,可能不会一次性修复所有函数地址。因此,提出了PLT表,用来延迟修复所需全局函数/全局变量地址,效果等同PE文件的延迟导入表。
如果是手工解析ELF/PE文件,能在解析修复对应表项时,对所需函数进行hook操作。但是通过系统加载解析的话,就无法及时的对指定函数hook操作。
但是延迟导入表的作用也在此刻凸显,该表项是根据需求进行修复,那么尝试修改该表项内容,再将其修复到程序中,是否就能达到hook等操作的效果。
- 该保护也是为了防止有人篡改延迟导入表内容。取消了延迟导入表。
- 将程序全部全局函数、变量都放置在GOT/IAT表中,在程序一开始加载就修复所有地址信息,防止后期被二次修改。
关于vs的延迟导入表的设置可以参考官网声明:
https://learn.microsoft.com/zh-cn/cpp/build/reference/delay-delay-load-import-settings?view=msvc-170
不当之处,敬请斧正。