参考教材:计算机系统基础 第二版 袁春风 机械工业出版社
参考慕课:计算机系统基础(四):编程与调试实践 https://www.icourse163.org/learn/NJU-1449521162
计算机系统实验导航
实验一:环境安装 https://blog.csdn.net/weixin_46291251/article/details/122477054
实验二:数据的存储与运算 https://blog.csdn.net/weixin_46291251/article/details/122478255
实验三:程序的机器级表示 https://blog.csdn.net/weixin_46291251/article/details/122478979
实验四:二进制程序逆向工程 https://blog.csdn.net/weixin_46291251/article/details/122479554
实验五:缓冲区溢出攻击 https://blog.csdn.net/weixin_46291251/article/details/122479798
实验六:程序的链接 https://blog.csdn.net/weixin_46291251/article/details/122480049
实验源码: xxx
准备
实验内容:
1 C 语句与机器级指令的对应关系,IA-32 基本指令的执行;
2 C 语言程序中过程调用的执行过程和栈帧结构;
3 缓冲区溢出攻击。
实验目标:
1 掌握程序的机器级表示相关概念;
2 理解 C 语言程序对应机器级指令的执行和过程调用实现;
3 掌握程序的基本调试方法和相关实验工具的运用。
实验任务:
1 学习 MOOC 内容
https://www.icourse163.org/learn/NJU-1449521162
- 第四周 程序的机器级表示
第 4 讲 控制转移指令
第 5 讲 栈和过程调用
第 6 讲 缓冲区溢出
2 完成作业
题目一:比较按值传递和按地址传递
对下列程序代码进行反汇编,指出过程调用中相关语句,比较按值传递参数和按地址传递参数,画出过程调用中栈帧结构图,并给出解释说明。
程序代码和注释说明
Swap.c
1.#include <stdio.h>
2.int swap(int x,int y)
3.{
4. int t=x;
5. x=y;
6. y=t;
7.}
8.void main()
9.{
10. int a=15, b=22;
11. swap(a, b);
12. printf("a=%d\tb=%d\n", a, b);
13.}
Swap2.c
1.#include <stdio.h>
2.
3.int swap(int *x,int *y)
4.{
5. int t=*x;
6. *x=*y;
7. *y=t;
8.}
9.void main()
10.{
11. int a=15, b=22;
12. swap(&a,&b);
13. printf("a=%d\tb=%d\n",a,b);
14.}
过程P调用过程Q的过程:
①P的准备阶段
②从P控制转移到Q: call指令
③Q的准备阶段
④执行Q的过程体( 函数体)
⑤Q的恢复阶段
⑥从Q返回到P: ret指令
实验结果记录
编译swap1,反汇编swap2并打开反汇编文档:
查看main函数中的swap对应的汇编代码:
其中:
前四个指令是过程调用的准备工作:将入口参数送入栈中。
Call指令用来实现swap过程的调用。
Add用来做过程调用的结束工作,回收入口参数的栈空间。
然后查看swap函数对应的反汇编代码:
其中:
前三条做过程体执行的准备工作,第一条保存调用者的ebp值,然后建立自己的栈空间,然后为自己的非静态变量分空间。
然后后面是过程体,做两个数的交换
最后用leave指令结束工作,回收栈空间,ret返回调用者。
下面用gdb调试这个程序:
将断点设置在main函数处,然后执行一条c语句,使得执行点停留在swap前面。
然后查看eip的值即为swap语句对应的位置。
再查看ebp,esp的值。
通过获取到的寄存器的值,查看当前的栈帧内容:
Main的栈帧就是ebp 和esp指向的栈空间。
观察分析上述栈帧结构,画出结构图:
然后执行call指令前面的几条指令:即
再次查看当前栈帧内容:
结构图为:
对比执行前的栈帧,可以发现这四个指令就是main调用swap的一个准备过程
然后执行call指令,再显示当前eip 、ebp 、esp 的内容,再显示当前栈帧内容:
Call指令将目标地址送入eip寄存器中,改变了程序执行的顺序,同时将下一跳指令的地址作为返回地址送入栈中。
然后程序执行进入swap过程:
执行Swap的前两条指令,显示ebp和esp的内容:
再显示当前栈帧和main栈帧的内容:
上面显示的单元内显示了main的栈帧空间。
Swap指令的第三条指令是一个减法指令,执行这条指令,然后显示当前的ebp和esp内容,显示当前栈帧内容
这依旧是过程调用的步骤三,swap的准备工作,给非静态局部变量分配栈空间。
然后执行三条c语句,实现两个变量值的交换,把程序的断点停留在leave之前:
然后显示当前的ebp和esp内容,显示当前栈帧内容
可以看出main栈帧中ab的值进行了交换,swap过程通过ab地址读写了ab的内容。
然后执行leave指令,然后显示当前的ebp和esp内容,显示当前栈帧内容
可以看出栈帧又还原为main的栈帧了,这就是过程调用的步骤5,即swap的结束工作:回收栈空间。
现在执行return指令,显示然后显示当前的eip、ebp和esp内容,显示当前栈帧内容。
可看出返回地址被弹出送入eip寄存器。程序从swap跳转到call的吓一条指令处,所以swap调用结束。
下一条指令是add指令,执行这条指令,通过观察栈帧,分析可知,其回收了入口参数的栈空间,即main的结束工作。
程序执行结束。
对按值传递的swap1程序也用同样的方法分析,经过对比可知:
按地址传递方式比按值传递方式多了lea地址传送指令,其是把a和b的地址作为入口参数传送进栈,在按值传送过程中仅仅把值传送进栈,过程调用中add和call指令是完全一致的。
在被调用的swap过程中前三条指令和后三条指令是一致的,即准备和结束工作是一样的。
但是过程体中的指令有区别,按地址传递方式使用入口参数的内容作为地址,用寄存器间接寻址方式读写了调用者a和b的内容。而按值传递方式仅仅是读写了入口参数中的内容,在过程调用结束的时候会回收入口参数栈空间,回收后相当于什么也没做。
题目二:缓冲区溢出
编译执行如下 C 语言程序(bug.c 和 hack.c),指出该程序的漏洞,对程序代码进行反汇编,采用 gdb 跟踪程序执行,分析程序执行过程中的栈帧结构,改变 hack.c 程序代码中的输入字符串 code,使程序转到攻击函数 hacker()执行。画出程序执行过程中的栈帧结构图,并给出解释说明。
程序代码和注释说明
C 语言程序 1:bug.c
1.#include <stdio.h>
2.#include "string.h"
3.void outputs(char *str)
4.{
5. char buffer[16];
6. strcpy(buffer, str);
7. printf("%s\n", buffer);
8.}
9.
10.void hacker(void)
11.{
12. printf("being hacked \n");
13.}
14.
15.int main(int argc, char *argv[])
16.{
17. outputs(argv[1]);
18. printf("yes cheney\n");
19. return 1;
20.}
C 语言程序 2:hack.c
#include "stdio.h"
#include "string.h"
char code[]="0123456789abcdef"
"abcdabcd"
"\x28\xfe\xff\xbf"
"\x9c\x91\x04\x08"
"\xd9\x91\x04\x08";
int main(void)
{
char *arg[3];
arg[0]= "./bug";
arg[1]=code;
arg[2]=NULL;
execve(arg[0], arg, NULL);
return 0;
}
实验结果记录
a2缓冲区溢出攻击程序的执行步骤:
1.关闭栈随机化(只需要执行一次)
sudo sysctl -w kernel.randomize _va_ space=0
2.编译程序,同时关闭栈溢出检测,生成32位应用程序,支持栈段可执行: ;
gcc -00 -m32 -g -fno-stack-protector -z execstack -no-pie -fno-pic a2.c -0 a2
gcc -00 -m32 -g -fno-stack-protector -z execstack -no-pie -fno-pic b.c-o b
3.反汇编并保存到文本文件
objdump-Sa2 > a2.txt
objdump-S b > b.txt
4.调试执行a2,完善a2.c中的code内容。
5.重新编译a2,修改填充的ebp值,要求与调试中b的main的ebp值一致。
6.执行./a2,观察执行结果。
由于实现设计到的指令较长且较多,故编写shell脚本自动化的完成以上工作,之后执行时只需要执行一次shell脚本即可。内容如下:
执行上述脚本之后,用gdb调试程序hack:
断点设置在main然后单步调试,直到当前语句停留在bug的main函数上,然后输出eip、ebp、esp的值,记录下ebp的值。
显示当前栈帧内容,这是当前main函数的栈帧范围。
然后继续执行程序,继续执行s命令看到进入了outputs过程,直至执行完字符串复制的库函数,然后显示当前的eip、ebp和esp内容。
查看bug.c 中hack的首地址,如下:
然后修改hack.c的内容如下:
填入随机的几个字符串用于填充缓冲区,然后填入上述ebp的值和hack的首地址,如下:
然后运行:
可见,成功进入hack函数bing执行。
但是hack过程执行ret指令时没有正确的返回地址,造成了段错误。
分析程序的执行过程可知:
正常执行hack程序时会执行位于bug程序的output函数,输出字符串后,该函数就会返回hack的main继续执行,但是进过如上的修改之后buffer赋值时会越界,用bug中hacker函数的首地址代替了outputs的返回地址,所以output执行结束时进入了hacker函数执行,从而完成了缓冲区溢出攻击,但是hacker结束之后没有正确的返回地址所以报段错误。
分析程序执行outputs时的栈帧结构可得如下结构图:
但是我们希望的过程是:hack的执行调用的bug的执行,bug调用outputs的执行,outputs的返回导致了hacker的执行,而hacker结束后返回到调用outputs的语句之后继续执行,这样就看不出执行过程中调用了hacker函数,完成了比较隐蔽的缓冲区溢出攻击,函数调用示意图如下:
下面继续改动hack.c从而实现上述过程:
经过以上分析,目前缺少的就是执行完hacker后无法转移到正确的地方继续执行,所以只需要在hacker返回地址处填写正确的地址即可。
需要返回的地址就是执行print语句前的一条指令的地址,查询反汇编文件可知:
先查询最新的ebp的值:
然后将新的ebp和该地址写入hack.c的字符串中,如图:
分析此时的栈帧结构可得如下栈帧结构图:
然后重新执行编译等操作,执行程序得:
可见:程序即被攻击(执行了hacker函数)又正确返回了(输出了正确的字符串),从而完成了缓冲区溢出攻击。
造成缓冲区溢出攻击的原因:程序没有对栈中作为缓冲区的buffer数组进行越界检查,给攻击者提供了一个漏洞。