0%

【pdkx培训】pwn tips

📌 Pwn 核心寄存器速查笔记

在 64 位系统(x86_64)中,寄存器通常以 R 开头(如 RAX,64位);在 32 位系统(x86)中,以 E 开头(如 EAX,32位)。以下主要以 64 位为例。

1. 函数调用约定与参数寄存器(Pwn 核心)

在 64 位 Linux(System V AMD64 ABI)中,函数调用的前 6 个参数通过寄存器传递,这是构造 ROP 链(控制 rdi, rsi 等)时最频繁打交道的寄存器

  • RDI (Destination Index):第 1 个参数
    • Pwn 场景:执行 system("/bin/sh") 时,RDI 必须指向 "/bin/sh" 的地址。
  • RSI (Source Index):第 2 个参数
    • Pwn 场景:控制 read(0, buf, size) 的写入缓冲区地址。
  • RDX (Data Register):第 3 个参数
    • Pwn 场景:控制 readwrite 的长度(size),或者在 mprotect 中控制权限(如 7 代表可读可写可执行)。
  • RCX第 4 个参数
  • R8第 5 个参数
  • R9第 6 个参数

💡 注意:如果参数超过 6 个,剩下的参数会通过栈(Stack)传递。而在 32 位系统中,所有参数默认都通过栈传递。

2. 特殊功能寄存器

  • RAX (Accumulator Register):累加器 / 函数返回值 / 系统调用号
    • 返回值:函数执行完毕后,返回值通常存放在 RAX 中(例如 pwntools 接收泄露地址时就是看它)。
    • 系统调用(Syscall):在利用 syscall 触发 execve 时,必须先将系统调用号(64位下 execve59,即 0x3bread0write1)存入 RAX
  • RIP (Instruction Pointer):指令指针寄存器
    • Pwn 场景:指向当前即将执行的指令地址。劫持程序执行流(PC Control)的本质就是控制 RIP 的值。

3. 栈帧控制寄存器(堆栈溢出核心)

  • RSP (Stack Pointer):栈顶指针
    • Pwn 场景:指向当前栈帧的顶部。pushpopcallret 指令都会隐式修改 RSP 的值。在栈溢出中,RSP 决定了当前 ROP 链的执行位置(Stack Pivoting 技术就是通过修改 RSP 来将栈劫持到堆或其他内存区域)。
  • RBP (Base Pointer):栈底指针
    • Pwn 场景:指向当前栈帧的底部。常用于基址寻址局部变量。在栈溢出漏洞中,通常需要覆盖 Saved RBP(EBP)来到达返回地址;在某些特殊的溢出(如 off-by-one 导致 one-byte overflow)中,通过修改 RBP 的最低字节可以实现 Leave-Ret 触发的栈劫持。

4. 通用数据与通用寄存器

虽然它们有默认含义,但在实际汇编代码和 ROP 链中,经常被用作临时的数据中转站:

  • RBX (Base Register):基地址寄存器,常用于内存寻址或作为通用寄存器。
  • R10 - R15:通用寄存器。在 ROP 链中,如果找不到直接给 RDI/RSI/RDX 赋值的 Gadget,有时需要利用类似 __libc_csu_init 中的 Gadget(万能 Gadget),通过 R12~`R15` 中转来控制参数寄存器。

5. 状态与段寄存器(辅助分析)

  • RFLAGS:标志寄存器(如 ZF 零标志, SF 符号标志)。
    • Pwn 场景:决定了条件跳转(如 je, jne 等格式的花指令或业务逻辑判断)是否成立。
  • FS / GS:段寄存器。
    • Pwn 场景:在 Linux 64 位中,FS 寄存器常用于指向线程局部存储(TLS)。Canary(栈保护金丝雀)的值就保存在 fs:[0x28] 处。 分析绕过 Canary 时会涉及到这个概念。

check list

接受泄露的地址

1
2
3
puts_leak = p.recv(6)
# 2. 用 ljust(8, b'\x00') 补齐到 8 字节,然后解包
puts_addr = u64(puts_leak.ljust(8, b"\x00"))

自动寻找format的offset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *


# 1. 定义一个用于自动化测试的函数
def exec_fmt(payload):
# 每次测试都启动一个新的本地进程,防止互相干扰
io = process("./Voting Machine 2")

# 根据程序的提示跳过前面的废话,直到出现输入点
io.sendlineafter(b"Topic: ", payload)

# 接收 printf 打印出来的结果并返回
# 注意:这里需要返回 printf 触发后的那段回显
result = io.recvall()
io.close()
return result


# 2. 让 pwntools 自动探测!
# 它会自己生成各种 payload 传给 exec_fmt,直到算出正确答案
autofmt = FmtStr(exec_fmt)

# 3. 见证奇迹的时刻
log.success(f"自动探测到的最佳偏移量 (offset): {autofmt.offset}")
log.success(f"自动探测到的需要填充的垫片长度 (padlen): {autofmt.padlen}")

使用python代码将recv()的内容还原到栈中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def print_stack_visual(full_stack_bytes, start_addr=0x7ffcfdec5000):
"""
将接收到的二进制栈数据,按照64位(8字节)一行进行可视化打印
"""
print("\n" + "="*30 + " 内存栈结构视图 " + "="*30)
print(f"{'内存地址 (Addr)':<18} | {'十六进制数据 (Hex Data)':<24} | {'ASCII 字符'}")
print("-"*75)

# 每 8 个字节作为栈的一个单元(QWORD)
for i in range(0, len(full_stack_bytes), 8):
chunk = full_stack_bytes[i:i+8]
if not chunk:
break

# 补齐不足 8 字节的部分(通常在末尾)
chunk_padded = chunk.ljust(8, b'\x00')

# 转换为 16 进制显示(小端序为了方便阅读,这里直接按内存原序显示)
hex_str = " ".join(f"{b:02x}" for b in chunk_padded)

# 转换为可见字符显示
ascii_str = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk_padded)

# 打印当前行
current_addr = start_addr + i
print(f"0x{current_addr:012x} | {hex_str:<24} | {ascii_str}")
print("="*76 + "\n")

# ==================== 使用方法 ====================
# 在你的脚本拿到 full_stack 之后,直接调用这个函数:
# print_stack_visual(full_stack)