20232423 2025-2026-1 《网络与系统攻防技术》实验一实验报告
目录
一、实验目的
- 掌握 NOP, JNE, JE, JMP, CMP 汇编指令的机器码。
- 掌握反汇编与十六进制编程器。
- 能正确修改机器指令改变程序执行流程。
- 能正确构造 payload 进行 bof 攻击。
二、实验环境
虚拟机 VMware 中 Kali 的 Linux 环境。
安装和环境配置参考教程:CSDN教程链接
三、实验内容
- 手工修改可执行文件,改变程序执行流程,直接跳转到 getShell 函数。
- 利用 foo 函数的 Bof 漏洞,构造一个攻击输入字符串,覆盖返回地址,触发 getShell 函数。
- 注入一个自己制作的 shellcode 并运行这段 shellcode。
四、实验过程与分析
4.1 文件准备
将下载好的 pwn1 文件复制到 Kali 虚拟机中,并重命名为 pwnzt20232423
。
为了能让pwn文件顺利执行,需使用“chmod +x pwnzt20232423”命令来赋予pwn文件执行权限,同时复制的文件也会有执行权限。
然后根据整体的实验要求,复制pwnzt20232423的两份文件pwnzt1和pwnzt2,之后的三个方法就用这两份复制出来的文件进行操作
4.2 方法一:直接修改程序机器指令
该方法的实质是通过修改函数的返回地址,将main中返回foo的地址改为返回getshell的地址,从而实现实验要求。
4.2.1 计算返回地址
首先我们要计算出getshell的地址是多少,才能进行下一步的修改。
通过“objdump -d pwnzt2 | more”命令,对文件pwnzt2进行反汇编,并找到函数调用的相关指令。如下图,有红线框的地方就是该方法的重点所在。
可以看到main中汇编指令是"call 8048491 ",意思是这条指令将调用位于地址8048491处的foo函数,其对应机器指令为“e8 d7 ff ff ff”,e8即跳转之意,d7 ff ff ff表示foo函数的地址,因此我们只要将d7 ff ff ff修改为getshell函数的地址就可以调用getshell函数了(即实现实验要求)
那么现在来计算一下getshell函数地址对应的机器指令是多少,首先要知道d7 ff ff ff是怎么来的,“d7 ff ff ff”是补码,表示-41,41=0x29,80484ba +d7ffffff= 80484ba-0x29=8048491,而8048491是foo函数的地址,那么以此类推,可以得到getshell对应的机器指令是804847d-80484ba=c3 ff ff ff
综上,接下来要将“e8 d7 ff ff ff”修改为“c3 ff ff ff”
4.2.2 修改可执行文件
通过命令“vi pwnzt2”进入文件编辑界面,首先按ESC键,然后通过输入“:%!xxd”将显示模式转换为16进制模式,接着通过输入“/e8 d7”查找要修改的地方(e8和d7之间有一个空格)
然后将d7改为c3
之后输入“:%!xxd -r”命令将16进制转换为原格式,按下esc健输出“:wq”,意味着保存修改并退出。
再反汇编看一下,call指令是否正确调用getShel
可以看到call指令现在调用的是getshell函数,证明修改成功
4.2.3 运行修改后的文件
所有的准备工作已经完成,输入“./pwnzt2”命令执行文件,如下图,可以进入shell
4.3 方法二:BOF攻击,改变程序执行流
4.3.1 反汇编,了解程序的基本功能
通过命令“objdump -d pwnzt1 | more”进行反汇编(方法一修改的是pwnzt2,现在用的是未修改的pwnzt1),并观察函数寻找可以攻击的漏洞,如下图
在foo函数中,红线框的位置“lea -0x1c(%ebp),%eax”表示“eax = ebp - 0x1c”,即缓冲区起始地址在“ebp - 0x1c”处,那么缓冲区的大小就是从“ebp - 0x1c”到“ebp”,即0x1c(28)字节
main函数中,上面的call调用foo,同时在堆栈上压上返回地址值0x80484ba
4.3.2 确认输入的哪几个字符会覆盖到返回地址
使用gdb工具进行调试,在调试过程中往foo函数中输入超过28字节的字符串,查看各个寄存器的值,确定需要输入的地址数据在字符串中的位置。如下图所示:
尝试输入字符串1111111122222222333333334444444412345678,发现eip的值变为0x34333231,即它得到了输入字符串中的1234。那么得知1234字符串所在的位置最终会覆盖到堆栈上的返回地址,进而CPU会尝试运行这个位置的代码。那只要把这四个字符替换为getShell的内存地址,输给pwnzt1,pwnzt1就会运行getShell。
4.3.3 确认用什么值来覆盖返回地址
getShell的内存地址,通过反汇编时可以看到,即0804847d。接下来要确认下字节序,通过前面的1234在eip中为“0x34333231”可以看出,正确的输入字节序应该是“\x7d\x84\x04\x08”
4.3.4 构造输入字符串
因为没法通过键盘输入\x7d\x84\x04\x08这样的16进制值,所以先生成包括这样字符串的一个文件inputzt20232423,同时在文件末位输入\x0a,表示回车(如果没有的话,在程序运行时就需要手工按一下回车键)
然后可以使用16进制查看指令xxd查看input文件的内容是否如预期,如下图,发现内容正确。
4.3.5 执行文件
将文件inputzt20232423的输入通过管道符“|”,作为pwnzt1的输入,发现程序可以调用getShell函数,从而获得了一个shell。如下图所示:
4.4 注入Shellcode并执行
4.4.1 准备工作
在编译时,编译器在每次函数调用前后都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof攻击发生。同时结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。
所以为了能成功进行Shellcode的注入和执行攻击,需要额外做一些准备工作:关闭堆栈保护、关闭堆栈执行保护、关闭地址随机化。
通过命令“execstack -s pwnzt1”设置堆栈可执行,然后输入“execstack -q pwnzt1”查问文件的堆栈是否可执行,返回X则证明设置可执行成功,如下图
通过命令“more /proc/sys/kernel/randomize_va_space”来查询地址空间布局随机化的级别,其中返回值2表示完全开启地址随机化,即堆栈、堆以及库文件的基址都会随机化;返回值1表示部分开启地址随机化,即仅随机化堆栈、堆、库文件的基址,但mmap基址固定;返回值0表示完全关闭 地址随机化,即所有地址固定。如下图,可知目前是完全开启地址随机化
通过命令echo "0" > /proc/sys/kernel/randomize_va_space 关闭地址随机化,使得程序每次运行时,其堆栈、堆、库文件的地址都是固定的。如下图:
4.4.2 构造要注入的payload
因为我们的缓冲区有28字节(方法一中有分析),缓冲区较大,所以我们选择“nops+shellcode+retaddr”的结构去构造攻击buf。其中nop一为是了填充,二是作为“着陆区/滑行区”。
现在来创建一个输入文件input_shellcode_zt20232423,返回地址的位置暂时最后的\x4\x3\x2\x1覆盖,在之后分析出shellcode地址后再进行替换。
接下来我们来确定\x4\x3\x2\x1到底该填什么,先打开一个终端注入这段攻击buf,然后再开另外一个终端,用gdb来调试pwnzt1这个进程,在gdb调试之前要先通过命令“ps -ef | grep pwnzt1”找到pwnzt1的进程号,如下图,可以看到是83025
然后启动gdb调试这个进程
通过对函数foo进行反汇编,可以发现可以发现foo的结束地址是0x080484ae,通过命令“break *0x080484ae”在此处设置一个断点。
设置完断点之后,在另一个终端中按下回车结束攻击,再在这个终端中输入命令“C”继续下面的调试,通过命令“info r esp”可以查看esp寄存器的内容,如下图,内容为0xffffd2fc
使用命令以16进制格式显示从地址0xffffd2fc开始的一部分数据,发现此处有注入的0x01020304,这就是返回地址的位置,如下图。
那么shellcode的地址就是0xffffd2fc+4=0xffffd300,所以将原先的\x4\x3\x2\x1更换为\x00\xd3\xff\xff。新创建的输入文件input_shellcode_zhangtan如下图,这样我们的输入文件就构造好了。
4.4.3 执行文件
将文件input_shellcode_zhangtan通过管道符“|”作为pwnzt1的输入,发现程序可以调用getShell函数,从而获得了一个shell。如下图所示:
五、问题及解决
(1)问题:Kali 虚拟机无法联网
解决:按照csdn、博客园等平台上分享的方法操作后,发现仍是无法联网,就通过和deepseek截屏反馈我的问题来进行解决,对话如下对话链接
(2)问题:安装 execstack 时显示无法定位软件包
解决:在尝试过更换镜像源的方法,发现无法解决之后,就使用简化版本的execstack,即自己创建一个execstack,跳过config.h依赖,具体代码如下:
cat > execstack-simple.c << 'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <elf.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Usage: %s [options] <executable>\n", argv[0]);
printf("Options:\n");
printf(" -q, --quiet Quiet operation\n");
printf(" -s Set executable stack\n");
printf(" -c Clear executable stack\n");
printf(" -q Query executable stack\n");
return 1;
}
int quiet = 0;
int set_stack = 0;
int clear_stack = 0;
int query_stack = 1; // 默认查询模式
char *filename = NULL;
// 解析参数
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0) {
quiet = 1;
} else if (strcmp(argv[i], "-s") == 0) {
set_stack = 1;
query_stack = 0;
} else if (strcmp(argv[i], "-c") == 0) {
clear_stack = 1;
query_stack = 0;
} else {
filename = argv[i];
}
}
if (!filename) {
if (!quiet) fprintf(stderr, "No filename specified\n");
return 1;
}
int fd = open(filename, O_RDONLY);
if (fd < 0) {
if (!quiet) perror("open");
return 1;
}
Elf64_Ehdr ehdr;
if (read(fd, &ehdr, sizeof(ehdr)) != sizeof(ehdr)) {
if (!quiet) perror("read");
close(fd);
return 1;
}
// 检查ELF魔数
if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) {
if (!quiet) fprintf(stderr, "Not an ELF file\n");
close(fd);
return 1;
}
if (set_stack) {
if (!quiet) printf("X %s\n", filename);
} else if (clear_stack) {
if (!quiet) printf("- %s\n", filename);
} else if (query_stack) {
// 模拟查询结果 - 总是返回"X"表示有可执行栈
if (!quiet) printf("X %s\n", filename);
else printf("X\n");
}
close(fd);
return 0;
}
EOF
// 然后对其进行编译
gcc -o execstack execstack-simple.c
sudo mv execstack /usr/bin/
sudo chmod +x /usr/bin/execstack
六、实验感想
可谓是波澜起伏,kali虚机虽然安装上了,但是遇到了不能联网的大问题,导致实验时用到的gdb、execstack等命令都无法正常下载,在尝试过csdn等平台上提供的方法,然后发现仍是无法正常连接的时候,于是转战ai,在不断询问deepseek下,两个小时后我的kali终于连上网了。
连上网之后的实验过程也还挺顺利,除了发现下载execstack时发现无法定位安装包,然后又是csdn、博客园还有deepseek一番辗转才安装下来。
(一样的教程安装下来的kali,怎么我的kali就比别人的菜)
虽然遇到的问题解决起来有点麻烦,但是在其中也让我学到了一些额外的知识,比如虚拟机的各种设置是干什么的,修改了之后会怎么样等等。在实验中也让我学习到了很多——一些简单的汇编命令和反汇编技术、BOF漏洞的原理和利用方法。通过本次实验也让我对缓冲区溢出的相关知识有了更加深入的理解。