堆栈溢出保护机制 | 逆向工程
2023-5-6 09:6:5 Author: 0x00实验室(查看原文) 阅读量:18 收藏

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

不当之处,敬请斧正。


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg5MDY2MTUyMA==&mid=2247489192&idx=1&sn=4b951956f5461a2cb27cf313f8c13f9d&chksm=cfd86957f8afe04182c49716b8181b182fc7ef30e5c7fb97358014c7c2b831322926dacf46c7#rd
如有侵权请联系:admin#unsafe.sh