前言

在学习看反汇编程序的时候,使用VS2019编译的release版本的程序里经常会出现__security_cookie相关的代码

经过查阅资料,了解到这块代码会编译器增加到函数的开始与结束的位置,用来防止栈溢出

好奇心驱使本文来探究一番这个机制是怎么防止栈溢出的

随便写一段简单的代码:

#include<stdio.h>
int main(){
	char szName[] = "hello selph";
	printf("%s",szName);
	return 0;
}

使用VS2019编译release版本,调试运行,查看反汇编:

int main()
{
; 这里是函数栈帧操作, 保存ebp, 抬高esp留出栈空间给局部变量使用
00621040  push        ebp  
00621041  mov         ebp,esp  
00621043  sub         esp,10h

; 这里将一个常量(__security_cookie)拿出来放到eax里
00621046  mov         eax,dword ptr [__security_cookie (0623004h)]  

; 然后将eax和ebp进行异或操作, 这里的ebp是进入函数前的esp
0062104B  xor         eax,ebp  

; 将计算结果存入ebp-4的位置, 实际上就是存到第一个局部变量里了
0062104D  mov         dword ptr [ebp-4],eax  

; 以下部分是程序内容本身, 本文不对此进行分析, 故省略
...
	return 0;
}
; 程序执行结束后 
; 将程序开头计算的结果取出来放到ecx
00621073  mov         ecx,dword ptr [ebp-4]  

; 对printf函数的参数进行平栈操作, C/C++默认是_cdecl函数调用约定,故调用者负责平栈
00621076  add         esp,8  

; 将取出来的值再次与ebp进异或计算
00621079  xor         ecx,ebp  

; 清空eax, 准备接收返回值
0062107B  xor         eax,eax  

; 调用函数
0062107D  call        __security_check_cookie (0621086h)  

; 栈帧操作, 还原栈帧到进入调用前
00621082  mov         esp,ebp  
00621084  pop         ebp  
00621085  ret  

这里在进入程序之前,将进入函数前的栈顶指针esp与一个常量进行异或操作

在位操作里,仅有异或和取反是可以反推的,连续对同一个数异或2次的值=没有进行任何计算的时候的值

这里在程序结束之后,将之前计算的结果再次与进入函数前的栈顶指针esp进行异或操作

此时如果不出意外的话,计算结果应该会变回先前用来异或的那个常量__security_cookie,值存在ecx里

然后调用函数__security_check_cookie

查看该函数的反汇编:

; 这里将ecx, 也就是经过对同一个值两次异或操作的__security_cookie 与 __security_cookie进行对比
06211086  cmp         ecx,dword ptr [__security_cookie (6213004h)]  

; 不相同就跳转到 failure
0621108C  bnd jne     failure (6211091h)
; 相同就返回
0621108F  bnd ret

; 无条件跳转到 __report_gsfailure 函数中去
failure:
06211091  bnd jmp     __report_gsfailure (6211310h)  

网上复制了一段关于BND前缀的介绍:

BND前缀是Intel MPX(内存保护扩展)的一部分,指示应根据BND0至BND3寄存器中指定的界限检查返回目标(或通常为分支目标,因为BND可以应用于任何控制流指令) ,否则将产生异常-表示潜在的堆栈溢出,编程错误或恶意代码攻击。

关于bnd前缀嘛,这里就当他没用吧

__security_check_cookie函数非常简单,判断__security_cookie经过进入程序前,程序结束后两次异或操作后,是否为原来的值,为原来的值则表示栈中的该位置没有被各种栈操作影响,也就是说不影响程序接下来的执行,接下来就直接返回

如果该值变了,则表示发生了栈溢出,则跳转到__report_gsfailure函数中去报告错误,程序将不再继续执行

程序的栈结构变化

进入函数前后的栈的变化:

image-20210518114526023

通过call进入函数,会压入返回地址,然后进入函数代码

在函数代码里,会入栈保存原来的ebp然后抬高esp栈顶指针

是否有防溢出机制__security_cookie的区别就体现在接下来的操作上了:

  • 如果有该机制,则会往ebp-4的位置上存入__security_cookie异或值,用于后来再次进行异或检查栈中该位置是否被改变用
  • 如果无该机制,则ebp-4开始就是局部变量了
  • __security_cookie 的值是一个随机值,每次运行程序该值都不一样

关于该值是如何进行计算的,可通过搜索编译器CRT下的gs_support.cgs_cookie.c文件进行查看了解,会进行如下的运算:

  1. 获得 system time
  2. 与 GetCurrentProcessId() 异或
  3. 与 GetCurrentThreadId() 异或
  4. 与 GetTickCount() 异或
  5. 与 QueryPerformanceCounter()异或

这里用来计算的值能确保计算出来结果的唯一性,从而能保证cookie的唯一随机性,这里就不进行深入了。

参考链接:https://www.cnblogs.com/adylee/p/9936954.html

  • __security_cookie 机制并不是会给所有代码都进行生成,只会在存在溢出可能的函数里进行生成

  • 这种保护机制是由编译器提供的,可通过编译器设置进行关闭

    VS2019的设置位置在:项目右键->属性->C/C++->代码生成->安全检查->启用安全检查(/GS)

总结

__security_cookie 机制通过往局部变量的位置新增一个随机值,在程序开头和结尾分别与进入函数前的esp的值进行异或操作,经过两次异或如果相同,则表示栈中操作不会影响到程序接下来的执行

如果能在栈中的这个位置手工构造出这个用于检测的值,则栈溢出无法避免。