程序加载到内存后不使用默认的加载地址,将加载基址进行随机化,依赖重定位表进行地址修复。地址随机化之后,shellcode中固定的地址值将失效。
开启/关闭软件地址随机化。
struct IMAGE_NT_HEADERS NtHeader
struct DLL_CHARACTERISTICS DllCharacteristics
1:开启地址随机化
0:关闭地址随机化
WORD IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE : 1
struct IMAGE_OPTIONAL_HEADER64 OptionalHeader
想要开启地址随机化,需要当前程序中存在重定位表,如果程序没有重定位表,及时将该位置置为1,也无法进行地址随机化处理。
系统内存的分配与使用是符合页表机制的,具体的页表机制请自行百度。
每个页目录表和页表项都存在 基址与属性控制位,通过修改这些控制位,达到当前内存是否有执行、读、写等权限。
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 函数。
官方文献:
对于编译器识别为受缓冲区溢出问题影响的函数,编译器会在返回地址之前在堆栈上分配空间。调用该函数时,分配的空间中会加载一个安全 Cookie,在模块加载期间会对该 Cookie 进行一次计算。退出调用该函数时,在 64 位操作系统上展开帧的过程中,会调用帮助程序函数来确保 Cookie 的值依然相同。如果值不同,则指示可能已覆盖堆栈。 如果检测到不同的值,将终止进程。
下述情况不给予保护:
函数不包含缓冲区
函数使用无保护的关键字标记
__declspec(safebuffers)
函数在第一个语句中包含内嵌汇编代码
__declspec(naked)
缓冲区不是8字节类型且大小不大于4个字节
声明全部函数进行保护
#pragma strict_gs_check(on)
缓冲区溢出安全检查对 GS 缓冲区执行。GS 缓冲区可以是以下对象之一:
大于 4 个字节、具有两个以上元素且元素类型不是指针类型的数组。
大小超过 8 个字节且不包含指针的数据结构。
使用 _alloca 函数分配的缓冲区。(alloca是在栈上申请空间)
包含 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 ;
__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的异或、插入、检验等操作。
程序加载到内存中时,会解析当前程序结构,并将当前程序所需的动态库加载到内存中。最终修复程序与动态库之间的地址关联关系,形成一种调用、被调用的关系。
其中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
不当之处,敬请斧正。