软件安全第二次实验报告

实验目标

  1. 实验环境:Windows(wsl)或Linux下。

工具:Ollydbg、IDA、gdb、x64dbg等。

  1. 实验要求

(1)通过对程序输入的密码的长度、内容等修改,使用任意一种工具(Ollydbg)来验证缓冲区溢出的发生(参考提供的两个代码,附件/overflow_ret和附件/overflow_var),完成淹没相邻变量改变程序流程、完成淹没返回地址改变程序流程。(以上记作实验一)

(2)以StackOverrun程序(附件/StackOverrun.exe.zip)为靶子,通过使用(1)中的任意一种工具进行调试;分析PE格式加载到内存中的地址变化,请写出代码段地址,从文件存储到内存的变化;挑选其中一处函数的调用,详细分析,调用时sp,bp,ip的变化,要求以程序运行的顺序记录跳转时的这些寄存器的变化,特别是栈的变化,要求对这些内存各个部分的截图,并附加说明到报告中;推测程序的功能,并描述出来。(以上记作实验二)

(3)尝试修改StackOverrun程序的流程,通过淹没返回地址,比如可以尝试用jmp esp的方式(方式可以自选),让其调用bar函数并输出结果。(以上记作实验三)

实验一步骤与结果

(一)读懂密码验证程序

1.进入虚拟机,打开栈溢出实验文件夹下stackvar文件夹中的overflow_var.cpp文件。

图1

2.程序包含两部分:verify_password函数和main函数。

(1)首先分析verify_password函数:

a)传入参数:char *password是一个名为password的字符数组。

b)函数中定义了一个整型变量authenticated和一个长度为8的字符数组buffer[]。

c)调用strcmp函数比较password和常量PASSWORD两个字符串,并将比较结果赋值给authenticated。

d)进行字符串比较后,调用strcpy函数将字符串password的内容复制到buffer数组中。

e)函数最后返回变量authenticated。

(2)接下来分析main函数:

a)首先定义了一个初始值为0的整型变量valid_flag和一个长度为1024的字符数组password[]。

b)接下来程序开始一个while循环,循环中打印“please input password:”后等待用户输入,将输入值赋值给password。

c)调用veriy_password函数,将password作为参数传入,返回值赋值给valid_flag。

d)若valid_flag不等于0,即strcmp结果不为0,说明password与正确密码PASSWORD不符,打印提示信息“incorrect password!\n\n”,继续下一轮while循环。

e)若valid_flag等于0,即strcmp结果为0,说明password与正确密码PASSWORD相同,打印提示信息“Congratulation! You have passed the verification!\n”,并跳出while循环,程序结束。

(3)由以上分析可知,密码验证程序起到的作用是:正确密码为1234567。用户输入一个字符串,若与正确密码相同,则通过验证、程序结束;若与正确密码不同,则验证失败,继续下一次验证。

(二)创建可执行程序并进行测试

1.打开Visual C++,新建一个工程,命名为main2_project。

图2

图3

2.在工程中新建一个空白C++文件(选择C++ Source File),命名为main2.cpp。

图4

3.将main.cpp的代码内容复制到main2.cpp中。

图5

4.运行程序并输入密码进行尝试。

(1)输入不超过7位的错误密码,如“12333”、“12345”,程序输出“incorrect password!”后继续下一轮循环。

图6

(2)直至输入正确密码“1234567”,程序输出“Congratulation!You have passed the verification!”并结束。

图7

(3)输入8位密码如“12345678”、“qqqqqqqq”时,程序均判断密码正确、通过验证。

图8

(4)当输入比8位稍长的密码如“1234567890”时,程序判断密码错误。

图9

(5)当输入很长的密码如20位密码“12345678901234567890”时,程序不进行输出而直接结束。

图10

(三)开始调试并测试四种情况

1.使用Ollydbg打开我们生成的exe文件,开始运行,输入几个短的错误密码如“12345”等,找到我们要调试的代码段。

图11

2.找到strcpy语句(比较靠上),双击此语句设置断点。

图12

3.点击运行后,程序走到strcpy语句时自动停止,然后点击不进入函数的单步调试按钮,点击后程序运行到strcpy语句的下一行代码。

图13

4.在strcpy函数执行后,查看缓冲区的内容。输入情况分为以下四种:

(1)短的错误密码“123”:可以看到两个地址中的值被赋值了“123”,同时0012FB18也为123。

图14

程序执行到strcpy函数时的缓冲区情况

图15

strcpy函数执行后的缓冲区情况

(2)正确密码“1234567”:可以看到有两个地址中的值被赋值为“1234567”;且此情况与(1)情况均属于程序正常功能,比较后可知地址0012FABC处的值均为0012FB24,故推测此值为程序正常返回地址值。并且发现程序判断成密码输入正确并自动关闭。

图16

程序执行到strcpy函数时的缓冲区情况

图17

图18

strcpy函数执行后的缓冲区情况

(3)八位错误密码“12345678”:可以看到由于我们输入的字符串超过了buffer数组最大长度,结尾结束符’\0’将会覆盖邻接变量;但返回地址值0012FB24仍与此前相同,可见输入八位密码并未影响返回地址。

图19

程序执行到strcpy函数时的缓冲区情况

图20

图21

strcpy函数执行后的缓冲区情况

(4)二十位的错误密码“12345678901234567890”:可见由于我们填写的字符串过长,已经将邻接的变量覆盖了;而且返回地址值也不再是0012FB24,而是变成了0012FF80,说明20位的密码影响了返回地址值。

图22

程序执行到strcpy函数时的缓冲区情况

图23

图24

strcpy函数执行后的缓冲区情况

5.为了看到程序的输出结果,将strcpy处的断点取消,查找字符串找到程序输出的提示信息“Congratulation!…”并双击跳转到此处,双击设置断点。

图25

6.输入八位错误密码“12345678”,点击运行发现程序运行至断点处,单步步过至程序输出“Congratulation! You have passed the verification!”,可知八位错误密码可以通过程序验证。

图26

程序运行至断点处

图27

程序输出验证通过的提示信息

(四)验证补码

1.仍然在调用strcpy函数处设置断点,点击运行输入“01234567”,程序运行至断点处,并点击单步步过。

图28

程序运行至strcpy处时的缓冲区情况

图29

图30

strcpy执行后的缓冲区情况

2.由以上结果可知,函数返回地址0012FB24仍然正确未受影响。

图31

图32

3.取消strcpy处的断点,而是在输出“incorrect…”处设置断点,点击运行并单步步过直至程序输出“incorrect password!”,可知程序判断密码错误、验证失败,符合程序正常功能。

(五)分析原理

由以上操作及其结果,分成以下三种情况对此程序进行分析,分别为:淹没邻接变量、淹没返回地址、验证补码。

图33

程序堆栈情况

1.淹没邻接变量

当输入大于正确密码1234567的八位密码如“12345678”时,比字符数组buffer最大长度多一个字节,即为字符串结尾的空字符’\0’。

(1)函数verify_password执行strcmp语句后,由于所输入的password大于1234567,判断结果为authenticated=1。

(2)空字符淹没邻接变量authenticated,导致返回值authenticated=0。

(3)但返回地址未受影响,因此函数verify_password执行完毕后程序可以根据返回地址正确地返回主函数执行接下来的if判断语句。

程序根据verify_password函数返回值authenticated=0判断密码正确、通过验证。

图34

淹没邻接变量的堆栈情况

2.淹没返回地址

当输入很长的错误密码如“1234567891234567890”时,比字符数组buffer最大长度多出12个字节。

(1)函数verify_password执行strcmp语句后,由于所输入的password大于1234567,判断结果为authenticated=1。

(2)多出的前四个字符淹没邻接变量authenticated,导致返回值authenticated=“9012”。

(3)其余八个字符淹没堆栈指针及返回地址,导致返回地址错误,程序不能正确地返回主函数main,程序崩溃退出。

图35

淹没返回地址的堆栈情况

3.验证补码

当输入小于正确密码1234567的八位密码如“01234567”时,比字符数组buffer最大长度多一个字节,即为字符串结尾的空字符’\0’。

(1)函数verify_password执行strcmp语句后,由于所输入的password小于1234567,判断结果为authenticated=-1,在内存中-1采用补码0xFFFFFFFF表示。

(2)空字符淹没邻接变量authenticated,导致返回值authenticated在内存中表现为0xFFFFFF00,因此authenticated≠0。

(3)但返回地址未受影响,因此函数verify_password执行完毕后程序可以根据返回地址正确地返回主函数执行接下来的if判断语句。程序根据verify_password函数返回值authenticated≠0判断密码错误、验证失败。

图36

验证补码的堆栈情况

实验二步骤与结果

(一)初步分析

1.首先直接打开程序查看内容

图37

2.再打开ida查看,发现只有两个函数

图38

图39

(二)分析PE格式加载到内存中的地址变化

1.用x32dbg打开exe程序进行分析,以下是进入程序时的内存情况。。

图40

2.用x32dbg打开,可以看到foo()的起始地址为0x00401000,bar()的起始地址为00401060, 分析汇编代码知道main()函数的起始地址为0x00401070,可以在这三个位置分别设置断点,进行代码分析,观察内存中地址的变化。

图41

图42

3.从main函数开始,将401000(foo)的地址和字符串”Address of foo = %p\n”4060DC分别压入栈(esp为0019FF30)中,并转移至4010B0地址。

图43

图44

图45

4.在4010B0执行完后,再将401060(bar)的地址和字符串”Address of bar= %p\n”4060C4分别压入栈中,esp-8(8个字节),并转移至4010B0地址,并将返回地址压入栈中。

图46

图47

5.在4010B0执行完后。再将ecx压入栈中,跳转至401000处,其实就是跳转到foo函数中。此时esp继续-4,然后将main函数的返回地址压入栈中,进入foo函数。此时esp为0019FF20。

图48

图49

6.进入foo,esp进行-12,然后将esi,edi压入栈中,还有要打印的“ My stack looks like: %p %p %p %p %p %p %p %p %p %p ”的地址0x00406070压入栈,esp又减去12,再转移到 4010B0出,并压入返回地址,esp-4,此时esp为0019FF08。

图50

7.当程序走到40101C处时,此时esp为0019FF08,ebp为0019FF74。但是我们发现应该带着输入运行程序。

图51

8.我们带着输入123456,运行程序到401038处,此时将ecx(我们的输入)压入栈,还有push 40606C,压入返回地址,然后转移到4010B0,此时esp-8,然后返回。esp = 0019FF00。

图52

图53

图54

9.然后push406030,esp-4再esp+10。然后弹出esi,edi,esp+4,最后esp+c。最后esp = 0019FF24。

图55

图56

10.最后,返回main函数,esp+14。最终esp = 0019FF38。程序结束。

图57

图58

11.使用CFF Explorer查看PE节表信息(静态分析),从而找到.text段在文件中的偏移与其在内存中的相对虚拟地址(RVA)。用CFF Explorer打开程序,选择段头部。

图59

12.右侧表格中找到.text行(代码段),记录如下数据

字段名称 含义
VirtualAddress 节在内存中的相对地址(RVA) 0x00001000
Raw Address 节在文件中的偏移地址(文件存储地址) 0x00001000
VirtualSize 节在内存中的实际大小 0x00003BE6
Raw Size 节在文件中的存储大小 0x00004000

13.查看模块加载基址(动态分析)。查看程序在内存中被加载到的实际基址(ModuleBase),以计算.text段的实际内存虚拟地址。将程序通过x32dbg运行,在上方菜单栏中选择视图→模块选择StackOverrun.exe后右键选择在内存布局中转到。

图60

图61

14.可以看到模块加载基址为(ModuleBase)0x00400000,以及代码段地址(.text)为0x00401000。由此可见.text段地址变化:

文件存储地址(PointerToRawData)= 0x00001000

内存加载地址(VA)= 0x00401000

地址变化规律:VA = ModuleBase + VirtualAddress

(三)挑选其中一处函数的跳转,详细分析,跳转时sp,bp,ip的变化

1.挑选一下foo函数分析。程序跳转之前,此时的操作时将ecx压入栈,ecx = 00600EBE,00600EBE处的值其实就是我们输入的ABCDRFGHAB,也可以看到栈的具体值。eip:401095,esp:19FF28,ebp:19FF74。

图62

图63

图64

2.将ecx压入栈中后,ESP向上移动四个字节。eip:401096,esp:19FF24,ebp:19FF74。

图65

图66

3.执行CALL stack0ve.00401000 进行跳转,各个寄存器的情况如上所示,ESP向上移动四个字节,新增加的栈顶存储的是main()函数的返回地址 0040109B。eip:401000,esp:19FF20,ebp:19FF74。

图67

图68

图69

4.然后此时已经是foo函数的操作,继续执行SUB ESP,0C,栈顶向上移动12字节。eip:401003,esp:19FF14,ebp:19FF74。最后我们可以知道的是在调用函数时,先将调用参数入栈(0x00600EBE),然后将返回地址入栈(0X0040109B),最后将局部参数入栈。

图70

图71

图72

(四)推测程序的功能

注意到foo和bar函数的地址为401000和401060,以及当在foo函数中运行时,此时对应的输出即为esp向下的40个字节的栈的值。bar函数为直接打印一个字符串”Augh! I’ve been hacked!\n”。所以推测程序为由三个函数组成,一个main函数,两个子函数foo和bar,main函数打印两个子函数的起始地址,然后调用 foo 函数。foo函数打印当前栈顶向下40个字节的地址,bar函数打印一串字符串”Augh! I’ve been hacked!\n”。正常程序是不会调用bar函数。

图73

图74

实验三步骤与结果

修改StackOverrun程序的流程

1.查看foo函数可以知道,foo需要一个输入作为参数,StackOverrun\StackOverrun.exe” 1234567891,以1234567891运行程序。

图75

2.可以看到会打印出1234567891,并且在00401032和0040102B处,我们看到rep movsd是将前8位复制给buf,rep movsb,将剩下的复制过去。因此复制不进行字符串输入的检查,因此可以使用栈溢出攻击。并且这里存在返回地址,索性直接bar的地址(00401060)覆盖返回地址。所以可设计输入字符串为ABCDEFGHABCD`@,并运行。

图76

图77

图78

3.可以看到已经将返回地址通过栈溢出修改为bar的地址(00401060)。

图79

图80

4.然后可以成功调用bar函数,并打印出对应需要的结果Augh! I’ ve been hacked!。但是程序最后会出现异常,因为随意覆盖代码会导致栈不平衡。至此完成淹没返回地址调用bar函数,输入参数可为ABCDEFGHABCD`@。

图81

实验结论

通过本次实验,我对栈溢出攻击的原理、实现方式及其危害性有了系统而深入的理解,并成功完成了栈溢出攻击的实践操作。栈溢出,或称缓冲区溢出,是一种危害性极高的安全漏洞,其本质在于当程序向栈上的缓冲区写入数据时,若未对输入长度进行有效校验,导致写入的数据量超过了缓冲区自身的容量,多余的数据便会覆盖相邻的内存区域。

在函数调用栈中,局部变量与函数的返回地址紧密相邻。因此,一旦攻击者通过诸如strcpy等不安全的函数将精心构造的过长数据复制到缓冲区,不仅能淹没相邻变量改变程序逻辑,更关键的是可以覆盖函数的返回地址。这将直接劫持程序的执行流程,使程序在函数返回时跳转到攻击者指定的任意地址,从而执行恶意代码。

实验过程中,我使用x32dbg调试工具,具体分析了PE格式文件从硬盘加载到内存后的地址映射关系,并深入观察了函数调用过程中SP、BP、IP等寄存器的动态变化以及栈帧的构建与销毁过程。通过对StackOverrun程序这一“靶子”的调试分析,我直观地验证了上述理论。最终,我通过精确计算偏移并构造输入数据,成功利用淹没返回地址的技术修改了程序流程,实现了让其跳转至bar函数执行的目标。

这次实践让我深刻认识到,此类漏洞的根源在于开发阶段对内存安全缺乏足够的重视。因此,在软件开发中,实施严格的输入验证是至关重要的防御基石。我们必须避免使用gets、strcpy等存在已知风险的传统函数,转而采用具备边界检查的安全函数(如fgets、strncpy),从编码环节确保内存操作的可靠性,以从根本上杜绝溢出风险。这次实验不仅锻炼了我的动手调试与分析能力,更极大地强化了我对系统底层机制和安全编程原则的理解,对提升软件安全开发意识具有重要意义。

实验报告持续更新中…