参考
通用操作
chmod 777 pwn checksec --file={pwn} file pwn
|
from pwn import * io = remote("pwn.challenge.ctf.show",端口)
context.log_level = 'debug'
p = process('./pwn') p = remote('pwn.challenge.ctf.show', 28234)
payload = asm(shellcraft.sh()) io.sendline(payload) io.sendlineafter(b'Username:',payload) io.sendline(password)
io.recvuntil(b',') password = io.recv()
io.close() io.interactive()
|
ida
快捷键
- “Segments”(段)窗口:Shift + F7
函数
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
|
- 动态,开启NX保护,部分开启RELRO保护
ret2libc:例题1;例题2;例题3
- 动态 + 静态栈保护函数
爆破canary值绕过栈保护函数memcmp()
ret2libc【Return-to-libc】
(ai)一种栈溢出利用技术,当程序开启了 NX(数据执行保护) 导致无法在栈上执行 shellcode 时,攻击者通过覆盖返回地址,让程序跳转到 libc 库中的现有函数(如 system、execve)来执行恶意操作。
Stack
No canary found
- 后门函数
32位后门函数
32位后门函数指定参数 + NX + Partial RELRO
64位后门函数
- 有system()和/bin/sh
32位
64位
- 有system()和/sh:处理方式和/bin/sh一样

- 只有system()
32位
64位
- 无system()和/bin/sh
32位 + NX + Partial RELRO
64位
canary found
import subprocess import sys import re
def analyze_canary(filename): print(f"[*] 正在分析文件: {filename}") 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
try: asm = subprocess.check_output(['objdump', '-d', '-M', 'intel', filename]).decode() except Exception as e: print(f"[!] 错误: 无法运行 objdump - {e}") return
canary_pattern = "fs:\[0x28\]" if is_64 else "gs:\[0x14\]" fail_func_pattern = "__stack_chk_fail"
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 → 第一次解析后变成真实函数地址。
- .plt 本身代码段只读,只是无条件 jmp *.got.plt[n];
- 如果 .got.plt[n] 还没填好,那个 jmp 就会落到一段“解析”代码里,由动态链接器负责把地址填上。
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 的相关状态
在输出中,你会看到类似这样的行,直接表明了 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
gcc flag.c -o flag
nasm -f elf32 XXX.asm -o XXX.o ld -m elf_i386 XXX.o -o XXX
gcc -o flag flag.s
chmod 777 flag ./flag
gdb XXX run
|
ida操作
- 取消定义(Undefine) :
- 按下键盘上的 U 键。
- 这会把当前定义的数据“炸碎”,变成一堆原始的字节(通常显示为 db 且只有单个字节)。
- 重新定义为双字(Make Dword) :
- 确保光标还在 080490E8 这个起始位置。
- 按下键盘上的 D 键。
- 按第一次,它可能变成 db (byte)。
- 按第二次,变成 dw (word)。
- 按第三次 ,它就会变成 dd (dword) 。
此时,原本显示的 ‘Welc’ 就会变成十六进制数值 636C6557h
- 重新回到 ‘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” 在内存中存放的 起始地址 。