栈溢出实验

实验目的

  1. 通过对程序输入的密码的长度、内容等修改用Ollydbg来验证缓冲区溢出的发生;
  2. 完成淹没相邻变量改变程序流程实验;
  3. 完成淹没返回地址改变程序流程实验。

理解程序

阅读并理解代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <string>
#define PASSWORD "1234567"

int verify_password (char *password)
{
int authenticated;
char buffer[8];// add local buff
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}


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("Congratulation! You have passed the verification!\n");
break;
}
}
}
  1. 在主函数内输入密码password

  2. 跳转到子函数verify_password()判断输入的密码是否正确即判断输入的密码password与正确的密码1234567是否相等,如果相等则子函数返回0,否则返回非0,在函数返回之前将输入的password拷贝到数组buffer[8]里面

  3. 主函数在判断子函数verify_password()返回值:如果是0,则输出Congratulation! You have passed the verification!,结束程序,否则输出incorrect password!,继续输入密码password重复上述过程

    1565799697555

验证缓冲区溢出

(通过对程序输入的密码的长度、内容等修改验证缓冲区溢出的发生)

原理

下图是栈中缓冲区示意图,其中每一行占四个字节,发生缓冲区溢出只需设置数组buffer[8]的长度大于8字节。是buffer的值覆盖authenticated等的值。

1565799861740

操作过程

  1. 使用Ollydbg打开overflow_var.exe文件

    1565799976558

  2. 同时按下AltF9执行到用户代码如下图:在地址为00401724CALL test.00401014

    1565800007485

  3. 找到地址00401014JMP test.main,即此处向下为用户代码,通过分析用户代码,发现004010300040107F为子函数verify_password()反汇编代码

    1565800057648

  4. 分析反汇编代码,找到源代码strcpy(buffer,password)所在的位置发现在00401064处,设置断点。然后点击运行,输入密码为444程序走到这个语句时会自动停止,接着点击不进入函数的单步调试按钮

    1565800090252

  5. 调试过程中观察右下角栈数据,发现两个地址被赋值为444 其中4ASCII码0x34

    1565800116530

    1565800210253

  6. 继续运行,输入密码为AAAAAAAAABC,重复上述操作,得到结果如下,由于填写的字符串超过位数,已经将邻接的变量覆盖,即缓冲区已经溢出。其中AASCII码0x41。其中buffer[8]的地址为0012FB18——0012FB1Fauthenticated的地址为0012FB20——0012FB23

    1565800272406

    1565800278163

淹没相邻变量改变程序流程

原理

下图是栈中缓冲区示意图,其中每一行占四个字节,只需增加数组buffer[8]的长度将原来authenticated的值覆盖掉,更改为想要的值即可,即如果想要跳过密码,只需要将authenticated的值覆盖为0

1565799861740

操作过程

  1. 输入错误密码1234,查看buffer[8]authenticated的值

    1565800411998

    Buffer[0-7]:0x34333231 CCCCCC00 (其中00是字符串结束符标识)

    Authenticated:0XFFFFFFFF(因为1234<1234567,authenticated的值为-1,写成补码的形式即为0xFFFFFFFF)

    运行结果如下

    1565800478425

  2. 输入错误密码2345,查看buffer[8]authenticated的值

    1565800508388

    Buffer[0-7]:0x35343332 CCCCCC00 (其中00是字符串结束符标识)

    Authenticated:0X00000001 (因为2345>1234567,authenticated的值为1,写成补码的形式即为0x00000001)

    运行结果如下

    1565800555252

  3. 输入正确密码1234567,查看buffer[8]authenticated的值

    1565800579050

    Buffer[0-7]:0x34333231 00373635 (其中00是字符串结束符标识)

    Authenticated:0X00000000

    运行结果如下

    1565800609024

  4. 根据上述三次密码输入和对代码的分析,当authenticated的值为0x00000000时,才能得到正确的输出结果,即跳过密码。

    为此,采用将密码输入为8位(这8个数要比1234567大,否则会导致authenticate补码较大如0Xffffff00),即末尾的空白结束符将authenticated0x01淹没。

    随机输入密码23456789查看buffer[8]authenticated的值

    1565800684920

    Buffer[0-7]:0x35343332 39383736

    Authenticated:0X00000000

    Buffer的结束符淹没了authenticated的值使authenticated的值为0x00000000

    运行结果如下

    1565800712667

    显示密码输入正确,实现了淹没相邻变量改变程序流程。

淹没返回地址改变程序流程

原理

下图是栈中缓冲区示意图,其中每一行占四个字节。只需要先放置16个字符串,然后接下来的4个字节就能够淹没返回地址,达到控制返回地址的目的。

1565799861740

操作过程

  1. 由于返回地址的数据有些不是能通过可见的ASCII字符表示的,修改程序,让文件作为输入源

  2. 使用ollydbg打开该overflow_ret.exe文件,通过查找字符串发现如果密码正确,程序会到地址0x0040112F出继续执行程序, 接下来的工作就是将返回地址修改为0x0040112F

    1565800877672

  3. overflow_ret\Dbug目录下新建password.txt文件,使用UltraEdit进行编辑(记事本只能输入可见的文字,有局限),利用Ctrl+H进入二进制编辑模式,前16个字节输入AAAAAAAAAAAAAAAA,在17-20字节上填下地址0x0040112F(注意从右向左填) ,保存文件

    1565800935070

  4. 使用ollydbg再次调试overflow_ret.exe文件,会直接弹出Congratulation! You have passed the verification!,即密码输入正确

    1565800965098

  5. 由于修改了返回地址,导致栈平衡出错,最后会显示调试的程序无法处理例外

    1565800984564

思考题

  • StackOverrun程序为靶子,通过自己使用ollydbg调试,两个要求:其一,要求分析PE格式加载到内存中的地址变化;其二,挑选其中一处函数的跳转,详细分析,跳转时spbpip的变化,要求以程序运行的顺序记录跳转时的这些寄存器的变化。
  • 在不修改源代码的情况下,修改StackOverrun程序的流程,通过淹没返回地址,用jmp esp的方式,让其调用bar函数并输出结果

分析源代码

程序由三个函数组成,一个main函数,两个子函数foobar,main函数打印两个子函数的起始地址并调用foo函数。foo函数打印当前栈顶向下40个字节的地址,bar函数打印一串字符串,正常情况下程序不会调用bar函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <stdio.h>
#include <string.h>

void foo(const char* input)
{
char buf[10];

//What? No extra arguments supplied to printf?
//It's a cheap trick to view the stack 8-)
//We'll see this trick again when we look at format strings.
printf("My stack looks like:\n%p\n%p\n%p\n%p\n%p\n% p\n%p\n%p\n%p\n%p\n\n");

//Pass the user input straight to secure code public enemy #1.
strcpy(buf, input);
printf("%s\n", buf);

printf("Now the stack looks like:\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n%p\n\n");
}

void bar(void)
{
printf("Augh! I've been hacked!\n");
}

int main(int argc, char* argv[])
{
//Blatant cheating to make life easier on myself
printf("Address of foo = %p\n", foo);
printf("Address of bar = %p\n", bar);

foo(argv[1]);
return 0;
}

分析内存地址变化

根据程序显示,foo()的起始地址为0x00401000,bar()的起始地址为00401060,分析汇编代码知道main()函数的起始地址为0x00401070,在这三个位置分别设置断点,然后进行调试分析。

  1. 把入口地址0x00401000压入栈中

    1565834365217

  2. 把要打印的Address of foo = %p的地址0x004070DC压入栈中并转到地址0x004010B0

    1565834372312

  3. 执行返回后,将栈顶指针ESP地址增加8字节,ADD ESP,8

  4. 把入口地址0x00401000压入栈中

    1565834378838

  5. 把要打印的Address of bar = %p的地址0x004070C4压入栈中并转到地址0x004010B0

    1565834384587

  6. ECX压入栈中

    1565834389557

  7. 将返回地址0x0040109E压入栈中,并跳转到地址0x00401000处即跳转到函数foo

    1565834395688

  8. ESP地址降低0x0C字节SUB ESP,0C

    1565834400315

  9. ESIEDI压入栈

    1565834406262

  10. 把要打印的My stack looks like: %p %p %p %p %p %p %p %p %p %p 的地址0x00407070压入栈中并转到地址0x004010B0

    1565834411915

  11. 执行返回后,将栈顶指针ESP地址增加4字节,ADD ESP,4

    1565834418120

  12. 程序执行到0x0040101B处便无法执行已经执行完毕

    1565834422939

分析sp,bp,ip变化

分析程序由main()函数跳转到foo函数时sp,bp,ip的变化

  1. 在程序跳转之前位于地址0x00401098处,操作是将ECX压入栈

    1565834540332

    此时各个寄存器的情况如下,ECX的值为0x00000000ESP的值为0x0012FF7C,EBP的值为0x0012FFC0,EIP的值为0x00401098

    1565834562756

    内存分布情况如下图所示

    1565834577380

  2. 向下执行一步,此时ECX已经入栈,各个寄存器的情况如下,ESP向上移动四个字节,值为0x0012FF78,EBP的值不变,为0x0012FFC0EIP的值为0x00401099

    1565834608803

    内存分布情况如下图所示

    1565834620098

  3. 执行CALL stack0ve.00401000进行跳转,各个寄存器的情况如下,ESP向上移动四个字节,值为0x0012FF74, EBP的值不变,为0x0012FFC0EIP的值为0x00401000

    1565834665736

    内存分布情况如下图所示,新增加的栈顶存储的是main()函数的返回地址0x0040109E

    1565834685709

  4. 继续执行SUB ESP,0C,栈顶向上移动12字节,各个寄存器的情况如下,ESP值为0x0012FF68, EBP的值不变,为0x0012FFC0EIP的值为0x00401003

    1565834727293

    内存分布情况如下图所示,新增加的栈从0x0012FF6C0x0012FF73没有存储任何值,0x0012FF680x0012FF6B存储的是地址0x00407128

    1565834768220

  5. 小结:函数跳转时先将调用参数入栈(0x0012FF78),然后将返回地址入栈(0X0012FF74),最后将局部参数入栈(0x0012FF68-0x0012FF73)

修改StackOverrun程序的流程

通过淹没返回地址,用jmp esp的方式,让其调用bar()函数并输出结果

  1. 程序运行结果如下

    1565834834550

    根据上述分析和程序运行结果可知输出的十个地址中,0x0040109Emain()函数的返回地址。根据源代码分析,只需在程序后输入地址覆盖0x0040109E0x00401060即可

  2. 在程序后输入 “**AAAAAAAAAAAAAAAAAAAA@**" ,其中 "**@ **”的十六进制ASCII码0x601040得到结果如下

    1565835109100

  3. 发现输出的是个地址中最上面的两个与程序后面输入的内容无关,故输入为“**AAAAAAAAAAAA@** ”,程序运行结果如下,成功调用bar()`函数并输出结果

    1565835211633