缓冲区溢出的利用(二)

缓冲区溢出利用

我们这次的环境跟上一篇一样,上一篇只是分析了发生的原因,这一篇我们就对产生缓冲区溢出进行利用,还是之前那两个程序test1.exe and test2.exe的基础上进行讲解,所以上一篇一定要理解!!!

缓冲区溢出程序,test2.exe代码如下:

#include<stdio.h> 
#include<string.h> //引入头文件

char name[] = "betaobetaobetao";  //定义全局变量,!!! 注意,这里多了两个betao

int main()            //返回值 主函数main()
{
    char buffer[8];   //开辟8个字节的空间用来存储变量name
    strcpy(buffer,name);  //内置函数(作用):将变量name内容赋值给buffer变量
    printf("%s\n",buffer);  //输出
    getchar();               //方便观察 作用:等待用户输入按键
    return 0;                //返回值
}

这里我们需要分成几个步骤:

  • 1.精确定位返回地址的位置
  • 2.寻找覆盖原始返回地址的地址
  • 3.编写shellcode到相应的缓冲区(这一步会涉及很多东西,汇编语言等,所以下一篇说)

经过上一篇,补充内容中,我们通过程序报错的显示,很快的找到了Address,报错地址是0x006f6174,我们通过把十六进制转换成ASCII码:

image-20191221102439540

这里的tao正好是我们输入那一串长字符的最后三个字母,由于地址是4个字节表示,如果我将全局变量name赋值为betaobetaobeXXXX那么也就是说,四个X就是我们覆盖的返回地址,我们上次也说了,buffer变量只有8个字节的空间,后面四个字节aobe是父函数EBP的地址,到这里,我们也就解决了第一个问题----> 精确定位返回地址的位置

​ 这里还有一个问题需要说明一下,因为我们这个程序的局部变量buffer只有8个字节,因此很容易就能够被填充满,从而很容易就能够被定位,但是如果缓冲区空间很大,该如何定位呢?总不能还是重复betaobetaobetao...吧,我们这里使用26个大写字符与小写字符,一共52个字符进行测试,一次就可以验证52个字节的缓冲区空间。

​ 我们这里修改局部变量数组大小为80,我们加两端英文字符,也就是104个字符。

#include<stdio.h> 
#include<string.h> //引入头文件

char name[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";  //定义全局变量,!!!

int main()            //返回值 主函数main()
{
    char buffer[80];   //开辟80个字节的空间用来存储变量name !!!
    strcpy(buffer,name);  //内置函数(作用):将变量name内容赋值给buffer变量
    printf("%s\n",buffer);  //输出
    getchar();               //方便观察 作用:等待用户输入按键
    return 0;                //返回值
}

将以上C代码程序编译...,然后运行

image-20191221104145203

​ 在Address后可以发现,其值为0x6a696867,注意我们的系统是小端显示,也就是说,实际的字符应该是0x67、0x68、0x69、0x6a。那么把它转换成字母,可以知道是g、h、i、j

通过python转换成ASCII码:

image-20191221104444316

我们来分析一下,为什么是ghij,看下图

image-20191221105144039

ok,我们回到我们简单的程序

​ 经过上面的分析,我们还需要确定betaobetaobeXXXX中的最后四个“X”应该是什么地址。这里我们不能凭空创造一个地址,而是应该基于一个合法地址的基础之上。当然我们通过在OD中的观察,确实能够找到很多合适的地址,但是这种方法不具有通用性,毕竟要找一个确切的地址还是不那么方便的。解决这个问题的方法有很多种,但是最为常用最为经典的,就是jmp esp方法,也就是利用跳板进行跳转。

​ 这里的跳板是程序中原有的机器代码,它们都是能够跳转到一个寄存器内所存放的地址进行执行,如jmp espcall espjmp ecxcall eax等等。如果在函数返回的时候,CPU内寄存器刚好直接或者间接指向ShellCode的开头,这样就可以把对栈内存放的返回地址的那一个元素覆盖为相应的跳板地址。

​ 我们画图来理解一下,上面的话是什么意思.

image-20191221110334737

我们也用OD载入上一篇中的test1.exe,来具体分析一下,上面说的什么意思!

我们直接执行到mian函数最后一个地址,然后F8一步步执行到0x4010a6地址中, 即retn语句处,此时我们关注一下esp寄存器所保存的值:

image-20191221111335755

关注我标箭头的几个位置,我们继续执行:

image-20191221111542971

主要关注esp的值。可以发现,esp的值由刚才的0x0012FF84变成了0x0012FF88,从栈空间来看,即刚才那个值的下一个位置。不要忘了,0x0012FF84位置正式我们想要修改的返回地址的位置。

​ 总结一下,我们可以得知,当main函数执行完毕的时候,esp的值会自动变成返回地址的下一个位置,而esp的这种变化,一般是不受任何情况影响的。既然我们知道了这一个特性,那么其实就可以将返回地址修改为esp所保存的地址,也就是说,我们可以让程序跳转到esp所保存的地址中,去执行我们所构造的指令,以便让计算机执行。

​ 当然了,以上我所讲的是正常的情况,也就是返回地址没有被破坏的情况,那么如果返回地址被破坏了,esp还具备这种特性吗?不妨再用OD载入test4.exe这个存在缓冲区溢出问题的程序,来研究一下,因为我们之前的字符长度,没办法覆盖到shellcode部分,所以我们加长字符,c代码如下:

#include<stdio.h> 
#include<string.h> //引入头文件

char name[] = "betaobetaobetaobetaobetao";  //定义全局变量,!!!

int main()            //返回值 主函数main()
{
    char buffer[8];   //开辟80个字节的空间用来存储变量name !!!
    strcpy(buffer,name);  //内置函数(作用):将变量name内容赋值给buffer变量
    printf("%s\n",buffer);  //输出
    getchar();               //方便观察 作用:等待用户输入按键
    return 0;                //返回值
}

首先还是先来到main函数的retn的位置:

image-20191221112251752

image-20191221112542641

到这里我们就应该知道,为什么要跳转了吧???那么我们接下来要解决的就是,如果让程序跳转到 esp的位置呢,也就是执行jmp esp 这条指令 ,jmp esp指令对应的机器码是0xFFE4

我们接下来编写一个程序,来在user32.dll这个动态链接库中寻找jmp esp这个机器指令的内存地址(jmp esp很多动态链接库都有,这里只是做一个例子):

C代码如下:

#include <windows.h>
#include <stdio.h>
#define DLL_NAME "user32.dll"

int main()
{
    BYTE* ptr;
    int position,address;
    HINSTANCE handle;
    BOOL done_flag = FALSE;
    handle=LoadLibrary(DLL_NAME);
    if(!handle)
    {
        printf(" load dll erro !");
        exit(0);
    }
    ptr = (BYTE*)handle;
 
    for(position = 0; !done_flag; position++)
    {
        try
        {
            if(ptr[position] == 0xFF && ptr[position+1] == 0xE4)
            {
                //0xFFE4 is the opcode of jmp esp
                int address = (int)ptr + position;
                printf("OPCODE found at 0x%x\n",address);
            }
        }
        catch(...)
        {
            int address = (int)ptr + position;
            printf("END OF 0x%x\n", address);
            done_flag = true;
        }
    }
    getchar();
    return 0;
}

image-20191221144508319

注:编译运行,文件名必须是.cpp后缀,不能.c

由上图可以看到,地址非常多,这里我使用倒数第二行0x77e35b79,也就是说,我需要使用这个地址来覆盖程序的返回地址。这样,程序在返回时,就会执行jmp esp,从而跳到返回地址下一个位置去执行该地址处的语句。

​ 还有一个注意点:有很多种方法可以获取jmp esp,而且不同的系统这个地址可能是不同的。

image-20191221144833652

接下来我们进行地址的验证。我们随便附加一个进程可执行文件。

image-20191221145211651

总结

​ 到这里可以先总结一下我们即将编写name数组中的内容了,经过以上分析可知道,其形式为betaobetaobeXXXXShellcode...,前面12个为任意字符,XXXX返回地址我们使用0x77e35b79

Shellcode....便是我们想要计算机执行的代码。shellcode编写待更...(需要汇编基础)

本文链接:

https://www.betao.cn/archives/bf02.html
1 + 5 =
快来做第一个评论的人吧~