20242407 2026-2027-1 《网络与系统攻防技术》实验一实验报告
20242407 2026-2027-1 《网络与系统攻防技术》实验一实验报告
1. 基础知识
1.1 二进制程序分析基础
1.1.1 Linux ELF可执行文件
Linux系统中的可执行程序通常采用ELF(Executable and Linkable Format,可执行与可链接格式)文件格式。
ELF是一种用于描述可执行文件、目标文件以及动态链接库的文件格式,其中包含了程序运行所需的各种信息,例如:
- 程序入口地址;
- 代码段位置;
- 数据段位置;
- 程序加载方式;
- 符号信息等。
一个典型的ELF文件主要包含以下几个部分:
ELF Header
|
↓
Program Header Table
|
↓
代码段(.text)
|
↓
数据段(.data/.bss)
|
↓
Section Header Table
其中,与本实验关系最密切的是代码段(.text)。
代码段保存程序实际执行的机器指令,例如:
- 函数代码;
- 条件跳转指令;
- 无条件跳转指令;
- 函数调用指令。
本实验中的目标程序pwn1就是一个Linux ELF可执行文件。
由于实验目标是:
- 修改程序执行流程;
- 调用隐藏函数getShell;
- 利用程序漏洞控制执行过程;
因此首先需要对ELF文件进行分析,了解程序内部函数结构以及机器指令布局。
1.1.2 程序从源代码到机器码的过程
计算机CPU不能直接执行C语言等高级语言代码,而只能执行机器指令。
一个程序通常经历以下过程:
源代码
|
↓
编译器
|
↓
汇编代码
|
↓
机器码
|
↓
ELF可执行文件
其中:
- 高级语言方便程序员编写;
- 汇编语言描述CPU执行逻辑;
- 机器码是CPU最终执行的数据。
例如:
高级语言:
return 0;
经过编译后会转换为汇编指令:
mov eax,0
最终转换为CPU能够识别的机器码。
在网络攻防和逆向分析过程中,研究人员通常面对的是已经编译完成的二进制程序,因此需要通过反汇编技术恢复程序逻辑。
1.1.3 反汇编分析
反汇编(Disassembly)是将机器码转换为汇编代码的过程。
由于二进制程序中没有直接可阅读的源代码,因此反汇编是分析程序行为的重要方法。
通过反汇编,可以获取:
- 程序包含的函数;
- 函数之间的调用关系;
- 跳转指令的位置;
- 关键地址信息。
本实验中主要使用:
objdump -d pwn1
对pwn1进行反汇编。
通过分析输出结果,可以定位:
- main函数;
- foo函数;
- getShell函数。
其中: - main函数负责程序主要流程;
- foo函数负责处理用户输入;
- getShell函数虽然存在,但是正常流程不会调用。
因此,反汇编分析是后续修改程序流程和构造攻击payload的基础。
1.2 x86汇编指令与程序控制流
1.2.1 程序控制流概念
CPU执行程序的本质是不断读取当前地址中的机器指令,然后执行对应操作。
正常情况下,程序按照指令地址顺序执行。
但是函数调用、条件判断和跳转指令会改变CPU执行位置。
例如:
call foo
表示调用foo函数。
jmp address
表示直接跳转到指定地址。
因此,程序的执行流程实际上由大量控制流指令决定。
本实验中的三个实验方法,本质上都是改变程序控制流:
- 方法一:修改机器指令改变控制流;
- 方法二:覆盖返回地址改变控制流;
- 方法三:跳转执行Shellcode改变控制流。
1.2.2 CMP指令
CMP(Compare)指令用于比较两个操作数。
例如:
cmp eax,0
CMP执行:
eax - 0
但是不会保存计算结果,而是修改CPU标志寄存器。
常见标志包括:
- ZF(Zero Flag):零标志;
- SF(Sign Flag):符号标志;
- CF(Carry Flag):进位标志。
后续条件跳转指令会根据这些标志决定是否跳转。
例如:
cmp eax,0
je target
表示:
如果eax等于0,则跳转到target。
本实验要求掌握CMP指令,是因为程序中的条件判断和跳转逻辑通常由CMP和条件跳转共同完成。
1.2.3 JMP指令
JMP(Jump)是无条件跳转指令。
格式:
jmp address
执行后:
CPU直接修改指令指针EIP,使程序跳转到目标地址。
例如:
原流程:
main
|
foo
|
return
修改后:
main
|
getShell
只需要改变跳转目标,就可以改变程序执行路径。
本实验第一部分修改机器指令,就是利用类似原理改变程序流程。
1.2.4 JE和JNE指令
JE:
Jump Equal
表示相等时跳转。
例如:
cmp eax,1
je success
如果比较结果相等,则跳转到success。
JNE:
Jump Not Equal
表示不相等时跳转。
例如:
cmp eax,1
jne error
如果比较结果不相等,则跳转。
JE和JNE通常与CMP配合使用,是分析程序逻辑的重要指令。
1.2.5 NOP指令
NOP:
No Operation
表示空操作。
机器码:
90
执行NOP时:
- 不改变寄存器;
- 不修改数据;
- 不改变程序状态。
NOP常用于:
- 填充空间;
- 调整代码位置;
- 构造NOP滑板(NOP Sled)。
在漏洞利用过程中,NOP可以提高代码执行成功率。
1.3 可执行文件修改与机器码分析
1.3.1 机器码与十六进制表示
CPU执行的是二进制机器码。
为了方便查看,通常使用十六进制表示机器码。
例如:
55 89 e5
对应汇编:
push ebp
mov ebp,esp
因此:
- 修改机器码;
- 等价于修改程序执行逻辑。
本实验第一部分要求:
能正确修改机器指令改变程序执行流程。
其本质就是:
找到目标机器码位置 → 修改对应字节 → 改变CPU执行行为。
1.3.2 十六进制编辑器
十六进制编辑器用于直接查看和修改二进制文件。
相比修改源代码:
修改二进制文件可以直接影响最终执行文件。
本实验中:
- 使用反汇编定位需要修改的位置;
- 使用十六进制编辑器修改对应字节;
- 保存文件;
- 重新执行程序验证。
因此:
反汇编负责“寻找位置”,十六进制编辑器负责“修改内容”。
1.3.3 小端序存储方式
x86架构采用小端序(Little Endian)存储方式。
例如:
地址:
0x08048520
内存保存:
20 85 04 08
原因:
低有效字节存储在低地址。
在本实验中:
- 修改跳转地址;
- 构造payload;
都需要考虑小端序。
如果地址顺序错误,即使程序成功跳转,也无法执行正确代码。
1.4 函数调用栈与缓冲区溢出漏洞
1.4.1 函数调用栈
程序调用函数时,会在栈空间中创建对应的栈帧(Stack Frame)。
一个典型函数栈结构:
高地址
返回地址
----------------
保存EBP
----------------
局部变量
低地址
其中:
局部变量
保存函数内部使用的数据。
保存EBP
用于恢复调用环境。
返回地址
保存函数执行结束后应该返回的位置。
返回地址决定程序后续执行流程。
1.4.2 call和ret指令
函数调用:
call foo
执行:
- 保存返回地址;
- 跳转foo。
函数返回:
ret
执行:
- 从栈中读取返回地址;
- 跳转。
因此:
如果攻击者修改返回地址:
ret
|
攻击者指定地址
程序执行流程就会被控制。
1.4.3 缓冲区溢出漏洞
缓冲区溢出(Buffer Overflow)产生原因:
程序向固定大小内存区域写入数据时,没有检查输入长度。
例如:
char buffer[20];
如果输入:
AAAAAAAAAAAAAAAAAAAAAAAAAAAA
超过20字节的数据会覆盖后面的栈空间。
可能覆盖:
- 保存EBP;
- 返回地址。
如果覆盖返回地址:
程序执行流程就会改变。
1.4.4 BOF攻击流程
一次完整BOF攻击通常包括:
第一步:寻找漏洞
分析程序中存在危险输入的位置。
本实验:
foo函数。
第二步:分析栈布局
确定:
- buffer位置;
- 返回地址位置。
第三步:计算偏移
确定需要多少字节数据才能覆盖返回地址。
第四步:构造payload
形式:
padding + target address
本实验:
padding + getShell地址
第五步:执行攻击
程序执行:
foo
|
ret
|
getShell
|
Shell
证明BOF攻击成功。
1.5 Payload构造与Shellcode注入
1.5.1 Payload构造原理
Payload不是简单的数据输入,而是经过设计的攻击数据。
典型结构:
填充数据 + 控制数据
其中:
填充数据:
用于覆盖目标区域。
控制数据:
用于改变程序执行流程。
本实验中控制数据:
getShell函数地址
1.5.2 Shellcode基础
Shellcode是一段可以直接执行的机器代码。
与调用已有函数不同:
Shellcode攻击流程:
准备Shellcode
|
↓
注入内存
|
↓
改变执行流程
|
↓
CPU执行Shellcode
Shellcode不依赖程序原本存在的函数,因此更加灵活。
1.5.3 Shellcode与本实验关系
本实验第三部分要求:
注入自己制作的Shellcode并运行。
通过该过程可以理解:
- 代码注入;
- 控制流转移;
- CPU执行机器代码。
前三种方法本质都是:
控制程序执行位置
↓
执行目标代码
区别在于:
- 修改机器码:改变已有代码路径;
- BOF攻击:利用漏洞改变返回地址;
- Shellcode:执行攻击者提供的新代码。
2. 实验内容
本实验围绕pwn1程序完成三个实验目标:
- 手工修改可执行文件中的机器指令,改变程序执行流程,使程序跳转到getShell函数。
- 利用foo函数存在的BOF漏洞,构造payload覆盖返回地址。
- 制作并注入Shellcode,验证自定义代码执行过程。
3. 实验过程
3.1 实验环境准备
在正式进行逆向分析和漏洞利用之前,需要首先确认实验环境以及目标程序是否满足实验要求。
由于本实验需要对Linux
ELF可执行文件进行反汇编分析、动态调试以及二进制修改,因此需要确认:
- 当前实验环境配置正确;
- 实验目标程序存在;
- 程序具有执行权限;
- 目标文件格式符合实验要求。
首先查看当前实验环境主机名:
hostname
实验要求所有实验操作截图中的主机名需要符合个人实验环境要求,因此首先确认当前实验环境信息。
随后查看当前目录中的实验文件:
ls
确认实验目标程序存在。
为了确认程序权限以及文件类型,进一步执行:
ls -l pwn1_20242407
以及:
file pwn1_20242407
通过上述命令可以确认实验目标文件存在,并且属于Linux
ELF可执行文件,为后续反汇编和调试分析提供基础。
执行结果如下图所示:
3.2 手工修改可执行文件改变执行流程
本部分实验目标是通过修改pwn1可执行文件中的机器指令,改变程序原本的执行流程,使程序能够调用隐藏函数getShell。
正常情况下,程序执行流程如下:
main
|
foo
|
return
其中getShell函数虽然存在于程序内部,但是正常执行路径不会调用。
3.2.1 分析pwn1程序结构
由于实验提供的是已经编译完成的二进制文件,并没有直接提供源代码,因此无法通过源码了解程序逻辑。
为了分析pwn1内部结构,需要首先对程序进行反汇编,将机器码转换为可读的汇编代码。
执行:
objdump -d pwn1_20242407
objdump可以对ELF文件中的代码段进行静态反汇编,通过输出结果查看程序中的函数结构以及机器指令。
通过分析反汇编结果,可以定位main、foo以及getShell等关键函数。
执行结果如下图所示:

从图3-2中可以观察到程序中的主要函数,为后续定位目标函数和修改程序执行流程提供依据。
3.2.2 定位getShell函数地址
通过前面的反汇编分析,可以确认程序中存在getShell函数。
但是,如果希望程序跳转执行getShell,仅知道函数名称是不够的,还需要获得该函数在程序中的具体地址。
因此使用nm工具查看程序符号信息:
nm -n pwn1_20242407 | grep getshell
通过输出结果可以找到getShell函数对应地址。
该地址将在后续修改机器指令以及构造payload过程中使用。
执行结果如下图所示:

从图3-3中可以得到getShell函数入口地址,为后续分析函数内容和修改程序流程提供依据。
3.2.3 分析getShell函数功能
获得getShell函数地址后,需要进一步确认该函数具体执行内容。
执行:
objdump -d -M intel pwn1_20242407 | grep -A20 "<getShell>"
通过查看getShell函数汇编代码,可以确认该函数能够执行Shell相关操作。
执行结果如下图所示:

从图3-4中可以观察到,getShell函数内部调用了system函数,可以执行Shell相关操作。 因此,该函数符合实验目标,可以作为程序修改后的跳转目标。
3.2.4 修改机器指令改变执行流程
确定目标函数后,需要进一步修改程序中的机器指令,使程序执行过程中能够跳转到getShell函数。
由于CPU最终执行的是机器码,而不是汇编代码,因此需要使用十六进制编辑工具直接修改二进制文件。
修改过程中需要注意:
- 修改位置必须对应正确的机器指令;
- 修改后的跳转地址必须为getShell函数地址;
- x86架构采用小端序存储方式,地址写入时需要按照低字节优先方式保存。
修改前的二进制文件详细内容如下图所示:
.png)
修改后的二进制文件详细内容如下图所示:
.png)
修改完成后保存文件,并重新运行程序验证修改效果。
完成二进制文件修改后,需要重新运行程序验证修改是否成功。
执行:
./pwn1_20242407
通过运行结果判断程序控制流是否已经改变。
如果程序成功进入getShell函数,则说明修改位置和跳转地址均正确。
验证结果如下图所示:

3.3 利用foo函数BOF漏洞攻击
第二部分实验利用foo函数存在的缓冲区溢出漏洞,通过覆盖返回地址控制程序执行流程。
3.3.1 分析foo函数漏洞
为了利用BOF漏洞,需要首先分析foo函数内部结构。
进入gdb调试环境:
gdb pwn1_20242407
查看foo函数:
disassemble foo
执行结果如下图所示:

从图3-7中可以观察到foo函数的汇编结构。
函数开始位置:
push %ebp mov %esp,%ebp
表示函数建立新的栈帧。
随后:
sub $0x38,%esp
表示程序为局部变量分配0x38字节的栈空间。
继续分析可以发现:
lea -0x1c(%ebp),%eax
程序将栈中ebp-0x1c位置作为输入缓冲区地址。
最关键的是:
call 0x8048330 <gets@plt>
说明foo函数使用gets函数接收用户输入。
由于gets函数不会检查输入长度,当输入超过缓冲区大小时,多余数据可能覆盖相邻栈空间,包括保存的返回地址。
因此,可以确定foo函数存在缓冲区溢出漏洞,为后续构造payload覆盖返回地址提供基础。
3.3.2 分析函数栈结构并确定偏移
确定foo函数存在缓冲区溢出后,需要进一步分析函数运行时的栈空间结构。
设置断点:
break foo
运行程序:
run
查看当前栈空间:
x/40wx $esp
通过观察栈中的数据分布,可以计算buffer到返回地址之间的偏移量。
结果如下图所示:

从图3-8可以观察到foo函数执行时栈中的数据分布。
由于函数调用过程中返回地址保存在栈中,因此攻击目标是找到输入缓冲区与返回地址之间的距离。
结合foo函数中的:
sub $0x38,%esp
可以确定该函数为局部变量预留了0x38字节空间。 后续需要根据实际栈布局计算覆盖返回地址所需的输入长度,并以此构造payload。
3.3.3 构造payload覆盖返回地址
根据前面的栈空间分析结果,已经确定输入缓冲区与函数返回地址之间的偏移量。
通过foo函数反汇编结果可以发现:
lea -0x1c(%ebp),%eax
该指令表示程序将ebp-0x1c位置作为输入缓冲区。
由于x86函数调用过程中,返回地址保存在ebp+4位置,因此输入缓冲区到返回地址之间的距离为:
offset = (ebp+4)-(ebp-0x1c)
= 0x1c + 4
= 0x20
= 32字节
因此,需要填充32字节数据才能覆盖函数返回地址。
同时,根据前面对getShell函数的分析,可以得到目标函数地址:
getShell函数地址:0x0804847d
由于x86架构采用小端序存储方式,因此该地址写入payload时需要转换为:
\x7d\x84\x04\x08
因此,本实验构造的payload结构如下:
32字节填充数据 + getShell地址
其中:
- 填充数据用于覆盖buffer,并使输入位置到达返回地址;
- getShell地址用于覆盖原始返回地址,使程序执行ret指令时跳转到getShell函数。
最终payload如下:
"A"*32 + "\x7d\x84\x04\x08"
实际执行命令:
python3 -c 'import sys;sys.stdout.buffer.write(b"A"*32+b"\x7d\x84\x04\x08")'
执行结果如下图所示:

从图3-9可以观察到,payload由32字节填充数据和getShell函数地址组成,与前面对函数栈结构以及返回地址偏移的分析结果一致。
该payload将在后续步骤中输入目标程序,用于覆盖函数返回地址。
3.3.4 验证BOF攻击结果
完成payload构造后,将攻击字符串输入目标程序进行验证。
如果返回地址覆盖成功,foo函数执行ret指令时,程序执行流程将发生改变。
正常执行流程:
foo
|
ret
|
main继续执行
攻击成功后的执行流程:
foo
|
ret
|
getShell
|
Shell
执行结果如下图所示:

从图3-10可以观察到,程序成功进入Shell环境,并能够正常执行系统命令。
输入:
whoami
返回当前用户:
kali
随后输入:
id
返回当前用户ID以及用户组信息。
实验结果表明:
- payload成功覆盖了foo函数返回地址;
- 返回地址被修改为getShell函数地址;
- 程序执行流程被成功控制;
- BOF攻击达到实验目标。
3.4 Shellcode注入实验
前两个实验方法都是利用程序中已经存在的代码实现控制流改变。
而Shellcode注入不依赖程序已有函数,而是将攻击者编写的机器代码注入内存,并控制程序执行该代码。
3.4.1 制作Shellcode
Shellcode是一段可以直接执行的机器代码。
与前面调用程序内部getShell函数不同,Shellcode注入方式不依赖目标程序已有代码,而是由攻击者自行编写机器代码,并将其注入目标内存区域执行。
本实验中,需要首先将目标功能转换为CPU能够识别的机器指令,并以字节形式保存。
Shellcode示例如下:
shellcode = ( b"\x31\xc0..." )
本实验中编写的shellcode具体内容如下图所示:

从图3-11中可以看到,Shellcode由多组十六进制字节组成,这些字节对应CPU可以直接执行的机器指令。
完成Shellcode制作后,需要进一步构造注入payload,使程序能够跳转到Shellcode所在位置。
3.4.2 构造payload并执行Shellcode
完成Shellcode制作后,需要将Shellcode加入攻击payload,使程序执行流程能够跳转到Shellcode所在位置。
Shellcode注入payload通常由以下几个部分组成:
填充数据 + 返回地址 + NOP滑板 + Shellcode
其中:
- 填充数据用于覆盖原有栈空间;
- 返回地址用于改变程序执行位置;
- NOP滑板用于提高跳转到Shellcode的成功概率;
- Shellcode为最终执行的机器代码。
本实验中payload结构如下:
payload = b"A"*32
payload += b"\xa0\xcf\xff\xff"
payload += b"\x90"*100
payload += shellcode
其中:
A*32
用于填充缓冲区。
\x90
对应x86架构中的NOP指令,用于构造NOP滑板。
最终追加Shellcode,使程序跳转后执行自定义机器代码。
payload构造过程如下图所示:

完成payload构造后,将其输入目标程序进行执行。
通过gdb查看Shellcode注入后的内存区域:
x/120bx 0xffffcfa0
执行结果如下图所示:

从图3-13中可以观察到,目标内存区域中存在大量:
0x90
说明NOP滑板已经成功写入内存。
随后可以观察到Shellcode对应机器码已经位于目标内存区域。
继续执行程序后输出:
Hi!
说明CPU已经成功执行注入的Shellcode,实验目标完成。
4. 问题及解决方案
问题1:修改机器指令后程序未成功跳转到getShell函数
问题描述
在进行手工修改可执行文件实验时,完成机器码修改后重新运行程序,程序没有按照预期进入getShell函数。
原因分析
通过检查修改过程发现,问题主要出现在目标地址处理过程中。
由于x86架构采用小端序存储方式,函数地址不能直接按照常规十六进制形式写入二进制文件,而需要按照低字节在前的顺序保存。
如果地址格式错误,程序执行跳转指令时会读取错误地址,从而导致程序无法进入目标函数。
解决方案
首先通过符号分析重新确认getShell函数地址:
nm -n pwn1_20242407
获得:
0x0804847d
随后按照小端序转换为:
\x7d\x84\x04\x08
重新修改对应机器指令后,再次运行程序进行验证。
最终程序成功跳转到getShell函数。
问题2:Shellcode调试过程中无法直接判断执行状态
问题描述
在进行Shellcode注入实验时,使用gdb调试程序过程中,调试器无法像普通函数一样直接显示Shellcode函数名称。
原因分析
Shellcode属于运行时动态注入的机器代码,并不是程序编译阶段存在的函数,因此程序符号表中不存在对应信息。
因此,gdb无法通过函数名称定位Shellcode执行位置。
解决方案
通过查看Shellcode注入后的内存区域,分析其中是否存在NOP滑板以及Shellcode机器码:
x/120bx 0xffffcfa0
观察内存中的:
0x90
等NOP指令以及Shellcode对应机器码。
随后继续执行程序:
continue
结合程序输出结果判断Shellcode是否成功执行。
最终确认Shellcode已经成功注入并运行。
5. 学习感悟、思考等
通过本次“逆向破解与BOF”实验,我对程序执行过程以及漏洞利用方法有了更加深入的理解。
在实验开始之前,我对于程序安全问题的理解更多停留在较高层面的概念,例如输入验证不足可能导致安全问题。但是通过本次实验,我认识到程序最终执行的是CPU能够识别的机器指令,程序运行过程中的每一次函数调用、跳转以及返回,本质上都依赖底层的地址和控制流。
在第一个实验方法中,通过修改可执行文件中的机器指令改变程序执行流程,使我理解了程序行为并不是固定不变的。程序最终执行结果取决于内存中的指令内容以及CPU当前执行位置。如果攻击者能够修改关键指令或者跳转地址,就可能改变程序原本设计好的执行逻辑。因此,在软件开发过程中,保护程序执行流程和避免非法修改具有重要意义。
在第二个实验方法中,通过利用foo函数中的缓冲区溢出漏洞完成BOF攻击,我对函数调用栈有了更加直观的认识。之前对于栈、返回地址等概念的理解主要停留在理论层面,而通过实际调试,我观察到了局部变量、保存寄存器以及返回地址在内存中的具体布局。
实验过程中,BOF攻击并不是简单输入大量字符,而是需要经过多个步骤:
- 分析目标函数是否存在漏洞;
- 确定缓冲区和返回地址之间的距离;
- 构造符合要求的payload;
- 控制程序返回位置。
这一过程让我认识到,漏洞利用实际上是建立在对程序运行机制深入理解的基础上的,而不是简单依靠某个固定攻击字符串。
在Shellcode注入实验中,我进一步理解了代码注入的基本思想。与前两种方法利用程序中已有代码不同,Shellcode注入尝试让程序执行攻击者提供的新代码。这让我认识到,程序漏洞的危险性不仅体现在数据被破坏,更重要的是攻击者可能进一步控制程序执行权限。
同时,通过本次实验,我也认识到了调试工具在安全分析中的重要作用。在实际实验过程中,并不是所有操作都能够一次成功,例如机器码修改可能导致程序无法正常执行,BOF攻击可能由于偏移计算错误而失败,Shellcode执行过程中也可能出现地址无法识别等情况。
面对这些问题,需要结合反汇编结果、调试信息以及程序运行现象逐步分析原因,而不是盲目修改参数。这种分析问题和解决问题的过程,提高了我使用Linux工具进行程序分析的能力。
从防御角度来看,本实验中的漏洞主要来源于程序设计过程中缺少必要的安全检查。例如:
- 对用户输入长度缺少限制;
- 使用不安全的输入函数;
- 缺少有效的内存保护机制。
因此,在实际软件开发过程中,需要重视安全编码规范,加强输入验证,并合理使用现代操作系统提供的安全保护机制。
通过本次实验,我不仅掌握了ELF分析、反汇编、BOF攻击以及Shellcode注入等技术,也更加理解了攻击与防御之间的关系。网络安全不仅需要了解攻击方法,更需要从攻击过程反向分析系统存在的问题,并设计更加安全可靠的软件。
参考资料
[1] 《深入理解计算机系统(Computer Systems: A Programmer's Perspective)》
[2] 《Linux系统编程(Linux System Programming)》
[3] Intel® 64 and IA-32 Architectures Software Developer’s Manual
[4] 《实验一 逆向破解与BOF》
浙公网安备 33010602011771号