栈溢出基础实验

TOC

  1. 1. 环境
  2. 2. 分析

修改函数返回地址,原理是利用栈溢出修改函数返回地址。

环境

//file: main.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define PASSWORD "1234567"

int verify_password(char *password)
{
int authenticated;
char buffer[8];
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)
{
printf("incorrect password!\n");
}
else
{
printf("congratulation! you passed the verification!\n");
}

fclose(fp);
}

编译器:VS2017 x86

编译选项:/GS- //GS为安全检测选项,vs2017默认开启/GS选项,/GS实验会使本实验失败

**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.8.1
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x86'

D:\test>cl main.c /GS-
Microsoft (R) C/C++ Optimizing Compiler Version 19.15.26726 for x86
Copyright (C) Microsoft Corporation. All rights reserved.

main.c
Microsoft (R) Incremental Linker Version 14.15.26726.0
Copyright (C) Microsoft Corporation. All rights reserved.

/out:main.exe
main.obj

分析

用键盘输入字符的ASCIl表示范围有限,很多值(如0x11、0x12等符号)无法直接用键盘输入,所以将程序的输入为从文件中读取字符串。

开始动手之前,我们先理清思路,看看要达到实验目的我们都需要做哪些工作。

  1. 要摸清楚栈中的状况,如函数地址距离缓冲区的偏移量等。这虽然可以通过分析代码得到,但我还是推荐从动态调试中获得这些信息。
  2. 要得到程序中密码验证通过的指令地址,以便程序直接跳去这个分支执行。
  3. 要在password.xt文件的相应偏移处填上这个地址。

这样verify-password函数返回后就会直接跳转到验证通过的正确分支去执行了。

首先用IDA加载得到可执行PE文件,如下所示。

.text:00131040 ; =============== S U B R O U T I N E =======================================
.text:00131040
.text:00131040 ; Attributes: bp-based frame
.text:00131040
.text:00131040 sub_131040 proc near ; CODE XREF: start-8D↓p
.text:00131040
.text:00131040 var_408 = byte ptr -408h
.text:00131040 var_8 = dword ptr -8
.text:00131040 var_4 = dword ptr -4
.text:00131040
.text:00131040 push ebp
.text:00131041 mov ebp, esp
.text:00131043 sub esp, 408h
.text:00131049 mov [ebp+var_8], 0
.text:00131050 push offset aRb ; "rb"
.text:00131055 push offset aPasswordTxt ; "password.txt"
.text:0013105A call sub_1349EC
.text:0013105F add esp, 8
.text:00131062 mov [ebp+var_4], eax
.text:00131065 cmp [ebp+var_4], 0
.text:00131069 jnz short loc_13108A
.text:0013106B push offset aErrorThereIsNo ; "Error :there is no password.txt here\n"
.text:00131070 push 2
.text:00131072 call sub_1347CC
.text:00131077 add esp, 4
.text:0013107A push eax
.text:0013107B call sub_131170
.text:00131080 add esp, 8
.text:00131083 push 0 ; uExitCode
.text:00131085 call sub_13AD0A
.text:0013108A
.text:0013108A loc_13108A: ; CODE XREF: sub_131040+29↑j
.text:0013108A lea eax, [ebp+var_408]
.text:00131090 push eax
.text:00131091 push offset aS ; "%s"
.text:00131096 mov ecx, [ebp+var_4]
.text:00131099 push ecx
.text:0013109A call sub_1311B0
.text:0013109F add esp, 0Ch
.text:001310A2 lea edx, [ebp+var_408]
.text:001310A8 push edx
.text:001310A9 call sub_131000
.text:001310AE add esp, 4
.text:001310B1 mov [ebp+var_8], eax
.text:001310B4 cmp [ebp+var_8], 0
.text:001310B8 jz short loc_1310C9
.text:001310BA push offset aIncorrectPassw ; "incorrect password!\n"
.text:001310BF call sub_1311F0
.text:001310C4 add esp, 4
.text:001310C7 jmp short loc_1310D6
.text:001310C9 ; ---------------------------------------------------------------------------
.text:001310C9
.text:001310C9 loc_1310C9: ; CODE XREF: sub_131040+78↑j
.text:001310C9 push offset aCongratulation ; "congratulation! you passed the verifica"...
.text:001310CE call sub_1311F0
.text:001310D3 add esp, 4
.text:001310D6
.text:001310D6 loc_1310D6: ; CODE XREF: sub_131040+87↑j
.text:001310D6 mov eax, [ebp+var_4]
.text:001310D9 push eax
.text:001310DA call sub_1348C6
.text:001310DF add esp, 4
.text:001310E2 jmp short loc_1310E6
.text:001310E4 ; ---------------------------------------------------------------------------
.text:001310E4 jmp short loc_1310E8
.text:001310E6 ; ---------------------------------------------------------------------------
.text:001310E6
.text:001310E6 loc_1310E6: ; CODE XREF: sub_131040+A2↑j
.text:001310E6 xor eax, eax
.text:001310E8
.text:001310E8 loc_1310E8: ; CODE XREF: sub_131040+A4↑j
.text:001310E8 mov esp, ebp
.text:001310EA pop ebp
.text:001310EB retn
.text:001310EB sub_131040 endp ; sp-analysis failed
.text:001310EB
.text:001310EB ; ---------------------------------------------------------------------------
.text:001310EC align 10h
.text:001310F0
.text:001310F0 ; =============== S U B R O U T I N E =======================================

.text:00131000 ; =============== S U B R O U T I N E =======================================
.text:00131000
.text:00131000 ; Attributes: bp-based frame
.text:00131000
.text:00131000 sub_131000 proc near ; CODE XREF: sub_131040+69↓p
.text:00131000
.text:00131000 var_C = byte ptr -0Ch
.text:00131000 var_4 = dword ptr -4
.text:00131000 arg_0 = dword ptr 8
.text:00131000
.text:00131000 push ebp
.text:00131001 mov ebp, esp
.text:00131003 sub esp, 0Ch
.text:00131006 push offset a1234567 ; "1234567"
.text:0013100B mov eax, [ebp+arg_0]
.text:0013100E push eax
.text:0013100F call sub_13A860
.text:00131014 add esp, 8
.text:00131017 mov [ebp+var_4], eax
.text:0013101A mov ecx, [ebp+arg_0]
.text:0013101D push ecx
.text:0013101E lea edx, [ebp+var_C]
.text:00131021 push edx
.text:00131022 call sub_13A8F0
.text:00131027 add esp, 8
.text:0013102A mov eax, [ebp+var_4]
.text:0013102D mov esp, ebp
.text:0013102F pop ebp
.text:00131030 retn
.text:00131030 sub_131000 endp
.text:00131030
.text:00131030 ; ---------------------------------------------------------------------------
.text:00131031 align 10h
.text:00131040
.text:00131040 ; =============== S U B R O U T I N E =======================================

阅读上面显示的反汇编代码,可以知道通过验证的程序分支的指令地址为0x001310C7。0x001310A9处的函数调用就是verify_pasword函数,之后在0x001310B1处将EAX中的函数返回值取出,在0x001310B4处与0比较,然后决定跳转到提示验证错误的分支或提示验证通过的分支。

调用verify_password的时候。如果我们把返回地址覆盖成这个0x001310C9地址,那么在0x001310A9处的函数调用返回后,程序将跳转到验证通过的分支,而不是进入0x001310C7处分支判断代码。这样我们就可以按照如下方法构造password.xt中的数据。

仍然出于字节对齐、容易辨认的目的,我们将“1234”作为一个输入单元。buffer[8]共需要两个这样的单元。第3个输入单元将authenticated覆盖;第4个输入单元将前栈帧EBP值覆盖:第5个输入单元将返回地址覆盖。为了把第5个输入单元的ASCⅡ码值0x修改成验证通过分支的指令地址0x001310C9,我们将借助十六进制编辑工具Notepad++的HexEdit插件或者vscode的hexdump插件来完成(Ox40、0x11等ASCIⅡ码对应的符号很难用键盘输入)。

步骤1:创建一个名为password.xt的文件,并用编辑器打开,在其中写入5个“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 1234

步骤2:使用编辑器的hex编辑模式

步骤3:将最后4个字节修改成新的返回地址,注意这里是按照“内存数据”排列的,由于“大顶机”的缘故,为了让最终的“数值数据”为0x001310C9,我们需要逆序输入这4个字节。

  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: C9 10 13 00 I...

在运行该程序时便完成了栈溢出攻击。

栈溢出基础实验1

由于栈内EBP等被覆盖为无效值,使得程序在退出时堆栈无法平衡,导致崩溃。虽然如此,我们已经成功地淹没了返回地址,并让处理器如我们设想的那样,在函数返回时直接跳转到了提示验证通过的分支。

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